Avoid calculating readableHash in DerivedState if snapshot wasn't modified
Allows to skip readableHash check, which is doing extra work in cases when no writes have occurred between derived state reads.
Test: DerivedSnapshotStateTests
(cherry picked from https://android-review.googlesource.com/q/commit:40c07baebcfb10eafb94cb0bfcd83699d2cbd90e)
Merged-In: Iadb225542a94e5df2a59021696ad151eefa930c8
Change-Id: Iadb225542a94e5df2a59021696ad151eefa930c8
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 a131262..538ada1 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
@@ -72,6 +72,9 @@
val Unset = Any()
}
+ var validSnapshotId: Int = 0
+ var validSnapshotWriteCount: Int = 0
+
var dependencies: IdentityArrayMap<StateObject, Int>? = null
var result: Any? = Unset
var resultHash: Int = 0
@@ -86,8 +89,22 @@
override fun create(): StateRecord = ResultRecord<T>()
- fun isValid(derivedState: DerivedState<*>, snapshot: Snapshot): Boolean =
- result !== Unset && resultHash == readableHash(derivedState, snapshot)
+ fun isValid(derivedState: DerivedState<*>, snapshot: Snapshot): Boolean {
+ val snapshotChanged = sync {
+ validSnapshotId != snapshot.id || validSnapshotWriteCount != snapshot.writeCount
+ }
+ val isValid = result !== Unset &&
+ (!snapshotChanged || resultHash == readableHash(derivedState, snapshot))
+
+ if (isValid && snapshotChanged) {
+ sync {
+ validSnapshotId = snapshot.id
+ validSnapshotWriteCount = snapshot.writeCount
+ }
+ }
+
+ return isValid
+ }
fun readableHash(derivedState: DerivedState<*>, snapshot: Snapshot): Int {
var hash = 7
@@ -186,11 +203,15 @@
) {
readable.dependencies = newDependencies
readable.resultHash = readable.readableHash(this, currentSnapshot)
+ readable.validSnapshotId = snapshot.id
+ readable.validSnapshotWriteCount = snapshot.writeCount
readable
} else {
val writable = first.newWritableRecord(this, currentSnapshot)
writable.dependencies = newDependencies
writable.resultHash = writable.readableHash(this, currentSnapshot)
+ writable.validSnapshotId = snapshot.id
+ writable.validSnapshotWriteCount = snapshot.writeCount
writable.result = result
writable
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index 1df196c..12f2471 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -63,6 +63,13 @@
open var id: Int = id
internal set
+ internal open var writeCount: Int
+ get() = 0
+ @Suppress("UNUSED_PARAMETER")
+ set(value) {
+ error("Updating write count is not supported for this snapshot")
+ }
+
/**
* The root snapshot for this snapshot. For non-nested snapshots this is always `this`. For
* nested snapshot it is the parent's [root].
@@ -1021,6 +1028,8 @@
(modified ?: IdentityArraySet<StateObject>().also { modified = it }).add(state)
}
+ override var writeCount: Int = 0
+
override var modified: IdentityArraySet<StateObject>? = null
internal var merged: List<StateObject>? = null
@@ -1501,6 +1510,12 @@
@Suppress("UNUSED_PARAMETER")
set(value) = unsupported()
+ override var writeCount: Int
+ get() = currentSnapshot.writeCount
+ set(value) {
+ currentSnapshot.writeCount = value
+ }
+
override val readOnly: Boolean
get() = currentSnapshot.readOnly
@@ -2119,6 +2134,7 @@
@PublishedApi
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
+ snapshot.writeCount += 1
snapshot.writeObserver?.invoke(state)
}
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
index 3ec3fb7..5aba102 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
@@ -1165,6 +1165,51 @@
assertEquals(1, usedRecords(state3 as StateObject))
}
+ @Test
+ fun testWriteCount() {
+ val state = mutableStateOf<Int>(0)
+ val writtenStates = mutableListOf<Any>()
+ val snapshot = takeMutableSnapshot { write ->
+ writtenStates.add(write)
+ }
+ try {
+ snapshot.enter {
+ assertEquals(0, writtenStates.size)
+ assertEquals(0, snapshot.writeCount)
+ state.value = 2
+ assertEquals(1, writtenStates.size)
+ assertEquals(1, snapshot.writeCount)
+ }
+ } finally {
+ snapshot.dispose()
+ }
+ assertEquals(1, writtenStates.size)
+ assertEquals(state, writtenStates[0])
+ assertEquals(0, current.writeCount)
+ }
+
+ @Test
+ fun testTransparentSnapshotWriteCount() {
+ val state = mutableStateOf<Int>(0)
+ val transparentSnapshot = TransparentObserverMutableSnapshot(
+ parentSnapshot = currentSnapshot() as? MutableSnapshot,
+ specifiedReadObserver = null,
+ specifiedWriteObserver = null,
+ mergeParentObservers = false,
+ ownsParentSnapshot = false
+ )
+ try {
+ transparentSnapshot.enter {
+ assertEquals(0, transparentSnapshot.writeCount)
+ state.value = 2
+ assertEquals(1, transparentSnapshot.writeCount)
+ }
+ } finally {
+ transparentSnapshot.dispose()
+ }
+ assertEquals(1, current.writeCount)
+ }
+
private fun usedRecords(state: StateObject): Int {
var used = 0
var current: StateRecord? = state.firstStateRecord