https://kotlinlang.org logo
a

Arsen

10/30/2021, 10:56 PM
Have question about "Snapshot system" in multithreaded environment: Is there a way to read global snapshot's state value to make decission of "apply() or dispose() previously taken mutable snapshot" in a sync manner (i.e. lock on mutex to avoid "advanceGlobalSnapshot" between "check statement" and mySnapshot.apply call). For simple cases, there is a SnapshotMutationPolicy to make decission that depends on "previous" value, but let's assume that for some reason we need to check value of another "State object". Consider following example:
Copy code
val catsState = mutableStateListOf<Cat>()
val sortState = mutableStateOf("ASC") // [ASC, DESC]

// ------ Worker Thread ---------
val snapshot = Snapshot.takeMutableSnapshot()

val sort = sortState.value
val cats = fetchCoolCatsFromNetwork(sort)

snapshot.enter {            
    catsState.clear() 
    catsState.addAll(cats) 
    
    Snapshot.lockOnGlobalSnapshot { // Does it possible?
        val sortOfActualGlobalSnapshot = sortState.value
        if(sortOfActualGlobalSnapshot == sort) {
            snapshot.apply()
        } else {
            snapshot.dispose()
        }
    }
}
P.S. State hoisted to ViewModel
🧵 2
z

Zach Klippenstein (he/him) [MOD]

11/01/2021, 5:17 PM
I don't think the snapshot system supports this. I think you could store your cats list and the sort order in the same object then write a mutation policy for that.
a

Arsen

11/01/2021, 6:51 PM
Thansk for the answer. Am I right, that absent of this feature is design decision(encapsulation) rather than technical issue? No doubt, encapsulation is good principle, though i would prefer flexebility on how to compose states (e.g. towards low coupling).
z

Zach Klippenstein (he/him) [MOD]

11/01/2021, 10:14 PM
Chuck would be the person to ask, but he’s on vacation atm. I’ll try to follow up later
👌 1
c

Chuck Jazdzewski [G]

11/02/2021, 5:11 PM
As @Zach Klippenstein (he/him) [MOD] points out, you can use a mutation policy on a
Pair
of values, or similar, to ensure they are consistent with each other. Using the mutation policy is the correct mechanism as it is called during
apply
and works with nested snapshots (which
lockOnGlobalSnapshot
wouldn't) so it can be used to ensure that order of the sort requested is consistent with the order of the sort retrieved. To accomplish this, for example, the mutation policy for a
Pair
would allow states to merge if the
first
fields are identical (the sort order) and the
second
field goes from
null
to a value (the collection). It would then return the record with the non-null value. If the current sort order is not consistent with the collection then the apply fails. The worker thread can then check apply result and re-query the server for a new collection if the
apply
fails. I am on vacation now or I would provide an example of this. I am back on Monday. The snapshot system guarantees snapshot consistency not serialization. A note about this is in the internal comment for the
apply
method and references the following paper that describes a crossing write (the technical description of what your code is an example of) https://arxiv.org/pdf/1412.2324.pdf. Code that requires crossing-write consistency (i.e. serialization) is not currently supported as supporting them would be rather expensive. Snapshot consistency requires consistency requirements be expressed in terms of consistent writes which using a
Pair
as described above does. A quibble with your example: a snapshot should always have its
dispose()
called. Disposing a snapshot that has not been applied will discard any changes in the snapshot; however, an applied snapshot should also be disposed. Not disposing of a snapshot can be a serious memory leak as it might cause accumulation of state records that cannot be reused. This typically doesn't happen in the example you have (since it is a child of the global snapshot) but nested snapshots keep their parents in, at minimum, a zombie state if they are not disposed.
👍 1
a

Arsen

11/30/2021, 1:59 AM
@Chuck Jazdzewski [G] Thanks for answer. I have read mentioned paper + chapter about Snapshots from "Compose Internals" book. Now I have few more questions: 1. Paper about BOHM is well written (understandable). Is there paper/article that describes Compose's Snapshot System in same fashion? 2. Does Compose' Snapshots applies changes in a batch as BOHM or one by one? 3. Can you provide some details about "expensiveness" of supporting serializability? Would it be still expensive for NonNestable Snapshot (which applies directly to global one). 4. Is it expensive to fetch actual(global) values for list of some StateObjects (Read-Set) inside Snapshot#apply function. (for Non-Nested Snapshot). 5. Is it expensive to resolve "one way write dependency" instead of crossing one? i.e. be able to discard appling "list of cats" if global "sort" differs from local, at the same time(within another transaction) ignore any validations when writing to "sort" field itself. Actions like "user changed sort" ussualy hides List at all, providing Loading indicator and emmiting SideEffect to fetch list of cats asynchronously.
c

Chuck Jazdzewski [G]

11/30/2021, 2:10 AM
(1): https://dev.to/zachklipp/introduction-to-the-compose-snapshot-system-19cn if you have not seen this already. Zach does a good job of explaining things. (2): Snapshots are applied atomically. (3): In requires installing a read observer which observes all reads and records them. As reads much more frequent than writes, this tracking can get expensive. Currently I only track writes except when explicitly observing such as during composition, layout and draw. (4): The record lists are usually short (1-2 records per objects) so finding the correct one is very fast. (5): Both require tracking reads which is the expensive part. I will give this some more thought. I believe if could have something like a
takeSerializableSnapshot()
that tracks reads without slowing down the cases that don't need this serializable consistency.
👍 1