Jetpack Composeのrecompose後。GlobalSnapshotへの適応のコードリーディング

https://qiita.com/takahirom/items/0e0a3559d95b49399c3b

の続きです。

今度まとめた記事を書く予定なので、この記事は読まなくていいです。

前回までで、recomposeの実装を読んでいくことができました。
これからSlotTableに反映していくところ
つまり3から4の間あたりをみていきます。

1: recordComposerModificationsLocked() を呼ぶ。Recomposer.snapshotInvalidationsを見て、IdentityScopeMapを使って変更点に対しての影響を受けるスコープを取得し、Recomposer.compositionInvalidationsやComposition.invalidationsというフィールドに変更を入れる。
2: compositionInvalidationsをtoRecomposeに入れる。
3: toRecomposeに対して、performRecompose(composition, modifiedValues)、(内部でdoCompose())することでtoApplyを作成。SlotTableを見て、ここで木を見ていき、木への変更をrecordしていく。
4: recordされたtoApplyに対してapplyChanges()をすることで、SlotTableの書き換えが走る。 (予想。コード未読)

前回はContentなどの実際のユーザーが定義した関数の呼び出しを見ていきましたが、それが終わった後の動きについて追っていきます。

```
Recomposer.runRecomposeAndApplyChanges()
└── Recomposer.performRecompose()
    └── CompositionImpl.recompose()
        └── CompositionImpl.doCompose()
            └── CompositionImpl.skipCurrentGroup()
                └── CompositionImpl.recomposeToGroupEnd()
                    ├── RecomposeScopeImpl.compose()
                    │   └── Content() 前回まで読んでいたもの
                    │       └── Node2()
                    ├── *CompositionImpl.recordUpsAndDowns()* ← Now reading
                    └── *SlotReader.skipToGroupEnd()* ← Now reading
```

```kotlin
    /**
     * Recompose any invalidate child groups of the current parent group. This should be called
     * after the group is started but on or before the first child group. It is intended to be
     * called instead of [skipReaderToGroupEnd] if any child groups are invalid. If no children
     * are invalid it will call [skipReaderToGroupEnd].
     */
    private fun recomposeToGroupEnd() {
        val wasComposing = isComposing
        isComposing = true
        var recomposed = false

        val parent = reader.parent
        val end = parent + reader.groupSize(parent)
        val recomposeIndex = nodeIndex
        val recomposeCompoundKey = compoundKeyHash
        val oldGroupNodeCount = groupNodeCount
        var oldGroup = parent

        var firstInRange = invalidations.firstInRange(reader.currentGroup, end)
        while (firstInRange != null) {
            val location = firstInRange.location

            invalidations.removeLocation(location)

            if (firstInRange.isInvalid()) {
...
                firstInRange.scope.compose(this) // ← ここを前まで呼んだ
...
        }

        if (recomposed) {
            recordUpsAndDowns(oldGroup, parent, parent) // ← ここから
            reader.skipToGroupEnd()
            val parentGroupNodes = updatedNodeCount(parent)
            nodeIndex = recomposeIndex + parentGroupNodes
            groupNodeCount = oldGroupNodeCount + parentGroupNodes
        } else {
            // No recompositions were requested in the range, skip it.
            skipReaderToGroupEnd()
        }
        compoundKeyHash = recomposeCompoundKey

        isComposing = wasComposing
    }

```

## CompositionImpl.recordUpsAndDowns() & reader.skipToGroupEnd()
デバッガーの動きを見ましたが、今回のrecomposeではほぼ何も行われないようでした。

recordという名前がついているので、changesの内容を確認しておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/4e8451ff-6164-4e61-aad7-265ef67e2570.png)

これは以前の記事と変わっていなそうでした。
https://qiita.com/takahirom/items/11e3ed72eb2f83440b12#%E3%81%BE%E3%81%A8%E3%82%81

