Fix potential deadlock in snapshot list and map

Adding content to a SnapshotStateList or SnapshotStateMap can
encounter a deadlock if the modification is concurrent with
a direct write to the state record. This was made signficantly
more likely to be encountered  with the changes introduced by
93fcae828b that uses direct writes to release unused records.

The locks are now ordered in that a snapshot lock is never
attempted to be taken when a map or list lock is held.

Fixes: 269402895
Test: ./gradlew :composer:r:r:tDUT
(cherry picked from https://android-review.googlesource.com/q/commit:5e7f64d97283fb2be1db3f0d5c6dcd1a55c1a425)
Merged-In: Ie8b87caecabc88f6a6dd610805d964aed3489b85
Change-Id: Ie8b87caecabc88f6a6dd610805d964aed3489b85
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt
index cf562db..2ae56e8 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt
@@ -106,8 +106,8 @@
 
     override fun addAll(elements: Collection<T>) = conditionalUpdate { it.addAll(elements) }
     override fun clear() {
-        synchronized(sync) {
-            writable {
+        writable {
+            synchronized(sync) {
                 list = persistentListOf()
                 modification++
             }
@@ -167,8 +167,8 @@
             val builder = oldList!!.builder()
             result = block(builder)
             val newList = builder.build()
-            if (newList == oldList || synchronized(sync) {
-                writable {
+            if (newList == oldList || writable {
+                 synchronized(sync) {
                     if (modification == currentModification) {
                         list = newList
                         modification++
@@ -201,8 +201,8 @@
                     result = false
                     break
                 }
-                if (synchronized(sync) {
-                    writable {
+                if (writable {
+                    synchronized(sync) {
                         if (modification == currentModification) {
                             list = newList
                             modification++
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateMap.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateMap.kt
index aceccb7..9b77e7ce 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateMap.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateMap.kt
@@ -137,8 +137,8 @@
             val builder = oldMap!!.builder()
             result = block(builder)
             val newMap = builder.build()
-            if (newMap == oldMap || synchronized(sync) {
-                writable {
+            if (newMap == oldMap || writable {
+                synchronized(sync) {
                     if (modification == currentModification) {
                         map = newMap
                         modification++
@@ -153,8 +153,8 @@
 
     private inline fun update(block: (PersistentMap<K, V>) -> PersistentMap<K, V>) = withCurrent {
         val newMap = block(map)
-        if (newMap !== map) synchronized(sync) {
-            writable {
+        if (newMap !== map) writable {
+            synchronized(sync) {
                 map = newMap
                 modification++
             }
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateListTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateListTests.kt
index d03fa3f..41be7ba 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateListTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateListTests.kt
@@ -27,6 +27,9 @@
 import kotlin.test.assertFailsWith
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.consumeEach
+import kotlinx.test.IgnoreJsTarget
 
 class SnapshotStateListTests {
     @Test
@@ -567,7 +570,9 @@
         }
     }
 
-    @Test @OptIn(ExperimentalCoroutinesApi::class)
+    @Test(timeout = 10_000)
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @IgnoreJsTarget // Not relevant in a single threaded environment
     fun concurrentGlobalModifications_addAll(): Unit = runTest {
         repeat(100) {
             val list = mutableStateListOf<Int>()
@@ -587,6 +592,104 @@
         }
     }
 
+    @Test(timeout = 5000)
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @IgnoreJsTarget // Not relevant in a single threaded environment
+    fun concurrentMixingWriteApply_add(): Unit = runTest {
+        repeat(1000) {
+            val lists = Array(100) { mutableStateListOf<Int>() }.toList()
+            val channel = Channel<Unit>(Channel.CONFLATED)
+            coroutineScope {
+                // Launch mutator
+                launch(Dispatchers.Default) {
+                    repeat(100) { index ->
+                        lists.fastForEach { list ->
+                            list.add(index)
+                        }
+
+                        // Simulate the write observer
+                        channel.trySend(Unit)
+                    }
+                    channel.close()
+                }
+
+                // Simulate the global snapshot manager
+                launch(Dispatchers.Default) {
+                    channel.consumeEach {
+                        Snapshot.notifyObjectsInitialized()
+                    }
+                }
+            }
+        }
+        // Should only get here if the above doesn't deadlock.
+    }
+
+    @Test(timeout = 5000)
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @IgnoreJsTarget // Not relevant in a single threaded environment
+    fun concurrentMixingWriteApply_addAll_clear(): Unit = runTest {
+        repeat(100) {
+            val lists = Array(100) { mutableStateListOf<Int>() }.toList()
+            val data = Array(100) { index -> index }.toList()
+            val channel = Channel<Unit>(Channel.CONFLATED)
+            coroutineScope {
+                // Launch mutator
+                launch(Dispatchers.Default) {
+                    repeat(100) {
+                        lists.fastForEach { list ->
+                            list.addAll(data)
+                            list.clear()
+                        }
+                        // Simulate the write observer
+                        channel.trySend(Unit)
+                    }
+                    channel.close()
+                }
+
+                // Simulate the global snapshot manager
+                launch(Dispatchers.Default) {
+                    channel.consumeEach {
+                        Snapshot.notifyObjectsInitialized()
+                    }
+                }
+            }
+        }
+        // Should only get here if the above doesn't deadlock.
+    }
+
+    @Test(timeout = 5000)
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @IgnoreJsTarget // Not relevant in a single threaded environment
+    fun concurrentMixingWriteApply_addAll_removeRange(): Unit = runTest {
+        repeat(10) {
+            val lists = Array(100) { mutableStateListOf<Int>() }.toList()
+            val data = Array(100) { index -> index }.toList()
+            val channel = Channel<Unit>(Channel.CONFLATED)
+            coroutineScope {
+                // Launch mutator
+                launch(Dispatchers.Default) {
+                    repeat(100) {
+                        lists.fastForEach { list ->
+                            list.addAll(data)
+                            list.removeRange(0, data.size)
+                        }
+                        // Simulate the write observer
+                        channel.trySend(Unit)
+                    }
+                    channel.close()
+                }
+
+                // Simulate the global snapshot manager
+                launch(Dispatchers.Default) {
+                    channel.consumeEach {
+                        Snapshot.notifyObjectsInitialized()
+                    }
+                }
+            }
+        }
+        // Should only get here if the above doesn't deadlock.
+    }
+
     @Test
     fun modificationAcrossSnapshots() {
         val list = mutableStateListOf<Int>()
@@ -628,8 +731,8 @@
 
     private fun <T> expected(expected: List<T>, actual: List<T>) {
         assertEquals(expected.size, actual.size)
-        (0 until expected.size).forEach {
+        expected.indices.forEach {
             assertEquals(expected[it], actual[it])
         }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateMapTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateMapTests.kt
index 70d93d7..c816968 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateMapTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateMapTests.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:Suppress("ConvertArgumentToSet")
+
 package androidx.compose.runtime.snapshots
 
 import androidx.compose.runtime.mutableStateMapOf
@@ -27,6 +29,8 @@
 import kotlin.test.assertFailsWith
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.consumeEach
 import kotlinx.test.IgnoreJsTarget
 
 class SnapshotStateMapTests {
@@ -565,6 +569,71 @@
         }
     }
 
+    @Test(timeout = 5000)
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @IgnoreJsTarget // Not relevant in a single threaded environment
+    fun concurrentMixingWriteApply_set(): Unit = runTest {
+        repeat(100) {
+            val maps = Array(100) { mutableStateMapOf<Int, Int>() }.toList()
+            val channel = Channel<Unit>(Channel.CONFLATED)
+            coroutineScope {
+                // Launch mutator
+                launch(Dispatchers.Default) {
+                    repeat(100) { index ->
+                        maps.fastForEach { map ->
+                            map[index] = index
+                        }
+
+                        // Simulate the write observer
+                        channel.trySend(Unit)
+                    }
+                    channel.close()
+                }
+
+                // Simulate the global snapshot manager
+                launch(Dispatchers.Default) {
+                    channel.consumeEach {
+                        Snapshot.notifyObjectsInitialized()
+                    }
+                }
+            }
+        }
+        // Should only get here if the above doesn't deadlock.
+    }
+
+    @Test(timeout = 5000)
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @IgnoreJsTarget // Not relevant in a single threaded environment
+    fun concurrentMixingWriteApply_clear(): Unit = runTest {
+        repeat(100) {
+            val maps = Array(100) { mutableStateMapOf<Int, Int>() }.toList()
+            val channel = Channel<Unit>(Channel.CONFLATED)
+            coroutineScope {
+                // Launch mutator
+                launch(Dispatchers.Default) {
+                    repeat(100) {
+                        maps.fastForEach { map ->
+                            repeat(10) { index -> map[index] = index }
+                            map.clear()
+                        }
+
+                        // Simulate the write observer
+                        channel.trySend(Unit)
+                    }
+                    channel.close()
+                }
+
+                // Simulate the global snapshot manager
+                launch(Dispatchers.Default) {
+                    channel.consumeEach {
+                        Snapshot.notifyObjectsInitialized()
+                    }
+                }
+            }
+        }
+        // Should only get here if the above doesn't deadlock.
+    }
+
     private fun validateRead(
         initialMap: MutableMap<Int, Float> = defaultMap(),
         block: (Map<Int, Float>, Map<Int, Float>) -> Unit