Oliver.O
11/17/2022, 3:00 PMSnapshot
multi-threading: "_Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied_". Details in 🧵.Oliver.O
11/17/2022, 3:01 PMGlobalSnapshotManager
has its own thread.
Also, each frontend continuously receives asynchronous updates to MutableState
from the backend.
class TestFrontend {
// ...
fun launchIn(testScope: CoroutineScope) {
GlobalSnapshotManager.ensureStarted()
job = (testScope + Executors.newSingleThreadExecutor().asCoroutineDispatcher()).launch {
withContext(SixtyFpsMonotonicFrameClock) {
val viewTree = ViewNode()
val recomposer = Recomposer(coroutineContext)
val composition = Composition(ViewNode.Applier(viewTree), recomposer)
try {
composition.setContent {
FrontendView(/* ... */) // uses LaunchedEffect to run backend communications
}
recomposer.runRecomposeAndApplyChanges()
} finally {
composition.dispose()
}
}
}
}
// ...
}
So far, the setup works on 4 CPU cores with 20 frontends, sometimes with 200 (depending on JVM warm-up), but fails with 2000: "Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied"
I have found this explanation on thread-local snapshots: https://kotlinlang.slack.com/archives/CJLTWPH7S/p1613583271168600?thread_ts=1613581738.163700&cid=CJLTWPH7S
My questions are:
1. Is the Snapshot system capable of supporting multiple `Recomposer`s as long as each of them runs on its own "main" thread?
2. Do asynchronous updates to MutableState
have to run on the corresponding "main" thread?
3. What else should I look for when structuring coroutines and thread confinement?Oliver.O
11/17/2022, 4:01 PMMutableState
onto the respective frontend's "main" thread, my test ran with 2000 concurrent frontends without crashing.Oliver.O
11/17/2022, 10:31 PMMutableState
updates work from threads other than the respective "main". Running a top-level Composable like this works with 20 concurrent frontends, but produces the read error above with 2000 frontends:
@Composable
fun FrontendView(id: Int, testApplication: TestApplication) {
val service = remember { TestFrontendService(id, testApplication) } // initializes MutableState
LaunchedEffect(Unit) {
withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
service.launchIn(this) // changes MutableState
}
}
// Composables accessing MutableState from service
}
Removing withContext(<http://Dispatchers.IO|Dispatchers.IO>)
makes it work.Oliver.O
11/17/2022, 10:46 PMThreadLocal
which would allow coroutine-style concurrency, eliminating the need to create thousands of threads.
Any ideas?Zach Klippenstein (he/him) [MOD]
11/17/2022, 10:56 PMZach Klippenstein (he/him) [MOD]
11/17/2022, 11:07 PMOliver.O
11/17/2022, 11:12 PMGlobalSnapshotManager
like so:
object GlobalSnapshotManager {
private val started = AtomicBoolean(false)
fun ensureStarted() {
if (started.compareAndSet(false, true)) {
val snapshotWriteObservation = Channel<Unit>(Channel.CONFLATED)
CoroutineScope(singleThreadDispatcher()).launch {
snapshotWriteObservation.consumeEach {
Snapshot.sendApplyNotifications()
}
}
Snapshot.registerGlobalWriteObserver {
logger.debug("write observed: $it")
snapshotWriteObservation.trySend(Unit)
}
}
}
}
Oliver.O
11/17/2022, 11:17 PMConsider a network call made from aon theLaunchedEffect
dispatcher that updates aIO
with the response. This code mutates a snapshot state value, without a snapshot, in a background thread (remember: Compose only wraps compositions in snapshots, and effects are executed outside of compositions).MutableState
Zach Klippenstein (he/him) [MOD]
11/17/2022, 11:21 PMOliver.O
11/17/2022, 11:27 PM