```kotlin
0 = record { _, slots, _ -> slots.advanceBy(distance) }
10 進める

1 = private val startRootGroup: Change = { _, slots, _ -> slots.ensureStarted(0) }
ensureStartedGroupが始まったので、いろんなWriterのcurrentGroupなどを更新をかけるもの。で、引数が0なのでrootでやる。

2 = recordSlotTableOperation { _, slots, _ -> slots.ensureStarted(anchor) }
ensureStartedGroupが始まったので、いろんなWriterのcurrentGroupなどを更新をかけるもの。で、引数がanchorなのでcurrent groupでやる

3 = private val removeCurrentGroupInstance: Change = { _, slots, rememberManager ->
slots.removeCurrentGroup(rememberManager)
}

4 = { _, slots, _ -> slots.endGroup() }

5 = { applier, _, _ -> applier.remove(removeIndex, count) }
```

なのでさかのぼって次はendRoot()になりました。

```kotlin
    private fun doCompose(
        invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>,
        content: (@Composable () -> Unit)?
    ) {
        runtimeCheck(!isComposing) { "Reentrant composition is not supported" }
        trace("Compose:recompose") {
            snapshot = currentSnapshot()
            invalidationsRequested.forEach { scope, set ->
                val location = scope.anchor?.location ?: return
                invalidations.add(Invalidation(scope, location, set))
            }
            invalidations.sortBy { it.location }
            nodeIndex = 0
            var complete = false
            isComposing = true
            try {
                startRoot()
                // Ignore reads of derivedStatOf recalculations
                observeDerivedStateRecalculations( // ← ここも終わった
                    start = {
                        childrenComposing++
                    },
                    done = {
                        childrenComposing--
                    },
                ) {
                    if (content != null) {
                        startGroup(invocationKey, invocation)

                        invokeComposable(this, content)
                        endGroup()
                    } else {
                        skipCurrentGroup() // ここが呼ばれていたみたい。
                    }
                }
                endRoot() // ←　次はここ
                complete = true
            } finally {
                isComposing = false
                invalidations.clear()
                providerUpdates.clear()
                if (!complete) abortRoot()
            }
        }
    }
```


```
Recomposer.runRecomposeAndApplyChanges()
└── Recomposer.performRecompose()
    └── CompositionImpl.recompose()
        └── CompositionImpl.doCompose()
            ├── CompositionImpl.skipCurrentGroup()
            │   └── CompositionImpl.recomposeToGroupEnd()
            │       ├── RecomposeScopeImpl.compose()
            │       │   └── Content()
            │       │       └── Node2()
            │       ├── CompositionImpl.recordUpsAndDowns()
            │       └── SlotReader.skipToGroupEnd()
            └── *endRoot()* ← Now reading
```



```kotlin
    /**
     * End the composition. This should be called, and only be called, to end the first group in
     * the composition.
     * compositionの終わり。最初のcompositionが終わった時のみ呼ばれるべき。
     */
    @OptIn(InternalComposeApi::class)
    private fun endRoot() {
// 以下ではほとんど何もしません
        endGroup()
        parentContext.doneComposing()
        endGroup()
// 上記ではほとんど何もしません
        recordEndRoot()
        finalizeCompose()
        reader.close()
    }
```

## recordEndRoot()

```
Recomposer.runRecomposeAndApplyChanges()
└── Recomposer.performRecompose()
    └── CompositionImpl.recompose()
        └── CompositionImpl.doCompose()
            ├── CompositionImpl.skipCurrentGroup()
            │   └── CompositionImpl.recomposeToGroupEnd()
            │       ├── RecomposeScopeImpl.compose()
            │       │   └── Content()
            │       │       └── Node2()
            │       ├── CompositionImpl.recordUpsAndDowns()
            │       └── SlotReader.skipToGroupEnd()
            └── ComposerImpl.endRoot()
                ├── ComposerImpl.endGroup()
                ├── Recomposer.doneComposing()
                ├── ComposerImpl.endGroup()
                └── *ComposerImpl.recordEndRoot()* ← Now reading
```

