Use IdentityArrayMap instead of HashMap inside DerivedState

Updates DerivedState implementation to use IdentityArrayMap for faster iteration. This change exposes internal `keys` array of dependency map with potentially empty elements, which we have to skip over when reading.

Changes `DerivedState.currentValue` to only return the value without forcing record of transitive dependency reads.

Change-Id: Ica516bba042781f03e21fda7628b9788f9e018bd
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 1b652e8..65a96b1 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
@@ -710,7 +710,9 @@
                 // Record derived state dependency mapping
                 if (value is DerivedState<*>) {
                     derivedStates.removeScope(value)
-                    value.dependencies.forEach { dependency ->
+                    for (dependency in value.dependencies) {
+                        // skip over empty objects from dependency array
+                        if (dependency == null) break
                         derivedStates.add(dependency, value)
                     }
                 }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt
index ee172c5..4563c63 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt
@@ -18,6 +18,7 @@
 @file:JvmMultifileClass
 package androidx.compose.runtime
 
+import androidx.compose.runtime.collection.IdentityArrayMap
 import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentList
 import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentListOf
 import androidx.compose.runtime.snapshots.Snapshot
@@ -50,7 +51,7 @@
      * The [dependencies] list can be used to determine when a [StateObject] appears in the apply
      * observer set, if the state could affect value of this derived state.
      */
-    val dependencies: Set<StateObject>
+    val dependencies: Array<Any?>
 
     /**
      * Mutation policy that controls how changes are handled after state dependencies update.
@@ -77,7 +78,7 @@
             val Unset = Any()
         }
 
-        var dependencies: HashMap<StateObject, Int>? = null
+        var dependencies: IdentityArrayMap<StateObject, Int>? = null
         var result: Any? = Unset
         var resultHash: Int = 0
 
@@ -99,9 +100,9 @@
             val dependencies = sync { dependencies }
             if (dependencies != null) {
                 notifyObservers(derivedState) {
-                    for ((stateObject, readLevel) in dependencies.entries) {
+                    dependencies.forEach { stateObject, readLevel ->
                         if (readLevel != 1) {
-                            continue
+                            return@forEach
                         }
 
                         if (stateObject is DerivedSnapshotState<*>) {
@@ -140,9 +141,9 @@
             // for correct invalidation later
             if (forceDependencyReads) {
                 notifyObservers(this) {
-                    val dependencies = readable.dependencies ?: emptyMap()
+                    val dependencies = readable.dependencies
                     val invalidationNestedLevel = calculationBlockNestedLevel.get() ?: 0
-                    for ((dependency, nestedLevel) in dependencies) {
+                    dependencies?.forEach { dependency, nestedLevel ->
                         calculationBlockNestedLevel.set(nestedLevel + invalidationNestedLevel)
                         snapshot.readObserver?.invoke(dependency)
                     }
@@ -153,7 +154,7 @@
         }
         val nestedCalculationLevel = calculationBlockNestedLevel.get() ?: 0
 
-        val newDependencies = HashMap<StateObject, Int>()
+        val newDependencies = IdentityArrayMap<StateObject, Int>()
         val result = notifyObservers(this) {
             calculationBlockNestedLevel.set(nestedCalculationLevel + 1)
 
@@ -218,18 +219,23 @@
             // value is used instead which doesn't notify. This allow the read observer to read the
             // value and only update the cache once.
             Snapshot.current.readObserver?.invoke(this)
-            return currentValue
+            return first.withCurrent {
+                @Suppress("UNCHECKED_CAST")
+                currentRecord(it, Snapshot.current, true, calculation).result as T
+            }
         }
 
     override val currentValue: T
         get() = first.withCurrent {
             @Suppress("UNCHECKED_CAST")
-            currentRecord(it, Snapshot.current, true, calculation).result as T
+            currentRecord(it, Snapshot.current, false, calculation).result as T
         }
 
-    override val dependencies: Set<StateObject>
+    override val dependencies: Array<Any?>
         get() = first.withCurrent {
-            currentRecord(it, Snapshot.current, false, calculation).dependencies?.keys ?: emptySet()
+            val record = currentRecord(it, Snapshot.current, false, calculation)
+            @Suppress("UNCHECKED_CAST")
+            record.dependencies?.keys ?: emptyArray()
         }
 
     override fun toString(): String = first.withCurrent {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
index 55240ef..3a7cf06 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
@@ -279,6 +279,8 @@
                 dependencyToDerivedStates.removeScope(value)
                 val dependencies = value.dependencies
                 for (dependency in dependencies) {
+                    // skip over dependency array
+                    if (dependency == null) break
                     dependencyToDerivedStates.add(dependency, value)
                 }
                 derivedStateToValue[value] = value.currentValue