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