以下でSlotTable上で3移動してからendGroup()を呼ぶという操作をrecordします。

```
private val endGroupInstance: Change = { _, slots, _ -> slots.endGroup() }

    private fun recordEndRoot() {
        if (startedGroup) {
            recordSlotTableOperation(change = endGroupInstance)
            startedGroup = false
        }
    }

    /**
     * Record a change ensuring, when it is applied, the write matches the current slot in the
     * reader.
     * 
     */
    private fun recordSlotTableOperation(forParent: Boolean = false, change: Change) {
        realizeOperationLocation(forParent)
        record(change)
    }

    private fun realizeOperationLocation(forParent: Boolean = false) {
        val location = if (forParent) reader.parent else reader.currentGroup
        val distance = location - writersReaderDelta
        require(distance >= 0) { "Tried to seek backward" }
// メモ
// forParent = false
// writersReaderDelta = 13
// location = 16
        if (distance > 0) { // distanceは3になる！！！
            record { _, slots, _ -> slots.advanceBy(distance) }
            writersReaderDelta = location
        }
    }
```

changesが2つ増えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/41c08971-7286-c7fa-5ec1-31f2fb35ca42.png)


```kotlin
0 = record { _, slots, _ -> slots.advanceBy(distance) }
10 進める

1 = private val startRootGroup: Change = { _, slots, _ -> slots.ensureStarted(0) }
ensureStartedGroupが始まったので、いろんなWriterのcurrentGroupなどを更新をかけるもの。で、引数が0なのでrootでやる。

2 = recordSlotTableOperation { _, slots, _ -> slots.ensureStarted(anchor) }
ensureStartedGroupが始まったので、いろんなWriterのcurrentGroupなどを更新をかけるもの。で、引数がanchorなのでcurrent groupでやる

3 = private val removeCurrentGroupInstance: Change = { _, slots, rememberManager ->
slots.removeCurrentGroup(rememberManager)
}

4 = { _, slots, _ -> slots.endGroup() }

5 = { applier, _, _ -> applier.remove(removeIndex, count) }

6 = { _, slots, _ -> slots.advanceBy(distance) }// (distance = 3)
7 = { _, slots, _ -> slots.endGroup() }
```

## endRoot()に戻る

他の `finalizeCompose()`や `reader.close()` はフィールドに0を入れて初期化したりなどでした。

```kotlin
    /**
     * End the composition. This should be called, and only be called, to end the first group in
     * the composition.
     */
    @OptIn(InternalComposeApi::class)
    private fun endRoot() {
        endGroup()
        parentContext.doneComposing()
        endGroup()
        recordEndRoot()
        finalizeCompose()
        reader.close()
    }
```


戻っていきます。

途中でComposerImpl.invalidationsを初期化したりします。


```kotlin
                invalidations.clear()
                providerUpdates.clear()
```


## Recomposer.composing()

```
Recomposer.runRecomposeAndApplyChanges()
└── Recomposer.performRecompose()
    └── *Recomposer.composing()* ← Now reading
        └── CompositionImpl.recompose()
            └── Snapshot.enter(block)
                └── CompositionImpl.doCompose()
                    ├── CompositionImpl.skipCurrentGroup()
                    │   └── CompositionImpl.recomposeToGroupEnd()
                    │       ├── RecomposeScopeImpl.compose()
                    │       │   └── Content()
                    │       │       └── Node2()
                    │       ├── CompositionImpl.recordUpsAndDowns()
                    │       └── SlotReader.skipToGroupEnd()
                    └── ComposerImpl.endRoot()
                        ├── ComposerImpl.endGroup()
                        ├── Recomposer.doneComposing()
                        ├── ComposerImpl.endGroup()
                        └── ComposerImpl.recordEndRoot()
                            └── ComposerImpl.recordSlotTableOperation(forParent = false, change = { _, slots, _ -> slots.endGroup() })
                                └── ComposerImpl.realizeOperationLocation(forParent = false)
```

