Reduce re-reading state in compositions
Uses token from RecomposeScope to avoid re-inserting state and derived state data inside composition when read happens second time within the same scope.
Test: existing composition tests
Change-Id: I5a806537e1c8f7855f0c6973af266181d741beae
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
index f0cb27f..56ef072 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
@@ -23,6 +23,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
@@ -179,6 +180,52 @@
}
}
}
+
+ @UiThreadTest
+ @Test
+ fun benchmark_10_derivedState_reads_compose() = runBlockingTestWithFrameClock {
+ val state1 by mutableStateOf(1)
+ val state2 by mutableStateOf(3)
+ val state3 by mutableStateOf(6)
+ val list by derivedStateOf {
+ List(state1 + state2 + state3) { "$it" }
+ }
+
+ measureCompose {
+ Column {
+ for (i in list.indices) {
+ Text(list[i])
+ }
+ }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun benchmark_10_derivedState_reads_recompose() = runBlockingTestWithFrameClock {
+ var state1 by mutableStateOf(1)
+ var state2 by mutableStateOf(3)
+ val state3 by mutableStateOf(6)
+ val list by derivedStateOf {
+ List(state1 + state2 + state3) { "$it" }
+ }
+
+ measureRecomposeSuspending {
+ compose {
+ Column {
+ for (i in list.indices) {
+ Text(list[i])
+ }
+ }
+ }
+ update {
+ state1 += 1
+ }
+ reset {
+ state1 = 1
+ }
+ }
+ }
}
class ColorModel(color: Color = Color.Black) {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index ddbb91e..cb4e9ee 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -709,8 +709,8 @@
observations.removeValueIf { scope ->
scope in conditionallyInvalidatedScopes || invalidated?.let { scope in it } == true
}
- cleanUpDerivedStateObservations()
conditionallyInvalidatedScopes.clear()
+ cleanUpDerivedStateObservations()
} else {
invalidated?.let {
observations.removeValueIf { scope -> scope in it }
@@ -720,8 +720,10 @@
}
private fun cleanUpDerivedStateObservations() {
- derivedStates.removeValueIf { derivedValue -> derivedValue !in observations }
- conditionallyInvalidatedScopes.removeValueIf { scope -> !scope.isConditional }
+ derivedStates.removeValueIf { derivedState -> derivedState !in observations }
+ if (conditionallyInvalidatedScopes.isNotEmpty()) {
+ conditionallyInvalidatedScopes.removeValueIf { scope -> !scope.isConditional }
+ }
}
override fun recordReadOf(value: Any) {
@@ -729,19 +731,20 @@
if (!areChildrenComposing) {
composer.currentRecomposeScope?.let {
it.used = true
- observations.add(value, it)
+ val alreadyRead = it.recordRead(value)
+ if (!alreadyRead) {
+ observations.add(value, it)
- // Record derived state dependency mapping
- if (value is DerivedState<*>) {
- derivedStates.removeScope(value)
- for (dependency in value.dependencies) {
- // skip over empty objects from dependency array
- if (dependency == null) break
- derivedStates.add(dependency, value)
+ // Record derived state dependency mapping
+ if (value is DerivedState<*>) {
+ derivedStates.removeScope(value)
+ for (dependency in value.dependencies) {
+ // skip over empty objects from dependency array
+ if (dependency == null) break
+ derivedStates.add(dependency, value)
+ }
}
}
-
- it.recordRead(value)
}
}
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
index 8f72d6ad8..38a8945 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
@@ -253,17 +253,26 @@
/**
* Track instances that were read in scope.
+ * @return whether the value was already read in scope during current pass
*/
- fun recordRead(instance: Any) {
- if (rereading) return
- (trackedInstances ?: IdentityArrayIntMap().also { trackedInstances = it })
+ fun recordRead(instance: Any): Boolean {
+ if (rereading) return false // Re-reading should force composition to update its tracking
+
+ val token = (trackedInstances ?: IdentityArrayIntMap().also { trackedInstances = it })
.add(instance, currentToken)
+
+ if (token == currentToken) {
+ return true
+ }
+
if (instance is DerivedState<*>) {
val tracked = trackedDependencies ?: IdentityArrayMap<DerivedState<*>, Any?>().also {
trackedDependencies = it
}
tracked[instance] = instance.currentValue
}
+
+ return false
}
/**