If snapshots and compose state seem like magic to ...
# compose
z
If snapshots and compose state seem like magic to you, I recently gave a talk you might be interested in. Links in 🧵
👌 3
🧙 1
🔝 5
🙏 14
👏🏽 1
👏🏼 1
👏 4
m
I was there. Good talk.
m
Thanks! Very interesting. I didn't quite follow why Compose requires full blown MVCC, given that whilst user input is being processed the UI thread is blocked. You mention at the start that composition requires a consistent snapshot, but I had understood that nothing is changing during composition anyway. Is that not the case?
z
Not necessarily. Other threads are still running and they can still change state at any time. Eg a network response comes back on a background thread and updates some state object with the response data.
m
Hmm, is that allowed? I can write a mutableState from any thread at any time? The docs are very coroutine heavy and don't seem to discuss thread safety much at all. That would certainly simplify some things in my app where I need to use non-Kotlin/blocking libraries.
If you can do that it'd definitely be worth talking about it in the docs, as that's a rather unusual capability for a GUI toolkit. I can't think of any that are even slightly thread safe except, maybe, the old Motif library.
z
Yep it’s very much allowed, because snapshots use MVCC 🔁 😁
Other state holders like MutableStateFlow also allow updating from any thread, but they’re harder to use correctly when doing so.
It would be cool if the docs went a little deeper into how to use actual snapshots effectively to work with state from multiple threads, one of us should file a feature request.
m
Yes, code samples/docs that discuss this in more detail would be great. I'm used to classical GUI toolkits so I tend to think in terms of doing anything slow on a background thread and then invoking a lambda on the UI thread to update things. In Compose I guess I'd instead take a snapshot, then start a thread and pass the snapshot to the thread so it can read the UI state consistently even if the user is continuing to type in the background, and then when done I'd make changes to that snapshot and try to "apply" it, converting conflicts into some notification to the user that the server-side state has changed.
I don't know how to file a feature request for Compose I'm afraid.
(I'm only really interested in Compose for Desktop at the moment...)
z
There’s a link to the tracker in this channel’s subject.
You could take the snapshot in the main thread if you need to synchronize, otherwise you can just use
Snapshot.withMutableState
on your background thread.
m
Ah, great, I see it.
z
I don’t even know if there are separate docs for CfD, but at any rate these docs would be platform agnostic.
z
Oh, one important detail to note is that snapshots only provide thread safety between different snapshots. If two threads are working in the same snapshot, you need to do your own synchronization. It’s still safe to use the same snapshot from multiple threads (eg create on one then pass to another to actually run code in), but those threads access to state will not be isolated/consistent/etc while running code concurrently in that snapshot.
m
Sure. That is reasonable.
I'm exploring how to integrate Compose directly with JDBC connections, or libraries that wrap JDBC. There seems to be stuff available about Room, but for actual remote databases without HTTP stacks in the picture it's a bit less obvious especially once stability gets into the picture. Lots of different possible approaches and it'll take some experimentation to figure out what's best, I guess.
So to confirm I understood right - the reason background threads can set mutable states at will without synchronization, is because the UI thread is always operating on a snapshot, and thus the "current" / HEAD state that will be written to by the background threads is the one that isn't being read by anything except yourself.
z
Basically yea. Technically only code ran by the compose runtime will be in a snapshot on the main thread though - if you launch a coroutine on the Main dispatcher, it won’t get its own snapshot, it will use the global snapshot like any other thread, unless you explicitly specify otherwise.
m
I see. By "launch a coroutine" does that include stuff like LaunchedEffect / SideEffect?
z
LaunchedEffect yes. SideEffect does not have anything to do with coroutines and I’m actually not sure off the top of my head if they’re run in snapshots - will find out and get back to you.
m
Is that an explicit design decision or more of a missing feature? It feels like it could be a source of bugs if LaunchedEffects/threads can race. But I suppose, it is expected that a LaunchedEffect writes into the current snapshot as otherwise maybe it couldn't work.
z
I think it’s at least a little bit intentional, although I don’t have first-hand knowledge ( @Adam Powell could probably be more authoritative). Compose can never ensure everything runs in a dedicated non-global snapshot - any code can register callbacks with code that doesn’t know about snapshots, so eg any time you pass an event handler lambda to something that doesn’t specify snapshot behavior it could potentially be running in a dedicated snapshot or not. So it’s more consistent in a way to have the general rule: if you’re running code outside of composition that needs a snapshot, be explicit about it and create your own. The way snapshots nest mean you can do this regardless of whether that happens to be nested in the global snapshot or something else. Also, there might be performance reasons. While snapshots are pretty lightweight, they’re not free, and so creating a snapshot for every little effect would be doing redundant work in probably the majority of cases (eg where you’re only writing to a single state and don’t actually need a dedicated snapshot).
m
Yeah, fair enough. Thanks.
z
Following up on my earlier message, I’m not seeing any snapshots around running side effects.
a
yeah they're performed in the global snapshot today. We've talked about having a meta-snapshot encompass the whole frame (recomposition -> layout -> drawing) which would necessarily include `SideEffect`s and `DisposableEffect`s and any other
RememberObserver
callbacks, and we've played around with some ideas around CoroutineContext elements that would take a snapshot on resume and commit it on suspend, effectively forming a better alternative to the global snapshot for anything running via the composition effect context. The latter has some performance implications to consider, as well as we don't have the api hooks in kotlinx.coroutines to handle the case where committing that kind of implicit snapshot might fail due to a data merge error
ideally in that case we'd have the continuation resume with an exception, but we don't have access to the outgoing continuation from the thread context element to do that
so expect this to get tighter over time and the need for any possibly-paranoid
Snapshot.withMutableSnapshot {}
wrappers and such to go away eventually
if you want to try out what that would look like though, you can work up your own thread context element using the experimental apis here: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]mpose/runtime/snapshots/Snapshot.kt;l=162?q=snapshot%20unsafe that's what they're there for