ここではSnapshotを取得します。Snapshotはゲームのセーブポイントみたいなものです。
引数でreadObserverとwriteObserverを渡しますが、これでComposition中の(enterを呼んだ中の)MutableStateへの変更を収集します。

```kotlin
    private inline fun <T> composing(
        composition: ControlledComposition,
        modifiedValues: IdentityArraySet<Any>?,
        block: () -> T
    ): T {
        val snapshot = Snapshot.takeMutableSnapshot(
            readObserverOf(composition), writeObserverOf(composition, modifiedValues)
        )
        try {
            return snapshot.enter(block)
        } finally {
            applyAndCheck(snapshot)
        }
    }
```


enterを呼んだあとの変更をapplyAndCheck()でsnapshot.apply()でGlobalSnapshotに変更を適応します。
これをしないとGlobalSnapshotでMutableStateの状態は変わりません。

```kotlin
    private fun applyAndCheck(snapshot: MutableSnapshot) {
        val applyResult = snapshot.apply()
        if (applyResult is SnapshotApplyResult.Failure) {
            error(
                "Unsupported concurrent change during composition. A state object was " +
                    "modified by composition as well as being modified outside composition."
            )
            // TODO(chuckj): Consider lifting this restriction by forcing a recompose
        }
    }
```

snapshot.apply()については論文へのリンクが登場するなど面白い部分なのですが、あとで見ていくことになります。

そして、runRecomposeAndApplyChanges()に戻ってきます。

```
*Recomposer.runRecomposeAndApplyChanges()* ← Now reading
└── Recomposer.performRecompose()
    └── Recomposer.composing()
        └── CompositionImpl.recompose()
            └── Snapshot.enter(block)
                └── CompositionImpl.doCompose()
                    ├── CompositionImpl.skipCurrentGroup()
                    │   └── CompositionImpl.recomposeToGroupEnd()
                    │       ├── RecomposeScopeImpl.compose()
                    │       │   └── Content()
                    │       │       └── Node2()
                    │       ├── CompositionImpl.recordUpsAndDowns()
                    │       └── SlotReader.skipToGroupEnd()
                    └── ComposerImpl.endRoot()
                        ├── ComposerImpl.endGroup()
                        ├── Recomposer.doneComposing()
                        ├── ComposerImpl.endGroup()
                        └── ComposerImpl.recordEndRoot()
                            └── ComposerImpl.recordSlotTableOperation(forParent = false, change = { _, slots, _ -> slots.endGroup() })
                                └── ComposerImpl.realizeOperationLocation(forParent = false)

```

```kotlin
                    val modifiedValues = IdentityArraySet<Any>()
                    val alreadyComposed = IdentityArraySet<ControlledComposition>()
                    while (toRecompose.isNotEmpty()) {
                        try {
                            toRecompose.fastForEach { composition ->
                                alreadyComposed.add(composition)
                                performRecompose(composition, modifiedValues)?.let {
                                    toApply += it // toApplyにchanges = SlotTableへ適応されるべき変更が入っている
                                } // ←　ここが行われていた
                            }
                        } finally {
                            toRecompose.clear() // ←ここ ！！
                        }

```

ここで以下3つぐらい前の記事で作っていたtoRecomposeがclearされます。

https://qiita.com/takahirom/items/d2a89560f8ff2065a7c0

そして最後に以下のapply 処理が行われるのですが、それは次の記事で。

```kotlin
                    if (toApply.isNotEmpty()) {
                        changeCount++

                        // Perform apply changes
                        try {
                            toApply.fastForEach { composition ->
                                composition.applyChanges()
                            }
                        } finally {
                            toApply.clear()
                        }
                    }

                    synchronized(stateLock) {
                        deriveStateLocked()
                    }
```