Arsen

    Arsen

    10 months ago
    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:
    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
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    10 months ago
    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.
    Arsen

    Arsen

    10 months ago
    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).
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    10 months ago
    Chuck would be the person to ask, but he’s on vacation atm. I’ll try to follow up later
    Chuck Jazdzewski [G]

    Chuck Jazdzewski [G]

    10 months ago
    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.
    Arsen

    Arsen

    9 months ago
    @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.
    Chuck Jazdzewski [G]

    Chuck Jazdzewski [G]

    9 months ago
    (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.