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