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
     }
 
     /**