diff options
| author | 2025-01-03 14:42:16 -0800 | |
|---|---|---|
| committer | 2025-01-03 14:42:16 -0800 | |
| commit | 5955304a6bfe2537dd04e82b450c22dca62fe20f (patch) | |
| tree | 461e9c67029b71833898ca54d6e8630b31421885 | |
| parent | 67c7f485a9a0369b054a15a6b060894bf82bb105 (diff) | |
| parent | cdff97f1f5b958a8d0a3977b3d62dcd12a89b49e (diff) | |
Merge changes from topic "kairos-rename" into main
* changes:
[kairos] rename many APIs
[kairos] remove most internal usage of `suspend fun`
49 files changed, 5322 insertions, 5513 deletions
diff --git a/packages/SystemUI/utils/kairos/Android.bp b/packages/SystemUI/utils/kairos/Android.bp index 1442591eab99..e10de9978252 100644 --- a/packages/SystemUI/utils/kairos/Android.bp +++ b/packages/SystemUI/utils/kairos/Android.bp @@ -22,7 +22,7 @@ package { java_library { name: "kairos", host_supported: true, - kotlincflags: ["-opt-in=com.android.systemui.kairos.ExperimentalFrpApi"], + kotlincflags: ["-opt-in=com.android.systemui.kairos.ExperimentalKairosApi"], srcs: ["src/**/*.kt"], static_libs: [ "kotlin-stdlib", @@ -32,6 +32,7 @@ java_library { java_test { name: "kairos-test", + kotlincflags: ["-opt-in=com.android.systemui.kairos.ExperimentalKairosApi"], optimize: { enabled: false, }, diff --git a/packages/SystemUI/utils/kairos/README.md b/packages/SystemUI/utils/kairos/README.md index 85f622ca05f3..5174c45a8d82 100644 --- a/packages/SystemUI/utils/kairos/README.md +++ b/packages/SystemUI/utils/kairos/README.md @@ -22,22 +22,21 @@ you can view the semantics for `Kairos` [here](docs/semantics.md). ## Usage -First, stand up a new `FrpNetwork`. All reactive events and state is kept +First, stand up a new `KairosNetwork`. All reactive events and state is kept consistent within a single network. ``` kotlin val coroutineScope: CoroutineScope = ... -val frpNetwork = coroutineScope.newFrpNetwork() +val network = coroutineScope.launchKairosNetwork() ``` -You can use the `FrpNetwork` to stand-up a network of reactive events and state. -Events are modeled with `TFlow` (short for "transactional flow"), and state -`TState` (short for "transactional state"). +You can use the `KairosNetwork` to stand-up a network of reactive events and +state. Events are modeled with `Events`, and states with `State`. ``` kotlin -suspend fun activate(network: FrpNetwork) { +suspend fun activate(network: KairosNetwork) { network.activateSpec { - val input = network.mutableTFlow<Unit>() + val input = network.mutableEvents<Unit>() // Launch a long-running side-effect that emits to the network // every second. launchEffect { @@ -47,7 +46,7 @@ suspend fun activate(network: FrpNetwork) { } } // Accumulate state - val count: TState<Int> = input.fold { _, i -> i + 1 } + val count: State<Int> = input.foldState { _, i -> i + 1 } // Observe events to perform side-effects in reaction to them input.observe { println("Got event ${count.sample()} at time: ${System.currentTimeMillis()}") @@ -56,7 +55,7 @@ suspend fun activate(network: FrpNetwork) { } ``` -`FrpNetwork.activateSpec` will suspend indefinitely; cancelling the invocation +`KairosNetwork.activateSpec` will suspend indefinitely; cancelling the invocation will tear-down all effects and obervers running within the lambda. ## Resources diff --git a/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md b/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md index 9f7fd022f019..afe64377676d 100644 --- a/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md +++ b/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md @@ -2,117 +2,116 @@ ## Key differences -* Kairos evaluates all events (`TFlow` emissions + observers) in a transaction. +* Kairos evaluates all events (`Events` emissions + observers) in a transaction. -* Kairos splits `Flow` APIs into two distinct types: `TFlow` and `TState` +* Kairos splits `Flow` APIs into two distinct types: `Events` and `State` - * `TFlow` is roughly equivalent to `SharedFlow` w/ a replay cache that + * `Events` is roughly equivalent to `SharedFlow` w/ a replay cache that exists for the duration of the current Kairos transaction and shared with `SharingStarted.WhileSubscribed()` - * `TState` is roughly equivalent to `StateFlow` shared with + * `State` is roughly equivalent to `StateFlow` shared with `SharingStarted.Eagerly`, but the current value can only be queried within a Kairos transaction, and the value is only updated at the end of the transaction * Kairos further divides `Flow` APIs based on how they internally use state: - * **FrpTransactionScope:** APIs that internally query some state need to be + * **TransactionScope:** APIs that internally query some state need to be performed within an Kairos transaction * this scope is available from the other scopes, and from most lambdas passed to other Kairos APIs - * **FrpStateScope:** APIs that internally accumulate state in reaction to - events need to be performed within an FRP State scope (akin to a - `CoroutineScope`) + * **StateScope:** APIs that internally accumulate state in reaction to events + need to be performed within a State scope (akin to a `CoroutineScope`) - * this scope is a side-effect-free subset of FrpBuildScope, and so can be - used wherever you have an FrpBuildScope + * this scope is a side-effect-free subset of BuildScope, and so can be + used wherever you have an BuildScope - * **FrpBuildScope:** APIs that perform external side-effects (`Flow.collect`) - need to be performed within an FRP Build scope (akin to a `CoroutineScope`) + * **BuildScope:** APIs that perform external side-effects (`Flow.collect`) + need to be performed within a Build scope (akin to a `CoroutineScope`) - * this scope is available from `FrpNetwork.activateSpec { … }` + * this scope is available from `Network.activateSpec { … }` * All other APIs can be used anywhere ## emptyFlow() -Use `emptyTFlow` +Use `emptyEvents` ``` kotlin -// this TFlow emits nothing -val noEvents: TFlow<Int> = emptyTFlow +// this Events emits nothing +val noEvents: Events<Int> = emptyEvents ``` ## map { … } -Use `TFlow.map` / `TState.map` +Use `Events.map` / `State.map` ``` kotlin -val anInt: TState<Int> = … -val squared: TState<Int> = anInt.map { it * it } -val messages: TFlow<String> = … -val messageLengths: TFlow<Int> = messages.map { it.size } +val anInt: State<Int> = … +val squared: State<Int> = anInt.map { it * it } +val messages: Events<String> = … +val messageLengths: Events<Int> = messages.map { it.size } ``` ## filter { … } / mapNotNull { … } -### I have a TFlow +### I have an Events -Use `TFlow.filter` / `TFlow.mapNotNull` +Use `Events.filter` / `Events.mapNotNull` ``` kotlin -val messages: TFlow<String> = … -val nonEmpty: TFlow<String> = messages.filter { it.isNotEmpty() } +val messages: Events<String> = … +val nonEmpty: Events<String> = messages.filter { it.isNotEmpty() } ``` -### I have a TState +### I have a State -Convert the `TState` to `TFlow` using `TState.stateChanges`, then use -`TFlow.filter` / `TFlow.mapNotNull` +Convert the `State` to `Events` using `State.stateChanges`, then use +`Events.filter` / `Events.mapNotNull` -If you need to convert back to `TState`, use `TFlow.hold(initialValue)` on the -result. +If you need to convert back to `State`, use `Events.holdState(initialValue)` on +the result. ``` kotlin -tState.stateChanges.filter { … }.hold(initialValue) +state.stateChanges.filter { … }.holdState(initialValue) ``` -Note that `TFlow.hold` is only available within an `FrpStateScope` in order to -track the lifetime of the state accumulation. +Note that `Events.holdState` is only available within an `StateScope` in order +to track the lifetime of the state accumulation. ## combine(...) { … } -### I have TStates +### I have States -Use `combine(TStates)` +Use `combine(States)` ``` kotlin -val someInt: TState<Int> = … -val someString: TState<String> = … -val model: TState<MyModel> = combine(someInt, someString) { i, s -> MyModel(i, s) } +val someInt: State<Int> = … +val someString: State<String> = … +val model: State<MyModel> = combine(someInt, someString) { i, s -> MyModel(i, s) } ``` -### I have TFlows +### I have Events -Convert the TFlows to TStates using `TFlow.hold(initialValue)`, then use -`combine(TStates)` +Convert the Events to States using `Events.holdState(initialValue)`, then use +`combine(States)` If you want the behavior of Flow.combine where nothing is emitted until each -TFlow has emitted at least once, you can use filter: +Events has emitted at least once, you can use filter: ``` kotlin // null used as an example, can use a different sentinel if needed -combine(tFlowA.hold(null), tFlowB.hold(null)) { a, b -> +combine(eventsA.holdState(null), eventsB.holdState(null)) { a, b -> a?.let { b?.let { … } } } .filterNotNull() ``` -Note that `TFlow.hold` is only available within an `FrpStateScope` in order to -track the lifetime of the state accumulation. +Note that `Events.holdState` is only available within an `StateScope` in order +to track the lifetime of the state accumulation. #### Explanation @@ -126,7 +125,7 @@ has emitted at least once. This often bites developers. As a workaround, developers generally append `.onStart { emit(initialValue) }` to the `Flows` that don't immediately emit. -Kairos avoids this gotcha by forcing usage of `TState` for `combine`, thus +Kairos avoids this gotcha by forcing usage of `State` for `combine`, thus ensuring that there is always a current value to be combined for each input. ## collect { … } @@ -134,197 +133,197 @@ ensuring that there is always a current value to be combined for each input. Use `observe { … }` ``` kotlin -val job: Job = tFlow.observe { println("observed: $it") } +val job: Job = events.observe { println("observed: $it") } ``` -Note that `observe` is only available within an `FrpBuildScope` in order to -track the lifetime of the observer. `FrpBuildScope` can only come from a -top-level `FrpNetwork.transaction { … }`, or a sub-scope created by using a -`-Latest` operator. +Note that `observe` is only available within a `BuildScope` in order to track +the lifetime of the observer. `BuildScope` can only come from a top-level +`Network.transaction { … }`, or a sub-scope created by using a `-Latest` +operator. ## sample(flow) { … } -### I want to sample a TState +### I want to sample a State -Use `TState.sample()` to get the current value of a `TState`. This can be -invoked anywhere you have access to an `FrpTransactionScope`. +Use `State.sample()` to get the current value of a `State`. This can be +invoked anywhere you have access to an `TransactionScope`. ``` kotlin -// the lambda passed to map receives an FrpTransactionScope, so it can invoke +// the lambda passed to map receives an TransactionScope, so it can invoke // sample -tFlow.map { tState.sample() } +events.map { state.sample() } ``` #### Explanation -To keep all state-reads consistent, the current value of a TState can only be -queried within a Kairos transaction, modeled with `FrpTransactionScope`. Note -that both `FrpStateScope` and `FrpBuildScope` extend `FrpTransactionScope`. +To keep all state-reads consistent, the current value of a State can only be +queried within a Kairos transaction, modeled with `TransactionScope`. Note that +both `StateScope` and `BuildScope` extend `TransactionScope`. -### I want to sample a TFlow +### I want to sample an Events -Convert to a `TState` by using `TFlow.hold(initialValue)`, then use `sample`. +Convert to a `State` by using `Events.holdState(initialValue)`, then use `sample`. -Note that `hold` is only available within an `FrpStateScope` in order to track +Note that `holdState` is only available within an `StateScope` in order to track the lifetime of the state accumulation. ## stateIn(scope, sharingStarted, initialValue) -Use `TFlow.hold(initialValue)`. There is no need to supply a sharingStarted -argument; all states are accumulated eagerly. +Use `Events.holdState(initialValue)`. There is no need to supply a +sharingStarted argument; all states are accumulated eagerly. ``` kotlin -val ints: TFlow<Int> = … -val lastSeenInt: TState<Int> = ints.hold(initialValue = 0) +val ints: Events<Int> = … +val lastSeenInt: State<Int> = ints.holdState(initialValue = 0) ``` -Note that `hold` is only available within an `FrpStateScope` in order to track +Note that `holdState` is only available within an `StateScope` in order to track the lifetime of the state accumulation (akin to the scope parameter of -`Flow.stateIn`). `FrpStateScope` can only come from a top-level -`FrpNetwork.transaction { … }`, or a sub-scope created by using a `-Latest` -operator. Also note that `FrpBuildScope` extends `FrpStateScope`. +`Flow.stateIn`). `StateScope` can only come from a top-level +`Network.transaction { … }`, or a sub-scope created by using a `-Latest` +operator. Also note that `BuildScope` extends `StateScope`. ## distinctUntilChanged() -Use `distinctUntilChanged` like normal. This is only available for `TFlow`; -`TStates` are already `distinctUntilChanged`. +Use `distinctUntilChanged` like normal. This is only available for `Events`; +`States` are already `distinctUntilChanged`. ## merge(...) -### I have TFlows +### I have Eventss -Use `merge(TFlows) { … }`. The lambda argument is used to disambiguate multiple +Use `merge(Events) { … }`. The lambda argument is used to disambiguate multiple simultaneous emissions within the same transaction. #### Explanation -Under Kairos's rules, a `TFlow` may only emit up to once per transaction. This -means that if we are merging two or more `TFlows` that are emitting at the same -time (within the same transaction), the resulting merged `TFlow` must emit a +Under Kairos's rules, an `Events` may only emit up to once per transaction. This +means that if we are merging two or more `Events` that are emitting at the same +time (within the same transaction), the resulting merged `Events` must emit a single value. The lambda argument allows the developer to decide what to do in this case. -### I have TStates +### I have States -If `combine` doesn't satisfy your needs, you can use `TState.stateChanges` to -convert to a `TFlow`, and then `merge`. +If `combine` doesn't satisfy your needs, you can use `State.changes` to +convert to a `Events`, and then `merge`. ## conflatedCallbackFlow { … } -Use `tFlow { … }`. +Use `events { … }`. As a shortcut, if you already have a `conflatedCallbackFlow { … }`, you can -convert it to a TFlow via `Flow.toTFlow()`. +convert it to an Events via `Flow.toEvents()`. -Note that `tFlow` is only available within an `FrpBuildScope` in order to track -the lifetime of the input registration. +Note that `events` is only available within a `BuildScope` in order to track the +lifetime of the input registration. ## first() -### I have a TState +### I have a State -Use `TState.sample`. +Use `State.sample`. -### I have a TFlow +### I have an Events -Use `TFlow.nextOnly`, which works exactly like `Flow.first` but instead of -suspending it returns a `TFlow` that emits once. +Use `Events.nextOnly`, which works exactly like `Flow.first` but instead of +suspending it returns a `Events` that emits once. The naming is intentionally different because `first` implies that it is the first-ever value emitted from the `Flow` (which makes sense for cold `Flows`), whereas `nextOnly` indicates that only the next value relative to the current transaction (the one `nextOnly` is being invoked in) will be emitted. -Note that `nextOnly` is only available within an `FrpStateScope` in order to -track the lifetime of the state accumulation. +Note that `nextOnly` is only available within an `StateScope` in order to track +the lifetime of the state accumulation. ## flatMapLatest { … } If you want to use -Latest to cancel old side-effects, similar to what the Flow -Latest operators offer for coroutines, see `mapLatest`. -### I have a TState… +### I have a State… -#### …and want to switch TStates +#### …and want to switch States -Use `TState.flatMap` +Use `State.flatMap` ``` kotlin -val flattened = tState.flatMap { a -> getTState(a) } +val flattened = state.flatMap { a -> gestate(a) } ``` -#### …and want to switch TFlows +#### …and want to switch Events -Use `TState<TFlow<T>>.switch()` +Use `State<Events<T>>.switchEvents()` ``` kotlin -val tFlow = tState.map { a -> getTFlow(a) }.switch() +val events = state.map { a -> getEvents(a) }.switchEvents() ``` -### I have a TFlow… +### I have an Events… -#### …and want to switch TFlows +#### …and want to switch Events -Use `hold` to convert to a `TState<TFlow<T>>`, then use `switch` to switch to -the latest `TFlow`. +Use `holdState` to convert to a `State<Events<T>>`, then use `switchEvents` to +switch to the latest `Events`. ``` kotlin -val tFlow = tFlowOfFlows.hold(emptyTFlow).switch() +val events = eventsOfFlows.holdState(emptyEvents).switchEvents() ``` -#### …and want to switch TStates +#### …and want to switch States -Use `hold` to convert to a `TState<TState<T>>`, then use `flatMap` to switch to -the latest `TState`. +Use `holdState` to convert to a `State<State<T>>`, then use `flatMap` to switch +to the latest `State`. ``` kotlin -val tState = tFlowOfStates.hold(tStateOf(initialValue)).flatMap { it } +val state = eventsOfStates.holdState(stateOf(initialValue)).flatMap { it } ``` ## mapLatest { … } / collectLatest { … } -`FrpStateScope` and `FrpBuildScope` both provide `-Latest` operators that +`StateScope` and `BuildScope` both provide `-Latest` operators that automatically cancel old work when new values are emitted. ``` kotlin -val currentModel: TState<SomeModel> = … -val mapped: TState<...> = currentModel.mapLatestBuild { model -> +val currentModel: State<SomeModel> = … +val mapped: State<...> = currentModel.mapLatestBuild { model -> effect { "new model in the house: $model" } model.someState.observe { "someState: $it" } - val someData: TState<SomeInfo> = + val someData: State<SomeInfo> = getBroadcasts(model.uri) .map { extractInfo(it) } - .hold(initialInfo) + .holdState(initialInfo) … } ``` ## flowOf(...) -### I want a TState +### I want a State -Use `tStateOf(initialValue)`. +Use `stateOf(initialValue)`. -### I want a TFlow +### I want an Events Use `now.map { initialValue }` -Note that `now` is only available within an `FrpTransactionScope`. +Note that `now` is only available within an `TransactionScope`. #### Explanation -`TFlows` are not cold, and so there isn't a notion of "emit this value once +`Events` are not cold, and so there isn't a notion of "emit this value once there is a collector" like there is for `Flow`. The closest analog would be -`TState`, since the initial value is retained indefinitely until there is an +`State`, since the initial value is retained indefinitely until there is an observer. However, it is often useful to immediately emit a value within the -current transaction, usually when using a `flatMap` or `switch`. In these cases, -using `now` explicitly models that the emission will occur within the current -transaction. +current transaction, usually when using a `flatMap` or `switchEvents`. In these +cases, using `now` explicitly models that the emission will occur within the +current transaction. ``` kotlin -fun <T> FrpTransactionScope.tFlowOf(value: T): TFlow<T> = now.map { value } +fun <T> TransactionScope.eventsOf(value: T): Events<T> = now.map { value } ``` ## MutableStateFlow / MutableSharedFlow -Use `MutableTState(frpNetwork, initialValue)` and `MutableTFlow(frpNetwork)`. +Use `MutableState(frpNetwork, initialValue)` and `MutableEvents(frpNetwork)`. diff --git a/packages/SystemUI/utils/kairos/docs/semantics.md b/packages/SystemUI/utils/kairos/docs/semantics.md index d43bb4447061..c8e468050037 100644 --- a/packages/SystemUI/utils/kairos/docs/semantics.md +++ b/packages/SystemUI/utils/kairos/docs/semantics.md @@ -33,39 +33,39 @@ sealed class Time : Comparable<Time> { typealias Transactional<T> = (Time) -> T -typealias TFlow<T> = SortedMap<Time, T> +typealias Events<T> = SortedMap<Time, T> private fun <T> SortedMap<Time, T>.pairwise(): List<Pair<Pair<Time, T>, Pair<Time<T>>>> = // NOTE: pretend evaluation is lazy, so that error() doesn't immediately throw (toList() + Pair(Time.Infinity, error("no value"))).zipWithNext() -class TState<T> internal constructor( +class State<T> internal constructor( internal val current: Transactional<T>, - val stateChanges: TFlow<T>, + val stateChanges: Events<T>, ) -val emptyTFlow: TFlow<Nothing> = emptyMap() +val emptyEvents: Events<Nothing> = emptyMap() -fun <A, B> TFlow<A>.map(f: FrpTransactionScope.(A) -> B): TFlow<B> = - mapValues { (t, a) -> FrpTransactionScope(t).f(a) } +fun <A, B> Events<A>.map(f: TransactionScope.(A) -> B): Events<B> = + mapValues { (t, a) -> TransactionScope(t).f(a) } -fun <A> TFlow<A>.filter(f: FrpTransactionScope.(A) -> Boolean): TFlow<A> = - filter { (t, a) -> FrpTransactionScope(t).f(a) } +fun <A> Events<A>.filter(f: TransactionScope.(A) -> Boolean): Events<A> = + filter { (t, a) -> TransactionScope(t).f(a) } fun <A> merge( - first: TFlow<A>, - second: TFlow<A>, + first: Events<A>, + second: Events<A>, onCoincidence: Time.(A, A) -> A, -): TFlow<A> = +): Events<A> = first.toMutableMap().also { result -> second.forEach { (t, a) -> result.merge(t, a) { f, s -> - FrpTranscationScope(t).onCoincidence(f, a) + TransactionScope(t).onCoincidence(f, a) } } }.toSortedMap() -fun <A> TState<TFlow<A>>.switch(): TFlow<A> { +fun <A> State<Events<A>>.switchEvents(): Events<A> { val truncated = listOf(Pair(Time.BigBang, current.invoke(Time.BigBang))) + stateChanges.dropWhile { (time, _) -> time < time0 } val events = @@ -77,7 +77,7 @@ fun <A> TState<TFlow<A>>.switch(): TFlow<A> { return events.toSortedMap() } -fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> { +fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> { val truncated = listOf(Pair(Time.BigBang, current.invoke(Time.BigBang))) + stateChanges.dropWhile { (time, _) -> time < time0 } val events = @@ -89,24 +89,24 @@ fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> { return events.toSortedMap() } -typealias GroupedTFlow<K, V> = TFlow<Map<K, V>> +typealias GroupedEvents<K, V> = Events<Map<K, V>> -fun <K, V> TFlow<Map<K, V>>.groupByKey(): GroupedTFlow<K, V> = this +fun <K, V> Events<Map<K, V>>.groupByKey(): GroupedEvents<K, V> = this -fun <K, V> GroupedTFlow<K, V>.eventsForKey(key: K): TFlow<V> = +fun <K, V> GroupedEvents<K, V>.eventsForKey(key: K): Events<V> = map { m -> m[k] }.filter { it != null }.map { it!! } -fun <A, B> TState<A>.map(f: (A) -> B): TState<B> = - TState( +fun <A, B> State<A>.map(f: (A) -> B): State<B> = + State( current = { t -> f(current.invoke(t)) }, stateChanges = stateChanges.map { f(it) }, ) -fun <A, B, C> TState<A>.combineWith( - other: TState<B>, +fun <A, B, C> State<A>.combineWith( + other: State<B>, f: (A, B) -> C, -): TState<C> = - TState( +): State<C> = + State( current = { t -> f(current.invoke(t), other.current.invoke(t)) }, stateChanges = run { val aChanges = @@ -129,7 +129,7 @@ fun <A, B, C> TState<A>.combineWith( }, ) -fun <A> TState<TState<A>>.flatten(): TState<A> { +fun <A> State<State<A>>.flatten(): State<A> { val changes = stateChanges .pairwise() @@ -144,55 +144,55 @@ fun <A> TState<TState<A>>.flatten(): TState<A> { inWindow } } - return TState( + return State( current = { t -> current.invoke(t).current.invoke(t) }, stateChanges = changes.toSortedMap(), ) } -open class FrpTranscationScope internal constructor( +open class TransactionScope internal constructor( internal val currentTime: Time, ) { - val now: TFlow<Unit> = + val now: Events<Unit> = sortedMapOf(currentTime to Unit) fun <A> Transactional<A>.sample(): A = invoke(currentTime) - fun <A> TState<A>.sample(): A = + fun <A> State<A>.sample(): A = current.sample() } -class FrpStateScope internal constructor( +class StateScope internal constructor( time: Time, internal val stopTime: Time, -): FrpTransactionScope(time) { +): TransactionScope(time) { - fun <A, B> TFlow<A>.fold( + fun <A, B> Events<A>.foldState( initialValue: B, - f: FrpTransactionScope.(B, A) -> B, - ): TState<B> { + f: TransactionScope.(B, A) -> B, + ): State<B> { val truncated = dropWhile { (t, _) -> t < currentTime } .takeWhile { (t, _) -> t <= stopTime } - val folded = + val foldStateed = truncated .scan(Pair(currentTime, initialValue)) { (_, b) (t, a) -> - Pair(t, FrpTransactionScope(t).f(a, b)) + Pair(t, TransactionScope(t).f(a, b)) } val lookup = { t1 -> - folded.lastOrNull { (t0, _) -> t0 < t1 }?.value ?: initialValue + foldStateed.lastOrNull { (t0, _) -> t0 < t1 }?.value ?: initialValue } - return TState(lookup, folded.toSortedMap()) + return State(lookup, foldStateed.toSortedMap()) } - fun <A> TFlow<A>.hold(initialValue: A): TState<A> = - fold(initialValue) { _, a -> a } + fun <A> Events<A>.holdState(initialValue: A): State<A> = + foldState(initialValue) { _, a -> a } - fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally( + fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally( initialValues: Map<K, V> - ): TState<Map<K, V>> = - fold(initialValues) { patch, map -> + ): State<Map<K, V>> = + foldState(initialValues) { patch, map -> val eithers = patch.map { (k, v) -> if (v is Just) Left(k to v.value) else Right(k) } @@ -203,18 +203,18 @@ class FrpStateScope internal constructor( updated } - fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - initialTFlows: Map<K, TFlow<V>>, - ): TFlow<Map<K, V>> = - foldMapIncrementally(initialTFlows).map { it.merge() }.switch() + fun <K : Any, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( + initialEventss: Map<K, Events<V>>, + ): Events<Map<K, V>> = + foldStateMapIncrementally(initialEventss).map { it.merge() }.switchEvents() - fun <K, A, B> TFlow<Map<K, Maybe<A>>.mapLatestStatefulForKey( - transform: suspend FrpStateScope.(A) -> B, - ): TFlow<Map<K, Maybe<B>>> = + fun <K, A, B> Events<Map<K, Maybe<A>>.mapLatestStatefulForKey( + transform: suspend StateScope.(A) -> B, + ): Events<Map<K, Maybe<B>>> = pairwise().map { ((t0, patch), (t1, _)) -> patch.map { (k, ma) -> ma.map { a -> - FrpStateScope(t0, t1).transform(a) + StateScope(t0, t1).transform(a) } } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt new file mode 100644 index 000000000000..3cba163dcb5f --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt @@ -0,0 +1,862 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.util.Maybe +import com.android.systemui.kairos.util.just +import com.android.systemui.kairos.util.map +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.dropWhile +import kotlinx.coroutines.flow.scan +import kotlinx.coroutines.launch + +/** A function that modifies the KairosNetwork. */ +typealias BuildSpec<A> = BuildScope.() -> A + +/** + * Constructs a [BuildSpec]. The passed [block] will be invoked with a [BuildScope] that can be used + * to perform network-building operations, including adding new inputs and outputs to the network, + * as well as all operations available in [TransactionScope]. + */ +@ExperimentalKairosApi +@Suppress("NOTHING_TO_INLINE") +inline fun <A> buildSpec(noinline block: BuildScope.() -> A): BuildSpec<A> = block + +/** Applies the [BuildSpec] within this [BuildScope]. */ +@ExperimentalKairosApi +inline operator fun <A> BuildScope.invoke(block: BuildScope.() -> A) = run(block) + +/** Operations that add inputs and outputs to a Kairos network. */ +@ExperimentalKairosApi +interface BuildScope : StateScope { + + /** + * A [KairosNetwork] handle that is bound to this [BuildScope]. + * + * It supports all of the standard functionality by which external code can interact with this + * Kairos network, but all [activated][KairosNetwork.activateSpec] [BuildSpec]s are bound as + * children to this [BuildScope], such that when this [BuildScope] is destroyed, all children + * are also destroyed. + */ + val kairosNetwork: KairosNetwork + + /** + * Defers invoking [block] until after the current [BuildScope] code-path completes, returning a + * [DeferredValue] that can be used to reference the result. + * + * Useful for recursive definitions. + * + * @see deferredBuildScopeAction + * @see DeferredValue + */ + fun <R> deferredBuildScope(block: BuildScope.() -> R): DeferredValue<R> + + /** + * Defers invoking [block] until after the current [BuildScope] code-path completes. + * + * Useful for recursive definitions. + * + * @see deferredBuildScope + */ + fun deferredBuildScopeAction(block: BuildScope.() -> Unit) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * Unlike [mapLatestBuild], these modifications are not undone with each subsequent emission of + * the original [Events]. + * + * **NOTE:** This API does not [observe] the original [Events], meaning that unless the returned + * (or a downstream) [Events] is observed separately, [transform] will not be invoked, and no + * internal side-effects will occur. + */ + fun <A, B> Events<A>.mapBuild(transform: BuildScope.(A) -> B): Events<B> + + /** + * Invokes [block] whenever this [Events] emits a value, allowing side-effects to be safely + * performed in reaction to the emission. + * + * Specifically, [block] is deferred to the end of the transaction, and is only actually + * executed if this [BuildScope] is still active by that time. It can be deactivated due to a + * -Latest combinator, for example. + * + * Shorthand for: + * ```kotlin + * events.observe { effect { ... } } + * ``` + */ + fun <A> Events<A>.observe( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + block: EffectScope.(A) -> Unit = {}, + ): Job + + /** + * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [initialSpecs] + * immediately. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the + * same key are undone (any registered [observers][observe] are unregistered, and any pending + * [side-effects][effect] are cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildSpec] will be undone with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey( + initialSpecs: DeferredValue<Map<K, BuildSpec<B>>>, + numKeys: Int? = null, + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> + + /** + * Creates an instance of an [Events] with elements that are from [builder]. + * + * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the + * provided [MutableState]. + * + * By default, [builder] is only running while the returned [Events] is being + * [observed][observe]. If you want it to run at all times, simply add a no-op observer: + * ```kotlin + * events { ... }.apply { observe() } + * ``` + */ + fun <T> events( + name: String? = null, + builder: suspend EventProducerScope<T>.() -> Unit, + ): Events<T> + + /** + * Creates an instance of an [Events] with elements that are emitted from [builder]. + * + * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the + * provided [MutableState]. + * + * By default, [builder] is only running while the returned [Events] is being + * [observed][observe]. If you want it to run at all times, simply add a no-op observer: + * ```kotlin + * events { ... }.apply { observe() } + * ``` + * + * In the event of backpressure, emissions are *coalesced* into batches. When a value is + * [emitted][CoalescingEventProducerScope.emit] from [builder], it is merged into the batch via + * [coalesce]. Once the batch is consumed by the kairos network in the next transaction, the + * batch is reset back to [getInitialValue]. + */ + fun <In, Out> coalescingEvents( + getInitialValue: () -> Out, + coalesce: (old: Out, new: In) -> Out, + builder: suspend CoalescingEventProducerScope<In>.() -> Unit, + ): Events<Out> + + /** + * Creates a new [BuildScope] that is a child of this one. + * + * This new scope can be manually cancelled via the returned [Job], or will be cancelled + * automatically when its parent is cancelled. Cancellation will unregister all + * [observers][observe] and cancel all scheduled [effects][effect]. + * + * The return value from [block] can be accessed via the returned [DeferredValue]. + */ + fun <A> asyncScope(block: BuildSpec<A>): Pair<DeferredValue<A>, Job> + + // TODO: once we have context params, these can all become extensions: + + /** + * Returns an [Events] containing the results of applying the given [transform] function to each + * value of the original [Events]. + * + * Unlike [Events.map], [transform] can perform arbitrary asynchronous code. This code is run + * outside of the current Kairos transaction; when [transform] returns, the returned value is + * emitted from the result [Events] in a new transaction. + * + * Shorthand for: + * ```kotlin + * events.mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten() + * ``` + */ + fun <A, B> Events<A>.mapAsyncLatest(transform: suspend (A) -> B): Events<B> = + mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten() + + /** + * Invokes [block] whenever this [Events] emits a value. [block] receives an [BuildScope] that + * can be used to make further modifications to the Kairos network, and/or perform side-effects + * via [effect]. + * + * @see observe + */ + fun <A> Events<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job = + mapBuild(block).observe() + + /** + * Returns a [StateFlow] whose [value][StateFlow.value] tracks the current + * [value of this State][State.sample], and will emit at the same rate as [State.changes]. + * + * Note that the [value][StateFlow.value] is not available until the *end* of the current + * transaction. If you need the current value before this time, then use [State.sample]. + */ + fun <A> State<A>.toStateFlow(): StateFlow<A> { + val uninitialized = Any() + var initialValue: Any? = uninitialized + val innerStateFlow = MutableStateFlow<Any?>(uninitialized) + deferredBuildScope { + initialValue = sample() + changes.observe { + innerStateFlow.value = it + initialValue = null + } + } + + @Suppress("UNCHECKED_CAST") + fun getValue(innerValue: Any?): A = + when { + innerValue !== uninitialized -> innerValue as A + initialValue !== uninitialized -> initialValue as A + else -> + error( + "Attempted to access StateFlow.value before Kairos transaction has completed." + ) + } + + return object : StateFlow<A> { + override val replayCache: List<A> + get() = innerStateFlow.replayCache.map(::getValue) + + override val value: A + get() = getValue(innerStateFlow.value) + + override suspend fun collect(collector: FlowCollector<A>): Nothing { + innerStateFlow.collect { collector.emit(getValue(it)) } + } + } + } + + /** + * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits the current + * [value][State.sample] of this [State] followed by all [changes]. + */ + fun <A> State<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> { + val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1) + deferredBuildScope { + result.tryEmit(sample()) + changes.observe { a -> result.tryEmit(a) } + } + return result + } + + /** + * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits values + * whenever this [Events] emits. + */ + fun <A> Events<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> { + val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1) + observe { a -> result.tryEmit(a) } + return result + } + + /** + * Returns a [State] that holds onto the value returned by applying the most recently emitted + * [BuildSpec] from the original [Events], or the value returned by applying [initialSpec] if + * nothing has been emitted since it was constructed. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] are undone + * (any registered [observers][observe] are unregistered, and any pending [side-effects][effect] + * are cancelled). + */ + fun <A> Events<BuildSpec<A>>.holdLatestSpec(initialSpec: BuildSpec<A>): State<A> { + val (changes: Events<A>, initApplied: DeferredValue<A>) = applyLatestSpec(initialSpec) + return changes.holdStateDeferred(initApplied) + } + + /** + * Returns a [State] containing the value returned by applying the [BuildSpec] held by the + * original [State]. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] are undone + * (any registered [observers][observe] are unregistered, and any pending [side-effects][effect] + * are cancelled). + */ + fun <A> State<BuildSpec<A>>.applyLatestSpec(): State<A> { + val (appliedChanges: Events<A>, init: DeferredValue<A>) = + changes.applyLatestSpec(buildSpec { sample().applySpec() }) + return appliedChanges.holdStateDeferred(init) + } + + /** + * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the + * original [Events]. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] are undone + * (any registered [observers][observe] are unregistered, and any pending [side-effects][effect] + * are cancelled). + */ + fun <A> Events<BuildSpec<A>>.applyLatestSpec(): Events<A> = applyLatestSpec(buildSpec {}).first + + /** + * Returns an [Events] that switches to a new [Events] produced by [transform] every time the + * original [Events] emits a value. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * When the original [Events] emits a new value, those changes are undone (any registered + * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled). + */ + fun <A, B> Events<A>.flatMapLatestBuild(transform: BuildScope.(A) -> Events<B>): Events<B> = + mapCheap { buildSpec { transform(it) } }.applyLatestSpec().flatten() + + /** + * Returns a [State] by applying [transform] to the value held by the original [State]. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * When the value held by the original [State] changes, those changes are undone (any registered + * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled). + */ + fun <A, B> State<A>.flatMapLatestBuild(transform: BuildScope.(A) -> State<B>): State<B> = + mapLatestBuild { transform(it) }.flatten() + + /** + * Returns a [State] that transforms the value held inside this [State] by applying it to the + * [transform]. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * When the value held by the original [State] changes, those changes are undone (any registered + * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled). + */ + fun <A, B> State<A>.mapLatestBuild(transform: BuildScope.(A) -> B): State<B> = + mapCheapUnsafe { buildSpec { transform(it) } }.applyLatestSpec() + + /** + * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [initialSpec] + * immediately. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] are undone + * (any registered [observers][observe] are unregistered, and any pending [side-effects][effect] + * are cancelled). + */ + fun <A : Any?, B> Events<BuildSpec<B>>.applyLatestSpec( + initialSpec: BuildSpec<A> + ): Pair<Events<B>, DeferredValue<A>> { + val (events, result) = + mapCheap { spec -> mapOf(Unit to just(spec)) } + .applyLatestSpecForKey(initialSpecs = mapOf(Unit to initialSpec), numKeys = 1) + val outEvents: Events<B> = + events.mapMaybe { + checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" } + } + val outInit: DeferredValue<A> = deferredBuildScope { + val initResult: Map<Unit, A> = result.get() + check(Unit in initResult) { + "applyLatest: expected initial result, but none present in: $initResult" + } + @Suppress("UNCHECKED_CAST") + initResult.getOrDefault(Unit) { null } as A + } + return Pair(outEvents, outInit) + } + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + */ + fun <A, B> Events<A>.mapLatestBuild(transform: BuildScope.(A) -> B): Events<B> = + mapCheap { buildSpec { transform(it) } }.applyLatestSpec() + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValue] immediately. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + */ + fun <A, B> Events<A>.mapLatestBuild( + initialValue: A, + transform: BuildScope.(A) -> B, + ): Pair<Events<B>, DeferredValue<B>> = + mapLatestBuildDeferred(deferredOf(initialValue), transform) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValue] immediately. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + */ + fun <A, B> Events<A>.mapLatestBuildDeferred( + initialValue: DeferredValue<A>, + transform: BuildScope.(A) -> B, + ): Pair<Events<B>, DeferredValue<B>> = + mapCheap { buildSpec { transform(it) } } + .applyLatestSpec(initialSpec = buildSpec { transform(initialValue.get()) }) + + /** + * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [initialSpecs] + * immediately. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the + * same key are undone (any registered [observers][observe] are unregistered, and any pending + * [side-effects][effect] are cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildSpec] will be undone with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey( + initialSpecs: Map<K, BuildSpec<B>>, + numKeys: Int? = null, + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> = + applyLatestSpecForKey(deferredOf(initialSpecs), numKeys) + + /** + * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the + * original [Events]. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the + * same key are undone (any registered [observers][observe] are unregistered, and any pending + * [side-effects][effect] are cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildSpec] will be undone with no replacement. + */ + fun <K, A> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey( + numKeys: Int? = null + ): Events<Map<K, Maybe<A>>> = + applyLatestSpecForKey<K, A, Nothing>(deferredOf(emptyMap()), numKeys).first + + /** + * Returns a [State] containing the latest results of applying each [BuildSpec] emitted from the + * original [Events]. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the + * same key are undone (any registered [observers][observe] are unregistered, and any pending + * [side-effects][effect] are cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildSpec] will be undone with no replacement. + */ + fun <K, A> Events<Map<K, Maybe<BuildSpec<A>>>>.holdLatestSpecForKey( + initialSpecs: DeferredValue<Map<K, BuildSpec<A>>>, + numKeys: Int? = null, + ): State<Map<K, A>> { + val (changes, initialValues) = applyLatestSpecForKey(initialSpecs, numKeys) + return changes.foldStateMapIncrementally(initialValues) + } + + /** + * Returns a [State] containing the latest results of applying each [BuildSpec] emitted from the + * original [Events]. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the + * same key are undone (any registered [observers][observe] are unregistered, and any pending + * [side-effects][effect] are cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildSpec] will be undone with no replacement. + */ + fun <K, A> Events<Map<K, Maybe<BuildSpec<A>>>>.holdLatestSpecForKey( + initialSpecs: Map<K, BuildSpec<A>> = emptyMap(), + numKeys: Int? = null, + ): State<Map<K, A>> = holdLatestSpecForKey(deferredOf(initialSpecs), numKeys) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValues] immediately. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildScope] will be undone with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey( + initialValues: DeferredValue<Map<K, A>>, + numKeys: Int? = null, + transform: BuildScope.(K, A) -> B, + ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> = + map { patch -> patch.mapValues { (k, v) -> v.map { buildSpec { transform(k, it) } } } } + .applyLatestSpecForKey( + deferredBuildScope { + initialValues.get().mapValues { (k, v) -> buildSpec { transform(k, v) } } + }, + numKeys = numKeys, + ) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValues] immediately. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildScope] will be undone with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey( + initialValues: Map<K, A>, + numKeys: Int? = null, + transform: BuildScope.(K, A) -> B, + ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> = + mapLatestBuildForKey(deferredOf(initialValues), numKeys, transform) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildScope] will be undone with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey( + numKeys: Int? = null, + transform: BuildScope.(K, A) -> B, + ): Events<Map<K, Maybe<B>>> = mapLatestBuildForKey(emptyMap(), numKeys, transform).first + + /** Returns a [Deferred] containing the next value to be emitted from this [Events]. */ + fun <R> Events<R>.nextDeferred(): Deferred<R> { + lateinit var next: CompletableDeferred<R> + val job = nextOnly().observe { next.complete(it) } + next = CompletableDeferred<R>(parent = job) + return next + } + + /** Returns a [State] that reflects the [StateFlow.value] of this [StateFlow]. */ + fun <A> StateFlow<A>.toState(): State<A> { + val initial = value + return events { dropWhile { it == initial }.collect { emit(it) } }.holdState(initial) + } + + /** Returns an [Events] that emits whenever this [Flow] emits. */ + fun <A> Flow<A>.toEvents(name: String? = null): Events<A> = + events(name) { collect { emit(it) } } + + /** + * Shorthand for: + * ```kotlin + * flow.toEvents().holdState(initialValue) + * ``` + */ + fun <A> Flow<A>.toState(initialValue: A): State<A> = toEvents().holdState(initialValue) + + /** + * Shorthand for: + * ```kotlin + * flow.scan(initialValue, operation).toEvents().holdState(initialValue) + * ``` + */ + fun <A, B> Flow<A>.scanToState(initialValue: B, operation: (B, A) -> B): State<B> = + scan(initialValue, operation).toEvents().holdState(initialValue) + + /** + * Shorthand for: + * ```kotlin + * flow.scan(initialValue) { a, f -> f(a) }.toEvents().holdState(initialValue) + * ``` + */ + fun <A> Flow<(A) -> A>.scanToState(initialValue: A): State<A> = + scanToState(initialValue) { a, f -> f(a) } + + /** + * Invokes [block] whenever this [Events] emits a value. [block] receives an [BuildScope] that + * can be used to make further modifications to the Kairos network, and/or perform side-effects + * via [effect]. + * + * With each invocation of [block], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + */ + fun <A> Events<A>.observeLatestBuild(block: BuildScope.(A) -> Unit = {}): Job = + mapLatestBuild { block(it) }.observe() + + /** + * Invokes [block] whenever this [Events] emits a value, allowing side-effects to be safely + * performed in reaction to the emission. + * + * With each invocation of [block], running effects from the previous invocation are cancelled. + */ + fun <A> Events<A>.observeLatest(block: EffectScope.(A) -> Unit = {}): Job { + var innerJob: Job? = null + return observeBuild { + innerJob?.cancel() + innerJob = effect { block(it) } + } + } + + /** + * Invokes [block] with the value held by this [State], allowing side-effects to be safely + * performed in reaction to the state changing. + * + * With each invocation of [block], running effects from the previous invocation are cancelled. + */ + fun <A> State<A>.observeLatest(block: EffectScope.(A) -> Unit = {}): Job = launchScope { + var innerJob = effect { block(sample()) } + changes.observeBuild { + innerJob.cancel() + innerJob = effect { block(it) } + } + } + + /** + * Applies [block] to the value held by this [State]. [block] receives an [BuildScope] that can + * be used to make further modifications to the Kairos network, and/or perform side-effects via + * [effect]. + * + * [block] can perform modifications to the Kairos network via its [BuildScope] receiver. With + * each invocation of [block], changes from the previous invocation are undone (any registered + * [observers][observe] are unregistered, and any pending [side-effects][effect] are cancelled). + */ + fun <A> State<A>.observeLatestBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope { + var innerJob: Job = launchScope { block(sample()) } + changes.observeBuild { + innerJob.cancel() + innerJob = launchScope { block(it) } + } + } + + /** Applies the [BuildSpec] within this [BuildScope]. */ + fun <A> BuildSpec<A>.applySpec(): A = this() + + /** + * Applies the [BuildSpec] within this [BuildScope], returning the result as an [DeferredValue]. + */ + fun <A> BuildSpec<A>.applySpecDeferred(): DeferredValue<A> = deferredBuildScope { applySpec() } + + /** + * Invokes [block] on the value held in this [State]. [block] receives an [BuildScope] that can + * be used to make further modifications to the Kairos network, and/or perform side-effects via + * [effect]. + */ + fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope { + block(sample()) + changes.observeBuild(block) + } + + /** + * Invokes [block] with the current value of this [State], re-invoking whenever it changes, + * allowing side-effects to be safely performed in reaction value changing. + * + * Specifically, [block] is deferred to the end of the transaction, and is only actually + * executed if this [BuildScope] is still active by that time. It can be deactivated due to a + * -Latest combinator, for example. + * + * If the [State] is changing within the *current* transaction (i.e. [changes] is presently + * emitting) then [block] will be invoked for the first time with the new value; otherwise, it + * will be invoked with the [current][sample] value. + */ + fun <A> State<A>.observe(block: EffectScope.(A) -> Unit = {}): Job = + now.map { sample() }.mergeWith(changes) { _, new -> new }.observe { block(it) } +} + +/** + * Returns an [Events] that emits the result of [block] once it completes. [block] is evaluated + * outside of the current Kairos transaction; when it completes, the returned [Events] emits in a + * new transaction. + * + * Shorthand for: + * ``` + * events { emitter: MutableEvents<A> -> + * val a = block() + * emitter.emit(a) + * } + * ``` + */ +@ExperimentalKairosApi +fun <A> BuildScope.asyncEvent(block: suspend () -> A): Events<A> = + events { + // TODO: if block completes synchronously, it would be nice to emit within this + // transaction + emit(block()) + } + .apply { observe() } + +/** + * Performs a side-effect in a safe manner w/r/t the current Kairos transaction. + * + * Specifically, [block] is deferred to the end of the current transaction, and is only actually + * executed if this [BuildScope] is still active by that time. It can be deactivated due to a + * -Latest combinator, for example. + * + * Shorthand for: + * ```kotlin + * now.observe { block() } + * ``` + */ +@ExperimentalKairosApi +fun BuildScope.effect( + context: CoroutineContext = EmptyCoroutineContext, + block: EffectScope.() -> Unit, +): Job = now.observe(context) { block() } + +/** + * Launches [block] in a new coroutine, returning a [Job] bound to the coroutine. + * + * This coroutine is not actually started until the *end* of the current Kairos transaction. This is + * done because the current [BuildScope] might be deactivated within this transaction, perhaps due + * to a -Latest combinator. If this happens, then the coroutine will never actually be started. + * + * Shorthand for: + * ```kotlin + * effect { effectCoroutineScope.launch { block() } } + * ``` + */ +@ExperimentalKairosApi +fun BuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asyncEffect(block) + +/** + * Launches [block] in a new coroutine, returning the result as a [Deferred]. + * + * This coroutine is not actually started until the *end* of the current Kairos transaction. This is + * done because the current [BuildScope] might be deactivated within this transaction, perhaps due + * to a -Latest combinator. If this happens, then the coroutine will never actually be started. + * + * Shorthand for: + * ```kotlin + * CompletableDeferred<R>.apply { + * effect { effectCoroutineScope.launch { complete(coroutineScope { block() }) } } + * } + * .await() + * ``` + */ +@ExperimentalKairosApi +fun <R> BuildScope.asyncEffect(block: suspend CoroutineScope.() -> R): Deferred<R> { + val result = CompletableDeferred<R>() + val job = now.observe { effectCoroutineScope.launch { result.complete(coroutineScope(block)) } } + val handle = job.invokeOnCompletion { result.cancel() } + result.invokeOnCompletion { + handle.dispose() + job.cancel() + } + return result +} + +/** Like [BuildScope.asyncScope], but ignores the result of [block]. */ +@ExperimentalKairosApi +fun BuildScope.launchScope(block: BuildSpec<*>): Job = asyncScope(block).second + +/** + * Creates an instance of an [Events] with elements that are emitted from [builder]. + * + * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided + * [MutableState]. + * + * By default, [builder] is only running while the returned [Events] is being + * [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer: + * ```kotlin + * events { ... }.apply { observe() } + * ``` + * + * In the event of backpressure, emissions are *coalesced* into batches. When a value is + * [emitted][CoalescingEventProducerScope.emit] from [builder], it is merged into the batch via + * [coalesce]. Once the batch is consumed by the Kairos network in the next transaction, the batch + * is reset back to [initialValue]. + */ +@ExperimentalKairosApi +fun <In, Out> BuildScope.coalescingEvents( + initialValue: Out, + coalesce: (old: Out, new: In) -> Out, + builder: suspend CoalescingEventProducerScope<In>.() -> Unit, +): Events<Out> = coalescingEvents(getInitialValue = { initialValue }, coalesce, builder) + +/** + * Creates an instance of an [Events] with elements that are emitted from [builder]. + * + * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided + * [MutableState]. + * + * By default, [builder] is only running while the returned [Events] is being + * [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer: + * ```kotlin + * events { ... }.apply { observe() } + * ``` + * + * In the event of backpressure, emissions are *conflated*; any older emissions are dropped and only + * the most recent emission will be used when the Kairos network is ready. + */ +@ExperimentalKairosApi +fun <T> BuildScope.conflatedEvents( + builder: suspend CoalescingEventProducerScope<T>.() -> Unit +): Events<T> = + coalescingEvents<T, Any?>(initialValue = Any(), coalesce = { _, new -> new }, builder = builder) + .mapCheap { + @Suppress("UNCHECKED_CAST") + it as T + } + +/** Scope for emitting to a [BuildScope.coalescingEvents]. */ +interface CoalescingEventProducerScope<in T> { + /** + * Inserts [value] into the current batch, enqueueing it for emission from this [Events] if not + * already pending. + * + * Backpressure occurs when [emit] is called while the Kairos network is currently in a + * transaction; if called multiple times, then emissions will be coalesced into a single batch + * that is then processed when the network is ready. + */ + fun emit(value: T) +} + +/** Scope for emitting to a [BuildScope.events]. */ +interface EventProducerScope<in T> { + /** + * Emits a [value] to this [Events], suspending the caller until the Kairos transaction + * containing the emission has completed. + */ + suspend fun emit(value: T) +} + +/** + * Suspends forever. Upon cancellation, runs [block]. Useful for unregistering callbacks inside of + * [BuildScope.events] and [BuildScope.coalescingEvents]. + */ +suspend fun awaitClose(block: () -> Unit): Nothing = + try { + awaitCancellation() + } finally { + block() + } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt index ae9b8c85910f..a26d5f8f122e 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt @@ -25,42 +25,42 @@ import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.conflate /** - * Returns a [TFlow] that emits the value sampled from the [Transactional] produced by each emission - * of the original [TFlow], within the same transaction of the original emission. + * Returns an [Events] that emits the value sampled from the [Transactional] produced by each + * emission of the original [Events], within the same transaction of the original emission. */ -@ExperimentalFrpApi -fun <A> TFlow<Transactional<A>>.sampleTransactionals(): TFlow<A> = map { it.sample() } +@ExperimentalKairosApi +fun <A> Events<Transactional<A>>.sampleTransactionals(): Events<A> = map { it.sample() } -/** @see FrpTransactionScope.sample */ -@ExperimentalFrpApi -fun <A, B, C> TFlow<A>.sample( - state: TState<B>, - transform: suspend FrpTransactionScope.(A, B) -> C, -): TFlow<C> = map { transform(it, state.sample()) } +/** @see TransactionScope.sample */ +@ExperimentalKairosApi +fun <A, B, C> Events<A>.sample( + state: State<B>, + transform: TransactionScope.(A, B) -> C, +): Events<C> = map { transform(it, state.sample()) } -/** @see FrpTransactionScope.sample */ -@ExperimentalFrpApi -fun <A, B, C> TFlow<A>.sample( - transactional: Transactional<B>, - transform: suspend FrpTransactionScope.(A, B) -> C, -): TFlow<C> = map { transform(it, transactional.sample()) } +/** @see TransactionScope.sample */ +@ExperimentalKairosApi +fun <A, B, C> Events<A>.sample( + sampleable: Transactional<B>, + transform: TransactionScope.(A, B) -> C, +): Events<C> = map { transform(it, sampleable.sample()) } /** - * Like [sample], but if [state] is changing at the time it is sampled ([stateChanges] is emitting), - * then the new value is passed to [transform]. + * Like [sample], but if [state] is changing at the time it is sampled ([changes] is emitting), then + * the new value is passed to [transform]. * * Note that [sample] is both more performant, and safer to use with recursive definitions. You will * generally want to use it rather than this. * * @see sample */ -@ExperimentalFrpApi -fun <A, B, C> TFlow<A>.samplePromptly( - state: TState<B>, - transform: suspend FrpTransactionScope.(A, B) -> C, -): TFlow<C> = +@ExperimentalKairosApi +fun <A, B, C> Events<A>.samplePromptly( + state: State<B>, + transform: TransactionScope.(A, B) -> C, +): Events<C> = sample(state) { a, b -> These.thiz<Pair<A, B>, B>(a to b) } - .mergeWith(state.stateChanges.map { These.that(it) }) { thiz, that -> + .mergeWith(state.changes.map { These.that(it) }) { thiz, that -> These.both((thiz as These.This).thiz, (that as These.That).that) } .mapMaybe { these -> @@ -75,199 +75,204 @@ fun <A, B, C> TFlow<A>.samplePromptly( } /** - * Returns a cold [Flow] that, when collected, emits from this [TFlow]. [network] is needed to - * transactionally connect to / disconnect from the [TFlow] when collection starts/stops. + * Returns a cold [Flow] that, when collected, emits from this [Events]. [network] is needed to + * transactionally connect to / disconnect from the [Events] when collection starts/stops. */ -@ExperimentalFrpApi -fun <A> TFlow<A>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +@ExperimentalKairosApi +fun <A> Events<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate() /** - * Returns a cold [Flow] that, when collected, emits from this [TState]. [network] is needed to - * transactionally connect to / disconnect from the [TState] when collection starts/stops. + * Returns a cold [Flow] that, when collected, emits from this [State]. [network] is needed to + * transactionally connect to / disconnect from the [State] when collection starts/stops. */ -@ExperimentalFrpApi -fun <A> TState<A>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +@ExperimentalKairosApi +fun <A> State<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate() /** - * Returns a cold [Flow] that, when collected, applies this [FrpSpec] in a new transaction in this - * [network], and then emits from the returned [TFlow]. + * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this + * [network], and then emits from the returned [Events]. * - * When collection is cancelled, so is the [FrpSpec]. This means all ongoing work is cleaned up. + * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up. */ -@ExperimentalFrpApi -@JvmName("flowSpecToColdConflatedFlow") -fun <A> FrpSpec<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +@ExperimentalKairosApi +@JvmName("eventsSpecToColdConflatedFlow") +fun <A> BuildSpec<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate() /** - * Returns a cold [Flow] that, when collected, applies this [FrpSpec] in a new transaction in this - * [network], and then emits from the returned [TState]. + * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this + * [network], and then emits from the returned [State]. * - * When collection is cancelled, so is the [FrpSpec]. This means all ongoing work is cleaned up. + * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up. */ -@ExperimentalFrpApi +@ExperimentalKairosApi @JvmName("stateSpecToColdConflatedFlow") -fun <A> FrpSpec<TState<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +fun <A> BuildSpec<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate() /** * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in - * this [network], and then emits from the returned [TFlow]. + * this [network], and then emits from the returned [Events]. */ -@ExperimentalFrpApi +@ExperimentalKairosApi @JvmName("transactionalFlowToColdConflatedFlow") -fun <A> Transactional<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +fun <A> Transactional<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate() /** * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in - * this [network], and then emits from the returned [TState]. + * this [network], and then emits from the returned [State]. */ -@ExperimentalFrpApi +@ExperimentalKairosApi @JvmName("transactionalStateToColdConflatedFlow") -fun <A> Transactional<TState<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +fun <A> Transactional<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate() /** - * Returns a cold [Flow] that, when collected, applies this [FrpStateful] in a new transaction in - * this [network], and then emits from the returned [TFlow]. + * Returns a cold [Flow] that, when collected, applies this [Stateful] in a new transaction in this + * [network], and then emits from the returned [Events]. * - * When collection is cancelled, so is the [FrpStateful]. This means all ongoing work is cleaned up. + * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up. */ -@ExperimentalFrpApi +@ExperimentalKairosApi @JvmName("statefulFlowToColdConflatedFlow") -fun <A> FrpStateful<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +fun <A> Stateful<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate() /** * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in - * this [network], and then emits from the returned [TState]. + * this [network], and then emits from the returned [State]. * - * When collection is cancelled, so is the [FrpStateful]. This means all ongoing work is cleaned up. + * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up. */ -@ExperimentalFrpApi +@ExperimentalKairosApi @JvmName("statefulStateToColdConflatedFlow") -fun <A> FrpStateful<TState<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +fun <A> Stateful<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate() -/** Return a [TFlow] that emits from the original [TFlow] only when [state] is `true`. */ -@ExperimentalFrpApi -fun <A> TFlow<A>.filter(state: TState<Boolean>): TFlow<A> = filter { state.sample() } +/** Return an [Events] that emits from the original [Events] only when [state] is `true`. */ +@ExperimentalKairosApi +fun <A> Events<A>.filter(state: State<Boolean>): Events<A> = filter { state.sample() } private fun Iterable<Boolean>.allTrue() = all { it } private fun Iterable<Boolean>.anyTrue() = any { it } -/** Returns a [TState] that is `true` only when all of [states] are `true`. */ -@ExperimentalFrpApi -fun allOf(vararg states: TState<Boolean>): TState<Boolean> = combine(*states) { it.allTrue() } +/** Returns a [State] that is `true` only when all of [states] are `true`. */ +@ExperimentalKairosApi +fun allOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.allTrue() } -/** Returns a [TState] that is `true` when any of [states] are `true`. */ -@ExperimentalFrpApi -fun anyOf(vararg states: TState<Boolean>): TState<Boolean> = combine(*states) { it.anyTrue() } +/** Returns a [State] that is `true` when any of [states] are `true`. */ +@ExperimentalKairosApi +fun anyOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.anyTrue() } -/** Returns a [TState] containing the inverse of the Boolean held by the original [TState]. */ -@ExperimentalFrpApi fun not(state: TState<Boolean>): TState<Boolean> = state.mapCheapUnsafe { !it } +/** Returns a [State] containing the inverse of the Boolean held by the original [State]. */ +@ExperimentalKairosApi fun not(state: State<Boolean>): State<Boolean> = state.mapCheapUnsafe { !it } /** - * Represents a modal FRP sub-network. + * Represents a modal Kairos sub-network. * - * When [enabled][enableMode], all network modifications are applied immediately to the FRP network. - * When the returned [TFlow] emits a [FrpBuildMode], that mode is enabled and replaces this mode, - * undoing all modifications in the process (any registered [observers][FrpBuildScope.observe] are - * unregistered, and any pending [side-effects][FrpBuildScope.effect] are cancelled). + * When [enabled][enableMode], all network modifications are applied immediately to the Kairos + * network. When the returned [Events] emits a [BuildMode], that mode is enabled and replaces this + * mode, undoing all modifications in the process (any registered [observers][BuildScope.observe] + * are unregistered, and any pending [side-effects][BuildScope.effect] are cancelled). * - * Use [compiledFrpSpec] to compile and stand-up a mode graph. + * Use [compiledBuildSpec] to compile and stand-up a mode graph. * - * @see FrpStatefulMode + * @see StatefulMode */ -@ExperimentalFrpApi -fun interface FrpBuildMode<out A> { +@ExperimentalKairosApi +fun interface BuildMode<out A> { /** - * Invoked when this mode is enabled. Returns a value and a [TFlow] that signals a switch to a + * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a * new mode. */ - suspend fun FrpBuildScope.enableMode(): Pair<A, TFlow<FrpBuildMode<A>>> + fun BuildScope.enableMode(): Pair<A, Events<BuildMode<A>>> } /** - * Returns an [FrpSpec] that, when [applied][FrpBuildScope.applySpec], stands up a modal-transition - * graph starting with this [FrpBuildMode], automatically switching to new modes as they are - * produced. + * Returns an [BuildSpec] that, when [applied][BuildScope.applySpec], stands up a modal-transition + * graph starting with this [BuildMode], automatically switching to new modes as they are produced. * - * @see FrpBuildMode + * @see BuildMode */ -@ExperimentalFrpApi -val <A> FrpBuildMode<A>.compiledFrpSpec: FrpSpec<TState<A>> - get() = frpSpec { - var modeChangeEvents by TFlowLoop<FrpBuildMode<A>>() - val activeMode: TState<Pair<A, TFlow<FrpBuildMode<A>>>> = +@ExperimentalKairosApi +val <A> BuildMode<A>.compiledBuildSpec: BuildSpec<State<A>> + get() = buildSpec { + var modeChangeEvents by EventsLoop<BuildMode<A>>() + val activeMode: State<Pair<A, Events<BuildMode<A>>>> = modeChangeEvents - .map { it.run { frpSpec { enableMode() } } } - .holdLatestSpec(frpSpec { enableMode() }) + .map { it.run { buildSpec { enableMode() } } } + .holdLatestSpec(buildSpec { enableMode() }) modeChangeEvents = - activeMode.map { statefully { it.second.nextOnly() } }.applyLatestStateful().switch() + activeMode + .map { statefully { it.second.nextOnly() } } + .applyLatestStateful() + .switchEvents() activeMode.map { it.first } } /** - * Represents a modal FRP sub-network. + * Represents a modal Kairos sub-network. * * When [enabled][enableMode], all state accumulation is immediately started. When the returned - * [TFlow] emits a [FrpBuildMode], that mode is enabled and replaces this mode, stopping all state + * [Events] emits a [BuildMode], that mode is enabled and replaces this mode, stopping all state * accumulation in the process. * * Use [compiledStateful] to compile and stand-up a mode graph. * - * @see FrpBuildMode + * @see BuildMode */ -@ExperimentalFrpApi -fun interface FrpStatefulMode<out A> { +@ExperimentalKairosApi +fun interface StatefulMode<out A> { /** - * Invoked when this mode is enabled. Returns a value and a [TFlow] that signals a switch to a + * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a * new mode. */ - suspend fun FrpStateScope.enableMode(): Pair<A, TFlow<FrpStatefulMode<A>>> + fun StateScope.enableMode(): Pair<A, Events<StatefulMode<A>>> } /** - * Returns an [FrpStateful] that, when [applied][FrpStateScope.applyStateful], stands up a - * modal-transition graph starting with this [FrpStatefulMode], automatically switching to new modes - * as they are produced. + * Returns an [Stateful] that, when [applied][StateScope.applyStateful], stands up a + * modal-transition graph starting with this [StatefulMode], automatically switching to new modes as + * they are produced. * - * @see FrpBuildMode + * @see BuildMode */ -@ExperimentalFrpApi -val <A> FrpStatefulMode<A>.compiledStateful: FrpStateful<TState<A>> +@ExperimentalKairosApi +val <A> StatefulMode<A>.compiledStateful: Stateful<State<A>> get() = statefully { - var modeChangeEvents by TFlowLoop<FrpStatefulMode<A>>() - val activeMode: TState<Pair<A, TFlow<FrpStatefulMode<A>>>> = + var modeChangeEvents by EventsLoop<StatefulMode<A>>() + val activeMode: State<Pair<A, Events<StatefulMode<A>>>> = modeChangeEvents .map { it.run { statefully { enableMode() } } } .holdLatestStateful(statefully { enableMode() }) modeChangeEvents = - activeMode.map { statefully { it.second.nextOnly() } }.applyLatestStateful().switch() + activeMode + .map { statefully { it.second.nextOnly() } } + .applyLatestStateful() + .switchEvents() activeMode.map { it.first } } /** - * Runs [spec] in this [FrpBuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns - * a [TState] that holds the result of the currently-active [FrpSpec]. + * Runs [spec] in this [BuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns a + * [State] that holds the result of the currently-active [BuildSpec]. */ -@ExperimentalFrpApi -fun <A> FrpBuildScope.rebuildOn(rebuildSignal: TFlow<*>, spec: FrpSpec<A>): TState<A> = +@ExperimentalKairosApi +fun <A> BuildScope.rebuildOn(rebuildSignal: Events<*>, spec: BuildSpec<A>): State<A> = rebuildSignal.map { spec }.holdLatestSpec(spec) /** - * Like [stateChanges] but also includes the old value of this [TState]. + * Like [changes] but also includes the old value of this [State]. * * Shorthand for: * ``` kotlin * stateChanges.map { WithPrev(previousValue = sample(), newValue = it) } * ``` */ -@ExperimentalFrpApi -val <A> TState<A>.transitions: TFlow<WithPrev<A, A>> - get() = stateChanges.map { WithPrev(previousValue = sample(), newValue = it) } +@ExperimentalKairosApi +val <A> State<A>.transitions: Events<WithPrev<A, A>> + get() = changes.map { WithPrev(previousValue = sample(), newValue = it) } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt new file mode 100644 index 000000000000..7e257f2831af --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import kotlinx.coroutines.CoroutineScope + +/** + * Scope for external side-effects triggered by the Kairos network. This still occurs within the + * context of a transaction, so general suspending calls are disallowed to prevent blocking the + * transaction. You can use [effectCoroutineScope] to [launch][kotlinx.coroutines.launch] new + * coroutines to perform long-running asynchronous work. This scope is alive for the duration of the + * containing [BuildScope] that this side-effect scope is running in. + */ +@ExperimentalKairosApi +interface EffectScope : TransactionScope { + /** + * A [CoroutineScope] whose lifecycle lives for as long as this [EffectScope] is alive. This is + * generally until the [Job][kotlinx.coroutines.Job] returned by [BuildScope.effect] is + * cancelled. + */ + @ExperimentalKairosApi val effectCoroutineScope: CoroutineScope + + /** + * A [KairosNetwork] instance that can be used to transactionally query / modify the Kairos + * network. + * + * The lambda passed to [KairosNetwork.transact] on this instance will receive an [BuildScope] + * that is lifetime-bound to this [EffectScope]. Once this [EffectScope] is no longer alive, any + * modifications to the Kairos network performed via this [KairosNetwork] instance will be + * undone (any registered [observers][BuildScope.observe] are unregistered, and any pending + * [side-effects][BuildScope.effect] are cancelled). + */ + @ExperimentalKairosApi val kairosNetwork: KairosNetwork +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt new file mode 100644 index 000000000000..bd9b45d3be4c --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt @@ -0,0 +1,577 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.internal.CompletableLazy +import com.android.systemui.kairos.internal.DemuxImpl +import com.android.systemui.kairos.internal.EventsImpl +import com.android.systemui.kairos.internal.Init +import com.android.systemui.kairos.internal.InitScope +import com.android.systemui.kairos.internal.InputNode +import com.android.systemui.kairos.internal.Network +import com.android.systemui.kairos.internal.NoScope +import com.android.systemui.kairos.internal.activated +import com.android.systemui.kairos.internal.cached +import com.android.systemui.kairos.internal.constInit +import com.android.systemui.kairos.internal.demuxMap +import com.android.systemui.kairos.internal.filterImpl +import com.android.systemui.kairos.internal.filterJustImpl +import com.android.systemui.kairos.internal.init +import com.android.systemui.kairos.internal.map +import com.android.systemui.kairos.internal.mapImpl +import com.android.systemui.kairos.internal.mergeNodes +import com.android.systemui.kairos.internal.mergeNodesLeft +import com.android.systemui.kairos.internal.neverImpl +import com.android.systemui.kairos.internal.switchDeferredImplSingle +import com.android.systemui.kairos.internal.switchPromptImplSingle +import com.android.systemui.kairos.internal.util.hashString +import com.android.systemui.kairos.util.Either +import com.android.systemui.kairos.util.Left +import com.android.systemui.kairos.util.Maybe +import com.android.systemui.kairos.util.Right +import com.android.systemui.kairos.util.just +import com.android.systemui.kairos.util.map +import com.android.systemui.kairos.util.toMaybe +import java.util.concurrent.atomic.AtomicReference +import kotlin.reflect.KProperty +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope + +/** A series of values of type [A] available at discrete points in time. */ +@ExperimentalKairosApi +sealed class Events<out A> { + companion object { + /** An [Events] with no values. */ + val empty: Events<Nothing> = EmptyFlow + } +} + +/** AN [Events] with no values. */ +@ExperimentalKairosApi val emptyEvents: Events<Nothing> = Events.empty + +/** + * A forward-reference to an [Events]. Useful for recursive definitions. + * + * This reference can be used like a standard [Events], but will throw an error if its [loopback] is + * unset before the end of the first transaction which accesses it. + */ +@ExperimentalKairosApi +class EventsLoop<A> : Events<A>() { + private val deferred = CompletableLazy<Events<A>>() + + internal val init: Init<EventsImpl<A>> = + init(name = null) { deferred.value.init.connect(evalScope = this) } + + /** The [Events] this reference is referring to. */ + var loopback: Events<A>? = null + set(value) { + value?.let { + check(!deferred.isInitialized()) { "TFlowLoop.loopback has already been set." } + deferred.setValue(value) + field = value + } + } + + operator fun getValue(thisRef: Any?, property: KProperty<*>): Events<A> = this + + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Events<A>) { + loopback = value + } + + override fun toString(): String = "${this::class.simpleName}@$hashString" +} + +/** + * Returns an [Events] that acts as a deferred-reference to the [Events] produced by this [Lazy]. + * + * When the returned [Events] is accessed by the Kairos network, the [Lazy]'s [value][Lazy.value] + * will be queried and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi fun <A> Lazy<Events<A>>.defer(): Events<A> = deferInline { value } + +/** + * Returns an [Events] that acts as a deferred-reference to the [Events] produced by this + * [DeferredValue]. + * + * When the returned [Events] is accessed by the Kairos network, the [DeferredValue] will be queried + * and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> DeferredValue<Events<A>>.defer(): Events<A> = deferInline { unwrapped.value } + +/** + * Returns an [Events] that acts as a deferred-reference to the [Events] produced by [block]. + * + * When the returned [Events] is accessed by the Kairos network, [block] will be invoked and the + * returned [Events] will be used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> deferredEvents(block: KairosScope.() -> Events<A>): Events<A> = deferInline { + NoScope.block() +} + +/** Returns an [Events] that emits the new value of this [State] when it changes. */ +@ExperimentalKairosApi +val <A> State<A>.changes: Events<A> + get() = EventsInit(init(name = null) { init.connect(evalScope = this).changes }) + +/** + * Returns an [Events] that contains only the [just] results of applying [transform] to each value + * of the original [Events]. + * + * @see mapNotNull + */ +@ExperimentalKairosApi +fun <A, B> Events<A>.mapMaybe(transform: TransactionScope.(A) -> Maybe<B>): Events<B> = + map(transform).filterJust() + +/** + * Returns an [Events] that contains only the non-null results of applying [transform] to each value + * of the original [Events]. + * + * @see mapMaybe + */ +@ExperimentalKairosApi +fun <A, B> Events<A>.mapNotNull(transform: TransactionScope.(A) -> B?): Events<B> = mapMaybe { + transform(it).toMaybe() +} + +/** Returns an [Events] containing only values of the original [Events] that are not null. */ +@ExperimentalKairosApi +fun <A> Events<A?>.filterNotNull(): Events<A> = mapCheap { it.toMaybe() }.filterJust() + +/** Shorthand for `mapNotNull { it as? A }`. */ +@ExperimentalKairosApi +inline fun <reified A> Events<*>.filterIsInstance(): Events<A> = + mapCheap { it as? A }.filterNotNull() + +/** Shorthand for `mapMaybe { it }`. */ +@ExperimentalKairosApi +fun <A> Events<Maybe<A>>.filterJust(): Events<A> = + EventsInit(constInit(name = null, filterJustImpl { init.connect(evalScope = this) })) + +/** + * Returns an [Events] containing the results of applying [transform] to each value of the original + * [Events]. + */ +@ExperimentalKairosApi +fun <A, B> Events<A>.map(transform: TransactionScope.(A) -> B): Events<B> { + val mapped: EventsImpl<B> = mapImpl({ init.connect(evalScope = this) }) { a, _ -> transform(a) } + return EventsInit(constInit(name = null, mapped.cached())) +} + +/** + * Like [map], but the emission is not cached during the transaction. Use only if [transform] is + * fast and pure. + * + * @see map + */ +@ExperimentalKairosApi +fun <A, B> Events<A>.mapCheap(transform: TransactionScope.(A) -> B): Events<B> = + EventsInit( + constInit(name = null, mapImpl({ init.connect(evalScope = this) }) { a, _ -> transform(a) }) + ) + +/** + * Returns an [Events] that invokes [action] before each value of the original [Events] is emitted. + * Useful for logging and debugging. + * + * ``` + * pulse.onEach { foo(it) } == pulse.map { foo(it); it } + * ``` + * + * Note that the side effects performed in [onEach] are only performed while the resulting [Events] + * is connected to an output of the Kairos network. If your goal is to reliably perform side effects + * in response to an [Events], use the output combinators available in [BuildScope], such as + * [BuildScope.toSharedFlow] or [BuildScope.observe]. + */ +@ExperimentalKairosApi +fun <A> Events<A>.onEach(action: TransactionScope.(A) -> Unit): Events<A> = map { + action(it) + it +} + +/** + * Returns an [Events] containing only values of the original [Events] that satisfy the given + * [predicate]. + */ +@ExperimentalKairosApi +fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> { + val pulse = filterImpl({ init.connect(evalScope = this) }) { predicate(it) } + return EventsInit(constInit(name = null, pulse)) +} + +/** + * Splits an [Events] of pairs into a pair of [Events], where each returned [Events] emits half of + * the original. + * + * Shorthand for: + * ```kotlin + * val lefts = map { it.first } + * val rights = map { it.second } + * return Pair(lefts, rights) + * ``` + */ +@ExperimentalKairosApi +fun <A, B> Events<Pair<A, B>>.unzip(): Pair<Events<A>, Events<B>> { + val lefts = map { it.first } + val rights = map { it.second } + return lefts to rights +} + +/** + * Merges the given [Events] into a single [Events] that emits events from both. + * + * Because [Events] can only emit one value per transaction, the provided [transformCoincidence] + * function is used to combine coincident emissions to produce the result value to be emitted by the + * merged [Events]. + */ +@ExperimentalKairosApi +fun <A> Events<A>.mergeWith( + other: Events<A>, + name: String? = null, + transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a }, +): Events<A> { + val node = + mergeNodes( + name = name, + getPulse = { init.connect(evalScope = this) }, + getOther = { other.init.connect(evalScope = this) }, + ) { a, b -> + transformCoincidence(a, b) + } + return EventsInit(constInit(name = null, node)) +} + +/** + * Merges the given [Events] into a single [Events] that emits events from all. All coincident + * emissions are collected into the emitted [List], preserving the input ordering. + * + * @see mergeWith + * @see mergeLeft + */ +@ExperimentalKairosApi +fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge() + +/** + * Merges the given [Events] into a single [Events] that emits events from all. In the case of + * coincident emissions, the emission from the left-most [Events] is emitted. + * + * @see merge + */ +@ExperimentalKairosApi +fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft() + +/** + * Merges the given [Events] into a single [Events] that emits events from all. + * + * Because [Events] can only emit one value per transaction, the provided [transformCoincidence] + * function is used to combine coincident emissions to produce the result value to be emitted by the + * merged [Events]. + */ +// TODO: can be optimized to avoid creating the intermediate list +fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> = + merge(*events).map { l -> l.reduce(transformCoincidence) } + +/** + * Merges the given [Events] into a single [Events] that emits events from all. All coincident + * emissions are collected into the emitted [List], preserving the input ordering. + * + * @see mergeWith + * @see mergeLeft + */ +@ExperimentalKairosApi +fun <A> Iterable<Events<A>>.merge(): Events<List<A>> = + EventsInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } })) + +/** + * Merges the given [Events] into a single [Events] that emits events from all. In the case of + * coincident emissions, the emission from the left-most [Events] is emitted. + * + * @see merge + */ +@ExperimentalKairosApi +fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> = + EventsInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } })) + +/** + * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are + * collected into the emitted [List], preserving the input ordering. + * + * @see mergeWith + */ +@ExperimentalKairosApi fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge() + +/** + * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are + * collected into the emitted [Map], and are given the same key of the associated [Events] in the + * input [Map]. + * + * @see mergeWith + */ +@ExperimentalKairosApi +fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> = + asSequence() + .map { (k, events) -> events.map { a -> k to a } } + .toList() + .merge() + .map { it.toMap() } + +/** + * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple + * downstream [Events]. + * + * The input [Events] emits [Map] instances that specify which downstream [Events] the associated + * value will be emitted from. These downstream [Events] can be obtained via + * [GroupedEvents.eventsForKey]. + * + * An example: + * ``` + * val fooEvents: Events<Map<String, Foo>> = ... + * val fooById: GroupedEvents<String, Foo> = fooEvents.groupByKey() + * val fooBar: Events<Foo> = fooById["bar"] + * ``` + * + * This is semantically equivalent to `val fooBar = fooEvents.mapNotNull { map -> map["bar"] }` but + * is significantly more efficient; specifically, using [mapNotNull] in this way incurs a `O(n)` + * performance hit, where `n` is the number of different [mapNotNull] operations used to filter on a + * specific key's presence in the emitted [Map]. [groupByKey] internally uses a [HashMap] to lookup + * the appropriate downstream [Events], and so operates in `O(1)`. + * + * Note that the returned [GroupedEvents] should be cached and re-used to gain the performance + * benefit. + * + * @see selector + */ +@ExperimentalKairosApi +fun <K, A> Events<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedEvents<K, A> = + GroupedEvents(demuxMap({ init.connect(this) }, numKeys)) + +/** + * Shorthand for `map { mapOf(extractKey(it) to it) }.groupByKey()` + * + * @see groupByKey + */ +@ExperimentalKairosApi +fun <K, A> Events<A>.groupBy( + numKeys: Int? = null, + extractKey: TransactionScope.(A) -> K, +): GroupedEvents<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys) + +/** + * Returns two new [Events] that contain elements from this [Events] that satisfy or don't satisfy + * [predicate]. + * + * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }` + * but is more efficient; specifically, [partition] will only invoke [predicate] once per element. + */ +@ExperimentalKairosApi +fun <A> Events<A>.partition( + predicate: TransactionScope.(A) -> Boolean +): Pair<Events<A>, Events<A>> { + val grouped: GroupedEvents<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate) + return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false)) +} + +/** + * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain + * [Left] values, and [Pair.second] will contain [Right] values. + * + * Using this is equivalent to using [filterIsInstance] in conjunction with [map] twice, once for + * [Left]s and once for [Right]s, but is slightly more efficient; specifically, the + * [filterIsInstance] check is only performed once per element. + */ +@ExperimentalKairosApi +fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> { + val (left, right) = partition { it is Left } + return Pair(left.mapCheap { (it as Left).value }, right.mapCheap { (it as Right).value }) +} + +/** + * A mapping from keys of type [K] to [Events] emitting values of type [A]. + * + * @see groupByKey + */ +@ExperimentalKairosApi +class GroupedEvents<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) { + /** + * Returns an [Events] that emits values of type [A] that correspond to the given [key]. + * + * @see groupByKey + */ + fun eventsForKey(key: K): Events<A> = EventsInit(constInit(name = null, impl.eventsForKey(key))) + + /** + * Returns an [Events] that emits values of type [A] that correspond to the given [key]. + * + * @see groupByKey + */ + operator fun get(key: K): Events<A> = eventsForKey(key) +} + +/** + * Returns an [Events] that switches to the [Events] contained within this [State] whenever it + * changes. + * + * This switch does take effect until the *next* transaction after [State] changes. For a switch + * that takes effect immediately, see [switchEventsPromptly]. + */ +@ExperimentalKairosApi +fun <A> State<Events<A>>.switchEvents(name: String? = null): Events<A> { + val patches = + mapImpl({ init.connect(this).changes }) { newFlow, _ -> newFlow.init.connect(this) } + return EventsInit( + constInit( + name = null, + switchDeferredImplSingle( + name = name, + getStorage = { + init.connect(this).getCurrentWithEpoch(this).first.init.connect(this) + }, + getPatches = { patches }, + ), + ) + ) +} + +/** + * Returns an [Events] that switches to the [Events] contained within this [State] whenever it + * changes. + * + * This switch takes effect immediately within the same transaction that [State] changes. In + * general, you should prefer [switchEvents] over this method. It is both safer and more performant. + */ +// TODO: parameter to handle coincidental emission from both old and new +@ExperimentalKairosApi +fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> { + val patches = + mapImpl({ init.connect(this).changes }) { newFlow, _ -> newFlow.init.connect(this) } + return EventsInit( + constInit( + name = null, + switchPromptImplSingle( + getStorage = { + init.connect(this).getCurrentWithEpoch(this).first.init.connect(this) + }, + getPatches = { patches }, + ), + ) + ) +} + +/** + * A mutable [Events] that provides the ability to [emit] values to the network, handling + * backpressure by coalescing all emissions into batches. + * + * @see KairosNetwork.coalescingMutableEvents + */ +@ExperimentalKairosApi +class CoalescingMutableEvents<in In, Out> +internal constructor( + internal val name: String?, + internal val coalesce: (old: Lazy<Out>, new: In) -> Out, + internal val network: Network, + private val getInitialValue: () -> Out, + internal val impl: InputNode<Out> = InputNode(), +) : Events<Out>() { + internal val storage = AtomicReference(false to lazy { getInitialValue() }) + + override fun toString(): String = "${this::class.simpleName}@$hashString" + + /** + * Inserts [value] into the current batch, enqueueing it for emission from this [Events] if not + * already pending. + * + * Backpressure occurs when [emit] is called while the Kairos network is currently in a + * transaction; if called multiple times, then emissions will be coalesced into a single batch + * that is then processed when the network is ready. + */ + fun emit(value: In) { + val (scheduled, _) = + storage.getAndUpdate { (_, batch) -> true to CompletableLazy(coalesce(batch, value)) } + if (!scheduled) { + @Suppress("DeferredResultUnused") + network.transaction( + "CoalescingMutableEvents${name?.let { "($name)" }.orEmpty()}.emit" + ) { + val (_, batch) = storage.getAndSet(false to lazy { getInitialValue() }) + impl.visit(this, batch.value) + } + } + } +} + +/** + * A mutable [Events] that provides the ability to [emit] values to the network, handling + * backpressure by suspending the emitter. + * + * @see KairosNetwork.coalescingMutableEvents + */ +@ExperimentalKairosApi +class MutableEvents<T> +internal constructor(internal val network: Network, internal val impl: InputNode<T> = InputNode()) : + Events<T>() { + internal val name: String? = null + + private val storage = AtomicReference<Job?>(null) + + override fun toString(): String = "${this::class.simpleName}@$hashString" + + /** + * Emits a [value] to this [Events], suspending the caller until the Kairos transaction + * containing the emission has completed. + */ + suspend fun emit(value: T) { + coroutineScope { + var jobOrNull: Job? = null + val newEmit = + async(start = CoroutineStart.LAZY) { + jobOrNull?.join() + network.transaction("MutableEvents.emit") { impl.visit(this, value) }.await() + } + jobOrNull = storage.getAndSet(newEmit) + newEmit.await() + } + } +} + +private data object EmptyFlow : Events<Nothing>() + +internal class EventsInit<out A>(val init: Init<EventsImpl<A>>) : Events<A>() { + override fun toString(): String = "${this::class.simpleName}@$hashString" +} + +internal val <A> Events<A>.init: Init<EventsImpl<A>> + get() = + when (this) { + is EmptyFlow -> constInit("EmptyFlow", neverImpl) + is EventsInit -> init + is EventsLoop -> init + is CoalescingMutableEvents<*, A> -> constInit(name, impl.activated()) + is MutableEvents -> constInit(name, impl.activated()) + } + +private inline fun <A> deferInline(crossinline block: InitScope.() -> Events<A>): Events<A> = + EventsInit(init(name = null) { block().init.connect(evalScope = this) }) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpBuildScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpBuildScope.kt deleted file mode 100644 index 209a402bd629..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpBuildScope.kt +++ /dev/null @@ -1,885 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:OptIn(ExperimentalCoroutinesApi::class) - -package com.android.systemui.kairos - -import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.just -import com.android.systemui.kairos.util.map -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.RestrictsSuspension -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.dropWhile -import kotlinx.coroutines.flow.scan -import kotlinx.coroutines.launch - -/** A function that modifies the FrpNetwork. */ -typealias FrpSpec<A> = suspend FrpBuildScope.() -> A - -/** - * Constructs an [FrpSpec]. The passed [block] will be invoked with an [FrpBuildScope] that can be - * used to perform network-building operations, including adding new inputs and outputs to the - * network, as well as all operations available in [FrpTransactionScope]. - */ -@ExperimentalFrpApi -@Suppress("NOTHING_TO_INLINE") -inline fun <A> frpSpec(noinline block: suspend FrpBuildScope.() -> A): FrpSpec<A> = block - -/** Applies the [FrpSpec] within this [FrpBuildScope]. */ -@ExperimentalFrpApi -inline operator fun <A> FrpBuildScope.invoke(block: FrpBuildScope.() -> A) = run(block) - -/** Operations that add inputs and outputs to an FRP network. */ -@ExperimentalFrpApi -@RestrictsSuspension -interface FrpBuildScope : FrpStateScope { - - /** TODO: Javadoc */ - @ExperimentalFrpApi - fun <R> deferredBuildScope(block: suspend FrpBuildScope.() -> R): FrpDeferredValue<R> - - /** TODO: Javadoc */ - @ExperimentalFrpApi fun deferredBuildScopeAction(block: suspend FrpBuildScope.() -> Unit) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * Unlike [mapLatestBuild], these modifications are not undone with each subsequent emission of - * the original [TFlow]. - * - * **NOTE:** This API does not [observe] the original [TFlow], meaning that unless the returned - * (or a downstream) [TFlow] is observed separately, [transform] will not be invoked, and no - * internal side-effects will occur. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapBuild(transform: suspend FrpBuildScope.(A) -> B): TFlow<B> - - /** - * Invokes [block] whenever this [TFlow] emits a value, allowing side-effects to be safely - * performed in reaction to the emission. - * - * Specifically, [block] is deferred to the end of the transaction, and is only actually - * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a - * -Latest combinator, for example. - * - * Shorthand for: - * ```kotlin - * tFlow.observe { effect { ... } } - * ``` - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.observe( - coroutineContext: CoroutineContext = EmptyCoroutineContext, - block: suspend FrpEffectScope.(A) -> Unit = {}, - ): Job - - /** - * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original - * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpecs] - * immediately. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same - * key are undone (any registered [observers][observe] are unregistered, and any pending - * [side-effects][effect] are cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpSpec] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey( - initialSpecs: FrpDeferredValue<Map<K, FrpSpec<B>>>, - numKeys: Int? = null, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> - - /** - * Creates an instance of a [TFlow] with elements that are from [builder]. - * - * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the - * provided [MutableTFlow]. - * - * By default, [builder] is only running while the returned [TFlow] is being - * [observed][observe]. If you want it to run at all times, simply add a no-op observer: - * ```kotlin - * tFlow { ... }.apply { observe() } - * ``` - */ - @ExperimentalFrpApi fun <T> tFlow(builder: suspend FrpProducerScope<T>.() -> Unit): TFlow<T> - - /** - * Creates an instance of a [TFlow] with elements that are emitted from [builder]. - * - * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the - * provided [MutableTFlow]. - * - * By default, [builder] is only running while the returned [TFlow] is being - * [observed][observe]. If you want it to run at all times, simply add a no-op observer: - * ```kotlin - * tFlow { ... }.apply { observe() } - * ``` - * - * In the event of backpressure, emissions are *coalesced* into batches. When a value is - * [emitted][FrpCoalescingProducerScope.emit] from [builder], it is merged into the batch via - * [coalesce]. Once the batch is consumed by the frp network in the next transaction, the batch - * is reset back to [getInitialValue]. - */ - @ExperimentalFrpApi - fun <In, Out> coalescingTFlow( - getInitialValue: () -> Out, - coalesce: (old: Out, new: In) -> Out, - builder: suspend FrpCoalescingProducerScope<In>.() -> Unit, - ): TFlow<Out> - - /** - * Creates a new [FrpBuildScope] that is a child of this one. - * - * This new scope can be manually cancelled via the returned [Job], or will be cancelled - * automatically when its parent is cancelled. Cancellation will unregister all - * [observers][observe] and cancel all scheduled [effects][effect]. - * - * The return value from [block] can be accessed via the returned [FrpDeferredValue]. - */ - @ExperimentalFrpApi fun <A> asyncScope(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job> - - // TODO: once we have context params, these can all become extensions: - - /** - * Returns a [TFlow] containing the results of applying the given [transform] function to each - * value of the original [TFlow]. - * - * Unlike [TFlow.map], [transform] can perform arbitrary asynchronous code. This code is run - * outside of the current FRP transaction; when [transform] returns, the returned value is - * emitted from the result [TFlow] in a new transaction. - * - * Shorthand for: - * ```kotlin - * tflow.mapLatestBuild { a -> asyncTFlow { transform(a) } }.flatten() - * ``` - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapAsyncLatest(transform: suspend (A) -> B): TFlow<B> = - mapLatestBuild { a -> asyncTFlow { transform(a) } }.flatten() - - /** - * Invokes [block] whenever this [TFlow] emits a value. [block] receives an [FrpBuildScope] that - * can be used to make further modifications to the FRP network, and/or perform side-effects via - * [effect]. - * - * @see observe - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.observeBuild(block: suspend FrpBuildScope.(A) -> Unit = {}): Job = - mapBuild(block).observe() - - /** - * Returns a [StateFlow] whose [value][StateFlow.value] tracks the current - * [value of this TState][TState.sample], and will emit at the same rate as - * [TState.stateChanges]. - * - * Note that the [value][StateFlow.value] is not available until the *end* of the current - * transaction. If you need the current value before this time, then use [TState.sample]. - */ - @ExperimentalFrpApi - fun <A> TState<A>.toStateFlow(): StateFlow<A> { - val uninitialized = Any() - var initialValue: Any? = uninitialized - val innerStateFlow = MutableStateFlow<Any?>(uninitialized) - deferredBuildScope { - initialValue = sample() - stateChanges.observe { - innerStateFlow.value = it - initialValue = null - } - } - - @Suppress("UNCHECKED_CAST") - fun getValue(innerValue: Any?): A = - when { - innerValue !== uninitialized -> innerValue as A - initialValue !== uninitialized -> initialValue as A - else -> - error( - "Attempted to access StateFlow.value before FRP transaction has completed." - ) - } - - return object : StateFlow<A> { - override val replayCache: List<A> - get() = innerStateFlow.replayCache.map(::getValue) - - override val value: A - get() = getValue(innerStateFlow.value) - - override suspend fun collect(collector: FlowCollector<A>): Nothing { - innerStateFlow.collect { collector.emit(getValue(it)) } - } - } - } - - /** - * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits the current - * [value][TState.sample] of this [TState] followed by all [stateChanges]. - */ - @ExperimentalFrpApi - fun <A> TState<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> { - val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1) - deferredBuildScope { - result.tryEmit(sample()) - stateChanges.observe { a -> result.tryEmit(a) } - } - return result - } - - /** - * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits values - * whenever this [TFlow] emits. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> { - val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1) - observe { a -> result.tryEmit(a) } - return result - } - - /** - * Returns a [TState] that holds onto the value returned by applying the most recently emitted - * [FrpSpec] from the original [TFlow], or the value returned by applying [initialSpec] if - * nothing has been emitted since it was constructed. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A> TFlow<FrpSpec<A>>.holdLatestSpec(initialSpec: FrpSpec<A>): TState<A> { - val (changes: TFlow<A>, initApplied: FrpDeferredValue<A>) = applyLatestSpec(initialSpec) - return changes.holdDeferred(initApplied) - } - - /** - * Returns a [TState] containing the value returned by applying the [FrpSpec] held by the - * original [TState]. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A> TState<FrpSpec<A>>.applyLatestSpec(): TState<A> { - val (appliedChanges: TFlow<A>, init: FrpDeferredValue<A>) = - stateChanges.applyLatestSpec(frpSpec { sample().applySpec() }) - return appliedChanges.holdDeferred(init) - } - - /** - * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original - * [TFlow]. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A> TFlow<FrpSpec<A>>.applyLatestSpec(): TFlow<A> = applyLatestSpec(frpSpec {}).first - - /** - * Returns a [TFlow] that switches to a new [TFlow] produced by [transform] every time the - * original [TFlow] emits a value. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * When the original [TFlow] emits a new value, those changes are undone (any registered - * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.flatMapLatestBuild( - transform: suspend FrpBuildScope.(A) -> TFlow<B> - ): TFlow<B> = mapCheap { frpSpec { transform(it) } }.applyLatestSpec().flatten() - - /** - * Returns a [TState] by applying [transform] to the value held by the original [TState]. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * When the value held by the original [TState] changes, those changes are undone (any - * registered [observers][observe] are unregistered, and any pending [effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TState<A>.flatMapLatestBuild( - transform: suspend FrpBuildScope.(A) -> TState<B> - ): TState<B> = mapLatestBuild { transform(it) }.flatten() - - /** - * Returns a [TState] that transforms the value held inside this [TState] by applying it to the - * [transform]. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * When the value held by the original [TState] changes, those changes are undone (any - * registered [observers][observe] are unregistered, and any pending [effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TState<A>.mapLatestBuild(transform: suspend FrpBuildScope.(A) -> B): TState<B> = - mapCheapUnsafe { frpSpec { transform(it) } }.applyLatestSpec() - - /** - * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original - * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpec] - * immediately. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A : Any?, B> TFlow<FrpSpec<B>>.applyLatestSpec( - initialSpec: FrpSpec<A> - ): Pair<TFlow<B>, FrpDeferredValue<A>> { - val (flow, result) = - mapCheap { spec -> mapOf(Unit to just(spec)) } - .applyLatestSpecForKey(initialSpecs = mapOf(Unit to initialSpec), numKeys = 1) - val outFlow: TFlow<B> = - flow.mapMaybe { - checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" } - } - val outInit: FrpDeferredValue<A> = deferredBuildScope { - val initResult: Map<Unit, A> = result.get() - check(Unit in initResult) { - "applyLatest: expected initial result, but none present in: $initResult" - } - @Suppress("UNCHECKED_CAST") - initResult.getOrDefault(Unit) { null } as A - } - return Pair(outFlow, outInit) - } - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapLatestBuild(transform: suspend FrpBuildScope.(A) -> B): TFlow<B> = - mapCheap { frpSpec { transform(it) } }.applyLatestSpec() - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValue] immediately. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapLatestBuild( - initialValue: A, - transform: suspend FrpBuildScope.(A) -> B, - ): Pair<TFlow<B>, FrpDeferredValue<B>> = - mapLatestBuildDeferred(deferredOf(initialValue), transform) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValue] immediately. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapLatestBuildDeferred( - initialValue: FrpDeferredValue<A>, - transform: suspend FrpBuildScope.(A) -> B, - ): Pair<TFlow<B>, FrpDeferredValue<B>> = - mapCheap { frpSpec { transform(it) } } - .applyLatestSpec(initialSpec = frpSpec { transform(initialValue.get()) }) - - /** - * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original - * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpecs] - * immediately. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same - * key are undone (any registered [observers][observe] are unregistered, and any pending - * [side-effects][effect] are cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpSpec] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey( - initialSpecs: Map<K, FrpSpec<B>>, - numKeys: Int? = null, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> = - applyLatestSpecForKey(deferredOf(initialSpecs), numKeys) - - /** - * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original - * [TFlow]. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same - * key are undone (any registered [observers][observe] are unregistered, and any pending - * [side-effects][effect] are cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpSpec] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey( - numKeys: Int? = null - ): TFlow<Map<K, Maybe<A>>> = - applyLatestSpecForKey<K, A, Nothing>(deferredOf(emptyMap()), numKeys).first - - /** - * Returns a [TState] containing the latest results of applying each [FrpSpec] emitted from the - * original [TFlow]. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same - * key are undone (any registered [observers][observe] are unregistered, and any pending - * [side-effects][effect] are cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpSpec] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.holdLatestSpecForKey( - initialSpecs: FrpDeferredValue<Map<K, FrpSpec<A>>>, - numKeys: Int? = null, - ): TState<Map<K, A>> { - val (changes, initialValues) = applyLatestSpecForKey(initialSpecs, numKeys) - return changes.foldMapIncrementally(initialValues) - } - - /** - * Returns a [TState] containing the latest results of applying each [FrpSpec] emitted from the - * original [TFlow]. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same - * key are undone (any registered [observers][observe] are unregistered, and any pending - * [side-effects][effect] are cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpSpec] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.holdLatestSpecForKey( - initialSpecs: Map<K, FrpSpec<A>> = emptyMap(), - numKeys: Int? = null, - ): TState<Map<K, A>> = holdLatestSpecForKey(deferredOf(initialSpecs), numKeys) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValues] immediately. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpBuildScope] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey( - initialValues: FrpDeferredValue<Map<K, A>>, - numKeys: Int? = null, - transform: suspend FrpBuildScope.(A) -> B, - ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> = - map { patch -> patch.mapValues { (_, v) -> v.map { frpSpec { transform(it) } } } } - .applyLatestSpecForKey( - deferredBuildScope { - initialValues.get().mapValues { (_, v) -> frpSpec { transform(v) } } - }, - numKeys = numKeys, - ) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValues] immediately. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpBuildScope] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey( - initialValues: Map<K, A>, - numKeys: Int? = null, - transform: suspend FrpBuildScope.(A) -> B, - ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> = - mapLatestBuildForKey(deferredOf(initialValues), numKeys, transform) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpBuildScope] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey( - numKeys: Int? = null, - transform: suspend FrpBuildScope.(A) -> B, - ): TFlow<Map<K, Maybe<B>>> = mapLatestBuildForKey(emptyMap(), numKeys, transform).first - - /** Returns a [Deferred] containing the next value to be emitted from this [TFlow]. */ - @ExperimentalFrpApi - fun <R> TFlow<R>.nextDeferred(): Deferred<R> { - lateinit var next: CompletableDeferred<R> - val job = nextOnly().observe { next.complete(it) } - next = CompletableDeferred<R>(parent = job) - return next - } - - /** Returns a [TState] that reflects the [StateFlow.value] of this [StateFlow]. */ - @ExperimentalFrpApi - fun <A> StateFlow<A>.toTState(): TState<A> { - val initial = value - return tFlow { dropWhile { it == initial }.collect { emit(it) } }.hold(initial) - } - - /** Returns a [TFlow] that emits whenever this [Flow] emits. */ - @ExperimentalFrpApi fun <A> Flow<A>.toTFlow(): TFlow<A> = tFlow { collect { emit(it) } } - - /** - * Shorthand for: - * ```kotlin - * flow.toTFlow().hold(initialValue) - * ``` - */ - @ExperimentalFrpApi - fun <A> Flow<A>.toTState(initialValue: A): TState<A> = toTFlow().hold(initialValue) - - /** - * Shorthand for: - * ```kotlin - * flow.scan(initialValue, operation).toTFlow().hold(initialValue) - * ``` - */ - @ExperimentalFrpApi - fun <A, B> Flow<A>.scanToTState(initialValue: B, operation: (B, A) -> B): TState<B> = - scan(initialValue, operation).toTFlow().hold(initialValue) - - /** - * Shorthand for: - * ```kotlin - * flow.scan(initialValue) { a, f -> f(a) }.toTFlow().hold(initialValue) - * ``` - */ - @ExperimentalFrpApi - fun <A> Flow<(A) -> A>.scanToTState(initialValue: A): TState<A> = - scanToTState(initialValue) { a, f -> f(a) } - - /** - * Invokes [block] whenever this [TFlow] emits a value. [block] receives an [FrpBuildScope] that - * can be used to make further modifications to the FRP network, and/or perform side-effects via - * [effect]. - * - * With each invocation of [block], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.observeLatestBuild(block: suspend FrpBuildScope.(A) -> Unit = {}): Job = - mapLatestBuild { block(it) }.observe() - - /** - * Invokes [block] whenever this [TFlow] emits a value, allowing side-effects to be safely - * performed in reaction to the emission. - * - * With each invocation of [block], running effects from the previous invocation are cancelled. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.observeLatest(block: suspend FrpEffectScope.(A) -> Unit = {}): Job { - var innerJob: Job? = null - return observeBuild { - innerJob?.cancel() - innerJob = effect { block(it) } - } - } - - /** - * Invokes [block] with the value held by this [TState], allowing side-effects to be safely - * performed in reaction to the state changing. - * - * With each invocation of [block], running effects from the previous invocation are cancelled. - */ - @ExperimentalFrpApi - fun <A> TState<A>.observeLatest(block: suspend FrpEffectScope.(A) -> Unit = {}): Job = - launchScope { - var innerJob = effect { block(sample()) } - stateChanges.observeBuild { - innerJob.cancel() - innerJob = effect { block(it) } - } - } - - /** - * Applies [block] to the value held by this [TState]. [block] receives an [FrpBuildScope] that - * can be used to make further modifications to the FRP network, and/or perform side-effects via - * [effect]. - * - * [block] can perform modifications to the FRP network via its [FrpBuildScope] receiver. With - * each invocation of [block], changes from the previous invocation are undone (any registered - * [observers][observe] are unregistered, and any pending [side-effects][effect] are cancelled). - */ - @ExperimentalFrpApi - fun <A> TState<A>.observeLatestBuild(block: suspend FrpBuildScope.(A) -> Unit = {}): Job = - launchScope { - var innerJob: Job = launchScope { block(sample()) } - stateChanges.observeBuild { - innerJob.cancel() - innerJob = launchScope { block(it) } - } - } - - /** Applies the [FrpSpec] within this [FrpBuildScope]. */ - @ExperimentalFrpApi suspend fun <A> FrpSpec<A>.applySpec(): A = this() - - /** - * Applies the [FrpSpec] within this [FrpBuildScope], returning the result as an - * [FrpDeferredValue]. - */ - @ExperimentalFrpApi - fun <A> FrpSpec<A>.applySpecDeferred(): FrpDeferredValue<A> = deferredBuildScope { applySpec() } - - /** - * Invokes [block] on the value held in this [TState]. [block] receives an [FrpBuildScope] that - * can be used to make further modifications to the FRP network, and/or perform side-effects via - * [effect]. - */ - @ExperimentalFrpApi - fun <A> TState<A>.observeBuild(block: suspend FrpBuildScope.(A) -> Unit = {}): Job = - launchScope { - block(sample()) - stateChanges.observeBuild(block) - } - - /** - * Invokes [block] with the current value of this [TState], re-invoking whenever it changes, - * allowing side-effects to be safely performed in reaction value changing. - * - * Specifically, [block] is deferred to the end of the transaction, and is only actually - * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a - * -Latest combinator, for example. - * - * If the [TState] is changing within the *current* transaction (i.e. [stateChanges] is - * presently emitting) then [block] will be invoked for the first time with the new value; - * otherwise, it will be invoked with the [current][sample] value. - */ - @ExperimentalFrpApi - fun <A> TState<A>.observe(block: suspend FrpEffectScope.(A) -> Unit = {}): Job = - now.map { sample() }.mergeWith(stateChanges) { _, new -> new }.observe { block(it) } -} - -/** - * Returns a [TFlow] that emits the result of [block] once it completes. [block] is evaluated - * outside of the current FRP transaction; when it completes, the returned [TFlow] emits in a new - * transaction. - * - * Shorthand for: - * ``` - * tFlow { emitter: MutableTFlow<A> -> - * val a = block() - * emitter.emit(a) - * } - * ``` - */ -@ExperimentalFrpApi -fun <A> FrpBuildScope.asyncTFlow(block: suspend () -> A): TFlow<A> = - tFlow { - // TODO: if block completes synchronously, it would be nice to emit within this - // transaction - emit(block()) - } - .apply { observe() } - -/** - * Performs a side-effect in a safe manner w/r/t the current FRP transaction. - * - * Specifically, [block] is deferred to the end of the current transaction, and is only actually - * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a - * -Latest combinator, for example. - * - * Shorthand for: - * ```kotlin - * now.observe { block() } - * ``` - */ -@ExperimentalFrpApi -fun FrpBuildScope.effect(block: suspend FrpEffectScope.() -> Unit): Job = now.observe { block() } - -/** - * Launches [block] in a new coroutine, returning a [Job] bound to the coroutine. - * - * This coroutine is not actually started until the *end* of the current FRP transaction. This is - * done because the current [FrpBuildScope] might be deactivated within this transaction, perhaps - * due to a -Latest combinator. If this happens, then the coroutine will never actually be started. - * - * Shorthand for: - * ```kotlin - * effect { frpCoroutineScope.launch { block() } } - * ``` - */ -@ExperimentalFrpApi -fun FrpBuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asyncEffect(block) - -/** - * Launches [block] in a new coroutine, returning the result as a [Deferred]. - * - * This coroutine is not actually started until the *end* of the current FRP transaction. This is - * done because the current [FrpBuildScope] might be deactivated within this transaction, perhaps - * due to a -Latest combinator. If this happens, then the coroutine will never actually be started. - * - * Shorthand for: - * ```kotlin - * CompletableDeferred<R>.apply { - * effect { frpCoroutineScope.launch { complete(coroutineScope { block() }) } } - * } - * .await() - * ``` - */ -@ExperimentalFrpApi -fun <R> FrpBuildScope.asyncEffect(block: suspend CoroutineScope.() -> R): Deferred<R> { - val result = CompletableDeferred<R>() - val job = now.observe { frpCoroutineScope.launch { result.complete(coroutineScope(block)) } } - val handle = job.invokeOnCompletion { result.cancel() } - result.invokeOnCompletion { - handle.dispose() - job.cancel() - } - return result -} - -/** Like [FrpBuildScope.asyncScope], but ignores the result of [block]. */ -@ExperimentalFrpApi fun FrpBuildScope.launchScope(block: FrpSpec<*>): Job = asyncScope(block).second - -/** - * Creates an instance of a [TFlow] with elements that are emitted from [builder]. - * - * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided - * [MutableTFlow]. - * - * By default, [builder] is only running while the returned [TFlow] is being - * [observed][FrpBuildScope.observe]. If you want it to run at all times, simply add a no-op - * observer: - * ```kotlin - * tFlow { ... }.apply { observe() } - * ``` - * - * In the event of backpressure, emissions are *coalesced* into batches. When a value is - * [emitted][FrpCoalescingProducerScope.emit] from [builder], it is merged into the batch via - * [coalesce]. Once the batch is consumed by the FRP network in the next transaction, the batch is - * reset back to [initialValue]. - */ -@ExperimentalFrpApi -fun <In, Out> FrpBuildScope.coalescingTFlow( - initialValue: Out, - coalesce: (old: Out, new: In) -> Out, - builder: suspend FrpCoalescingProducerScope<In>.() -> Unit, -): TFlow<Out> = coalescingTFlow(getInitialValue = { initialValue }, coalesce, builder) - -/** - * Creates an instance of a [TFlow] with elements that are emitted from [builder]. - * - * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided - * [MutableTFlow]. - * - * By default, [builder] is only running while the returned [TFlow] is being - * [observed][FrpBuildScope.observe]. If you want it to run at all times, simply add a no-op - * observer: - * ```kotlin - * tFlow { ... }.apply { observe() } - * ``` - * - * In the event of backpressure, emissions are *conflated*; any older emissions are dropped and only - * the most recent emission will be used when the FRP network is ready. - */ -@ExperimentalFrpApi -fun <T> FrpBuildScope.conflatedTFlow( - builder: suspend FrpCoalescingProducerScope<T>.() -> Unit -): TFlow<T> = - coalescingTFlow<T, Any?>(initialValue = Any(), coalesce = { _, new -> new }, builder = builder) - .mapCheap { - @Suppress("UNCHECKED_CAST") - it as T - } - -/** Scope for emitting to a [FrpBuildScope.coalescingTFlow]. */ -interface FrpCoalescingProducerScope<in T> { - /** - * Inserts [value] into the current batch, enqueueing it for emission from this [TFlow] if not - * already pending. - * - * Backpressure occurs when [emit] is called while the FRP network is currently in a - * transaction; if called multiple times, then emissions will be coalesced into a single batch - * that is then processed when the network is ready. - */ - fun emit(value: T) -} - -/** Scope for emitting to a [FrpBuildScope.tFlow]. */ -interface FrpProducerScope<in T> { - /** - * Emits a [value] to this [TFlow], suspending the caller until the FRP transaction containing - * the emission has completed. - */ - suspend fun emit(value: T) -} - -/** - * Suspends forever. Upon cancellation, runs [block]. Useful for unregistering callbacks inside of - * [FrpBuildScope.tFlow] and [FrpBuildScope.coalescingTFlow]. - */ -suspend fun awaitClose(block: () -> Unit): Nothing = - try { - awaitCancellation() - } finally { - block() - } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpEffectScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpEffectScope.kt deleted file mode 100644 index b39dcc131b1d..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpEffectScope.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.kairos - -import kotlin.coroutines.RestrictsSuspension -import kotlinx.coroutines.CoroutineScope - -/** - * Scope for external side-effects triggered by the Frp network. This still occurs within the - * context of a transaction, so general suspending calls are disallowed to prevent blocking the - * transaction. You can use [frpCoroutineScope] to [launch][kotlinx.coroutines.launch] new - * coroutines to perform long-running asynchronous work. This scope is alive for the duration of the - * containing [FrpBuildScope] that this side-effect scope is running in. - */ -@RestrictsSuspension -@ExperimentalFrpApi -interface FrpEffectScope : FrpTransactionScope { - /** - * A [CoroutineScope] whose lifecycle lives for as long as this [FrpEffectScope] is alive. This - * is generally until the [Job][kotlinx.coroutines.Job] returned by [FrpBuildScope.effect] is - * cancelled. - */ - @ExperimentalFrpApi val frpCoroutineScope: CoroutineScope - - /** - * A [FrpNetwork] instance that can be used to transactionally query / modify the FRP network. - * - * The lambda passed to [FrpNetwork.transact] on this instance will receive an [FrpBuildScope] - * that is lifetime-bound to this [FrpEffectScope]. Once this [FrpEffectScope] is no longer - * alive, any modifications to the FRP network performed via this [FrpNetwork] instance will be - * undone (any registered [observers][FrpBuildScope.observe] are unregistered, and any pending - * [side-effects][FrpBuildScope.effect] are cancelled). - */ - @ExperimentalFrpApi val frpNetwork: FrpNetwork -} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpNetwork.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpNetwork.kt deleted file mode 100644 index cec76886c06d..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpNetwork.kt +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.kairos - -import com.android.systemui.kairos.internal.BuildScopeImpl -import com.android.systemui.kairos.internal.Network -import com.android.systemui.kairos.internal.StateScopeImpl -import com.android.systemui.kairos.internal.util.awaitCancellationAndThen -import com.android.systemui.kairos.internal.util.childScope -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.coroutineContext -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.job -import kotlinx.coroutines.launch - -/** - * Marks declarations that are still **experimental** and shouldn't be used in general production - * code. - */ -@RequiresOptIn( - message = "This API is experimental and should not be used in general production code." -) -@Retention(AnnotationRetention.BINARY) -annotation class ExperimentalFrpApi - -/** - * External interface to an FRP network. Can be used to make transactional queries and modifications - * to the network. - */ -@ExperimentalFrpApi -interface FrpNetwork { - /** - * Runs [block] inside of a transaction, suspending until the transaction is complete. - * - * The [FrpBuildScope] receiver exposes methods that can be used to query or modify the network. - * If the network is cancelled while the caller of [transact] is suspended, then the call will - * be cancelled. - */ - @ExperimentalFrpApi suspend fun <R> transact(block: suspend FrpTransactionScope.() -> R): R - - /** - * Activates [spec] in a transaction, suspending indefinitely. While suspended, all observers - * and long-running effects are kept alive. When cancelled, observers are unregistered and - * effects are cancelled. - */ - @ExperimentalFrpApi suspend fun activateSpec(spec: FrpSpec<*>) - - /** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */ - @ExperimentalFrpApi - fun <In, Out> coalescingMutableTFlow( - coalesce: (old: Out, new: In) -> Out, - getInitialValue: () -> Out, - ): CoalescingMutableTFlow<In, Out> - - /** Returns a [MutableTFlow] that can emit values into this [FrpNetwork]. */ - @ExperimentalFrpApi fun <T> mutableTFlow(): MutableTFlow<T> - - /** Returns a [MutableTState]. with initial state [initialValue]. */ - @ExperimentalFrpApi - fun <T> mutableTStateDeferred(initialValue: FrpDeferredValue<T>): MutableTState<T> -} - -/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */ -@ExperimentalFrpApi -fun <In, Out> FrpNetwork.coalescingMutableTFlow( - coalesce: (old: Out, new: In) -> Out, - initialValue: Out, -): CoalescingMutableTFlow<In, Out> = - coalescingMutableTFlow(coalesce, getInitialValue = { initialValue }) - -/** Returns a [MutableTState]. with initial state [initialValue]. */ -@ExperimentalFrpApi -fun <T> FrpNetwork.mutableTState(initialValue: T): MutableTState<T> = - mutableTStateDeferred(deferredOf(initialValue)) - -/** Returns a [MutableTState]. with initial state [initialValue]. */ -@ExperimentalFrpApi -fun <T> MutableTState(network: FrpNetwork, initialValue: T): MutableTState<T> = - network.mutableTState(initialValue) - -/** Returns a [MutableTFlow] that can emit values into this [FrpNetwork]. */ -@ExperimentalFrpApi -fun <T> MutableTFlow(network: FrpNetwork): MutableTFlow<T> = network.mutableTFlow() - -/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */ -@ExperimentalFrpApi -fun <In, Out> CoalescingMutableTFlow( - network: FrpNetwork, - coalesce: (old: Out, new: In) -> Out, - initialValue: Out, -): CoalescingMutableTFlow<In, Out> = network.coalescingMutableTFlow(coalesce) { initialValue } - -/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */ -@ExperimentalFrpApi -fun <In, Out> CoalescingMutableTFlow( - network: FrpNetwork, - coalesce: (old: Out, new: In) -> Out, - getInitialValue: () -> Out, -): CoalescingMutableTFlow<In, Out> = network.coalescingMutableTFlow(coalesce, getInitialValue) - -/** - * Activates [spec] in a transaction and invokes [block] with the result, suspending indefinitely. - * While suspended, all observers and long-running effects are kept alive. When cancelled, observers - * are unregistered and effects are cancelled. - */ -@ExperimentalFrpApi -suspend fun <R> FrpNetwork.activateSpec(spec: FrpSpec<R>, block: suspend (R) -> Unit) { - activateSpec { - val result = spec.applySpec() - launchEffect { block(result) } - } -} - -internal class LocalFrpNetwork( - private val network: Network, - private val scope: CoroutineScope, - private val endSignal: TFlow<Any>, -) : FrpNetwork { - override suspend fun <R> transact(block: suspend FrpTransactionScope.() -> R): R = - network.transaction("FrpNetwork.transact") { runInTransactionScope { block() } }.await() - - override suspend fun activateSpec(spec: FrpSpec<*>) { - val job = - network - .transaction("FrpNetwork.activateSpec") { - val buildScope = - BuildScopeImpl( - stateScope = StateScopeImpl(evalScope = this, endSignal = endSignal), - coroutineScope = scope, - ) - buildScope.runInBuildScope { launchScope(spec) } - } - .await() - awaitCancellationAndThen { job.cancel() } - } - - override fun <In, Out> coalescingMutableTFlow( - coalesce: (old: Out, new: In) -> Out, - getInitialValue: () -> Out, - ): CoalescingMutableTFlow<In, Out> = - CoalescingMutableTFlow(null, coalesce, network, getInitialValue) - - override fun <T> mutableTFlow(): MutableTFlow<T> = MutableTFlow(network) - - override fun <T> mutableTStateDeferred(initialValue: FrpDeferredValue<T>): MutableTState<T> = - MutableTState(network, initialValue.unwrapped) -} - -/** - * Combination of an [FrpNetwork] and a [Job] that, when cancelled, will cancel the entire FRP - * network. - */ -@ExperimentalFrpApi -class RootFrpNetwork -internal constructor(private val network: Network, private val scope: CoroutineScope, job: Job) : - Job by job, FrpNetwork by LocalFrpNetwork(network, scope, emptyTFlow) - -/** Constructs a new [RootFrpNetwork] in the given [CoroutineScope]. */ -@ExperimentalFrpApi -fun CoroutineScope.newFrpNetwork( - context: CoroutineContext = EmptyCoroutineContext -): RootFrpNetwork { - val scope = childScope(context) - val network = Network(scope) - scope.launch(CoroutineName("newFrpNetwork scheduler")) { network.runInputScheduler() } - return RootFrpNetwork(network, scope, scope.coroutineContext.job) -} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt deleted file mode 100644 index ad6b2c8d04eb..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.kairos - -import kotlin.coroutines.RestrictsSuspension -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.suspendCancellableCoroutine - -/** Denotes [FrpScope] interfaces as [DSL markers][DslMarker]. */ -@DslMarker annotation class FrpScopeMarker - -/** - * Base scope for all FRP scopes. Used to prevent implicitly capturing other scopes from in lambdas. - */ -@FrpScopeMarker -@RestrictsSuspension -@ExperimentalFrpApi -interface FrpScope { - /** - * Returns the value held by the [FrpDeferredValue], suspending until available if necessary. - */ - @ExperimentalFrpApi - @OptIn(ExperimentalCoroutinesApi::class) - suspend fun <A> FrpDeferredValue<A>.get(): A = suspendCancellableCoroutine { k -> - unwrapped.invokeOnCompletion { ex -> - ex?.let { k.resumeWithException(ex) } ?: k.resume(unwrapped.getCompleted()) - } - } -} - -/** - * A value that may not be immediately (synchronously) available, but is guaranteed to be available - * before this transaction is completed. - * - * @see FrpScope.get - */ -@ExperimentalFrpApi -class FrpDeferredValue<out A> internal constructor(internal val unwrapped: Deferred<A>) - -/** - * Returns the value held by this [FrpDeferredValue], or throws [IllegalStateException] if it is not - * yet available. - * - * This API is not meant for general usage within the FRP network. It is made available mainly for - * debugging and logging. You should always prefer [get][FrpScope.get] if possible. - * - * @see FrpScope.get - */ -@ExperimentalFrpApi -@OptIn(ExperimentalCoroutinesApi::class) -fun <A> FrpDeferredValue<A>.getUnsafe(): A = unwrapped.getCompleted() - -/** Returns an already-available [FrpDeferredValue] containing [value]. */ -@ExperimentalFrpApi -fun <A> deferredOf(value: A): FrpDeferredValue<A> = FrpDeferredValue(CompletableDeferred(value)) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt deleted file mode 100644 index 058fc1037e58..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt +++ /dev/null @@ -1,780 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.kairos - -import com.android.systemui.kairos.combine as combinePure -import com.android.systemui.kairos.map as mapPure -import com.android.systemui.kairos.util.Just -import com.android.systemui.kairos.util.Left -import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.Right -import com.android.systemui.kairos.util.WithPrev -import com.android.systemui.kairos.util.just -import com.android.systemui.kairos.util.map -import com.android.systemui.kairos.util.none -import com.android.systemui.kairos.util.partitionEithers -import com.android.systemui.kairos.util.zipWith -import kotlin.coroutines.RestrictsSuspension - -typealias FrpStateful<R> = suspend FrpStateScope.() -> R - -/** - * Returns a [FrpStateful] that, when [applied][FrpStateScope.applyStateful], invokes [block] with - * the applier's [FrpStateScope]. - */ -// TODO: caching story? should each Scope have a cache of applied FrpStateful instances? -@ExperimentalFrpApi -@Suppress("NOTHING_TO_INLINE") -inline fun <A> statefully(noinline block: suspend FrpStateScope.() -> A): FrpStateful<A> = block - -/** - * Operations that accumulate state within the FRP network. - * - * State accumulation is an ongoing process that has a lifetime. Use `-Latest` combinators, such as - * [mapLatestStateful], to create smaller, nested lifecycles so that accumulation isn't running - * longer than needed. - */ -@ExperimentalFrpApi -@RestrictsSuspension -interface FrpStateScope : FrpTransactionScope { - - /** TODO */ - @ExperimentalFrpApi - // TODO: wish this could just be `deferred` but alas - fun <A> deferredStateScope(block: suspend FrpStateScope.() -> A): FrpDeferredValue<A> - - /** - * Returns a [TState] that holds onto the most recently emitted value from this [TFlow], or - * [initialValue] if nothing has been emitted since it was constructed. - * - * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s - * have been processed; this keeps the value of the [TState] consistent during the entire FRP - * transaction. - */ - @ExperimentalFrpApi fun <A> TFlow<A>.holdDeferred(initialValue: FrpDeferredValue<A>): TState<A> - - /** - * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s - * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally]. - * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - * initialTFlows: Map<K, TFlow<V>>, - * ): TFlow<Map<K, V>> = - * foldMapIncrementally(initialTFlows).map { it.merge() }.switch() - * ``` - * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * - * @see merge - */ - @ExperimentalFrpApi - fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>> - ): TFlow<Map<K, V>> - - /** - * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s - * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally]. - * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPrompt( - * initialTFlows: Map<K, TFlow<V>>, - * ): TFlow<Map<K, V>> = - * foldMapIncrementally(initialTFlows).map { it.merge() }.switchPromptly() - * ``` - * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * - * @see merge - */ - @ExperimentalFrpApi - fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly( - initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>> - ): TFlow<Map<K, V>> - - // TODO: everything below this comment can be made into extensions once we have context params - - /** - * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s - * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally]. - * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - * initialTFlows: Map<K, TFlow<V>>, - * ): TFlow<Map<K, V>> = - * foldMapIncrementally(initialTFlows).map { it.merge() }.switch() - * ``` - * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * - * @see merge - */ - @ExperimentalFrpApi - fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - initialTFlows: Map<K, TFlow<V>> = emptyMap() - ): TFlow<Map<K, V>> = mergeIncrementally(deferredOf(initialTFlows)) - - /** - * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s - * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally]. - * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPrompt( - * initialTFlows: Map<K, TFlow<V>>, - * ): TFlow<Map<K, V>> = - * foldMapIncrementally(initialTFlows).map { it.merge() }.switchPromptly() - * ``` - * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * - * @see merge - */ - @ExperimentalFrpApi - fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly( - initialTFlows: Map<K, TFlow<V>> = emptyMap() - ): TFlow<Map<K, V>> = mergeIncrementallyPromptly(deferredOf(initialTFlows)) - - /** Applies the [FrpStateful] within this [FrpStateScope]. */ - @ExperimentalFrpApi suspend fun <A> FrpStateful<A>.applyStateful(): A = this() - - /** - * Applies the [FrpStateful] within this [FrpStateScope], returning the result as an - * [FrpDeferredValue]. - */ - @ExperimentalFrpApi - fun <A> FrpStateful<A>.applyStatefulDeferred(): FrpDeferredValue<A> = deferredStateScope { - applyStateful() - } - - /** - * Returns a [TState] that holds onto the most recently emitted value from this [TFlow], or - * [initialValue] if nothing has been emitted since it was constructed. - * - * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s - * have been processed; this keeps the value of the [TState] consistent during the entire FRP - * transaction. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.hold(initialValue: A): TState<A> = holdDeferred(deferredOf(initialValue)) - - /** - * Returns a [TFlow] the emits the result of applying [FrpStatefuls][FrpStateful] emitted from - * the original [TFlow]. - * - * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission - * of the original [TFlow]. - */ - @ExperimentalFrpApi fun <A> TFlow<FrpStateful<A>>.applyStatefuls(): TFlow<A> - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. Unlike - * [mapLatestStateful], accumulation is not stopped with each subsequent emission of the - * original [TFlow]. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapStateful(transform: suspend FrpStateScope.(A) -> B): TFlow<B> = - mapPure { statefully { transform(it) } }.applyStatefuls() - - /** - * Returns a [TState] the holds the result of applying the [FrpStateful] held by the original - * [TState]. - * - * Unlike [applyLatestStateful], state accumulation is not stopped with each state change. - */ - @ExperimentalFrpApi - fun <A> TState<FrpStateful<A>>.applyStatefuls(): TState<A> = - stateChanges - .applyStatefuls() - .holdDeferred(initialValue = deferredStateScope { sampleDeferred().get()() }) - - /** Returns a [TFlow] that switches to the [TFlow] emitted by the original [TFlow]. */ - @ExperimentalFrpApi fun <A> TFlow<TFlow<A>>.flatten() = hold(emptyTFlow).switch() - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each - * invocation of [transform], state accumulation from previous invocation is stopped. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapLatestStateful(transform: suspend FrpStateScope.(A) -> B): TFlow<B> = - mapPure { statefully { transform(it) } }.applyLatestStateful() - - /** - * Returns a [TFlow] that switches to a new [TFlow] produced by [transform] every time the - * original [TFlow] emits a value. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each - * invocation of [transform], state accumulation from previous invocation is stopped. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.flatMapLatestStateful( - transform: suspend FrpStateScope.(A) -> TFlow<B> - ): TFlow<B> = mapLatestStateful(transform).flatten() - - /** - * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the - * original [TFlow]. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] is stopped. - */ - @ExperimentalFrpApi - fun <A> TFlow<FrpStateful<A>>.applyLatestStateful(): TFlow<A> = applyLatestStateful {}.first - - /** - * Returns a [TState] containing the value returned by applying the [FrpStateful] held by the - * original [TState]. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] is stopped. - */ - @ExperimentalFrpApi - fun <A> TState<FrpStateful<A>>.applyLatestStateful(): TState<A> { - val (changes, init) = stateChanges.applyLatestStateful { sample()() } - return changes.holdDeferred(init) - } - - /** - * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init] - * immediately. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] is stopped. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<FrpStateful<B>>.applyLatestStateful( - init: FrpStateful<A> - ): Pair<TFlow<B>, FrpDeferredValue<A>> { - val (flow, result) = - mapCheap { spec -> mapOf(Unit to just(spec)) } - .applyLatestStatefulForKey(init = mapOf(Unit to init), numKeys = 1) - val outFlow: TFlow<B> = - flow.mapMaybe { - checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" } - } - val outInit: FrpDeferredValue<A> = deferredTransactionScope { - val initResult: Map<Unit, A> = result.get() - check(Unit in initResult) { - "applyLatest: expected initial result, but none present in: $initResult" - } - @Suppress("UNCHECKED_CAST") - initResult.getOrDefault(Unit) { null } as A - } - return Pair(outFlow, outInit) - } - - /** - * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init] - * immediately. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateful] will be stopped with no replacement. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] with the same key is stopped. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey( - init: FrpDeferredValue<Map<K, FrpStateful<B>>>, - numKeys: Int? = null, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> - - /** - * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init] - * immediately. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] with the same key is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateful] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey( - init: Map<K, FrpStateful<B>>, - numKeys: Int? = null, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> = - applyLatestStatefulForKey(deferredOf(init), numKeys) - - /** - * Returns a [TState] containing the latest results of applying each [FrpStateful] emitted from - * the original [TFlow]. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] with the same key is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateful] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.holdLatestStatefulForKey( - init: FrpDeferredValue<Map<K, FrpStateful<A>>>, - numKeys: Int? = null, - ): TState<Map<K, A>> { - val (changes, initialValues) = applyLatestStatefulForKey(init, numKeys) - return changes.foldMapIncrementally(initialValues) - } - - /** - * Returns a [TState] containing the latest results of applying each [FrpStateful] emitted from - * the original [TFlow]. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] with the same key is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateful] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.holdLatestStatefulForKey( - init: Map<K, FrpStateful<A>> = emptyMap(), - numKeys: Int? = null, - ): TState<Map<K, A>> = holdLatestStatefulForKey(deferredOf(init), numKeys) - - /** - * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init] - * immediately. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] with the same key is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateful] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey( - numKeys: Int? = null - ): TFlow<Map<K, Maybe<A>>> = - applyLatestStatefulForKey(init = emptyMap<K, FrpStateful<*>>(), numKeys = numKeys).first - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValues] immediately. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each - * invocation of [transform], state accumulation from previous invocation is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateScope] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey( - initialValues: FrpDeferredValue<Map<K, A>>, - numKeys: Int? = null, - transform: suspend FrpStateScope.(A) -> B, - ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> = - mapPure { patch -> patch.mapValues { (_, v) -> v.map { statefully { transform(it) } } } } - .applyLatestStatefulForKey( - deferredStateScope { - initialValues.get().mapValues { (_, v) -> statefully { transform(v) } } - }, - numKeys = numKeys, - ) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValues] immediately. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each - * invocation of [transform], state accumulation from previous invocation is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateScope] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey( - initialValues: Map<K, A>, - numKeys: Int? = null, - transform: suspend FrpStateScope.(A) -> B, - ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> = - mapLatestStatefulForKey(deferredOf(initialValues), numKeys, transform) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each - * invocation of [transform], state accumulation from previous invocation is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateScope] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey( - numKeys: Int? = null, - transform: suspend FrpStateScope.(A) -> B, - ): TFlow<Map<K, Maybe<B>>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first - - /** - * Returns a [TFlow] that will only emit the next event of the original [TFlow], and then will - * act as [emptyTFlow]. - * - * If the original [TFlow] is emitting an event at this exact time, then it will be the only - * even emitted from the result [TFlow]. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.nextOnly(): TFlow<A> = - if (this === emptyTFlow) { - this - } else { - TFlowLoop<A>().also { - it.loopback = it.mapCheap { emptyTFlow }.hold(this@nextOnly).switch() - } - } - - /** Returns a [TFlow] that skips the next emission of the original [TFlow]. */ - @ExperimentalFrpApi - fun <A> TFlow<A>.skipNext(): TFlow<A> = - if (this === emptyTFlow) { - this - } else { - nextOnly().mapCheap { this@skipNext }.hold(emptyTFlow).switch() - } - - /** - * Returns a [TFlow] that emits values from the original [TFlow] up until [stop] emits a value. - * - * If the original [TFlow] emits at the same time as [stop], then the returned [TFlow] will emit - * that value. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.takeUntil(stop: TFlow<*>): TFlow<A> = - if (stop === emptyTFlow) { - this - } else { - stop.mapCheap { emptyTFlow }.nextOnly().hold(this).switch() - } - - /** - * Invokes [stateful] in a new [FrpStateScope] that is a child of this one. - * - * This new scope is stopped when [stop] first emits a value, or when the parent scope is - * stopped. Stopping will end all state accumulation; any [TStates][TState] returned from this - * scope will no longer update. - */ - @ExperimentalFrpApi - fun <A> childStateScope(stop: TFlow<*>, stateful: FrpStateful<A>): FrpDeferredValue<A> { - val (_, init: FrpDeferredValue<Map<Unit, A>>) = - stop - .nextOnly() - .mapPure { mapOf(Unit to none<FrpStateful<A>>()) } - .applyLatestStatefulForKey(init = mapOf(Unit to stateful), numKeys = 1) - return deferredStateScope { init.get().getValue(Unit) } - } - - /** - * Returns a [TFlow] that emits values from the original [TFlow] up to and including a value is - * emitted that satisfies [predicate]. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.takeUntil(predicate: suspend FrpTransactionScope.(A) -> Boolean): TFlow<A> = - takeUntil(filter(predicate)) - - /** - * Returns a [TState] that is incrementally updated when this [TFlow] emits a value, by applying - * [transform] to both the emitted value and the currently tracked state. - * - * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s - * have been processed; this keeps the value of the [TState] consistent during the entire FRP - * transaction. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.fold( - initialValue: B, - transform: suspend FrpTransactionScope.(A, B) -> B, - ): TState<B> { - lateinit var state: TState<B> - return mapPure { a -> transform(a, state.sample()) }.hold(initialValue).also { state = it } - } - - /** - * Returns a [TState] that is incrementally updated when this [TFlow] emits a value, by applying - * [transform] to both the emitted value and the currently tracked state. - * - * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s - * have been processed; this keeps the value of the [TState] consistent during the entire FRP - * transaction. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.foldDeferred( - initialValue: FrpDeferredValue<B>, - transform: suspend FrpTransactionScope.(A, B) -> B, - ): TState<B> { - lateinit var state: TState<B> - return mapPure { a -> transform(a, state.sample()) } - .holdDeferred(initialValue) - .also { state = it } - } - - /** - * Returns a [TState] that holds onto the result of applying the most recently emitted - * [FrpStateful] this [TFlow], or [init] if nothing has been emitted since it was constructed. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] is stopped. - * - * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s - * have been processed; this keeps the value of the [TState] consistent during the entire FRP - * transaction. - * - * Shorthand for: - * ```kotlin - * val (changes, initApplied) = applyLatestStateful(init) - * return changes.toTStateDeferred(initApplied) - * ``` - */ - @ExperimentalFrpApi - fun <A> TFlow<FrpStateful<A>>.holdLatestStateful(init: FrpStateful<A>): TState<A> { - val (changes, initApplied) = applyLatestStateful(init) - return changes.holdDeferred(initApplied) - } - - /** - * Returns a [TFlow] that emits the two most recent emissions from the original [TFlow]. - * [initialValue] is used as the previous value for the first emission. - * - * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }` - */ - @ExperimentalFrpApi - fun <S, T : S> TFlow<T>.pairwise(initialValue: S): TFlow<WithPrev<S, T>> { - val previous = hold(initialValue) - return mapCheap { new -> WithPrev(previousValue = previous.sample(), newValue = new) } - } - - /** - * Returns a [TFlow] that emits the two most recent emissions from the original [TFlow]. Note - * that the returned [TFlow] will not emit until the original [TFlow] has emitted twice. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.pairwise(): TFlow<WithPrev<A, A>> = - mapCheap { just(it) } - .pairwise(none) - .mapMaybe { (prev, next) -> prev.zipWith(next, ::WithPrev) } - - /** - * Returns a [TState] that holds both the current and previous values of the original [TState]. - * [initialPreviousValue] is used as the first previous value. - * - * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }` - */ - @ExperimentalFrpApi - fun <S, T : S> TState<T>.pairwise(initialPreviousValue: S): TState<WithPrev<S, T>> = - stateChanges - .pairwise(initialPreviousValue) - .holdDeferred(deferredTransactionScope { WithPrev(initialPreviousValue, sample()) }) - - /** - * Returns a [TState] holding a [Map] that is updated incrementally whenever this emits a value. - * - * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted - * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and - * an associated value of [none] will remove the key from the tracked [Map]. - */ - @ExperimentalFrpApi - fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally( - initialValues: FrpDeferredValue<Map<K, V>> - ): TState<Map<K, V>> = - foldDeferred(initialValues) { patch, map -> - val (adds: List<Pair<K, V>>, removes: List<K>) = - patch - .asSequence() - .map { (k, v) -> if (v is Just) Left(k to v.value) else Right(k) } - .partitionEithers() - val removed: Map<K, V> = map - removes.toSet() - val updated: Map<K, V> = removed + adds - updated - } - - /** - * Returns a [TState] holding a [Map] that is updated incrementally whenever this emits a value. - * - * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted - * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and - * an associated value of [none] will remove the key from the tracked [Map]. - */ - @ExperimentalFrpApi - fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally( - initialValues: Map<K, V> = emptyMap() - ): TState<Map<K, V>> = foldMapIncrementally(deferredOf(initialValues)) - - /** - * Returns a [TFlow] that wraps each emission of the original [TFlow] into an [IndexedValue], - * containing the emitted value and its index (starting from zero). - * - * Shorthand for: - * ``` - * val index = fold(0) { _, oldIdx -> oldIdx + 1 } - * sample(index) { a, idx -> IndexedValue(idx, a) } - * ``` - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.withIndex(): TFlow<IndexedValue<A>> { - val index = fold(0) { _, old -> old + 1 } - return sample(index) { a, idx -> IndexedValue(idx, a) } - } - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow] and its index (starting from zero). - * - * Shorthand for: - * ``` - * withIndex().map { (idx, a) -> transform(idx, a) } - * ``` - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapIndexed(transform: suspend FrpTransactionScope.(Int, A) -> B): TFlow<B> { - val index = fold(0) { _, i -> i + 1 } - return sample(index) { a, idx -> transform(idx, a) } - } - - /** Returns a [TFlow] where all subsequent repetitions of the same value are filtered out. */ - @ExperimentalFrpApi - fun <A> TFlow<A>.distinctUntilChanged(): TFlow<A> { - val state: TState<Any?> = hold(Any()) - return filter { it != state.sample() } - } - - /** - * Returns a new [TFlow] that emits at the same rate as the original [TFlow], but combines the - * emitted value with the most recent emission from [other] using [transform]. - * - * Note that the returned [TFlow] will not emit anything until [other] has emitted at least one - * value. - */ - @ExperimentalFrpApi - fun <A, B, C> TFlow<A>.sample( - other: TFlow<B>, - transform: suspend FrpTransactionScope.(A, B) -> C, - ): TFlow<C> { - val state = other.mapCheap { just(it) }.hold(none) - return sample(state) { a, b -> b.map { transform(a, it) } }.filterJust() - } - - /** - * Returns a [TState] that samples the [Transactional] held by the given [TState] within the - * same transaction that the state changes. - */ - @ExperimentalFrpApi - fun <A> TState<Transactional<A>>.sampleTransactionals(): TState<A> = - stateChanges - .sampleTransactionals() - .holdDeferred(deferredTransactionScope { sample().sample() }) - - /** - * Returns a [TState] that transforms the value held inside this [TState] by applying it to the - * given function [transform]. - */ - @ExperimentalFrpApi - fun <A, B> TState<A>.map(transform: suspend FrpTransactionScope.(A) -> B): TState<B> = - mapPure { transactionally { transform(it) } }.sampleTransactionals() - - /** - * Returns a [TState] whose value is generated with [transform] by combining the current values - * of each given [TState]. - * - * @see TState.combineWith - */ - @ExperimentalFrpApi - fun <A, B, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - transform: suspend FrpTransactionScope.(A, B) -> Z, - ): TState<Z> = - com.android.systemui.kairos - .combine(stateA, stateB) { a, b -> transactionally { transform(a, b) } } - .sampleTransactionals() - - /** - * Returns a [TState] whose value is generated with [transform] by combining the current values - * of each given [TState]. - * - * @see TState.combineWith - */ - @ExperimentalFrpApi - fun <A, B, C, D, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - stateC: TState<C>, - stateD: TState<D>, - transform: suspend FrpTransactionScope.(A, B, C, D) -> Z, - ): TState<Z> = - com.android.systemui.kairos - .combine(stateA, stateB, stateC, stateD) { a, b, c, d -> - transactionally { transform(a, b, c, d) } - } - .sampleTransactionals() - - /** Returns a [TState] by applying [transform] to the value held by the original [TState]. */ - @ExperimentalFrpApi - fun <A, B> TState<A>.flatMap( - transform: suspend FrpTransactionScope.(A) -> TState<B> - ): TState<B> = mapPure { transactionally { transform(it) } }.sampleTransactionals().flatten() - - /** - * Returns a [TState] whose value is generated with [transform] by combining the current values - * of each given [TState]. - * - * @see TState.combineWith - */ - @ExperimentalFrpApi - fun <A, Z> combine( - vararg states: TState<A>, - transform: suspend FrpTransactionScope.(List<A>) -> Z, - ): TState<Z> = combinePure(*states).map(transform) - - /** - * Returns a [TState] whose value is generated with [transform] by combining the current values - * of each given [TState]. - * - * @see TState.combineWith - */ - @ExperimentalFrpApi - fun <A, Z> Iterable<TState<A>>.combine( - transform: suspend FrpTransactionScope.(List<A>) -> Z - ): TState<Z> = combinePure().map(transform) - - /** - * Returns a [TState] by combining the values held inside the given [TState]s by applying them - * to the given function [transform]. - */ - @ExperimentalFrpApi - fun <A, B, C> TState<A>.combineWith( - other: TState<B>, - transform: suspend FrpTransactionScope.(A, B) -> C, - ): TState<C> = combine(this, other, transform) -} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt deleted file mode 100644 index a7ae1d9646b3..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.kairos - -import kotlin.coroutines.RestrictsSuspension - -/** - * FRP operations that are available while a transaction is active. - * - * These operations do not accumulate state, which makes [FrpTransactionScope] weaker than - * [FrpStateScope], but allows them to be used in more places. - */ -@ExperimentalFrpApi -@RestrictsSuspension -interface FrpTransactionScope : FrpScope { - - /** - * Returns the current value of this [Transactional] as a [FrpDeferredValue]. - * - * @see sample - */ - @ExperimentalFrpApi fun <A> Transactional<A>.sampleDeferred(): FrpDeferredValue<A> - - /** - * Returns the current value of this [TState] as a [FrpDeferredValue]. - * - * @see sample - */ - @ExperimentalFrpApi fun <A> TState<A>.sampleDeferred(): FrpDeferredValue<A> - - /** TODO */ - @ExperimentalFrpApi - fun <A> deferredTransactionScope( - block: suspend FrpTransactionScope.() -> A - ): FrpDeferredValue<A> - - /** A [TFlow] that emits once, within this transaction, and then never again. */ - @ExperimentalFrpApi val now: TFlow<Unit> - - /** - * Returns the current value held by this [TState]. Guaranteed to be consistent within the same - * transaction. - */ - @ExperimentalFrpApi suspend fun <A> TState<A>.sample(): A = sampleDeferred().get() - - /** - * Returns the current value held by this [Transactional]. Guaranteed to be consistent within - * the same transaction. - */ - @ExperimentalFrpApi suspend fun <A> Transactional<A>.sample(): A = sampleDeferred().get() -} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt new file mode 100644 index 000000000000..77598b30658a --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.internal.BuildScopeImpl +import com.android.systemui.kairos.internal.Network +import com.android.systemui.kairos.internal.StateScopeImpl +import com.android.systemui.kairos.internal.util.awaitCancellationAndThen +import com.android.systemui.kairos.internal.util.childScope +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.job +import kotlinx.coroutines.launch + +/** + * Marks declarations that are still **experimental** and shouldn't be used in general production + * code. + */ +@RequiresOptIn( + message = "This API is experimental and should not be used in general production code." +) +@Retention(AnnotationRetention.BINARY) +annotation class ExperimentalKairosApi + +/** + * External interface to a Kairos network of reactive components. Can be used to make transactional + * queries and modifications to the network. + */ +@ExperimentalKairosApi +interface KairosNetwork { + /** + * Runs [block] inside of a transaction, suspending until the transaction is complete. + * + * The [BuildScope] receiver exposes methods that can be used to query or modify the network. If + * the network is cancelled while the caller of [transact] is suspended, then the call will be + * cancelled. + */ + suspend fun <R> transact(block: TransactionScope.() -> R): R + + /** + * Activates [spec] in a transaction, suspending indefinitely. While suspended, all observers + * and long-running effects are kept alive. When cancelled, observers are unregistered and + * effects are cancelled. + */ + suspend fun activateSpec(spec: BuildSpec<*>) + + /** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ + fun <In, Out> coalescingMutableEvents( + coalesce: (old: Out, new: In) -> Out, + getInitialValue: () -> Out, + ): CoalescingMutableEvents<In, Out> + + /** Returns a [MutableState] that can emit values into this [KairosNetwork]. */ + fun <T> mutableEvents(): MutableEvents<T> + + /** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ + fun <T> conflatedMutableEvents(): CoalescingMutableEvents<T, T> + + /** Returns a [MutableState]. with initial state [initialValue]. */ + fun <T> mutableStateDeferred(initialValue: DeferredValue<T>): MutableState<T> +} + +/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <In, Out> KairosNetwork.coalescingMutableEvents( + coalesce: (old: Out, new: In) -> Out, + initialValue: Out, +): CoalescingMutableEvents<In, Out> = + coalescingMutableEvents(coalesce, getInitialValue = { initialValue }) + +/** Returns a [MutableState] with initial state [initialValue]. */ +@ExperimentalKairosApi +fun <T> KairosNetwork.mutableState(initialValue: T): MutableState<T> = + mutableStateDeferred(deferredOf(initialValue)) + +/** Returns a [MutableState] with initial state [initialValue]. */ +@ExperimentalKairosApi +fun <T> MutableState(network: KairosNetwork, initialValue: T): MutableState<T> = + network.mutableState(initialValue) + +/** Returns a [MutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <T> MutableEvents(network: KairosNetwork): MutableEvents<T> = network.mutableEvents() + +/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <In, Out> CoalescingMutableEvents( + network: KairosNetwork, + coalesce: (old: Out, new: In) -> Out, + initialValue: Out, +): CoalescingMutableEvents<In, Out> = network.coalescingMutableEvents(coalesce) { initialValue } + +/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <In, Out> CoalescingMutableEvents( + network: KairosNetwork, + coalesce: (old: Out, new: In) -> Out, + getInitialValue: () -> Out, +): CoalescingMutableEvents<In, Out> = network.coalescingMutableEvents(coalesce, getInitialValue) + +/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <T> ConflatedMutableEvents(network: KairosNetwork): CoalescingMutableEvents<T, T> = + network.conflatedMutableEvents() + +/** + * Activates [spec] in a transaction and invokes [block] with the result, suspending indefinitely. + * While suspended, all observers and long-running effects are kept alive. When cancelled, observers + * are unregistered and effects are cancelled. + */ +@ExperimentalKairosApi +suspend fun <R> KairosNetwork.activateSpec(spec: BuildSpec<R>, block: suspend (R) -> Unit) { + activateSpec { + val result = spec.applySpec() + launchEffect { block(result) } + } +} + +internal class LocalNetwork( + private val network: Network, + private val scope: CoroutineScope, + private val endSignal: Events<Any>, +) : KairosNetwork { + override suspend fun <R> transact(block: TransactionScope.() -> R): R = + network.transaction("KairosNetwork.transact") { block() }.await() + + override suspend fun activateSpec(spec: BuildSpec<*>) { + val stopEmitter = + CoalescingMutableEvents( + name = "activateSpec", + coalesce = { _, _: Unit -> }, + network = network, + getInitialValue = {}, + ) + val job = + network + .transaction("KairosNetwork.activateSpec") { + val buildScope = + BuildScopeImpl( + stateScope = + StateScopeImpl( + evalScope = this, + endSignal = mergeLeft(stopEmitter, endSignal), + ), + coroutineScope = scope, + ) + buildScope.launchScope(spec) + } + .await() + awaitCancellationAndThen { + stopEmitter.emit(Unit) + job.cancel() + } + } + + override fun <In, Out> coalescingMutableEvents( + coalesce: (old: Out, new: In) -> Out, + getInitialValue: () -> Out, + ): CoalescingMutableEvents<In, Out> = + CoalescingMutableEvents( + null, + coalesce = { old, new -> coalesce(old.value, new) }, + network, + getInitialValue, + ) + + override fun <T> conflatedMutableEvents(): CoalescingMutableEvents<T, T> = + CoalescingMutableEvents( + null, + coalesce = { _, new -> new }, + network, + { error("WTF: init value accessed for conflatedMutableEvents") }, + ) + + override fun <T> mutableEvents(): MutableEvents<T> = MutableEvents(network) + + override fun <T> mutableStateDeferred(initialValue: DeferredValue<T>): MutableState<T> = + MutableState(network, initialValue.unwrapped) +} + +/** + * Combination of an [KairosNetwork] and a [Job] that, when cancelled, will cancel the entire Kairos + * network. + */ +@ExperimentalKairosApi +class RootKairosNetwork +internal constructor(private val network: Network, private val scope: CoroutineScope, job: Job) : + Job by job, KairosNetwork by LocalNetwork(network, scope, emptyEvents) + +/** Constructs a new [RootKairosNetwork] in the given [CoroutineScope]. */ +@ExperimentalKairosApi +fun CoroutineScope.launchKairosNetwork( + context: CoroutineContext = EmptyCoroutineContext +): RootKairosNetwork { + val scope = childScope(context) + val network = Network(scope) + scope.launch(CoroutineName("launchKairosNetwork scheduler")) { network.runInputScheduler() } + return RootKairosNetwork(network, scope, scope.coroutineContext.job) +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt new file mode 100644 index 000000000000..ce3e9235efa8 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.internal.CompletableLazy + +/** Denotes [KairosScope] interfaces as [DSL markers][DslMarker]. */ +@DslMarker annotation class KairosScopeMarker + +/** + * Base scope for all Kairos scopes. Used to prevent implicitly capturing other scopes from in + * lambdas. + */ +@KairosScopeMarker +@ExperimentalKairosApi +interface KairosScope { + /** Returns the value held by the [DeferredValue], suspending until available if necessary. */ + fun <A> DeferredValue<A>.get(): A = unwrapped.value +} + +/** + * A value that may not be immediately (synchronously) available, but is guaranteed to be available + * before this transaction is completed. + * + * @see KairosScope.get + */ +@ExperimentalKairosApi +class DeferredValue<out A> internal constructor(internal val unwrapped: Lazy<A>) + +/** + * Returns the value held by this [DeferredValue], or throws [IllegalStateException] if it is not + * yet available. + * + * This API is not meant for general usage within the Kairos network. It is made available mainly + * for debugging and logging. You should always prefer [get][KairosScope.get] if possible. + * + * @see KairosScope.get + */ +@ExperimentalKairosApi fun <A> DeferredValue<A>.getUnsafe(): A = unwrapped.value + +/** Returns an already-available [DeferredValue] containing [value]. */ +@ExperimentalKairosApi +fun <A> deferredOf(value: A): DeferredValue<A> = DeferredValue(CompletableLazy(value)) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt new file mode 100644 index 000000000000..08b27c86c9b9 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt @@ -0,0 +1,528 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.internal.CompletableLazy +import com.android.systemui.kairos.internal.DerivedMapCheap +import com.android.systemui.kairos.internal.EventsImpl +import com.android.systemui.kairos.internal.Init +import com.android.systemui.kairos.internal.InitScope +import com.android.systemui.kairos.internal.Network +import com.android.systemui.kairos.internal.NoScope +import com.android.systemui.kairos.internal.Schedulable +import com.android.systemui.kairos.internal.StateImpl +import com.android.systemui.kairos.internal.StateSource +import com.android.systemui.kairos.internal.activated +import com.android.systemui.kairos.internal.cached +import com.android.systemui.kairos.internal.constInit +import com.android.systemui.kairos.internal.constState +import com.android.systemui.kairos.internal.filterImpl +import com.android.systemui.kairos.internal.flatMap +import com.android.systemui.kairos.internal.init +import com.android.systemui.kairos.internal.map +import com.android.systemui.kairos.internal.mapCheap +import com.android.systemui.kairos.internal.mapImpl +import com.android.systemui.kairos.internal.util.hashString +import com.android.systemui.kairos.internal.zipStateMap +import com.android.systemui.kairos.internal.zipStates +import kotlin.reflect.KProperty + +/** + * A time-varying value with discrete changes. Essentially, a combination of a [Transactional] that + * holds a value, and an [Events] that emits when the value changes. + */ +@ExperimentalKairosApi sealed class State<out A> + +/** A [State] that never changes. */ +@ExperimentalKairosApi +fun <A> stateOf(value: A): State<A> { + val operatorName = "stateOf" + val name = "$operatorName($value)" + return StateInit(constInit(name, constState(name, operatorName, value))) +} + +/** + * Returns a [State] that acts as a deferred-reference to the [State] produced by this [Lazy]. + * + * When the returned [State] is accessed by the Kairos network, the [Lazy]'s [value][Lazy.value] + * will be queried and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi fun <A> Lazy<State<A>>.defer(): State<A> = deferInline { value } + +/** + * Returns a [State] that acts as a deferred-reference to the [State] produced by this + * [DeferredValue]. + * + * When the returned [State] is accessed by the Kairos network, the [DeferredValue] will be queried + * and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> DeferredValue<State<A>>.defer(): State<A> = deferInline { unwrapped.value } + +/** + * Returns a [State] that acts as a deferred-reference to the [State] produced by [block]. + * + * When the returned [State] is accessed by the Kairos network, [block] will be invoked and the + * returned [State] will be used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> deferredState(block: KairosScope.() -> State<A>): State<A> = deferInline { NoScope.block() } + +/** + * Returns a [State] containing the results of applying [transform] to the value held by the + * original [State]. + */ +@ExperimentalKairosApi +fun <A, B> State<A>.map(transform: KairosScope.(A) -> B): State<B> { + val operatorName = "map" + val name = operatorName + return StateInit( + init(name) { + init.connect(evalScope = this).map(name, operatorName) { NoScope.transform(it) } + } + ) +} + +/** + * Returns a [State] that transforms the value held inside this [State] by applying it to the + * [transform]. + * + * Note that unlike [map], the result is not cached. This means that not only should [transform] be + * fast and pure, it should be *monomorphic* (1-to-1). Failure to do this means that [changes] for + * the returned [State] will operate unexpectedly, emitting at rates that do not reflect an + * observable change to the returned [State]. + */ +@ExperimentalKairosApi +fun <A, B> State<A>.mapCheapUnsafe(transform: KairosScope.(A) -> B): State<B> { + val operatorName = "map" + val name = operatorName + return StateInit( + init(name) { + init.connect(evalScope = this).mapCheap(name, operatorName) { NoScope.transform(it) } + } + ) +} + +/** + * Returns a [State] by combining the values held inside the given [State]s by applying them to the + * given function [transform]. + */ +@ExperimentalKairosApi +fun <A, B, C> State<A>.combineWith(other: State<B>, transform: KairosScope.(A, B) -> C): State<C> = + combine(this, other, transform) + +/** + * Splits a [State] of pairs into a pair of [Events][State], where each returned [State] holds half + * of the original. + * + * Shorthand for: + * ```kotlin + * val lefts = map { it.first } + * val rights = map { it.second } + * return Pair(lefts, rights) + * ``` + */ +@ExperimentalKairosApi +fun <A, B> State<Pair<A, B>>.unzip(): Pair<State<A>, State<B>> { + val left = map { it.first } + val right = map { it.second } + return left to right +} + +/** + * Returns a [State] by combining the values held inside the given [States][State] into a [List]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A> Iterable<State<A>>.combine(): State<List<A>> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + zipStates(name, operatorName, states = map { it.init.connect(evalScope = this) }) + } + ) +} + +/** + * Returns a [State] by combining the values held inside the given [States][State] into a [Map]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <K, A> Map<K, State<A>>.combine(): State<Map<K, A>> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + zipStateMap( + name, + operatorName, + states = mapValues { it.value.init.connect(evalScope = this) }, + ) + } + ) +} + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B> Iterable<State<A>>.combine(transform: KairosScope.(List<A>) -> B): State<B> = + combine().map(transform) + +/** + * Returns a [State] by combining the values held inside the given [State]s into a [List]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A> combine(vararg states: State<A>): State<List<A>> = states.asIterable().combine() + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B> combine(vararg states: State<A>, transform: KairosScope.(List<A>) -> B): State<B> = + states.asIterable().combine(transform) + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B, Z> combine( + stateA: State<A>, + stateB: State<B>, + transform: KairosScope.(A, B) -> Z, +): State<Z> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + val dl1 = stateA.init.connect(evalScope = this@init) + val dl2 = stateB.init.connect(evalScope = this@init) + zipStates(name, operatorName, dl1, dl2) { a, b -> NoScope.transform(a, b) } + } + ) +} + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B, C, Z> combine( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + transform: KairosScope.(A, B, C) -> Z, +): State<Z> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + val dl1 = stateA.init.connect(evalScope = this@init) + val dl2 = stateB.init.connect(evalScope = this@init) + val dl3 = stateC.init.connect(evalScope = this@init) + zipStates(name, operatorName, dl1, dl2, dl3) { a, b, c -> NoScope.transform(a, b, c) } + } + ) +} + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B, C, D, Z> combine( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + stateD: State<D>, + transform: KairosScope.(A, B, C, D) -> Z, +): State<Z> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + val dl1 = stateA.init.connect(evalScope = this@init) + val dl2 = stateB.init.connect(evalScope = this@init) + val dl3 = stateC.init.connect(evalScope = this@init) + val dl4 = stateD.init.connect(evalScope = this@init) + zipStates(name, operatorName, dl1, dl2, dl3, dl4) { a, b, c, d -> + NoScope.transform(a, b, c, d) + } + } + ) +} + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B, C, D, E, Z> combine( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + stateD: State<D>, + stateE: State<E>, + transform: KairosScope.(A, B, C, D, E) -> Z, +): State<Z> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + val dl1 = stateA.init.connect(evalScope = this@init) + val dl2 = stateB.init.connect(evalScope = this@init) + val dl3 = stateC.init.connect(evalScope = this@init) + val dl4 = stateD.init.connect(evalScope = this@init) + val dl5 = stateE.init.connect(evalScope = this@init) + zipStates(name, operatorName, dl1, dl2, dl3, dl4, dl5) { a, b, c, d, e -> + NoScope.transform(a, b, c, d, e) + } + } + ) +} + +/** Returns a [State] by applying [transform] to the value held by the original [State]. */ +@ExperimentalKairosApi +fun <A, B> State<A>.flatMap(transform: KairosScope.(A) -> State<B>): State<B> { + val operatorName = "flatMap" + val name = operatorName + return StateInit( + init(name) { + init.connect(this).flatMap(name, operatorName) { a -> + NoScope.transform(a).init.connect(this) + } + } + ) +} + +/** Shorthand for `flatMap { it }` */ +@ExperimentalKairosApi fun <A> State<State<A>>.flatten() = flatMap { it } + +/** + * Returns a [StateSelector] that can be used to efficiently check if the input [State] is currently + * holding a specific value. + * + * An example: + * ``` + * val intState: State<Int> = ... + * val intSelector: StateSelector<Int> = intState.selector() + * // Tracks if lInt is holding 1 + * val isOne: State<Boolean> = intSelector.whenSelected(1) + * ``` + * + * This is semantically equivalent to `val isOne = intState.map { i -> i == 1 }`, but is + * significantly more efficient; specifically, using [State.map] in this way incurs a `O(n)` + * performance hit, where `n` is the number of different [State.map] operations used to track a + * specific value. [selector] internally uses a [HashMap] to lookup the appropriate downstream + * [State] to update, and so operates in `O(1)`. + * + * Note that the returned [StateSelector] should be cached and re-used to gain the performance + * benefit. + * + * @see groupByKey + */ +@ExperimentalKairosApi +fun <A> State<A>.selector(numDistinctValues: Int? = null): StateSelector<A> = + StateSelector( + this, + changes + .map { new -> mapOf(new to true, sampleDeferred().get() to false) } + .groupByKey(numDistinctValues), + ) + +/** + * Tracks the currently selected value of type [A] from an upstream [State]. + * + * @see selector + */ +@ExperimentalKairosApi +class StateSelector<in A> +internal constructor( + private val upstream: State<A>, + private val groupedChanges: GroupedEvents<A, Boolean>, +) { + /** + * Returns a [State] that tracks whether the upstream [State] is currently holding the given + * [value]. + * + * @see selector + */ + fun whenSelected(value: A): State<Boolean> { + val operatorName = "StateSelector#whenSelected" + val name = "$operatorName[$value]" + return StateInit( + init(name) { + DerivedMapCheap( + name, + operatorName, + upstream = upstream.init.connect(evalScope = this), + changes = groupedChanges.impl.eventsForKey(value), + ) { + it == value + } + } + ) + } + + operator fun get(value: A): State<Boolean> = whenSelected(value) +} + +/** + * A mutable [State] that provides the ability to manually [set its value][setValue]. + * + * Multiple invocations of [setValue] that occur before a transaction are conflated; only the most + * recent value is used. + * + * Effectively equivalent to: + * ``` kotlin + * ConflatedMutableEvents(kairosNetwork).holdState(initialValue) + * ``` + */ +@ExperimentalKairosApi +class MutableState<T> internal constructor(internal val network: Network, initialValue: Lazy<T>) : + State<T>() { + + private val input: CoalescingMutableEvents<Lazy<T>, Lazy<T>?> = + CoalescingMutableEvents( + name = null, + coalesce = { _, new -> new }, + network = network, + getInitialValue = { null }, + ) + + internal val state = run { + val changes = input.impl + val name = null + val operatorName = "MutableState" + lateinit var state: StateSource<T> + val mapImpl = mapImpl(upstream = { changes.activated() }) { it, _ -> it!!.value } + val calm: EventsImpl<T> = + filterImpl({ mapImpl }) { new -> + new != state.getCurrentWithEpoch(evalScope = this).first + } + .cached() + state = StateSource(name, operatorName, initialValue, calm) + @Suppress("DeferredResultUnused") + network.transaction("MutableState.init") { + calm.activate(evalScope = this, downstream = Schedulable.S(state))?.let { + (connection, needsEval) -> + state.upstreamConnection = connection + if (needsEval) { + schedule(state) + } + } + } + StateInit(constInit(name, state)) + } + + /** + * Sets the value held by this [State]. + * + * Invoking will cause a [state change event][State.changes] to emit with the new value, which + * will then be applied (and thus returned by [TransactionScope.sample]) after the transaction + * is complete. + * + * Multiple invocations of [setValue] that occur before a transaction are conflated; only the + * most recent value is used. + */ + fun setValue(value: T) = input.emit(CompletableLazy(value)) + + /** + * Sets the value held by this [State]. The [DeferredValue] will not be queried until this + * [State] is explicitly [sampled][TransactionScope.sample] or [observed][BuildScope.observe]. + * + * Invoking will cause a [state change event][State.changes] to emit with the new value, which + * will then be applied (and thus returned by [TransactionScope.sample]) after the transaction + * is complete. + * + * Multiple invocations of [setValue] that occur before a transaction are conflated; only the + * most recent value is used. + */ + fun setValueDeferred(value: DeferredValue<T>) = input.emit(value.unwrapped) +} + +/** A forward-reference to a [State], allowing for recursive definitions. */ +@ExperimentalKairosApi +class StateLoop<A> : State<A>() { + + private val name: String? = null + + private val deferred = CompletableLazy<State<A>>() + + internal val init: Init<StateImpl<A>> = + init(name) { deferred.value.init.connect(evalScope = this) } + + /** The [State] this [StateLoop] will forward to. */ + var loopback: State<A>? = null + set(value) { + value?.let { + check(!deferred.isInitialized()) { "StateLoop.loopback has already been set." } + deferred.setValue(value) + field = value + } + } + + operator fun getValue(thisRef: Any?, property: KProperty<*>): State<A> = this + + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: State<A>) { + loopback = value + } + + override fun toString(): String = "${this::class.simpleName}@$hashString" +} + +internal class StateInit<A> internal constructor(internal val init: Init<StateImpl<A>>) : + State<A>() { + override fun toString(): String = "${this::class.simpleName}@$hashString" +} + +internal val <A> State<A>.init: Init<StateImpl<A>> + get() = + when (this) { + is StateInit -> init + is StateLoop -> init + is MutableState -> state.init + } + +private inline fun <A> deferInline(crossinline block: InitScope.() -> State<A>): State<A> = + StateInit(init(name = null) { block().init.connect(evalScope = this) }) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt new file mode 100644 index 000000000000..b1f48bb1ce56 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt @@ -0,0 +1,760 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.util.Just +import com.android.systemui.kairos.util.Left +import com.android.systemui.kairos.util.Maybe +import com.android.systemui.kairos.util.Right +import com.android.systemui.kairos.util.WithPrev +import com.android.systemui.kairos.util.just +import com.android.systemui.kairos.util.map +import com.android.systemui.kairos.util.none +import com.android.systemui.kairos.util.partitionEithers +import com.android.systemui.kairos.util.zipWith + +// TODO: caching story? should each Scope have a cache of applied Stateful instances? +/** A computation that can accumulate [Events] into [State]. */ +typealias Stateful<R> = StateScope.() -> R + +/** + * Returns a [Stateful] that, when [applied][StateScope.applyStateful], invokes [block] with the + * applier's [StateScope]. + */ +@ExperimentalKairosApi +@Suppress("NOTHING_TO_INLINE") +inline fun <A> statefully(noinline block: StateScope.() -> A): Stateful<A> = block + +/** + * Operations that accumulate state within the Kairos network. + * + * State accumulation is an ongoing process that has a lifetime. Use `-Latest` combinators, such as + * [mapLatestStateful], to create smaller, nested lifecycles so that accumulation isn't running + * longer than needed. + */ +@ExperimentalKairosApi +interface StateScope : TransactionScope { + + /** + * Defers invoking [block] until after the current [StateScope] code-path completes, returning a + * [DeferredValue] that can be used to reference the result. + * + * Useful for recursive definitions. + * + * @see DeferredValue + */ + fun <A> deferredStateScope(block: StateScope.() -> A): DeferredValue<A> + + /** + * Returns a [State] that holds onto the most recently emitted value from this [Events], or + * [initialValue] if nothing has been emitted since it was constructed. + * + * Note that the value contained within the [State] is not updated until *after* all [Events] + * have been processed; this keeps the value of the [State] consistent during the entire Kairos + * transaction. + */ + fun <A> Events<A>.holdStateDeferred(initialValue: DeferredValue<A>): State<A> + + /** + * Returns an [Events] that emits from a merged, incrementally-accumulated collection of + * [Events] emitted from this, following the same "patch" rules as outlined in + * [foldStateMapIncrementally]. + * + * Conceptually this is equivalent to: + * ```kotlin + * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( + * initialEvents: Map<K, Events<V>>, + * ): Events<Map<K, V>> = + * foldMapIncrementally(initialEvents).map { it.merge() }.switchEvents() + * ``` + * + * While the behavior is equivalent to the conceptual definition above, the implementation is + * significantly more efficient. + * + * @see merge + */ + fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( + name: String? = null, + initialEvents: DeferredValue<Map<K, Events<V>>>, + ): Events<Map<K, V>> + + /** + * Returns an [Events] that emits from a merged, incrementally-accumulated collection of + * [Events] emitted from this, following the same "patch" rules as outlined in + * [foldStateMapIncrementally]. + * + * Conceptually this is equivalent to: + * ```kotlin + * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( + * initialEvents: Map<K, Events<V>>, + * ): Events<Map<K, V>> = + * foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly() + * ``` + * + * While the behavior is equivalent to the conceptual definition above, the implementation is + * significantly more efficient. + * + * @see merge + */ + fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( + initialEvents: DeferredValue<Map<K, Events<V>>>, + name: String? = null, + ): Events<Map<K, V>> + + // TODO: everything below this comment can be made into extensions once we have context params + + /** + * Returns an [Events] that emits from a merged, incrementally-accumulated collection of + * [Events] emitted from this, following the same "patch" rules as outlined in + * [foldStateMapIncrementally]. + * + * Conceptually this is equivalent to: + * ```kotlin + * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( + * initialEvents: Map<K, Events<V>>, + * ): Events<Map<K, V>> = + * foldMapIncrementally(initialEvents).map { it.merge() }.switchEvents() + * ``` + * + * While the behavior is equivalent to the conceptual definition above, the implementation is + * significantly more efficient. + * + * @see merge + */ + fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( + name: String? = null, + initialEvents: Map<K, Events<V>> = emptyMap(), + ): Events<Map<K, V>> = mergeIncrementally(name, deferredOf(initialEvents)) + + /** + * Returns an [Events] that emits from a merged, incrementally-accumulated collection of + * [Events] emitted from this, following the same "patch" rules as outlined in + * [foldStateMapIncrementally]. + * + * Conceptually this is equivalent to: + * ```kotlin + * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( + * initialEvents: Map<K, Events<V>>, + * ): Events<Map<K, V>> = + * foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly() + * ``` + * + * While the behavior is equivalent to the conceptual definition above, the implementation is + * significantly more efficient. + * + * @see merge + */ + fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( + initialEvents: Map<K, Events<V>> = emptyMap(), + name: String? = null, + ): Events<Map<K, V>> = mergeIncrementallyPromptly(deferredOf(initialEvents), name) + + /** Applies the [Stateful] within this [StateScope]. */ + fun <A> Stateful<A>.applyStateful(): A = this() + + /** + * Applies the [Stateful] within this [StateScope], returning the result as an [DeferredValue]. + */ + fun <A> Stateful<A>.applyStatefulDeferred(): DeferredValue<A> = deferredStateScope { + applyStateful() + } + + /** + * Returns a [State] that holds onto the most recently emitted value from this [Events], or + * [initialValue] if nothing has been emitted since it was constructed. + * + * Note that the value contained within the [State] is not updated until *after* all [Events] + * have been processed; this keeps the value of the [State] consistent during the entire Kairos + * transaction. + */ + fun <A> Events<A>.holdState(initialValue: A): State<A> = + holdStateDeferred(deferredOf(initialValue)) + + /** + * Returns an [Events] the emits the result of applying [Statefuls][Stateful] emitted from the + * original [Events]. + * + * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission + * of the original [Events]. + */ + fun <A> Events<Stateful<A>>.applyStatefuls(): Events<A> + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform state accumulation via its [StateScope] receiver. Unlike + * [mapLatestStateful], accumulation is not stopped with each subsequent emission of the + * original [Events]. + */ + fun <A, B> Events<A>.mapStateful(transform: StateScope.(A) -> B): Events<B> = + map { statefully { transform(it) } }.applyStatefuls() + + /** + * Returns a [State] the holds the result of applying the [Stateful] held by the original + * [State]. + * + * Unlike [applyLatestStateful], state accumulation is not stopped with each state change. + */ + fun <A> State<Stateful<A>>.applyStatefuls(): State<A> = + changes + .applyStatefuls() + .holdStateDeferred(initialValue = deferredStateScope { sampleDeferred().get()() }) + + /** Returns an [Events] that switches to the [Events] emitted by the original [Events]. */ + fun <A> Events<Events<A>>.flatten() = holdState(emptyEvents).switchEvents() + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform state accumulation via its [StateScope] receiver. With each + * invocation of [transform], state accumulation from previous invocation is stopped. + */ + fun <A, B> Events<A>.mapLatestStateful(transform: StateScope.(A) -> B): Events<B> = + map { statefully { transform(it) } }.applyLatestStateful() + + /** + * Returns an [Events] that switches to a new [Events] produced by [transform] every time the + * original [Events] emits a value. + * + * [transform] can perform state accumulation via its [StateScope] receiver. With each + * invocation of [transform], state accumulation from previous invocation is stopped. + */ + fun <A, B> Events<A>.flatMapLatestStateful(transform: StateScope.(A) -> Events<B>): Events<B> = + mapLatestStateful(transform).flatten() + + /** + * Returns an [Events] containing the results of applying each [Stateful] emitted from the + * original [Events]. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is + * stopped. + */ + fun <A> Events<Stateful<A>>.applyLatestStateful(): Events<A> = applyLatestStateful {}.first + + /** + * Returns a [State] containing the value returned by applying the [Stateful] held by the + * original [State]. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is + * stopped. + */ + fun <A> State<Stateful<A>>.applyLatestStateful(): State<A> { + val (changes, init) = changes.applyLatestStateful { sample()() } + return changes.holdStateDeferred(init) + } + + /** + * Returns an [Events] containing the results of applying each [Stateful] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [init] + * immediately. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is + * stopped. + */ + fun <A, B> Events<Stateful<B>>.applyLatestStateful( + init: Stateful<A> + ): Pair<Events<B>, DeferredValue<A>> { + val (events, result) = + mapCheap { spec -> mapOf(Unit to just(spec)) } + .applyLatestStatefulForKey(init = mapOf(Unit to init), numKeys = 1) + val outEvents: Events<B> = + events.mapMaybe { + checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" } + } + val outInit: DeferredValue<A> = deferredTransactionScope { + val initResult: Map<Unit, A> = result.get() + check(Unit in initResult) { + "applyLatest: expected initial result, but none present in: $initResult" + } + @Suppress("UNCHECKED_CAST") + initResult.getOrDefault(Unit) { null } as A + } + return Pair(outEvents, outInit) + } + + /** + * Returns an [Events] containing the results of applying each [Stateful] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [init] + * immediately. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [Stateful] will be stopped with no replacement. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] + * with the same key is stopped. + */ + fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey( + init: DeferredValue<Map<K, Stateful<B>>>, + numKeys: Int? = null, + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> + + /** + * Returns an [Events] containing the results of applying each [Stateful] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [init] + * immediately. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] + * with the same key is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [Stateful] will be stopped with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey( + init: Map<K, Stateful<B>>, + numKeys: Int? = null, + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> = + applyLatestStatefulForKey(deferredOf(init), numKeys) + + /** + * Returns a [State] containing the latest results of applying each [Stateful] emitted from the + * original [Events]. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] + * with the same key is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [Stateful] will be stopped with no replacement. + */ + fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey( + init: DeferredValue<Map<K, Stateful<A>>>, + numKeys: Int? = null, + ): State<Map<K, A>> { + val (changes, initialValues) = applyLatestStatefulForKey(init, numKeys) + return changes.foldStateMapIncrementally(initialValues) + } + + /** + * Returns a [State] containing the latest results of applying each [Stateful] emitted from the + * original [Events]. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] + * with the same key is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [Stateful] will be stopped with no replacement. + */ + fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey( + init: Map<K, Stateful<A>> = emptyMap(), + numKeys: Int? = null, + ): State<Map<K, A>> = holdLatestStatefulForKey(deferredOf(init), numKeys) + + /** + * Returns an [Events] containing the results of applying each [Stateful] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [init] + * immediately. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] + * with the same key is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [Stateful] will be stopped with no replacement. + */ + fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey( + numKeys: Int? = null + ): Events<Map<K, Maybe<A>>> = + applyLatestStatefulForKey(init = emptyMap<K, Stateful<*>>(), numKeys = numKeys).first + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValues] immediately. + * + * [transform] can perform state accumulation via its [StateScope] receiver. With each + * invocation of [transform], state accumulation from previous invocation is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [StateScope] will be stopped with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey( + initialValues: DeferredValue<Map<K, A>>, + numKeys: Int? = null, + transform: StateScope.(A) -> B, + ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> = + map { patch -> patch.mapValues { (_, v) -> v.map { statefully { transform(it) } } } } + .applyLatestStatefulForKey( + deferredStateScope { + initialValues.get().mapValues { (_, v) -> statefully { transform(v) } } + }, + numKeys = numKeys, + ) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValues] immediately. + * + * [transform] can perform state accumulation via its [StateScope] receiver. With each + * invocation of [transform], state accumulation from previous invocation is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [StateScope] will be stopped with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey( + initialValues: Map<K, A>, + numKeys: Int? = null, + transform: StateScope.(A) -> B, + ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> = + mapLatestStatefulForKey(deferredOf(initialValues), numKeys, transform) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform state accumulation via its [StateScope] receiver. With each + * invocation of [transform], state accumulation from previous invocation is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [StateScope] will be stopped with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey( + numKeys: Int? = null, + transform: StateScope.(A) -> B, + ): Events<Map<K, Maybe<B>>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first + + /** + * Returns an [Events] that will only emit the next event of the original [Events], and then + * will act as [emptyEvents]. + * + * If the original [Events] is emitting an event at this exact time, then it will be the only + * even emitted from the result [Events]. + */ + fun <A> Events<A>.nextOnly(name: String? = null): Events<A> = + if (this === emptyEvents) { + this + } else { + EventsLoop<A>().also { + it.loopback = + it.mapCheap { emptyEvents }.holdState(this@nextOnly).switchEvents(name) + } + } + + /** Returns an [Events] that skips the next emission of the original [Events]. */ + fun <A> Events<A>.skipNext(): Events<A> = + if (this === emptyEvents) { + this + } else { + nextOnly().mapCheap { this@skipNext }.holdState(emptyEvents).switchEvents() + } + + /** + * Returns an [Events] that emits values from the original [Events] up until [stop] emits a + * value. + * + * If the original [Events] emits at the same time as [stop], then the returned [Events] will + * emit that value. + */ + fun <A> Events<A>.takeUntil(stop: Events<*>): Events<A> = + if (stop === emptyEvents) { + this + } else { + stop.mapCheap { emptyEvents }.nextOnly().holdState(this).switchEvents() + } + + /** + * Invokes [stateful] in a new [StateScope] that is a child of this one. + * + * This new scope is stopped when [stop] first emits a value, or when the parent scope is + * stopped. Stopping will end all state accumulation; any [States][State] returned from this + * scope will no longer update. + */ + fun <A> childStateScope(stop: Events<*>, stateful: Stateful<A>): DeferredValue<A> { + val (_, init: DeferredValue<Map<Unit, A>>) = + stop + .nextOnly() + .map { mapOf(Unit to none<Stateful<A>>()) } + .applyLatestStatefulForKey(init = mapOf(Unit to stateful), numKeys = 1) + return deferredStateScope { init.get().getValue(Unit) } + } + + /** + * Returns an [Events] that emits values from the original [Events] up to and including a value + * is emitted that satisfies [predicate]. + */ + fun <A> Events<A>.takeUntil(predicate: TransactionScope.(A) -> Boolean): Events<A> = + takeUntil(filter(predicate)) + + /** + * Returns a [State] that is incrementally updated when this [Events] emits a value, by applying + * [transform] to both the emitted value and the currently tracked state. + * + * Note that the value contained within the [State] is not updated until *after* all [Events] + * have been processed; this keeps the value of the [State] consistent during the entire Kairos + * transaction. + */ + fun <A, B> Events<A>.foldState( + initialValue: B, + transform: TransactionScope.(A, B) -> B, + ): State<B> { + lateinit var state: State<B> + return map { a -> transform(a, state.sample()) }.holdState(initialValue).also { state = it } + } + + /** + * Returns a [State] that is incrementally updated when this [Events] emits a value, by applying + * [transform] to both the emitted value and the currently tracked state. + * + * Note that the value contained within the [State] is not updated until *after* all [Events] + * have been processed; this keeps the value of the [State] consistent during the entire Kairos + * transaction. + */ + fun <A, B> Events<A>.foldStateDeferred( + initialValue: DeferredValue<B>, + transform: TransactionScope.(A, B) -> B, + ): State<B> { + lateinit var state: State<B> + return map { a -> transform(a, state.sample()) } + .holdStateDeferred(initialValue) + .also { state = it } + } + + /** + * Returns a [State] that holds onto the result of applying the most recently emitted [Stateful] + * this [Events], or [init] if nothing has been emitted since it was constructed. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is + * stopped. + * + * Note that the value contained within the [State] is not updated until *after* all [Events] + * have been processed; this keeps the value of the [State] consistent during the entire Kairos + * transaction. + * + * Shorthand for: + * ```kotlin + * val (changes, initApplied) = applyLatestStateful(init) + * return changes.toStateDeferred(initApplied) + * ``` + */ + fun <A> Events<Stateful<A>>.holdLatestStateful(init: Stateful<A>): State<A> { + val (changes, initApplied) = applyLatestStateful(init) + return changes.holdStateDeferred(initApplied) + } + + /** + * Returns an [Events] that emits the two most recent emissions from the original [Events]. + * [initialValue] is used as the previous value for the first emission. + * + * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }` + */ + fun <S, T : S> Events<T>.pairwise(initialValue: S): Events<WithPrev<S, T>> { + val previous = holdState(initialValue) + return mapCheap { new -> WithPrev(previousValue = previous.sample(), newValue = new) } + } + + /** + * Returns an [Events] that emits the two most recent emissions from the original [Events]. Note + * that the returned [Events] will not emit until the original [Events] has emitted twice. + */ + fun <A> Events<A>.pairwise(): Events<WithPrev<A, A>> = + mapCheap { just(it) } + .pairwise(none) + .mapMaybe { (prev, next) -> prev.zipWith(next, ::WithPrev) } + + /** + * Returns a [State] that holds both the current and previous values of the original [State]. + * [initialPreviousValue] is used as the first previous value. + * + * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }` + */ + fun <S, T : S> State<T>.pairwise(initialPreviousValue: S): State<WithPrev<S, T>> = + changes + .pairwise(initialPreviousValue) + .holdStateDeferred( + deferredTransactionScope { WithPrev(initialPreviousValue, sample()) } + ) + + /** + * Returns a [State] holding a [Map] that is updated incrementally whenever this emits a value. + * + * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted + * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and + * an associated value of [none] will remove the key from the tracked [Map]. + */ + fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally( + initialValues: DeferredValue<Map<K, V>> + ): State<Map<K, V>> = + foldStateDeferred(initialValues) { patch, map -> + val (adds: List<Pair<K, V>>, removes: List<K>) = + patch + .asSequence() + .map { (k, v) -> if (v is Just) Left(k to v.value) else Right(k) } + .partitionEithers() + val removed: Map<K, V> = map - removes.toSet() + val updated: Map<K, V> = removed + adds + updated + } + + /** + * Returns a [State] holding a [Map] that is updated incrementally whenever this emits a value. + * + * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted + * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and + * an associated value of [none] will remove the key from the tracked [Map]. + */ + fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally( + initialValues: Map<K, V> = emptyMap() + ): State<Map<K, V>> = foldStateMapIncrementally(deferredOf(initialValues)) + + /** + * Returns an [Events] that wraps each emission of the original [Events] into an [IndexedValue], + * containing the emitted value and its index (starting from zero). + * + * Shorthand for: + * ``` + * val index = fold(0) { _, oldIdx -> oldIdx + 1 } + * sample(index) { a, idx -> IndexedValue(idx, a) } + * ``` + */ + fun <A> Events<A>.withIndex(): Events<IndexedValue<A>> { + val index = foldState(0) { _, old -> old + 1 } + return sample(index) { a, idx -> IndexedValue(idx, a) } + } + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events] and its index (starting from zero). + * + * Shorthand for: + * ``` + * withIndex().map { (idx, a) -> transform(idx, a) } + * ``` + */ + fun <A, B> Events<A>.mapIndexed(transform: TransactionScope.(Int, A) -> B): Events<B> { + val index = foldState(0) { _, i -> i + 1 } + return sample(index) { a, idx -> transform(idx, a) } + } + + /** Returns an [Events] where all subsequent repetitions of the same value are filtered out. */ + fun <A> Events<A>.distinctUntilChanged(): Events<A> { + val state: State<Any?> = holdState(Any()) + return filter { it != state.sample() } + } + + /** + * Returns a new [Events] that emits at the same rate as the original [Events], but combines the + * emitted value with the most recent emission from [other] using [transform]. + * + * Note that the returned [Events] will not emit anything until [other] has emitted at least one + * value. + */ + fun <A, B, C> Events<A>.sample( + other: Events<B>, + transform: TransactionScope.(A, B) -> C, + ): Events<C> { + val state = other.mapCheap { just(it) }.holdState(none) + return sample(state) { a, b -> b.map { transform(a, it) } }.filterJust() + } + + /** + * Returns a [State] that samples the [Transactional] held by the given [State] within the same + * transaction that the state changes. + */ + fun <A> State<Transactional<A>>.sampleTransactionals(): State<A> = + changes + .sampleTransactionals() + .holdStateDeferred(deferredTransactionScope { sample().sample() }) + + /** + * Returns a [State] that transforms the value held inside this [State] by applying it to the + * given function [transform]. + */ + fun <A, B> State<A>.mapTransactionally(transform: TransactionScope.(A) -> B): State<B> = + map { transactionally { transform(it) } }.sampleTransactionals() + + /** + * Returns a [State] whose value is generated with [transform] by combining the current values + * of each given [State]. + * + * @see State.combineWithTransactionally + */ + fun <A, B, Z> combineTransactionally( + stateA: State<A>, + stateB: State<B>, + transform: TransactionScope.(A, B) -> Z, + ): State<Z> = + combine(stateA, stateB) { a, b -> transactionally { transform(a, b) } } + .sampleTransactionals() + + /** + * Returns a [State] whose value is generated with [transform] by combining the current values + * of each given [State]. + * + * @see State.combineWithTransactionally + */ + fun <A, B, C, Z> combineTransactionally( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + transform: TransactionScope.(A, B, C) -> Z, + ): State<Z> = + combine(stateA, stateB, stateC) { a, b, c -> transactionally { transform(a, b, c) } } + .sampleTransactionals() + + /** + * Returns a [State] whose value is generated with [transform] by combining the current values + * of each given [State]. + * + * @see State.combineWithTransactionally + */ + fun <A, B, C, D, Z> combineTransactionally( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + stateD: State<D>, + transform: TransactionScope.(A, B, C, D) -> Z, + ): State<Z> = + combine(stateA, stateB, stateC, stateD) { a, b, c, d -> + transactionally { transform(a, b, c, d) } + } + .sampleTransactionals() + + /** Returns a [State] by applying [transform] to the value held by the original [State]. */ + fun <A, B> State<A>.flatMapTransactionally( + transform: TransactionScope.(A) -> State<B> + ): State<B> = map { transactionally { transform(it) } }.sampleTransactionals().flatten() + + /** + * Returns a [State] whose value is generated with [transform] by combining the current values + * of each given [State]. + * + * @see State.combineWithTransactionally + */ + fun <A, Z> combineTransactionally( + vararg states: State<A>, + transform: TransactionScope.(List<A>) -> Z, + ): State<Z> = combine(*states).mapTransactionally(transform) + + /** + * Returns a [State] whose value is generated with [transform] by combining the current values + * of each given [State]. + * + * @see State.combineWithTransactionally + */ + fun <A, Z> Iterable<State<A>>.combineTransactionally( + transform: TransactionScope.(List<A>) -> Z + ): State<Z> = combine().mapTransactionally(transform) + + /** + * Returns a [State] by combining the values held inside the given [State]s by applying them to + * the given function [transform]. + */ + fun <A, B, C> State<A>.combineWithTransactionally( + other: State<B>, + transform: TransactionScope.(A, B) -> C, + ): State<C> = combineTransactionally(this, other, transform) +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TFlow.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TFlow.kt deleted file mode 100644 index 362a890f44e2..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TFlow.kt +++ /dev/null @@ -1,563 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.kairos - -import com.android.systemui.kairos.internal.DemuxImpl -import com.android.systemui.kairos.internal.Init -import com.android.systemui.kairos.internal.InitScope -import com.android.systemui.kairos.internal.InputNode -import com.android.systemui.kairos.internal.Network -import com.android.systemui.kairos.internal.NoScope -import com.android.systemui.kairos.internal.TFlowImpl -import com.android.systemui.kairos.internal.activated -import com.android.systemui.kairos.internal.cached -import com.android.systemui.kairos.internal.constInit -import com.android.systemui.kairos.internal.demuxMap -import com.android.systemui.kairos.internal.filterImpl -import com.android.systemui.kairos.internal.filterJustImpl -import com.android.systemui.kairos.internal.init -import com.android.systemui.kairos.internal.map -import com.android.systemui.kairos.internal.mapImpl -import com.android.systemui.kairos.internal.mergeNodes -import com.android.systemui.kairos.internal.mergeNodesLeft -import com.android.systemui.kairos.internal.neverImpl -import com.android.systemui.kairos.internal.switchDeferredImplSingle -import com.android.systemui.kairos.internal.switchPromptImplSingle -import com.android.systemui.kairos.internal.util.hashString -import com.android.systemui.kairos.util.Either -import com.android.systemui.kairos.util.Left -import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.Right -import com.android.systemui.kairos.util.just -import com.android.systemui.kairos.util.map -import com.android.systemui.kairos.util.toMaybe -import java.util.concurrent.atomic.AtomicReference -import kotlin.reflect.KProperty -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope - -/** A series of values of type [A] available at discrete points in time. */ -@ExperimentalFrpApi -sealed class TFlow<out A> { - companion object { - /** A [TFlow] with no values. */ - val empty: TFlow<Nothing> = EmptyFlow - } -} - -/** A [TFlow] with no values. */ -@ExperimentalFrpApi val emptyTFlow: TFlow<Nothing> = TFlow.empty - -/** - * A forward-reference to a [TFlow]. Useful for recursive definitions. - * - * This reference can be used like a standard [TFlow], but will hold up evaluation of the FRP - * network until the [loopback] reference is set. - */ -@ExperimentalFrpApi -class TFlowLoop<A> : TFlow<A>() { - private val deferred = CompletableDeferred<TFlow<A>>() - - internal val init: Init<TFlowImpl<A>> = - init(name = null) { deferred.await().init.connect(evalScope = this) } - - /** The [TFlow] this reference is referring to. */ - @ExperimentalFrpApi - var loopback: TFlow<A>? = null - set(value) { - value?.let { - check(deferred.complete(value)) { "TFlowLoop.loopback has already been set." } - field = value - } - } - - operator fun getValue(thisRef: Any?, property: KProperty<*>): TFlow<A> = this - - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: TFlow<A>) { - loopback = value - } - - override fun toString(): String = "${this::class.simpleName}@$hashString" -} - -/** TODO */ -@ExperimentalFrpApi fun <A> Lazy<TFlow<A>>.defer(): TFlow<A> = deferInline { value } - -/** TODO */ -@ExperimentalFrpApi -fun <A> FrpDeferredValue<TFlow<A>>.defer(): TFlow<A> = deferInline { unwrapped.await() } - -/** TODO */ -@ExperimentalFrpApi -fun <A> deferTFlow(block: suspend FrpScope.() -> TFlow<A>): TFlow<A> = deferInline { - NoScope.runInFrpScope(block) -} - -/** Returns a [TFlow] that emits the new value of this [TState] when it changes. */ -@ExperimentalFrpApi -val <A> TState<A>.stateChanges: TFlow<A> - get() = TFlowInit(init(name = null) { init.connect(evalScope = this).changes }) - -/** - * Returns a [TFlow] that contains only the [just] results of applying [transform] to each value of - * the original [TFlow]. - * - * @see mapNotNull - */ -@ExperimentalFrpApi -fun <A, B> TFlow<A>.mapMaybe(transform: suspend FrpTransactionScope.(A) -> Maybe<B>): TFlow<B> = - map(transform).filterJust() - -/** - * Returns a [TFlow] that contains only the non-null results of applying [transform] to each value - * of the original [TFlow]. - * - * @see mapMaybe - */ -@ExperimentalFrpApi -fun <A, B> TFlow<A>.mapNotNull(transform: suspend FrpTransactionScope.(A) -> B?): TFlow<B> = - mapMaybe { - transform(it).toMaybe() - } - -/** Returns a [TFlow] containing only values of the original [TFlow] that are not null. */ -@ExperimentalFrpApi -fun <A> TFlow<A?>.filterNotNull(): TFlow<A> = mapCheap { it.toMaybe() }.filterJust() - -/** Shorthand for `mapNotNull { it as? A }`. */ -@ExperimentalFrpApi -inline fun <reified A> TFlow<*>.filterIsInstance(): TFlow<A> = mapCheap { it as? A }.filterNotNull() - -/** Shorthand for `mapMaybe { it }`. */ -@ExperimentalFrpApi -fun <A> TFlow<Maybe<A>>.filterJust(): TFlow<A> = - TFlowInit(constInit(name = null, filterJustImpl { init.connect(evalScope = this) })) - -/** - * Returns a [TFlow] containing the results of applying [transform] to each value of the original - * [TFlow]. - */ -@ExperimentalFrpApi -fun <A, B> TFlow<A>.map(transform: suspend FrpTransactionScope.(A) -> B): TFlow<B> { - val mapped: TFlowImpl<B> = - mapImpl({ init.connect(evalScope = this) }) { a -> runInTransactionScope { transform(a) } } - return TFlowInit(constInit(name = null, mapped.cached())) -} - -/** - * Like [map], but the emission is not cached during the transaction. Use only if [transform] is - * fast and pure. - * - * @see map - */ -@ExperimentalFrpApi -fun <A, B> TFlow<A>.mapCheap(transform: suspend FrpTransactionScope.(A) -> B): TFlow<B> = - TFlowInit( - constInit( - name = null, - mapImpl({ init.connect(evalScope = this) }) { a -> - runInTransactionScope { transform(a) } - }, - ) - ) - -/** - * Returns a [TFlow] that invokes [action] before each value of the original [TFlow] is emitted. - * Useful for logging and debugging. - * - * ``` - * pulse.onEach { foo(it) } == pulse.map { foo(it); it } - * ``` - * - * Note that the side effects performed in [onEach] are only performed while the resulting [TFlow] - * is connected to an output of the FRP network. If your goal is to reliably perform side effects in - * response to a [TFlow], use the output combinators available in [FrpBuildScope], such as - * [FrpBuildScope.toSharedFlow] or [FrpBuildScope.observe]. - */ -@ExperimentalFrpApi -fun <A> TFlow<A>.onEach(action: suspend FrpTransactionScope.(A) -> Unit): TFlow<A> = map { - action(it) - it -} - -/** - * Returns a [TFlow] containing only values of the original [TFlow] that satisfy the given - * [predicate]. - */ -@ExperimentalFrpApi -fun <A> TFlow<A>.filter(predicate: suspend FrpTransactionScope.(A) -> Boolean): TFlow<A> { - val pulse = - filterImpl({ init.connect(evalScope = this) }) { runInTransactionScope { predicate(it) } } - return TFlowInit(constInit(name = null, pulse)) -} - -/** - * Splits a [TFlow] of pairs into a pair of [TFlows][TFlow], where each returned [TFlow] emits half - * of the original. - * - * Shorthand for: - * ```kotlin - * val lefts = map { it.first } - * val rights = map { it.second } - * return Pair(lefts, rights) - * ``` - */ -@ExperimentalFrpApi -fun <A, B> TFlow<Pair<A, B>>.unzip(): Pair<TFlow<A>, TFlow<B>> { - val lefts = map { it.first } - val rights = map { it.second } - return lefts to rights -} - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from both. - * - * Because [TFlow]s can only emit one value per transaction, the provided [transformCoincidence] - * function is used to combine coincident emissions to produce the result value to be emitted by the - * merged [TFlow]. - */ -@ExperimentalFrpApi -fun <A> TFlow<A>.mergeWith( - other: TFlow<A>, - transformCoincidence: suspend FrpTransactionScope.(A, A) -> A = { a, _ -> a }, -): TFlow<A> { - val node = - mergeNodes( - getPulse = { init.connect(evalScope = this) }, - getOther = { other.init.connect(evalScope = this) }, - ) { a, b -> - runInTransactionScope { transformCoincidence(a, b) } - } - return TFlowInit(constInit(name = null, node)) -} - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. All coincident - * emissions are collected into the emitted [List], preserving the input ordering. - * - * @see mergeWith - * @see mergeLeft - */ -@ExperimentalFrpApi -fun <A> merge(vararg flows: TFlow<A>): TFlow<List<A>> = flows.asIterable().merge() - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. In the case of - * coincident emissions, the emission from the left-most [TFlow] is emitted. - * - * @see merge - */ -@ExperimentalFrpApi -fun <A> mergeLeft(vararg flows: TFlow<A>): TFlow<A> = flows.asIterable().mergeLeft() - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. - * - * Because [TFlow]s can only emit one value per transaction, the provided [transformCoincidence] - * function is used to combine coincident emissions to produce the result value to be emitted by the - * merged [TFlow]. - */ -// TODO: can be optimized to avoid creating the intermediate list -fun <A> merge(vararg flows: TFlow<A>, transformCoincidence: (A, A) -> A): TFlow<A> = - merge(*flows).map { l -> l.reduce(transformCoincidence) } - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. All coincident - * emissions are collected into the emitted [List], preserving the input ordering. - * - * @see mergeWith - * @see mergeLeft - */ -@ExperimentalFrpApi -fun <A> Iterable<TFlow<A>>.merge(): TFlow<List<A>> = - TFlowInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } })) - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. In the case of - * coincident emissions, the emission from the left-most [TFlow] is emitted. - * - * @see merge - */ -@ExperimentalFrpApi -fun <A> Iterable<TFlow<A>>.mergeLeft(): TFlow<A> = - TFlowInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } })) - -/** - * Creates a new [TFlow] that emits events from all given [TFlow]s. All simultaneous emissions are - * collected into the emitted [List], preserving the input ordering. - * - * @see mergeWith - */ -@ExperimentalFrpApi fun <A> Sequence<TFlow<A>>.merge(): TFlow<List<A>> = asIterable().merge() - -/** - * Creates a new [TFlow] that emits events from all given [TFlow]s. All simultaneous emissions are - * collected into the emitted [Map], and are given the same key of the associated [TFlow] in the - * input [Map]. - * - * @see mergeWith - */ -@ExperimentalFrpApi -fun <K, A> Map<K, TFlow<A>>.merge(): TFlow<Map<K, A>> = - asSequence().map { (k, flowA) -> flowA.map { a -> k to a } }.toList().merge().map { it.toMap() } - -/** - * Returns a [GroupedTFlow] that can be used to efficiently split a single [TFlow] into multiple - * downstream [TFlow]s. - * - * The input [TFlow] emits [Map] instances that specify which downstream [TFlow] the associated - * value will be emitted from. These downstream [TFlow]s can be obtained via - * [GroupedTFlow.eventsForKey]. - * - * An example: - * ``` - * val sFoo: TFlow<Map<String, Foo>> = ... - * val fooById: GroupedTFlow<String, Foo> = sFoo.groupByKey() - * val fooBar: TFlow<Foo> = fooById["bar"] - * ``` - * - * This is semantically equivalent to `val fooBar = sFoo.mapNotNull { map -> map["bar"] }` but is - * significantly more efficient; specifically, using [mapNotNull] in this way incurs a `O(n)` - * performance hit, where `n` is the number of different [mapNotNull] operations used to filter on a - * specific key's presence in the emitted [Map]. [groupByKey] internally uses a [HashMap] to lookup - * the appropriate downstream [TFlow], and so operates in `O(1)`. - * - * Note that the result [GroupedTFlow] should be cached and re-used to gain the performance benefit. - * - * @see selector - */ -@ExperimentalFrpApi -fun <K, A> TFlow<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedTFlow<K, A> = - GroupedTFlow(demuxMap({ init.connect(this) }, numKeys)) - -/** - * Shorthand for `map { mapOf(extractKey(it) to it) }.groupByKey()` - * - * @see groupByKey - */ -@ExperimentalFrpApi -fun <K, A> TFlow<A>.groupBy( - numKeys: Int? = null, - extractKey: suspend FrpTransactionScope.(A) -> K, -): GroupedTFlow<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys) - -/** - * Returns two new [TFlow]s that contain elements from this [TFlow] that satisfy or don't satisfy - * [predicate]. - * - * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }` - * but is more efficient; specifically, [partition] will only invoke [predicate] once per element. - */ -@ExperimentalFrpApi -fun <A> TFlow<A>.partition( - predicate: suspend FrpTransactionScope.(A) -> Boolean -): Pair<TFlow<A>, TFlow<A>> { - val grouped: GroupedTFlow<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate) - return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false)) -} - -/** - * Returns two new [TFlow]s that contain elements from this [TFlow]; [Pair.first] will contain - * [Left] values, and [Pair.second] will contain [Right] values. - * - * Using this is equivalent to using [filterIsInstance] in conjunction with [map] twice, once for - * [Left]s and once for [Right]s, but is slightly more efficient; specifically, the - * [filterIsInstance] check is only performed once per element. - */ -@ExperimentalFrpApi -fun <A, B> TFlow<Either<A, B>>.partitionEither(): Pair<TFlow<A>, TFlow<B>> { - val (left, right) = partition { it is Left } - return Pair(left.mapCheap { (it as Left).value }, right.mapCheap { (it as Right).value }) -} - -/** - * A mapping from keys of type [K] to [TFlow]s emitting values of type [A]. - * - * @see groupByKey - */ -@ExperimentalFrpApi -class GroupedTFlow<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) { - /** - * Returns a [TFlow] that emits values of type [A] that correspond to the given [key]. - * - * @see groupByKey - */ - @ExperimentalFrpApi - fun eventsForKey(key: K): TFlow<A> = TFlowInit(constInit(name = null, impl.eventsForKey(key))) - - /** - * Returns a [TFlow] that emits values of type [A] that correspond to the given [key]. - * - * @see groupByKey - */ - @ExperimentalFrpApi operator fun get(key: K): TFlow<A> = eventsForKey(key) -} - -/** - * Returns a [TFlow] that switches to the [TFlow] contained within this [TState] whenever it - * changes. - * - * This switch does take effect until the *next* transaction after [TState] changes. For a switch - * that takes effect immediately, see [switchPromptly]. - */ -@ExperimentalFrpApi -fun <A> TState<TFlow<A>>.switch(): TFlow<A> = - TFlowInit( - constInit( - name = null, - switchDeferredImplSingle( - getStorage = { - init.connect(this).getCurrentWithEpoch(this).first.init.connect(this) - }, - getPatches = { - mapImpl({ init.connect(this).changes }) { newFlow -> - newFlow.init.connect(this) - } - }, - ), - ) - ) - -/** - * Returns a [TFlow] that switches to the [TFlow] contained within this [TState] whenever it - * changes. - * - * This switch takes effect immediately within the same transaction that [TState] changes. In - * general, you should prefer [switch] over this method. It is both safer and more performant. - */ -// TODO: parameter to handle coincidental emission from both old and new -@ExperimentalFrpApi -fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> = - TFlowInit( - constInit( - name = null, - switchPromptImplSingle( - getStorage = { - init.connect(this).getCurrentWithEpoch(this).first.init.connect(this) - }, - getPatches = { - mapImpl({ init.connect(this).changes }) { newFlow -> - newFlow.init.connect(this) - } - }, - ), - ) - ) - -/** - * A mutable [TFlow] that provides the ability to [emit] values to the flow, handling backpressure - * by coalescing all emissions into batches. - * - * @see FrpNetwork.coalescingMutableTFlow - */ -@ExperimentalFrpApi -class CoalescingMutableTFlow<In, Out> -internal constructor( - internal val name: String?, - internal val coalesce: (old: Out, new: In) -> Out, - internal val network: Network, - private val getInitialValue: () -> Out, - internal val impl: InputNode<Out> = InputNode(), -) : TFlow<Out>() { - internal val storage = AtomicReference(false to getInitialValue()) - - override fun toString(): String = "${this::class.simpleName}@$hashString" - - /** - * Inserts [value] into the current batch, enqueueing it for emission from this [TFlow] if not - * already pending. - * - * Backpressure occurs when [emit] is called while the FRP network is currently in a - * transaction; if called multiple times, then emissions will be coalesced into a single batch - * that is then processed when the network is ready. - */ - @ExperimentalFrpApi - fun emit(value: In) { - val (scheduled, _) = storage.getAndUpdate { (_, old) -> true to coalesce(old, value) } - if (!scheduled) { - @Suppress("DeferredResultUnused") - network.transaction("CoalescingMutableTFlow${name?.let { "($name)" }.orEmpty()}.emit") { - impl.visit(this, storage.getAndSet(false to getInitialValue()).second) - } - } - } -} - -/** - * A mutable [TFlow] that provides the ability to [emit] values to the flow, handling backpressure - * by suspending the emitter. - * - * @see FrpNetwork.coalescingMutableTFlow - */ -@ExperimentalFrpApi -class MutableTFlow<T> -internal constructor(internal val network: Network, internal val impl: InputNode<T> = InputNode()) : - TFlow<T>() { - internal val name: String? = null - - private val storage = AtomicReference<Job?>(null) - - override fun toString(): String = "${this::class.simpleName}@$hashString" - - /** - * Emits a [value] to this [TFlow], suspending the caller until the FRP transaction containing - * the emission has completed. - */ - @ExperimentalFrpApi - suspend fun emit(value: T) { - coroutineScope { - var jobOrNull: Job? = null - val newEmit = - async(start = CoroutineStart.LAZY) { - jobOrNull?.join() - network - .transaction("MutableTFlow($name).emit") { impl.visit(this, value) } - .await() - } - jobOrNull = storage.getAndSet(newEmit) - newEmit.await() - } - } - - // internal suspend fun emitInCurrentTransaction(value: T, evalScope: EvalScope) { - // if (storage.getAndSet(just(value)) is None) { - // impl.visit(evalScope) - // } - // } -} - -private data object EmptyFlow : TFlow<Nothing>() - -internal class TFlowInit<out A>(val init: Init<TFlowImpl<A>>) : TFlow<A>() { - override fun toString(): String = "${this::class.simpleName}@$hashString" -} - -internal val <A> TFlow<A>.init: Init<TFlowImpl<A>> - get() = - when (this) { - is EmptyFlow -> constInit("EmptyFlow", neverImpl) - is TFlowInit -> init - is TFlowLoop -> init - is CoalescingMutableTFlow<*, A> -> constInit(name, impl.activated()) - is MutableTFlow -> constInit(name, impl.activated()) - } - -private inline fun <A> deferInline(crossinline block: suspend InitScope.() -> TFlow<A>): TFlow<A> = - TFlowInit(init(name = null) { block().init.connect(evalScope = this) }) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt deleted file mode 100644 index 66aa2a950fcf..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt +++ /dev/null @@ -1,545 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.kairos - -import com.android.systemui.kairos.internal.DerivedMapCheap -import com.android.systemui.kairos.internal.Init -import com.android.systemui.kairos.internal.InitScope -import com.android.systemui.kairos.internal.Network -import com.android.systemui.kairos.internal.NoScope -import com.android.systemui.kairos.internal.Schedulable -import com.android.systemui.kairos.internal.TFlowImpl -import com.android.systemui.kairos.internal.TStateImpl -import com.android.systemui.kairos.internal.TStateSource -import com.android.systemui.kairos.internal.activated -import com.android.systemui.kairos.internal.cached -import com.android.systemui.kairos.internal.constInit -import com.android.systemui.kairos.internal.constS -import com.android.systemui.kairos.internal.filterImpl -import com.android.systemui.kairos.internal.flatMap -import com.android.systemui.kairos.internal.init -import com.android.systemui.kairos.internal.map -import com.android.systemui.kairos.internal.mapCheap -import com.android.systemui.kairos.internal.mapImpl -import com.android.systemui.kairos.internal.util.hashString -import com.android.systemui.kairos.internal.zipStateMap -import com.android.systemui.kairos.internal.zipStates -import kotlin.reflect.KProperty -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope - -/** - * A time-varying value with discrete changes. Essentially, a combination of a [Transactional] that - * holds a value, and a [TFlow] that emits when the value changes. - */ -@ExperimentalFrpApi sealed class TState<out A> - -/** A [TState] that never changes. */ -@ExperimentalFrpApi -fun <A> tStateOf(value: A): TState<A> { - val operatorName = "tStateOf" - val name = "$operatorName($value)" - return TStateInit(constInit(name, constS(name, operatorName, value))) -} - -/** TODO */ -@ExperimentalFrpApi fun <A> Lazy<TState<A>>.defer(): TState<A> = deferInline { value } - -/** TODO */ -@ExperimentalFrpApi -fun <A> FrpDeferredValue<TState<A>>.defer(): TState<A> = deferInline { unwrapped.await() } - -/** TODO */ -@ExperimentalFrpApi -fun <A> deferTState(block: suspend FrpScope.() -> TState<A>): TState<A> = deferInline { - NoScope.runInFrpScope(block) -} - -/** - * Returns a [TState] containing the results of applying [transform] to the value held by the - * original [TState]. - */ -@ExperimentalFrpApi -fun <A, B> TState<A>.map(transform: suspend FrpScope.(A) -> B): TState<B> { - val operatorName = "map" - val name = operatorName - return TStateInit( - init(name) { - init.connect(evalScope = this).map(name, operatorName) { - NoScope.runInFrpScope { transform(it) } - } - } - ) -} - -/** - * Returns a [TState] that transforms the value held inside this [TState] by applying it to the - * [transform]. - * - * Note that unlike [map], the result is not cached. This means that not only should [transform] be - * fast and pure, it should be *monomorphic* (1-to-1). Failure to do this means that [stateChanges] - * for the returned [TState] will operate unexpectedly, emitting at rates that do not reflect an - * observable change to the returned [TState]. - */ -@ExperimentalFrpApi -fun <A, B> TState<A>.mapCheapUnsafe(transform: suspend FrpScope.(A) -> B): TState<B> { - val operatorName = "map" - val name = operatorName - return TStateInit( - init(name) { - init.connect(evalScope = this).mapCheap(name, operatorName) { - NoScope.runInFrpScope { transform(it) } - } - } - ) -} - -/** - * Returns a [TState] by combining the values held inside the given [TState]s by applying them to - * the given function [transform]. - */ -@ExperimentalFrpApi -fun <A, B, C> TState<A>.combineWith( - other: TState<B>, - transform: suspend FrpScope.(A, B) -> C, -): TState<C> = combine(this, other, transform) - -/** - * Splits a [TState] of pairs into a pair of [TFlows][TState], where each returned [TState] holds - * half of the original. - * - * Shorthand for: - * ```kotlin - * val lefts = map { it.first } - * val rights = map { it.second } - * return Pair(lefts, rights) - * ``` - */ -@ExperimentalFrpApi -fun <A, B> TState<Pair<A, B>>.unzip(): Pair<TState<A>, TState<B>> { - val left = map { it.first } - val right = map { it.second } - return left to right -} - -/** - * Returns a [TState] by combining the values held inside the given [TStates][TState] into a [List]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A> Iterable<TState<A>>.combine(): TState<List<A>> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - zipStates(name, operatorName, states = map { it.init.connect(evalScope = this) }) - } - ) -} - -/** - * Returns a [TState] by combining the values held inside the given [TStates][TState] into a [Map]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <K, A> Map<K, TState<A>>.combine(): TState<Map<K, A>> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - zipStateMap( - name, - operatorName, - states = mapValues { it.value.init.connect(evalScope = this) }, - ) - } - ) -} - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B> Iterable<TState<A>>.combine(transform: suspend FrpScope.(List<A>) -> B): TState<B> = - combine().map(transform) - -/** - * Returns a [TState] by combining the values held inside the given [TState]s into a [List]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A> combine(vararg states: TState<A>): TState<List<A>> = states.asIterable().combine() - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B> combine( - vararg states: TState<A>, - transform: suspend FrpScope.(List<A>) -> B, -): TState<B> = states.asIterable().combine(transform) - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - transform: suspend FrpScope.(A, B) -> Z, -): TState<Z> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - coroutineScope { - val dl1: Deferred<TStateImpl<A>> = async { - stateA.init.connect(evalScope = this@init) - } - val dl2: Deferred<TStateImpl<B>> = async { - stateB.init.connect(evalScope = this@init) - } - zipStates(name, operatorName, dl1.await(), dl2.await()) { a, b -> - NoScope.runInFrpScope { transform(a, b) } - } - } - } - ) -} - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B, C, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - stateC: TState<C>, - transform: suspend FrpScope.(A, B, C) -> Z, -): TState<Z> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - coroutineScope { - val dl1: Deferred<TStateImpl<A>> = async { - stateA.init.connect(evalScope = this@init) - } - val dl2: Deferred<TStateImpl<B>> = async { - stateB.init.connect(evalScope = this@init) - } - val dl3: Deferred<TStateImpl<C>> = async { - stateC.init.connect(evalScope = this@init) - } - zipStates(name, operatorName, dl1.await(), dl2.await(), dl3.await()) { a, b, c -> - NoScope.runInFrpScope { transform(a, b, c) } - } - } - } - ) -} - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B, C, D, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - stateC: TState<C>, - stateD: TState<D>, - transform: suspend FrpScope.(A, B, C, D) -> Z, -): TState<Z> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - coroutineScope { - val dl1: Deferred<TStateImpl<A>> = async { - stateA.init.connect(evalScope = this@init) - } - val dl2: Deferred<TStateImpl<B>> = async { - stateB.init.connect(evalScope = this@init) - } - val dl3: Deferred<TStateImpl<C>> = async { - stateC.init.connect(evalScope = this@init) - } - val dl4: Deferred<TStateImpl<D>> = async { - stateD.init.connect(evalScope = this@init) - } - zipStates(name, operatorName, dl1.await(), dl2.await(), dl3.await(), dl4.await()) { - a, - b, - c, - d -> - NoScope.runInFrpScope { transform(a, b, c, d) } - } - } - } - ) -} - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B, C, D, E, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - stateC: TState<C>, - stateD: TState<D>, - stateE: TState<E>, - transform: suspend FrpScope.(A, B, C, D, E) -> Z, -): TState<Z> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - coroutineScope { - val dl1: Deferred<TStateImpl<A>> = async { - stateA.init.connect(evalScope = this@init) - } - val dl2: Deferred<TStateImpl<B>> = async { - stateB.init.connect(evalScope = this@init) - } - val dl3: Deferred<TStateImpl<C>> = async { - stateC.init.connect(evalScope = this@init) - } - val dl4: Deferred<TStateImpl<D>> = async { - stateD.init.connect(evalScope = this@init) - } - val dl5: Deferred<TStateImpl<E>> = async { - stateE.init.connect(evalScope = this@init) - } - zipStates( - name, - operatorName, - dl1.await(), - dl2.await(), - dl3.await(), - dl4.await(), - dl5.await(), - ) { a, b, c, d, e -> - NoScope.runInFrpScope { transform(a, b, c, d, e) } - } - } - } - ) -} - -/** Returns a [TState] by applying [transform] to the value held by the original [TState]. */ -@ExperimentalFrpApi -fun <A, B> TState<A>.flatMap(transform: suspend FrpScope.(A) -> TState<B>): TState<B> { - val operatorName = "flatMap" - val name = operatorName - return TStateInit( - init(name) { - init.connect(this).flatMap(name, operatorName) { a -> - NoScope.runInFrpScope { transform(a) }.init.connect(this) - } - } - ) -} - -/** Shorthand for `flatMap { it }` */ -@ExperimentalFrpApi fun <A> TState<TState<A>>.flatten() = flatMap { it } - -/** - * Returns a [TStateSelector] that can be used to efficiently check if the input [TState] is - * currently holding a specific value. - * - * An example: - * ``` - * val lInt: TState<Int> = ... - * val intSelector: TStateSelector<Int> = lInt.selector() - * // Tracks if lInt is holding 1 - * val isOne: TState<Boolean> = intSelector.whenSelected(1) - * ``` - * - * This is semantically equivalent to `val isOne = lInt.map { i -> i == 1 }`, but is significantly - * more efficient; specifically, using [TState.map] in this way incurs a `O(n)` performance hit, - * where `n` is the number of different [TState.map] operations used to track a specific value. - * [selector] internally uses a [HashMap] to lookup the appropriate downstream [TState] to update, - * and so operates in `O(1)`. - * - * Note that the result [TStateSelector] should be cached and re-used to gain the performance - * benefit. - * - * @see groupByKey - */ -@ExperimentalFrpApi -fun <A> TState<A>.selector(numDistinctValues: Int? = null): TStateSelector<A> = - TStateSelector( - this, - stateChanges - .map { new -> mapOf(new to true, sampleDeferred().get() to false) } - .groupByKey(numDistinctValues), - ) - -/** - * Tracks the currently selected value of type [A] from an upstream [TState]. - * - * @see selector - */ -@ExperimentalFrpApi -class TStateSelector<in A> -internal constructor( - private val upstream: TState<A>, - private val groupedChanges: GroupedTFlow<A, Boolean>, -) { - /** - * Returns a [TState] that tracks whether the upstream [TState] is currently holding the given - * [value]. - * - * @see selector - */ - @ExperimentalFrpApi - fun whenSelected(value: A): TState<Boolean> { - val operatorName = "TStateSelector#whenSelected" - val name = "$operatorName[$value]" - return TStateInit( - init(name) { - DerivedMapCheap( - name, - operatorName, - upstream = upstream.init.connect(evalScope = this), - changes = groupedChanges.impl.eventsForKey(value), - ) { - it == value - } - } - ) - } - - @ExperimentalFrpApi operator fun get(value: A): TState<Boolean> = whenSelected(value) -} - -/** TODO */ -@ExperimentalFrpApi -class MutableTState<T> -internal constructor(internal val network: Network, initialValue: Deferred<T>) : TState<T>() { - - private val input: CoalescingMutableTFlow<Deferred<T>, Deferred<T>?> = - CoalescingMutableTFlow( - name = null, - coalesce = { _, new -> new }, - network = network, - getInitialValue = { null }, - ) - - internal val tState = run { - val changes = input.impl - val name = null - val operatorName = "MutableTState" - lateinit var state: TStateSource<T> - val calm: TFlowImpl<T> = - filterImpl({ mapImpl(upstream = { changes.activated() }) { it!!.await() } }) { new -> - new != state.getCurrentWithEpoch(evalScope = this).first - } - .cached() - state = TStateSource(name, operatorName, initialValue, calm) - @Suppress("DeferredResultUnused") - network.transaction("MutableTState.init") { - calm.activate(evalScope = this, downstream = Schedulable.S(state))?.let { - (connection, needsEval) -> - state.upstreamConnection = connection - if (needsEval) { - schedule(state) - } - } - } - TStateInit(constInit(name, state)) - } - - /** TODO */ - @ExperimentalFrpApi fun setValue(value: T) = input.emit(CompletableDeferred(value)) - - @ExperimentalFrpApi - fun setValueDeferred(value: FrpDeferredValue<T>) = input.emit(value.unwrapped) -} - -/** A forward-reference to a [TState], allowing for recursive definitions. */ -@ExperimentalFrpApi -class TStateLoop<A> : TState<A>() { - - private val name: String? = null - - private val deferred = CompletableDeferred<TState<A>>() - - internal val init: Init<TStateImpl<A>> = - init(name) { deferred.await().init.connect(evalScope = this) } - - /** The [TState] this [TStateLoop] will forward to. */ - @ExperimentalFrpApi - var loopback: TState<A>? = null - set(value) { - value?.let { - check(deferred.complete(value)) { "TStateLoop.loopback has already been set." } - field = value - } - } - - @ExperimentalFrpApi - operator fun getValue(thisRef: Any?, property: KProperty<*>): TState<A> = this - - @ExperimentalFrpApi - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: TState<A>) { - loopback = value - } - - override fun toString(): String = "${this::class.simpleName}@$hashString" -} - -internal class TStateInit<A> internal constructor(internal val init: Init<TStateImpl<A>>) : - TState<A>() { - override fun toString(): String = "${this::class.simpleName}@$hashString" -} - -internal val <A> TState<A>.init: Init<TStateImpl<A>> - get() = - when (this) { - is TStateInit -> init - is TStateLoop -> init - is MutableTState -> tState.init - } - -private inline fun <A> deferInline( - crossinline block: suspend InitScope.() -> TState<A> -): TState<A> = TStateInit(init(name = null) { block().init.connect(evalScope = this) }) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt new file mode 100644 index 000000000000..225416992d52 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +/** + * Kairos operations that are available while a transaction is active. + * + * These operations do not accumulate state, which makes [TransactionScope] weaker than + * [StateScope], but allows them to be used in more places. + */ +@ExperimentalKairosApi +interface TransactionScope : KairosScope { + + /** + * Returns the current value of this [Transactional] as a [DeferredValue]. + * + * Compared to [sample], you may want to use this instead if you do not need to inspect the + * sampled value, but instead want to pass it to another Kairos API that accepts a + * [DeferredValue]. In this case, [sampleDeferred] is both safer and more performant. + * + * @see sample + */ + fun <A> Transactional<A>.sampleDeferred(): DeferredValue<A> + + /** + * Returns the current value of this [State] as a [DeferredValue]. + * + * Compared to [sample], you may want to use this instead if you do not need to inspect the + * sampled value, but instead want to pass it to another Kairos API that accepts a + * [DeferredValue]. In this case, [sampleDeferred] is both safer and more performant. + * + * @see sample + */ + fun <A> State<A>.sampleDeferred(): DeferredValue<A> + + /** + * Defers invoking [block] until after the current [TransactionScope] code-path completes, + * returning a [DeferredValue] that can be used to reference the result. + * + * Useful for recursive definitions. + * + * @see DeferredValue + */ + fun <A> deferredTransactionScope(block: TransactionScope.() -> A): DeferredValue<A> + + /** An [Events] that emits once, within this transaction, and then never again. */ + val now: Events<Unit> + + /** + * Returns the current value held by this [State]. Guaranteed to be consistent within the same + * transaction. + * + * @see sampleDeferred + */ + fun <A> State<A>.sample(): A = sampleDeferred().get() + + /** + * Returns the current value held by this [Transactional]. Guaranteed to be consistent within + * the same transaction. + * + * @see sampleDeferred + */ + fun <A> Transactional<A>.sample(): A = sampleDeferred().get() +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt index 6b1c8c8fc3e5..9485cd212603 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt @@ -16,57 +16,80 @@ package com.android.systemui.kairos +import com.android.systemui.kairos.internal.CompletableLazy import com.android.systemui.kairos.internal.InitScope import com.android.systemui.kairos.internal.NoScope import com.android.systemui.kairos.internal.TransactionalImpl import com.android.systemui.kairos.internal.init import com.android.systemui.kairos.internal.transactionalImpl import com.android.systemui.kairos.internal.util.hashString -import kotlinx.coroutines.CompletableDeferred /** * A time-varying value. A [Transactional] encapsulates the idea of some continuous state; each time * it is "sampled", a new result may be produced. * - * Because FRP operates over an "idealized" model of Time that can be passed around as a data type, - * [Transactional]s are guaranteed to produce the same result if queried multiple times at the same - * (conceptual) time, in order to preserve _referential transparency_. + * Because Kairos operates over an "idealized" model of Time that can be passed around as a data + * type, [Transactional]s are guaranteed to produce the same result if queried multiple times at the + * same (conceptual) time, in order to preserve _referential transparency_. */ -@ExperimentalFrpApi -class Transactional<out A> internal constructor(internal val impl: TState<TransactionalImpl<A>>) { +@ExperimentalKairosApi +class Transactional<out A> internal constructor(internal val impl: State<TransactionalImpl<A>>) { override fun toString(): String = "${this::class.simpleName}@$hashString" } /** A constant [Transactional] that produces [value] whenever it is sampled. */ -@ExperimentalFrpApi +@ExperimentalKairosApi fun <A> transactionalOf(value: A): Transactional<A> = - Transactional(tStateOf(TransactionalImpl.Const(CompletableDeferred(value)))) + Transactional(stateOf(TransactionalImpl.Const(CompletableLazy(value)))) -/** TODO */ -@ExperimentalFrpApi -fun <A> FrpDeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline { - unwrapped.await() -} +/** + * Returns a [Transactional] that acts as a deferred-reference to the [Transactional] produced by + * this [DeferredValue]. + * + * When the returned [Transactional] is accessed by the Kairos network, the [DeferredValue] will be + * queried and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> DeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline { unwrapped.value } -/** TODO */ -@ExperimentalFrpApi fun <A> Lazy<Transactional<A>>.defer(): Transactional<A> = deferInline { value } +/** + * Returns a [Transactional] that acts as a deferred-reference to the [Transactional] produced by + * this [Lazy]. + * + * When the returned [Transactional] is accessed by the Kairos network, the [Lazy]'s + * [value][Lazy.value] will be queried and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> Lazy<Transactional<A>>.defer(): Transactional<A> = deferInline { value } -/** TODO */ -@ExperimentalFrpApi -fun <A> deferTransactional(block: suspend FrpScope.() -> Transactional<A>): Transactional<A> = +/** + * Returns a [Transactional] that acts as a deferred-reference to the [Transactional] produced by + * [block]. + * + * When the returned [Transactional] is accessed by the Kairos network, [block] will be invoked and + * the returned [Transactional] will be used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> deferredTransactional(block: KairosScope.() -> Transactional<A>): Transactional<A> = deferInline { - NoScope.runInFrpScope(block) + NoScope.block() } private inline fun <A> deferInline( - crossinline block: suspend InitScope.() -> Transactional<A> + crossinline block: InitScope.() -> Transactional<A> ): Transactional<A> = - Transactional(TStateInit(init(name = null) { block().impl.init.connect(evalScope = this) })) + Transactional(StateInit(init(name = null) { block().impl.init.connect(evalScope = this) })) /** * Returns a [Transactional]. The passed [block] will be evaluated on demand at most once per * transaction; any subsequent sampling within the same transaction will receive a cached value. */ -@ExperimentalFrpApi -fun <A> transactionally(block: suspend FrpTransactionScope.() -> A): Transactional<A> = - Transactional(tStateOf(transactionalImpl { runInTransactionScope(block) })) +@ExperimentalKairosApi +fun <A> transactionally(block: TransactionScope.() -> A): Transactional<A> = + Transactional(stateOf(transactionalImpl { block() })) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt index 6f9612fab70a..d43a0bbf433e 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt @@ -16,18 +16,18 @@ package com.android.systemui.kairos.debug -import com.android.systemui.kairos.MutableTState -import com.android.systemui.kairos.TState -import com.android.systemui.kairos.TStateInit -import com.android.systemui.kairos.TStateLoop +import com.android.systemui.kairos.MutableState +import com.android.systemui.kairos.State +import com.android.systemui.kairos.StateInit +import com.android.systemui.kairos.StateLoop import com.android.systemui.kairos.internal.DerivedFlatten import com.android.systemui.kairos.internal.DerivedMap import com.android.systemui.kairos.internal.DerivedMapCheap import com.android.systemui.kairos.internal.DerivedZipped import com.android.systemui.kairos.internal.Init -import com.android.systemui.kairos.internal.TStateDerived -import com.android.systemui.kairos.internal.TStateImpl -import com.android.systemui.kairos.internal.TStateSource +import com.android.systemui.kairos.internal.StateDerived +import com.android.systemui.kairos.internal.StateImpl +import com.android.systemui.kairos.internal.StateSource import com.android.systemui.kairos.util.Just import com.android.systemui.kairos.util.Maybe import com.android.systemui.kairos.util.None @@ -87,12 +87,12 @@ data class Edge(val upstream: Any, val downstream: Any, val tag: Any? = null) data class Graph<T>(val nodes: Map<Any, T>, val edges: List<Edge>) -internal fun TState<*>.dump(infoMap: MutableMap<Any, InitInfo>, edges: MutableList<Edge>) { - val init: Init<TStateImpl<Any?>> = +internal fun State<*>.dump(infoMap: MutableMap<Any, InitInfo>, edges: MutableList<Edge>) { + val init: Init<StateImpl<Any?>> = when (this) { - is TStateInit -> init - is TStateLoop -> init - is MutableTState -> tState.init + is StateInit -> init + is StateLoop -> init + is MutableState -> state.init } when (val stateMaybe = init.getUnsafe()) { None -> { @@ -104,12 +104,12 @@ internal fun TState<*>.dump(infoMap: MutableMap<Any, InitInfo>, edges: MutableLi } } -internal fun TStateImpl<*>.dump(infoById: MutableMap<Any, InitInfo>, edges: MutableList<Edge>) { +internal fun StateImpl<*>.dump(infoById: MutableMap<Any, InitInfo>, edges: MutableList<Edge>) { val state = this if (state in infoById) return val stateInfo = when (state) { - is TStateDerived -> { + is StateDerived -> { val type = when (state) { is DerivedFlatten -> { @@ -151,7 +151,7 @@ internal fun TStateImpl<*>.dump(infoById: MutableMap<Any, InitInfo>, edges: Muta state.invalidatedEpoch, ) } - is TStateSource -> + is StateSource -> Source( state.name ?: state.operatorName, state.getStorageUnsafe(), @@ -174,30 +174,30 @@ internal fun TStateImpl<*>.dump(infoById: MutableMap<Any, InitInfo>, edges: Muta infoById[state] = Initialized(stateInfo) } -private fun <A> TStateImpl<A>.getUnsafe(): Maybe<A> = +private fun <A> StateImpl<A>.getUnsafe(): Maybe<A> = when (this) { - is TStateDerived -> getCachedUnsafe() - is TStateSource -> getStorageUnsafe() + is StateDerived -> getCachedUnsafe() + is StateSource -> getStorageUnsafe() is DerivedMapCheap<*, *> -> none } -private fun <A> TStateImpl<A>.getUnsafeWithEpoch(): Maybe<Pair<A, Long>> = +private fun <A> StateImpl<A>.getUnsafeWithEpoch(): Maybe<Pair<A, Long>> = when (this) { - is TStateDerived -> getCachedUnsafe().map { it to invalidatedEpoch } - is TStateSource -> getStorageUnsafe().map { it to writeEpoch } + is StateDerived -> getCachedUnsafe().map { it to invalidatedEpoch } + is StateSource -> getStorageUnsafe().map { it to writeEpoch } is DerivedMapCheap<*, *> -> none } /** - * Returns the current value held in this [TState], or [none] if the [TState] has not been + * Returns the current value held in this [State], or [none] if the [State] has not been * initialized. * * The returned [Long] is the *epoch* at which the internal cache was last updated. This can be used * to identify values which are out-of-date. */ -fun <A> TState<A>.sampleUnsafe(): Maybe<Pair<A, Long>> = +fun <A> State<A>.sampleUnsafe(): Maybe<Pair<A, Long>> = when (this) { - is MutableTState -> tState.init.getUnsafe().flatMap { it.getUnsafeWithEpoch() } - is TStateInit -> init.getUnsafe().flatMap { it.getUnsafeWithEpoch() } - is TStateLoop -> this.init.getUnsafe().flatMap { it.getUnsafeWithEpoch() } + is MutableState -> state.init.getUnsafe().flatMap { it.getUnsafeWithEpoch() } + is StateInit -> init.getUnsafe().flatMap { it.getUnsafeWithEpoch() } + is StateLoop -> this.init.getUnsafe().flatMap { it.getUnsafeWithEpoch() } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt index 7e6384925f38..07dc1dd2c79c 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt @@ -16,25 +16,24 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.CoalescingMutableTFlow -import com.android.systemui.kairos.FrpBuildScope -import com.android.systemui.kairos.FrpCoalescingProducerScope -import com.android.systemui.kairos.FrpDeferredValue -import com.android.systemui.kairos.FrpEffectScope -import com.android.systemui.kairos.FrpNetwork -import com.android.systemui.kairos.FrpProducerScope -import com.android.systemui.kairos.FrpSpec -import com.android.systemui.kairos.FrpStateScope -import com.android.systemui.kairos.FrpTransactionScope -import com.android.systemui.kairos.GroupedTFlow -import com.android.systemui.kairos.LocalFrpNetwork -import com.android.systemui.kairos.MutableTFlow -import com.android.systemui.kairos.TFlow -import com.android.systemui.kairos.TFlowInit +import com.android.systemui.kairos.BuildScope +import com.android.systemui.kairos.BuildSpec +import com.android.systemui.kairos.CoalescingEventProducerScope +import com.android.systemui.kairos.CoalescingMutableEvents +import com.android.systemui.kairos.DeferredValue +import com.android.systemui.kairos.EffectScope +import com.android.systemui.kairos.EventProducerScope +import com.android.systemui.kairos.Events +import com.android.systemui.kairos.EventsInit +import com.android.systemui.kairos.GroupedEvents +import com.android.systemui.kairos.KairosNetwork +import com.android.systemui.kairos.LocalNetwork +import com.android.systemui.kairos.MutableEvents +import com.android.systemui.kairos.TransactionScope import com.android.systemui.kairos.groupByKey import com.android.systemui.kairos.init import com.android.systemui.kairos.internal.util.childScope -import com.android.systemui.kairos.internal.util.mapValuesParallel +import com.android.systemui.kairos.internal.util.launchImmediate import com.android.systemui.kairos.launchEffect import com.android.systemui.kairos.mergeLeft import com.android.systemui.kairos.util.Just @@ -43,121 +42,81 @@ import com.android.systemui.kairos.util.None import com.android.systemui.kairos.util.just import com.android.systemui.kairos.util.map import java.util.concurrent.atomic.AtomicReference -import kotlin.coroutines.Continuation import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.startCoroutine -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableJob import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred import kotlinx.coroutines.Job import kotlinx.coroutines.cancel -import kotlinx.coroutines.completeWith import kotlinx.coroutines.job internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope: CoroutineScope) : - BuildScope, StateScope by stateScope { + InternalBuildScope, InternalStateScope by stateScope { private val job: Job get() = coroutineScope.coroutineContext.job - override val frpScope: FrpBuildScope = FrpBuildScopeImpl() - - override suspend fun <R> runInBuildScope(block: suspend FrpBuildScope.() -> R): R { - val complete = CompletableDeferred<R>(parent = coroutineContext.job) - block.startCoroutine( - frpScope, - object : Continuation<R> { - override val context: CoroutineContext - get() = EmptyCoroutineContext - - override fun resumeWith(result: Result<R>) { - complete.completeWith(result) - } - }, - ) - return complete.await() - } - - private fun <A, T : TFlow<A>, S> buildTFlow( - constructFlow: (InputNode<A>) -> Pair<T, S>, - builder: suspend S.() -> Unit, - ): TFlow<A> { - var job: Job? = null - val stopEmitter = newStopEmitter("buildTFlow") - // Create a child scope that will be kept alive beyond the end of this transaction. - val childScope = coroutineScope.childScope() - lateinit var emitter: Pair<T, S> - val inputNode = - InputNode<A>( - activate = { - check(job == null) { "already activated" } - job = - reenterBuildScope(this@BuildScopeImpl, childScope).runInBuildScope { - launchEffect { - builder(emitter.second) - stopEmitter.emit(Unit) - } - } - }, - deactivate = { - checkNotNull(job) { "already deactivated" }.cancel() - job = null - }, - ) - emitter = constructFlow(inputNode) - return with(frpScope) { emitter.first.takeUntil(mergeLeft(stopEmitter, endSignal)) } + override val kairosNetwork: KairosNetwork by lazy { + LocalNetwork(network, coroutineScope, endSignal) } - private fun <T> tFlowInternal(builder: suspend FrpProducerScope<T>.() -> Unit): TFlow<T> = - buildTFlow( - constructFlow = { inputNode -> - val flow = MutableTFlow(network, inputNode) - flow to - object : FrpProducerScope<T> { + override fun <T> events( + name: String?, + builder: suspend EventProducerScope<T>.() -> Unit, + ): Events<T> = + buildEvents( + name, + constructEvents = { inputNode -> + val events = MutableEvents(network, inputNode) + events to + object : EventProducerScope<T> { override suspend fun emit(value: T) { - flow.emit(value) + events.emit(value) } } }, builder = builder, ) - private fun <In, Out> coalescingTFlowInternal( + override fun <In, Out> coalescingEvents( getInitialValue: () -> Out, coalesce: (old: Out, new: In) -> Out, - builder: suspend FrpCoalescingProducerScope<In>.() -> Unit, - ): TFlow<Out> = - buildTFlow( - constructFlow = { inputNode -> - val flow = - CoalescingMutableTFlow(null, coalesce, network, getInitialValue, inputNode) - flow to - object : FrpCoalescingProducerScope<In> { + builder: suspend CoalescingEventProducerScope<In>.() -> Unit, + ): Events<Out> = + buildEvents( + constructEvents = { inputNode -> + val events = + CoalescingMutableEvents( + null, + coalesce = { old, new: In -> coalesce(old.value, new) }, + network, + getInitialValue, + inputNode, + ) + events to + object : CoalescingEventProducerScope<In> { override fun emit(value: In) { - flow.emit(value) + events.emit(value) } } }, builder = builder, ) - private fun <A> asyncScopeInternal(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job> { + override fun <A> asyncScope(block: BuildSpec<A>): Pair<DeferredValue<A>, Job> { val childScope = mutableChildBuildScope() - return FrpDeferredValue(deferAsync { childScope.runInBuildScope(block) }) to childScope.job + return DeferredValue(deferAsync { block(childScope) }) to childScope.job } - private fun <R> deferredInternal(block: suspend FrpBuildScope.() -> R): FrpDeferredValue<R> = - FrpDeferredValue(deferAsync { runInBuildScope(block) }) + override fun <R> deferredBuildScope(block: BuildScope.() -> R): DeferredValue<R> = + DeferredValue(deferAsync { block() }) - private fun deferredActionInternal(block: suspend FrpBuildScope.() -> Unit) { - deferAction { runInBuildScope(block) } + override fun deferredBuildScopeAction(block: BuildScope.() -> Unit) { + deferAction { block() } } - private fun <A> TFlow<A>.observeEffectInternal( - context: CoroutineContext, - block: suspend FrpEffectScope.(A) -> Unit, + override fun <A> Events<A>.observe( + coroutineContext: CoroutineContext, + block: EffectScope.(A) -> Unit, ): Job { val subRef = AtomicReference<Maybe<Output<A>>>(null) val childScope = coroutineScope.childScope() @@ -172,38 +131,26 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope } } } + val localNetwork = LocalNetwork(network, childScope, endSignal) // Defer so that we don't suspend the caller deferAction { val outputNode = Output<A>( - context = context, + context = coroutineContext, onDeath = { subRef.getAndSet(None)?.let { childScope.cancel() } }, onEmit = { output -> if (subRef.get() is Just) { // Not cancelled, safe to emit - val coroutine: suspend FrpEffectScope.() -> Unit = { block(output) } - val complete = CompletableDeferred<Unit>(parent = coroutineContext.job) - coroutine.startCoroutine( - object : FrpEffectScope, FrpTransactionScope by frpScope { - override val frpCoroutineScope: CoroutineScope = childScope - override val frpNetwork: FrpNetwork = - LocalFrpNetwork(network, childScope, endSignal) - }, - completion = - object : Continuation<Unit> { - override val context: CoroutineContext - get() = EmptyCoroutineContext - - override fun resumeWith(result: Result<Unit>) { - complete.completeWith(result) - } - }, - ) - complete.await() + val scope = + object : EffectScope, TransactionScope by this@BuildScopeImpl { + override val effectCoroutineScope: CoroutineScope = childScope + override val kairosNetwork: KairosNetwork = localNetwork + } + block(scope, output) } }, ) - with(frpScope) { this@observeEffectInternal.takeUntil(endSignal) } + this@observe.takeUntil(endSignal) .init .connect(evalScope = stateScope.evalScope) .activate(evalScope = stateScope.evalScope, outputNode.schedulable) @@ -213,71 +160,110 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope // Job's already been cancelled, schedule deactivation scheduleDeactivation(outputNode) } else if (needsEval) { - outputNode.schedule(evalScope = stateScope.evalScope) + outputNode.schedule(0, evalScope = stateScope.evalScope) } } ?: run { childScope.cancel() } } return childScope.coroutineContext.job } - private fun <A, B> TFlow<A>.mapBuildInternal( - transform: suspend FrpBuildScope.(A) -> B - ): TFlow<B> { + override fun <A, B> Events<A>.mapBuild(transform: BuildScope.(A) -> B): Events<B> { val childScope = coroutineScope.childScope() - return TFlowInit( + return EventsInit( constInit( "mapBuild", - mapImpl({ init.connect(evalScope = this) }) { spec -> + mapImpl({ init.connect(evalScope = this) }) { spec, _ -> reenterBuildScope(outerScope = this@BuildScopeImpl, childScope) - .runInBuildScope { transform(spec) } + .transform(spec) } .cached(), ) ) } - private fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestForKeyInternal( - init: FrpDeferredValue<Map<K, FrpSpec<B>>>, + override fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey( + initialSpecs: DeferredValue<Map<K, BuildSpec<B>>>, numKeys: Int?, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> { - val eventsByKey: GroupedTFlow<K, Maybe<FrpSpec<A>>> = groupByKey(numKeys) - val initOut: Deferred<Map<K, B>> = deferAsync { - init.unwrapped.await().mapValuesParallel { (k, spec) -> - val newEnd = with(frpScope) { eventsByKey[k].skipNext() } + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> { + val eventsByKey: GroupedEvents<K, Maybe<BuildSpec<A>>> = groupByKey(numKeys) + val initOut: Lazy<Map<K, B>> = deferAsync { + initialSpecs.unwrapped.value.mapValues { (k, spec) -> + val newEnd = eventsByKey[k] val newScope = childBuildScope(newEnd) - newScope.runInBuildScope(spec) + newScope.spec() } } val childScope = coroutineScope.childScope() - val changesNode: TFlowImpl<Map<K, Maybe<A>>> = - mapImpl(upstream = { this@applyLatestForKeyInternal.init.connect(evalScope = this) }) { - upstreamMap -> + val changesNode: EventsImpl<Map<K, Maybe<A>>> = + mapImpl(upstream = { this@applyLatestSpecForKey.init.connect(evalScope = this) }) { + upstreamMap, + _ -> reenterBuildScope(this@BuildScopeImpl, childScope).run { - upstreamMap.mapValuesParallel { (k: K, ma: Maybe<FrpSpec<A>>) -> + upstreamMap.mapValues { (k: K, ma: Maybe<BuildSpec<A>>) -> ma.map { spec -> - val newEnd = with(frpScope) { eventsByKey[k].skipNext() } + val newEnd = eventsByKey[k].skipNext() val newScope = childBuildScope(newEnd) - newScope.runInBuildScope(spec) + newScope.spec() } } } } - val changes: TFlow<Map<K, Maybe<A>>> = - TFlowInit(constInit("applyLatestForKey", changesNode.cached())) + val changes: Events<Map<K, Maybe<A>>> = + EventsInit(constInit("applyLatestForKey", changesNode.cached())) // Ensure effects are observed; otherwise init will stay alive longer than expected - changes.observeEffectInternal(EmptyCoroutineContext) {} - return changes to FrpDeferredValue(initOut) + changes.observe() + return changes to DeferredValue(initOut) + } + + private fun <A, T : Events<A>, S> buildEvents( + name: String? = null, + constructEvents: (InputNode<A>) -> Pair<T, S>, + builder: suspend S.() -> Unit, + ): Events<A> { + var job: Job? = null + val stopEmitter = newStopEmitter("buildEvents[$name]") + // Create a child scope that will be kept alive beyond the end of this transaction. + val childScope = coroutineScope.childScope() + lateinit var emitter: Pair<T, S> + val inputNode = + InputNode<A>( + activate = { + // It's possible that activation occurs after all effects have been run, due + // to a MuxDeferred switch-in. For this reason, we need to activate in a new + // transaction. + check(job == null) { "[$name] already activated" } + job = + childScope.launchImmediate { + network + .transaction("buildEvents") { + reenterBuildScope(this@BuildScopeImpl, childScope) + .launchEffect { + builder(emitter.second) + stopEmitter.emit(Unit) + } + } + .await() + .join() + } + }, + deactivate = { + checkNotNull(job) { "[$name] already deactivated" }.cancel() + job = null + }, + ) + emitter = constructEvents(inputNode) + return emitter.first.takeUntil(mergeLeft(stopEmitter, endSignal)) } - private fun newStopEmitter(name: String): CoalescingMutableTFlow<Unit, Unit> = - CoalescingMutableTFlow( + private fun newStopEmitter(name: String): CoalescingMutableEvents<Unit, Unit> = + CoalescingMutableEvents( name = name, coalesce = { _, _: Unit -> }, network = network, getInitialValue = {}, ) - private suspend fun childBuildScope(newEnd: TFlow<Any>): BuildScopeImpl { + private fun childBuildScope(newEnd: Events<Any>): BuildScopeImpl { val newCoroutineScope: CoroutineScope = coroutineScope.childScope() return BuildScopeImpl( stateScope = stateScope.childStateScope(newEnd), @@ -292,7 +278,7 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope (newCoroutineScope.coroutineContext.job as CompletableJob).complete() } ) - runInBuildScope { endSignal.nextOnly().observe { newCoroutineScope.cancel() } } + endSignalOnce.observe { newCoroutineScope.cancel() } } } @@ -315,42 +301,6 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope coroutineScope = childScope, ) } - - private inner class FrpBuildScopeImpl : FrpBuildScope, FrpStateScope by stateScope.frpScope { - - override fun <T> tFlow(builder: suspend FrpProducerScope<T>.() -> Unit): TFlow<T> = - tFlowInternal(builder) - - override fun <In, Out> coalescingTFlow( - getInitialValue: () -> Out, - coalesce: (old: Out, new: In) -> Out, - builder: suspend FrpCoalescingProducerScope<In>.() -> Unit, - ): TFlow<Out> = coalescingTFlowInternal(getInitialValue, coalesce, builder) - - override fun <A> asyncScope(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job> = - asyncScopeInternal(block) - - override fun <R> deferredBuildScope( - block: suspend FrpBuildScope.() -> R - ): FrpDeferredValue<R> = deferredInternal(block) - - override fun deferredBuildScopeAction(block: suspend FrpBuildScope.() -> Unit) = - deferredActionInternal(block) - - override fun <A> TFlow<A>.observe( - coroutineContext: CoroutineContext, - block: suspend FrpEffectScope.(A) -> Unit, - ): Job = observeEffectInternal(coroutineContext, block) - - override fun <A, B> TFlow<A>.mapBuild(transform: suspend FrpBuildScope.(A) -> B): TFlow<B> = - mapBuildInternal(transform) - - override fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey( - initialSpecs: FrpDeferredValue<Map<K, FrpSpec<B>>>, - numKeys: Int?, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> = - applyLatestForKeyInternal(initialSpecs, numKeys) - } } private fun EvalScope.reenterBuildScope( diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/DeferScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/DeferScope.kt index f65307c6106f..8a66f9a0d40d 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/DeferScope.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/DeferScope.kt @@ -16,33 +16,58 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.internal.util.asyncImmediate -import com.android.systemui.kairos.internal.util.launchImmediate -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Job -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.isActive - -internal typealias DeferScope = CoroutineScope - -internal inline fun DeferScope.deferAction( - start: CoroutineStart = CoroutineStart.UNDISPATCHED, - crossinline block: suspend () -> Unit, -): Job { - check(isActive) { "Cannot perform deferral, scope already closed." } - return launchImmediate(start, CoroutineName("deferAction")) { block() } +internal interface DeferScope { + fun deferAction(block: () -> Unit) + + fun <R> deferAsync(block: () -> R): Lazy<R> } -internal inline fun <R> DeferScope.deferAsync( - start: CoroutineStart = CoroutineStart.UNDISPATCHED, - crossinline block: suspend () -> R, -): Deferred<R> { - check(isActive) { "Cannot perform deferral, scope already closed." } - return asyncImmediate(start, CoroutineName("deferAsync")) { block() } +internal inline fun <A> deferScope(block: DeferScope.() -> A): A { + val scope = + object : DeferScope { + val deferrals = ArrayDeque<() -> Unit>() // TODO: store lazies instead? + + fun drainDeferrals() { + while (deferrals.isNotEmpty()) { + deferrals.removeFirst().invoke() + } + } + + override fun deferAction(block: () -> Unit) { + deferrals.add(block) + } + + override fun <R> deferAsync(block: () -> R): Lazy<R> = + lazy(block).also { deferrals.add { it.value } } + } + return scope.block().also { scope.drainDeferrals() } } -internal suspend inline fun <A> deferScope(noinline block: suspend DeferScope.() -> A): A = - coroutineScope(block) +internal object NoValue + +internal class CompletableLazy<T> : Lazy<T> { + + private var _value: Any? + + constructor() { + _value = NoValue + } + + constructor(init: T) { + _value = init + } + + fun setValue(value: T) { + check(_value === NoValue) { "CompletableLazy value already set" } + _value = value + } + + override val value: T + get() { + check(_value !== NoValue) { "CompletableLazy accessed before initialized" } + @Suppress("UNCHECKED_CAST") + return _value as T + } + + override fun isInitialized(): Boolean = _value !== NoValue +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt index 5f652525f036..d19a47eb873e 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt @@ -21,10 +21,8 @@ import com.android.systemui.kairos.internal.store.MapHolder import com.android.systemui.kairos.internal.store.MapK import com.android.systemui.kairos.internal.store.MutableMapK import com.android.systemui.kairos.internal.util.hashString -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch +import com.android.systemui.kairos.internal.util.logDuration import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock internal class DemuxNode<W, K, A>( private val branchNodeByKey: MutableMapK<W, K, DemuxNode<W, K, A>.BranchNode>, @@ -34,156 +32,111 @@ internal class DemuxNode<W, K, A>( val schedulable = Schedulable.N(this) - inline val mutex - get() = lifecycle.mutex - lateinit var upstreamConnection: NodeConnection<MapK<W, K, A>> @Volatile private var epoch: Long = Long.MIN_VALUE - suspend fun hasCurrentValueLocked(evalScope: EvalScope, key: K): Boolean = - evalScope.epoch == epoch && upstreamConnection.getPushEvent(evalScope).contains(key) + fun hasCurrentValueLocked(logIndent: Int, evalScope: EvalScope, key: K): Boolean = + evalScope.epoch == epoch && + upstreamConnection.getPushEvent(logIndent, evalScope).contains(key) - suspend fun hasCurrentValue(evalScope: EvalScope, key: K): Boolean = - mutex.withLock { hasCurrentValueLocked(evalScope, key) } + fun hasCurrentValue(logIndent: Int, evalScope: EvalScope, key: K): Boolean = + hasCurrentValueLocked(logIndent, evalScope, key) fun getAndMaybeAddDownstream(key: K): BranchNode = branchNodeByKey.getOrPut(key) { BranchNode(key) } - override suspend fun schedule(evalScope: EvalScope) = coroutineScope { - val upstreamResult = upstreamConnection.getPushEvent(evalScope) - mutex.withLock { + override fun schedule(logIndent: Int, evalScope: EvalScope) = + logDuration(logIndent, "DemuxNode.schedule") { + val upstreamResult = + logDuration("upstream.getPushEvent") { + upstreamConnection.getPushEvent(currentLogIndent, evalScope) + } updateEpoch(evalScope) for ((key, _) in upstreamResult) { - if (key !in branchNodeByKey) continue + if (!branchNodeByKey.contains(key)) continue val branch = branchNodeByKey.getValue(key) - // TODO: launchImmediate? - launch { branch.schedule(evalScope) } + branch.schedule(currentLogIndent, evalScope) } } - } - override suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) { - coroutineScope { - mutex.withLock { - for ((_, branchNode) in branchNodeByKey) { - branchNode.downstreamSet.adjustDirectUpstream( - coroutineScope = this, - scheduler, - oldDepth, - newDepth, - ) - } - } + override fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) { + for ((_, branchNode) in branchNodeByKey) { + branchNode.downstreamSet.adjustDirectUpstream(scheduler, oldDepth, newDepth) } } - override suspend fun moveIndirectUpstreamToDirect( + override fun moveIndirectUpstreamToDirect( scheduler: Scheduler, oldIndirectDepth: Int, oldIndirectSet: Set<MuxDeferredNode<*, *, *>>, newDirectDepth: Int, ) { - coroutineScope { - mutex.withLock { - for ((_, branchNode) in branchNodeByKey) { - branchNode.downstreamSet.moveIndirectUpstreamToDirect( - coroutineScope = this, - scheduler, - oldIndirectDepth, - oldIndirectSet, - newDirectDepth, - ) - } - } + for ((_, branchNode) in branchNodeByKey) { + branchNode.downstreamSet.moveIndirectUpstreamToDirect( + scheduler, + oldIndirectDepth, + oldIndirectSet, + newDirectDepth, + ) } } - override suspend fun adjustIndirectUpstream( + override fun adjustIndirectUpstream( scheduler: Scheduler, oldDepth: Int, newDepth: Int, removals: Set<MuxDeferredNode<*, *, *>>, additions: Set<MuxDeferredNode<*, *, *>>, ) { - coroutineScope { - mutex.withLock { - for ((_, branchNode) in branchNodeByKey) { - branchNode.downstreamSet.adjustIndirectUpstream( - coroutineScope = this, - scheduler, - oldDepth, - newDepth, - removals, - additions, - ) - } - } + for ((_, branchNode) in branchNodeByKey) { + branchNode.downstreamSet.adjustIndirectUpstream( + scheduler, + oldDepth, + newDepth, + removals, + additions, + ) } } - override suspend fun moveDirectUpstreamToIndirect( + override fun moveDirectUpstreamToIndirect( scheduler: Scheduler, oldDirectDepth: Int, newIndirectDepth: Int, newIndirectSet: Set<MuxDeferredNode<*, *, *>>, ) { - coroutineScope { - mutex.withLock { - for ((_, branchNode) in branchNodeByKey) { - branchNode.downstreamSet.moveDirectUpstreamToIndirect( - coroutineScope = this, - scheduler, - oldDirectDepth, - newIndirectDepth, - newIndirectSet, - ) - } - } + for ((_, branchNode) in branchNodeByKey) { + branchNode.downstreamSet.moveDirectUpstreamToIndirect( + scheduler, + oldDirectDepth, + newIndirectDepth, + newIndirectSet, + ) } } - override suspend fun removeIndirectUpstream( + override fun removeIndirectUpstream( scheduler: Scheduler, depth: Int, indirectSet: Set<MuxDeferredNode<*, *, *>>, ) { - coroutineScope { - mutex.withLock { - lifecycle.lifecycleState = DemuxLifecycleState.Dead - for ((_, branchNode) in branchNodeByKey) { - branchNode.downstreamSet.removeIndirectUpstream( - coroutineScope = this, - scheduler, - depth, - indirectSet, - ) - } - } + lifecycle.lifecycleState = DemuxLifecycleState.Dead + for ((_, branchNode) in branchNodeByKey) { + branchNode.downstreamSet.removeIndirectUpstream(scheduler, depth, indirectSet) } } - override suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int) { - coroutineScope { - mutex.withLock { - lifecycle.lifecycleState = DemuxLifecycleState.Dead - for ((_, branchNode) in branchNodeByKey) { - branchNode.downstreamSet.removeDirectUpstream( - coroutineScope = this, - scheduler, - depth, - ) - } - } + override fun removeDirectUpstream(scheduler: Scheduler, depth: Int) { + lifecycle.lifecycleState = DemuxLifecycleState.Dead + for ((_, branchNode) in branchNodeByKey) { + branchNode.downstreamSet.removeDirectUpstream(scheduler, depth) } } - suspend fun removeDownstreamAndDeactivateIfNeeded(key: K) { - val deactivate = - mutex.withLock { - branchNodeByKey.remove(key) - branchNodeByKey.isEmpty() - } + fun removeDownstreamAndDeactivateIfNeeded(key: K) { + branchNodeByKey.remove(key) + val deactivate = branchNodeByKey.isEmpty() if (deactivate) { // No need for mutex here; no more concurrent changes to can occur during this phase lifecycle.lifecycleState = DemuxLifecycleState.Inactive(spec) @@ -195,64 +148,64 @@ internal class DemuxNode<W, K, A>( epoch = evalScope.epoch } - suspend fun getPushEvent(evalScope: EvalScope, key: K): A = - upstreamConnection.getPushEvent(evalScope).getValue(key) + fun getPushEvent(logIndent: Int, evalScope: EvalScope, key: K): A = + logDuration(logIndent, "Demux.getPushEvent($key)") { + upstreamConnection.getPushEvent(currentLogIndent, evalScope).getValue(key) + } inner class BranchNode(val key: K) : PushNode<A> { - private val mutex = Mutex() - val downstreamSet = DownstreamSet() override val depthTracker: DepthTracker get() = upstreamConnection.depthTracker - override suspend fun hasCurrentValue(evalScope: EvalScope): Boolean = - hasCurrentValue(evalScope, key) + override fun hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean = + hasCurrentValue(logIndent, evalScope, key) - override suspend fun getPushEvent(evalScope: EvalScope): A = getPushEvent(evalScope, key) + override fun getPushEvent(logIndent: Int, evalScope: EvalScope): A = + getPushEvent(logIndent, evalScope, key) - override suspend fun addDownstream(downstream: Schedulable) { - mutex.withLock { downstreamSet.add(downstream) } + override fun addDownstream(downstream: Schedulable) { + downstreamSet.add(downstream) } - override suspend fun removeDownstream(downstream: Schedulable) { - mutex.withLock { downstreamSet.remove(downstream) } + override fun removeDownstream(downstream: Schedulable) { + downstreamSet.remove(downstream) } - override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) { - val canDeactivate = - mutex.withLock { - downstreamSet.remove(downstream) - downstreamSet.isEmpty() - } + override fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) { + downstreamSet.remove(downstream) + val canDeactivate = downstreamSet.isEmpty() if (canDeactivate) { removeDownstreamAndDeactivateIfNeeded(key) } } - override suspend fun deactivateIfNeeded() { - if (mutex.withLock { downstreamSet.isEmpty() }) { + override fun deactivateIfNeeded() { + if (downstreamSet.isEmpty()) { removeDownstreamAndDeactivateIfNeeded(key) } } - override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) { - if (mutex.withLock { downstreamSet.isEmpty() }) { + override fun scheduleDeactivationIfNeeded(evalScope: EvalScope) { + if (downstreamSet.isEmpty()) { evalScope.scheduleDeactivation(this) } } - suspend fun schedule(evalScope: EvalScope) { - if (!coroutineScope { mutex.withLock { scheduleAll(downstreamSet, evalScope) } }) { - evalScope.scheduleDeactivation(this) + fun schedule(logIndent: Int, evalScope: EvalScope) { + logDuration(logIndent, "DemuxBranchNode($key).schedule") { + if (!scheduleAll(currentLogIndent, downstreamSet, evalScope)) { + evalScope.scheduleDeactivation(this@BranchNode) + } } } } } internal fun <W, K, A> DemuxImpl( - upstream: TFlowImpl<MapK<W, K, A>>, + upstream: EventsImpl<MapK<W, K, A>>, numKeys: Int?, storeFactory: MutableMapK.Factory<W, K>, ): DemuxImpl<K, A> = @@ -263,28 +216,27 @@ internal fun <W, K, A> DemuxImpl( ) internal fun <K, A> demuxMap( - upstream: suspend EvalScope.() -> TFlowImpl<Map<K, A>>, + upstream: EvalScope.() -> EventsImpl<Map<K, A>>, numKeys: Int?, ): DemuxImpl<K, A> = - DemuxImpl(mapImpl(upstream) { MapHolder(it) }, numKeys, ConcurrentHashMapK.Factory()) + DemuxImpl(mapImpl(upstream) { it, _ -> MapHolder(it) }, numKeys, ConcurrentHashMapK.Factory()) internal class DemuxActivator<W, K, A>( private val numKeys: Int?, - private val upstream: TFlowImpl<MapK<W, K, A>>, + private val upstream: EventsImpl<MapK<W, K, A>>, private val storeFactory: MutableMapK.Factory<W, K>, ) { - suspend fun activate( + fun activate( evalScope: EvalScope, lifecycle: DemuxLifecycle<K, A>, ): Pair<DemuxNode<W, K, A>, Set<K>>? { val demux = DemuxNode(storeFactory.create(numKeys), lifecycle, this) - return upstream.activate(evalScope, downstream = demux.schedulable)?.let { (conn, needsEval) - -> + return upstream.activate(evalScope, demux.schedulable)?.let { (conn, needsEval) -> Pair( demux.apply { upstreamConnection = conn }, if (needsEval) { demux.updateEpoch(evalScope) - conn.getPushEvent(evalScope).keys + conn.getPushEvent(0, evalScope).keys } else { emptySet() }, @@ -294,10 +246,10 @@ internal class DemuxActivator<W, K, A>( } internal class DemuxImpl<in K, out A>(private val dmux: DemuxLifecycle<K, A>) { - fun eventsForKey(key: K): TFlowImpl<A> = TFlowCheap { downstream -> + fun eventsForKey(key: K): EventsImpl<A> = EventsImplCheap { downstream -> dmux.activate(evalScope = this, key)?.let { (branchNode, needsEval) -> branchNode.addDownstream(downstream) - val branchNeedsEval = needsEval && branchNode.hasCurrentValue(evalScope = this) + val branchNeedsEval = needsEval && branchNode.hasCurrentValue(0, evalScope = this) ActivationResult( connection = NodeConnection(branchNode, branchNode), needsEval = branchNeedsEval, @@ -311,31 +263,31 @@ internal class DemuxLifecycle<K, A>(@Volatile var lifecycleState: DemuxLifecycle override fun toString(): String = "TFlowDmuxState[$hashString][$lifecycleState][$mutex]" - suspend fun activate( - evalScope: EvalScope, - key: K, - ): Pair<DemuxNode<*, K, A>.BranchNode, Boolean>? = - mutex.withLock { - when (val state = lifecycleState) { - is DemuxLifecycleState.Dead -> null - is DemuxLifecycleState.Active -> - state.node.getAndMaybeAddDownstream(key) to - state.node.hasCurrentValueLocked(evalScope, key) - is DemuxLifecycleState.Inactive -> { - state.spec - .activate(evalScope, this@DemuxLifecycle) - .also { result -> - lifecycleState = - if (result == null) { - DemuxLifecycleState.Dead - } else { - DemuxLifecycleState.Active(result.first) - } - } - ?.let { (node, needsEval) -> - node.getAndMaybeAddDownstream(key) to (key in needsEval) - } - } + fun activate(evalScope: EvalScope, key: K): Pair<DemuxNode<*, K, A>.BranchNode, Boolean>? = + when (val state = lifecycleState) { + is DemuxLifecycleState.Dead -> { + null + } + + is DemuxLifecycleState.Active -> { + state.node.getAndMaybeAddDownstream(key) to + state.node.hasCurrentValueLocked(0, evalScope, key) + } + + is DemuxLifecycleState.Inactive -> { + state.spec + .activate(evalScope, this@DemuxLifecycle) + .also { result -> + lifecycleState = + if (result == null) { + DemuxLifecycleState.Dead + } else { + DemuxLifecycleState.Active(result.first) + } + } + ?.let { (node, needsEval) -> + node.getAndMaybeAddDownstream(key) to (key in needsEval) + } } } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt index afbd7120653c..80a294819fac 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt @@ -16,57 +16,54 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.FrpDeferredValue -import com.android.systemui.kairos.FrpTransactionScope -import com.android.systemui.kairos.TFlow -import com.android.systemui.kairos.TFlowInit -import com.android.systemui.kairos.TFlowLoop -import com.android.systemui.kairos.TState -import com.android.systemui.kairos.TStateInit +import com.android.systemui.kairos.DeferredValue +import com.android.systemui.kairos.Events +import com.android.systemui.kairos.EventsInit +import com.android.systemui.kairos.EventsLoop +import com.android.systemui.kairos.State +import com.android.systemui.kairos.StateInit +import com.android.systemui.kairos.TransactionScope import com.android.systemui.kairos.Transactional -import com.android.systemui.kairos.emptyTFlow +import com.android.systemui.kairos.emptyEvents import com.android.systemui.kairos.init import com.android.systemui.kairos.mapCheap -import com.android.systemui.kairos.switch -import kotlin.coroutines.Continuation -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.startCoroutine -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.completeWith -import kotlinx.coroutines.job +import com.android.systemui.kairos.switchEvents internal class EvalScopeImpl(networkScope: NetworkScope, deferScope: DeferScope) : - EvalScope, NetworkScope by networkScope, DeferScope by deferScope { - - private suspend fun <A> Transactional<A>.sample(): A = - impl.sample().sample(this@EvalScopeImpl).await() - - private suspend fun <A> TState<A>.sample(): A = - init.connect(evalScope = this@EvalScopeImpl).getCurrentWithEpoch(this@EvalScopeImpl).first - - private val <A> Transactional<A>.deferredValue: FrpDeferredValue<A> - get() = FrpDeferredValue(deferAsync { sample() }) + EvalScope, NetworkScope by networkScope, DeferScope by deferScope, TransactionScope { + + override fun <A> Transactional<A>.sampleDeferred(): DeferredValue<A> = + DeferredValue(deferAsync { impl.sample().sample(this@EvalScopeImpl).value }) + + override fun <A> State<A>.sampleDeferred(): DeferredValue<A> = + DeferredValue( + deferAsync { + init + .connect(evalScope = this@EvalScopeImpl) + .getCurrentWithEpoch(this@EvalScopeImpl) + .first + } + ) - private val <A> TState<A>.deferredValue: FrpDeferredValue<A> - get() = FrpDeferredValue(deferAsync { sample() }) + override fun <R> deferredTransactionScope(block: TransactionScope.() -> R): DeferredValue<R> = + DeferredValue(deferAsync { block() }) - private val nowInternal: TFlow<Unit> by lazy { - var result by TFlowLoop<Unit>() + override val now: Events<Unit> by lazy { + var result by EventsLoop<Unit>() result = - TStateInit( + StateInit( constInit( "now", - activatedTStateSource( + activatedStateSource( "now", "now", this, - { result.mapCheap { emptyTFlow }.init.connect(evalScope = this) }, - CompletableDeferred( - TFlowInit( + { result.mapCheap { emptyEvents }.init.connect(evalScope = this) }, + CompletableLazy( + EventsInit( constInit( "now", - TFlowCheap { + EventsImplCheap { ActivationResult( connection = NodeConnection(AlwaysNode, AlwaysNode), needsEval = true, @@ -78,42 +75,7 @@ internal class EvalScopeImpl(networkScope: NetworkScope, deferScope: DeferScope) ), ) ) - .switch() + .switchEvents() result } - - private fun <R> deferredInternal( - block: suspend FrpTransactionScope.() -> R - ): FrpDeferredValue<R> = FrpDeferredValue(deferAsync { runInTransactionScope(block) }) - - override suspend fun <R> runInTransactionScope(block: suspend FrpTransactionScope.() -> R): R { - val complete = CompletableDeferred<R>(parent = coroutineContext.job) - block.startCoroutine( - frpScope, - object : Continuation<R> { - override val context: CoroutineContext - get() = EmptyCoroutineContext - - override fun resumeWith(result: Result<R>) { - complete.completeWith(result) - } - }, - ) - return complete.await() - } - - override val frpScope: FrpTransactionScope = FrpTransactionScopeImpl() - - inner class FrpTransactionScopeImpl : FrpTransactionScope { - override fun <A> Transactional<A>.sampleDeferred(): FrpDeferredValue<A> = deferredValue - - override fun <A> TState<A>.sampleDeferred(): FrpDeferredValue<A> = deferredValue - - override fun <R> deferredTransactionScope( - block: suspend FrpTransactionScope.() -> R - ): FrpDeferredValue<R> = deferredInternal(block) - - override val now: TFlow<Unit> - get() = nowInternal - } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TFlowImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EventsImpl.kt index 784a2afe0992..e7978b8bc5ea 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TFlowImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EventsImpl.kt @@ -17,8 +17,8 @@ package com.android.systemui.kairos.internal /* Initialized TFlow */ -internal fun interface TFlowImpl<out A> { - suspend fun activate(evalScope: EvalScope, downstream: Schedulable): ActivationResult<A>? +internal fun interface EventsImpl<out A> { + fun activate(evalScope: EvalScope, downstream: Schedulable): ActivationResult<A>? } internal data class ActivationResult<out A>( @@ -26,34 +26,33 @@ internal data class ActivationResult<out A>( val needsEval: Boolean, ) -internal inline fun <A> TFlowCheap(crossinline cheap: CheapNodeSubscribe<A>) = - TFlowImpl { scope, ds -> +internal inline fun <A> EventsImplCheap(crossinline cheap: CheapNodeSubscribe<A>) = + EventsImpl { scope, ds -> scope.cheap(ds) } internal typealias CheapNodeSubscribe<A> = - suspend EvalScope.(downstream: Schedulable) -> ActivationResult<A>? + EvalScope.(downstream: Schedulable) -> ActivationResult<A>? internal data class NodeConnection<out A>( val directUpstream: PullNode<A>, val schedulerUpstream: PushNode<*>, ) -internal suspend fun <A> NodeConnection<A>.hasCurrentValue(evalScope: EvalScope): Boolean = - schedulerUpstream.hasCurrentValue(evalScope) +internal fun <A> NodeConnection<A>.hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean = + schedulerUpstream.hasCurrentValue(logIndent, evalScope) -internal suspend fun <A> NodeConnection<A>.removeDownstreamAndDeactivateIfNeeded( - downstream: Schedulable -) = schedulerUpstream.removeDownstreamAndDeactivateIfNeeded(downstream) +internal fun <A> NodeConnection<A>.removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) = + schedulerUpstream.removeDownstreamAndDeactivateIfNeeded(downstream) -internal suspend fun <A> NodeConnection<A>.scheduleDeactivationIfNeeded(evalScope: EvalScope) = +internal fun <A> NodeConnection<A>.scheduleDeactivationIfNeeded(evalScope: EvalScope) = schedulerUpstream.scheduleDeactivationIfNeeded(evalScope) -internal suspend fun <A> NodeConnection<A>.removeDownstream(downstream: Schedulable) = +internal fun <A> NodeConnection<A>.removeDownstream(downstream: Schedulable) = schedulerUpstream.removeDownstream(downstream) -internal suspend fun <A> NodeConnection<A>.getPushEvent(evalScope: EvalScope): A = - directUpstream.getPushEvent(evalScope) +internal fun <A> NodeConnection<A>.getPushEvent(logIndent: Int, evalScope: EvalScope): A = + directUpstream.getPushEvent(logIndent, evalScope) internal val <A> NodeConnection<A>.depthTracker: DepthTracker get() = schedulerUpstream.depthTracker diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt index b60c227bcfbe..d38bf21c5db1 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt @@ -24,10 +24,10 @@ import com.android.systemui.kairos.util.just import com.android.systemui.kairos.util.none internal inline fun <A> filterJustImpl( - crossinline getPulse: suspend EvalScope.() -> TFlowImpl<Maybe<A>> -): TFlowImpl<A> = + crossinline getPulse: EvalScope.() -> EventsImpl<Maybe<A>> +): EventsImpl<A> = DemuxImpl( - mapImpl(getPulse) { maybeResult -> + mapImpl(getPulse) { maybeResult, _ -> if (maybeResult is Just) { Single(maybeResult.value) } else { @@ -40,6 +40,9 @@ internal inline fun <A> filterJustImpl( .eventsForKey(Unit) internal inline fun <A> filterImpl( - crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>, - crossinline f: suspend EvalScope.(A) -> Boolean, -): TFlowImpl<A> = filterJustImpl { mapImpl(getPulse) { if (f(it)) just(it) else none }.cached() } + crossinline getPulse: EvalScope.() -> EventsImpl<A>, + crossinline f: EvalScope.(A) -> Boolean, +): EventsImpl<A> { + val mapped = mapImpl(getPulse) { it, _ -> if (f(it)) just(it) else none }.cached() + return filterJustImpl { mapped } +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt index 828f13b026d3..b956e44e0618 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt @@ -18,8 +18,6 @@ package com.android.systemui.kairos.internal import com.android.systemui.kairos.internal.util.Bag import java.util.TreeMap -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch /** * Tracks all upstream connections for Mux nodes. @@ -86,7 +84,7 @@ internal class DepthTracker { @Volatile private var dirty_depthIsDirect = true @Volatile private var dirty_isIndirectRoot = false - fun schedule(scheduler: Scheduler, node: MuxNode<*, *, *, *>) { + fun schedule(scheduler: Scheduler, node: MuxNode<*, *, *>) { if (dirty_depthIsDirect) { scheduler.schedule(dirty_directDepth, node) } else { @@ -192,30 +190,27 @@ internal class DepthTracker { return remainder } - suspend fun propagateChanges(scheduler: Scheduler, muxNode: MuxNode<*, *, *, *>) { + fun propagateChanges(scheduler: Scheduler, muxNode: MuxNode<*, *, *>) { if (isDirty()) { schedule(scheduler, muxNode) } } fun applyChanges( - coroutineScope: CoroutineScope, scheduler: Scheduler, downstreamSet: DownstreamSet, - muxNode: MuxNode<*, *, *, *>, + muxNode: MuxNode<*, *, *>, ) { when { dirty_depthIsDirect -> { if (snapshotIsDirect) { downstreamSet.adjustDirectUpstream( - coroutineScope, scheduler, oldDepth = snapshotDirectDepth, newDepth = dirty_directDepth, ) } else { downstreamSet.moveIndirectUpstreamToDirect( - coroutineScope, scheduler, oldIndirectDepth = snapshotIndirectDepth, oldIndirectSet = @@ -233,7 +228,6 @@ internal class DepthTracker { dirty_hasIndirectUpstream() || dirty_isIndirectRoot -> { if (snapshotIsDirect) { downstreamSet.moveDirectUpstreamToIndirect( - coroutineScope, scheduler, oldDirectDepth = snapshotDirectDepth, newIndirectDepth = dirty_indirectDepth, @@ -247,7 +241,6 @@ internal class DepthTracker { ) } else { downstreamSet.adjustIndirectUpstream( - coroutineScope, scheduler, oldDepth = snapshotIndirectDepth, newDepth = dirty_indirectDepth, @@ -274,14 +267,9 @@ internal class DepthTracker { muxNode.lifecycle.lifecycleState = MuxLifecycleState.Dead if (snapshotIsDirect) { - downstreamSet.removeDirectUpstream( - coroutineScope, - scheduler, - depth = snapshotDirectDepth, - ) + downstreamSet.removeDirectUpstream(scheduler, depth = snapshotDirectDepth) } else { downstreamSet.removeIndirectUpstream( - coroutineScope, scheduler, depth = snapshotIndirectDepth, indirectSet = @@ -352,7 +340,7 @@ internal class DepthTracker { internal class DownstreamSet { val outputs = HashSet<Output<*>>() - val stateWriters = mutableListOf<TStateSource<*>>() + val stateWriters = mutableListOf<StateSource<*>>() val muxMovers = HashSet<MuxDeferredNode<*, *, *>>() val nodes = HashSet<SchedulableNode>() @@ -374,125 +362,92 @@ internal class DownstreamSet { } } - fun adjustDirectUpstream( - coroutineScope: CoroutineScope, - scheduler: Scheduler, - oldDepth: Int, - newDepth: Int, - ) = - coroutineScope.run { - for (node in nodes) { - launch { node.adjustDirectUpstream(scheduler, oldDepth, newDepth) } - } + fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) { + for (node in nodes) { + node.adjustDirectUpstream(scheduler, oldDepth, newDepth) } + } fun moveIndirectUpstreamToDirect( - coroutineScope: CoroutineScope, scheduler: Scheduler, oldIndirectDepth: Int, oldIndirectSet: Set<MuxDeferredNode<*, *, *>>, newDirectDepth: Int, - ) = - coroutineScope.run { - for (node in nodes) { - launch { - node.moveIndirectUpstreamToDirect( - scheduler, - oldIndirectDepth, - oldIndirectSet, - newDirectDepth, - ) - } - } - for (mover in muxMovers) { - launch { - mover.moveIndirectPatchNodeToDirect(scheduler, oldIndirectDepth, oldIndirectSet) - } - } + ) { + for (node in nodes) { + node.moveIndirectUpstreamToDirect( + scheduler, + oldIndirectDepth, + oldIndirectSet, + newDirectDepth, + ) + } + for (mover in muxMovers) { + mover.moveIndirectPatchNodeToDirect(scheduler, oldIndirectDepth, oldIndirectSet) } + } fun adjustIndirectUpstream( - coroutineScope: CoroutineScope, scheduler: Scheduler, oldDepth: Int, newDepth: Int, removals: Set<MuxDeferredNode<*, *, *>>, additions: Set<MuxDeferredNode<*, *, *>>, - ) = - coroutineScope.run { - for (node in nodes) { - launch { - node.adjustIndirectUpstream(scheduler, oldDepth, newDepth, removals, additions) - } - } - for (mover in muxMovers) { - launch { - mover.adjustIndirectPatchNode( - scheduler, - oldDepth, - newDepth, - removals, - additions, - ) - } - } + ) { + for (node in nodes) { + node.adjustIndirectUpstream(scheduler, oldDepth, newDepth, removals, additions) + } + for (mover in muxMovers) { + mover.adjustIndirectPatchNode(scheduler, oldDepth, newDepth, removals, additions) } + } fun moveDirectUpstreamToIndirect( - coroutineScope: CoroutineScope, scheduler: Scheduler, oldDirectDepth: Int, newIndirectDepth: Int, newIndirectSet: Set<MuxDeferredNode<*, *, *>>, - ) = - coroutineScope.run { - for (node in nodes) { - launch { - node.moveDirectUpstreamToIndirect( - scheduler, - oldDirectDepth, - newIndirectDepth, - newIndirectSet, - ) - } - } - for (mover in muxMovers) { - launch { - mover.moveDirectPatchNodeToIndirect(scheduler, newIndirectDepth, newIndirectSet) - } - } + ) { + for (node in nodes) { + node.moveDirectUpstreamToIndirect( + scheduler, + oldDirectDepth, + newIndirectDepth, + newIndirectSet, + ) + } + for (mover in muxMovers) { + mover.moveDirectPatchNodeToIndirect(scheduler, newIndirectDepth, newIndirectSet) } + } fun removeIndirectUpstream( - coroutineScope: CoroutineScope, scheduler: Scheduler, depth: Int, indirectSet: Set<MuxDeferredNode<*, *, *>>, - ) = - coroutineScope.run { - for (node in nodes) { - launch { node.removeIndirectUpstream(scheduler, depth, indirectSet) } - } - for (mover in muxMovers) { - launch { mover.removeIndirectPatchNode(scheduler, depth, indirectSet) } - } - for (output in outputs) { - launch { output.kill() } - } + ) { + for (node in nodes) { + node.removeIndirectUpstream(scheduler, depth, indirectSet) + } + for (mover in muxMovers) { + mover.removeIndirectPatchNode(scheduler, depth, indirectSet) } + for (output in outputs) { + output.kill() + } + } - fun removeDirectUpstream(coroutineScope: CoroutineScope, scheduler: Scheduler, depth: Int) = - coroutineScope.run { - for (node in nodes) { - launch { node.removeDirectUpstream(scheduler, depth) } - } - for (mover in muxMovers) { - launch { mover.removeDirectPatchNode(scheduler) } - } - for (output in outputs) { - launch { output.kill() } - } + fun removeDirectUpstream(scheduler: Scheduler, depth: Int) { + for (node in nodes) { + node.removeDirectUpstream(scheduler, depth) + } + for (mover in muxMovers) { + mover.removeDirectPatchNode(scheduler) } + for (output in outputs) { + output.kill() + } + } fun clear() { outputs.clear() @@ -504,7 +459,7 @@ internal class DownstreamSet { // TODO: remove this indirection internal sealed interface Schedulable { - data class S constructor(val state: TStateSource<*>) : Schedulable + data class S constructor(val state: StateSource<*>) : Schedulable data class M constructor(val muxMover: MuxDeferredNode<*, *, *>) : Schedulable @@ -518,13 +473,14 @@ internal fun DownstreamSet.isEmpty() = @Suppress("NOTHING_TO_INLINE") internal inline fun DownstreamSet.isNotEmpty() = !isEmpty() -internal fun CoroutineScope.scheduleAll( +internal fun scheduleAll( + logIndent: Int, downstreamSet: DownstreamSet, evalScope: EvalScope, ): Boolean { - downstreamSet.nodes.forEach { launch { it.schedule(evalScope) } } - downstreamSet.muxMovers.forEach { launch { it.scheduleMover(evalScope) } } - downstreamSet.outputs.forEach { launch { it.schedule(evalScope) } } + downstreamSet.nodes.forEach { it.schedule(logIndent, evalScope) } + downstreamSet.muxMovers.forEach { it.scheduleMover(logIndent, evalScope) } + downstreamSet.outputs.forEach { it.schedule(logIndent, evalScope) } downstreamSet.stateWriters.forEach { evalScope.schedule(it) } return downstreamSet.isNotEmpty() } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt index 57db9a493e21..10a46775beb9 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt @@ -19,42 +19,37 @@ package com.android.systemui.kairos.internal import com.android.systemui.kairos.util.Maybe import com.android.systemui.kairos.util.just import com.android.systemui.kairos.util.none -import java.util.concurrent.atomic.AtomicBoolean -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi /** Performs actions once, when the reactive component is first connected to the network. */ -internal class Init<out A>(val name: String?, private val block: suspend InitScope.() -> A) { - - /** Has the initialization logic been evaluated yet? */ - private val initialized = AtomicBoolean() +internal class Init<out A>(val name: String?, private val block: InitScope.() -> A) { /** * Stores the result after initialization, as well as the id of the [Network] it's been * initialized with. */ - private val cache = CompletableDeferred<Pair<Any, A>>() + private val cache = CompletableLazy<Pair<Any, A>>() - suspend fun connect(evalScope: InitScope): A = - if (initialized.getAndSet(true)) { + fun connect(evalScope: InitScope): A = + if (cache.isInitialized()) { // Read from cache - val (networkId, result) = cache.await() + val (networkId, result) = cache.value check(networkId == evalScope.networkId) { "Network mismatch" } result } else { // Write to cache - block(evalScope).also { cache.complete(evalScope.networkId to it) } + block(evalScope).also { cache.setValue(evalScope.networkId to it) } } @OptIn(ExperimentalCoroutinesApi::class) fun getUnsafe(): Maybe<A> = - if (cache.isCompleted) { - just(cache.getCompleted().second) + if (cache.isInitialized()) { + just(cache.value.second) } else { none } } -internal fun <A> init(name: String?, block: suspend InitScope.() -> A) = Init(name, block) +internal fun <A> init(name: String?, block: InitScope.() -> A) = Init(name, block) internal fun <A> constInit(name: String?, value: A) = init(name) { value } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt index 1edc8c28b2ee..d7cbe8087f52 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt @@ -16,104 +16,103 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.internal.util.Key +import com.android.systemui.kairos.internal.util.logDuration import java.util.concurrent.atomic.AtomicBoolean -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock internal class InputNode<A>( - private val activate: suspend EvalScope.() -> Unit = {}, + private val activate: EvalScope.() -> Unit = {}, private val deactivate: () -> Unit = {}, -) : PushNode<A>, Key<A> { +) : PushNode<A> { private val downstreamSet = DownstreamSet() - private val mutex = Mutex() - private val activated = AtomicBoolean(false) + val activated = AtomicBoolean(false) - @Volatile private var epoch: Long = Long.MIN_VALUE + private val transactionCache = TransactionCache<A>() + private val epoch + get() = transactionCache.epoch override val depthTracker: DepthTracker = DepthTracker() - override suspend fun hasCurrentValue(evalScope: EvalScope): Boolean = epoch == evalScope.epoch + override fun hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean = + epoch == evalScope.epoch - suspend fun visit(evalScope: EvalScope, value: A) { - epoch = evalScope.epoch - evalScope.setResult(this, value) - coroutineScope { - if (!mutex.withLock { scheduleAll(downstreamSet, evalScope) }) { - evalScope.scheduleDeactivation(this@InputNode) - } + fun visit(evalScope: EvalScope, value: A) { + transactionCache.put(evalScope, value) + if (!scheduleAll(0, downstreamSet, evalScope)) { + evalScope.scheduleDeactivation(this@InputNode) } } - override suspend fun removeDownstream(downstream: Schedulable) { - mutex.withLock { downstreamSet.remove(downstream) } + override fun removeDownstream(downstream: Schedulable) { + downstreamSet.remove(downstream) } - override suspend fun deactivateIfNeeded() { - if (mutex.withLock { downstreamSet.isEmpty() && activated.getAndSet(false) }) { + override fun deactivateIfNeeded() { + if (downstreamSet.isEmpty() && activated.getAndSet(false)) { deactivate() } } - override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) { - if (mutex.withLock { downstreamSet.isEmpty() }) { + override fun scheduleDeactivationIfNeeded(evalScope: EvalScope) { + if (downstreamSet.isEmpty()) { evalScope.scheduleDeactivation(this) } } - override suspend fun addDownstream(downstream: Schedulable) { - mutex.withLock { downstreamSet.add(downstream) } + override fun addDownstream(downstream: Schedulable) { + downstreamSet.add(downstream) } - suspend fun addDownstreamAndActivateIfNeeded(downstream: Schedulable, evalScope: EvalScope) { - val needsActivation = - mutex.withLock { - val wasEmpty = downstreamSet.isEmpty() - downstreamSet.add(downstream) - wasEmpty && !activated.getAndSet(true) - } + fun addDownstreamAndActivateIfNeeded(downstream: Schedulable, evalScope: EvalScope) { + val needsActivation = run { + val wasEmpty = downstreamSet.isEmpty() + downstreamSet.add(downstream) + wasEmpty && !activated.getAndSet(true) + } if (needsActivation) { activate(evalScope) } } - override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) { - val needsDeactivation = - mutex.withLock { - downstreamSet.remove(downstream) - downstreamSet.isEmpty() && activated.getAndSet(false) - } + override fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) { + downstreamSet.remove(downstream) + val needsDeactivation = downstreamSet.isEmpty() && activated.getAndSet(false) if (needsDeactivation) { deactivate() } } - override suspend fun getPushEvent(evalScope: EvalScope): A = evalScope.getCurrentValue(this) + override fun getPushEvent(logIndent: Int, evalScope: EvalScope): A = + logDuration(logIndent, "Input.getPushEvent", false) { + transactionCache.getCurrentValue(evalScope) + } } -internal fun <A> InputNode<A>.activated() = TFlowCheap { downstream -> +internal fun <A> InputNode<A>.activated() = EventsImplCheap { downstream -> val input = this@activated addDownstreamAndActivateIfNeeded(downstream, evalScope = this) - ActivationResult(connection = NodeConnection(input, input), needsEval = hasCurrentValue(input)) + ActivationResult( + connection = NodeConnection(input, input), + needsEval = input.hasCurrentValue(0, evalScope = this), + ) } internal data object AlwaysNode : PushNode<Unit> { override val depthTracker = DepthTracker() - override suspend fun hasCurrentValue(evalScope: EvalScope): Boolean = true + override fun hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean = true - override suspend fun removeDownstream(downstream: Schedulable) {} + override fun removeDownstream(downstream: Schedulable) {} - override suspend fun deactivateIfNeeded() {} + override fun deactivateIfNeeded() {} - override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {} + override fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {} - override suspend fun addDownstream(downstream: Schedulable) {} + override fun addDownstream(downstream: Schedulable) {} - override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {} + override fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {} - override suspend fun getPushEvent(evalScope: EvalScope) = Unit + override fun getPushEvent(logIndent: Int, evalScope: EvalScope) = + logDuration(logIndent, "Always.getPushEvent", false) { Unit } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt index 80c40ba740a5..cd2214370d62 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt @@ -16,38 +16,25 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.FrpBuildScope -import com.android.systemui.kairos.FrpStateScope -import com.android.systemui.kairos.FrpTransactionScope -import com.android.systemui.kairos.TFlow -import com.android.systemui.kairos.internal.util.HeteroMap -import com.android.systemui.kairos.internal.util.Key +import com.android.systemui.kairos.BuildScope +import com.android.systemui.kairos.Events +import com.android.systemui.kairos.StateScope +import com.android.systemui.kairos.TransactionScope internal interface InitScope { val networkId: Any } -internal interface EvalScope : NetworkScope, DeferScope { - val frpScope: FrpTransactionScope +internal interface EvalScope : NetworkScope, DeferScope, TransactionScope - suspend fun <R> runInTransactionScope(block: suspend FrpTransactionScope.() -> R): R -} - -internal interface StateScope : EvalScope { - override val frpScope: FrpStateScope - - suspend fun <R> runInStateScope(block: suspend FrpStateScope.() -> R): R +internal interface InternalStateScope : EvalScope, StateScope { + val endSignal: Events<Any> + val endSignalOnce: Events<Any> - val endSignal: TFlow<Any> - - fun childStateScope(newEnd: TFlow<Any>): StateScope + fun childStateScope(newEnd: Events<Any>): InternalStateScope } -internal interface BuildScope : StateScope { - override val frpScope: FrpBuildScope - - suspend fun <R> runInBuildScope(block: suspend FrpBuildScope.() -> R): R -} +internal interface InternalBuildScope : InternalStateScope, BuildScope internal interface NetworkScope : InitScope { @@ -57,24 +44,15 @@ internal interface NetworkScope : InitScope { val compactor: Scheduler val scheduler: Scheduler - val transactionStore: HeteroMap + val transactionStore: TransactionStore fun scheduleOutput(output: Output<*>) fun scheduleMuxMover(muxMover: MuxDeferredNode<*, *, *>) - fun schedule(state: TStateSource<*>) + fun schedule(state: StateSource<*>) fun scheduleDeactivation(node: PushNode<*>) fun scheduleDeactivation(output: Output<*>) } - -internal fun <A> NetworkScope.setResult(node: Key<A>, result: A) { - transactionStore[node] = result -} - -internal fun <A> NetworkScope.getCurrentValue(key: Key<A>): A = - transactionStore.getOrError(key) { "No value for $key in transaction $epoch" } - -internal fun NetworkScope.hasCurrentValue(key: Key<*>): Boolean = transactionStore.contains(key) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt index a479c90cc4de..268fec4fa486 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt @@ -22,44 +22,39 @@ import com.android.systemui.kairos.internal.store.MapHolder import com.android.systemui.kairos.internal.store.MapK import com.android.systemui.kairos.internal.store.MutableMapK import com.android.systemui.kairos.internal.store.asMapHolder -import com.android.systemui.kairos.internal.util.asyncImmediate import com.android.systemui.kairos.internal.util.hashString -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock +import com.android.systemui.kairos.internal.util.logDuration internal typealias MuxResult<W, K, V> = MapK<W, K, PullNode<V>> /** Base class for muxing nodes, which have a (potentially dynamic) collection of upstream nodes. */ -internal sealed class MuxNode<W, K, V, Output>( - val lifecycle: MuxLifecycle<Output>, +internal sealed class MuxNode<W, K, V>( + val lifecycle: MuxLifecycle<W, K, V>, protected val storeFactory: MutableMapK.Factory<W, K>, -) : PushNode<Output> { +) : PushNode<MuxResult<W, K, V>> { - inline val mutex - get() = lifecycle.mutex + lateinit var upstreamData: MutableMapK<W, K, PullNode<V>> + lateinit var switchedIn: MutableMapK<W, K, BranchNode> - @Volatile lateinit var upstreamData: MutableMapK<W, K, PullNode<V>> - @Volatile lateinit var switchedIn: MutableMapK<W, K, BranchNode> + @Volatile var markedForCompaction = false + @Volatile var markedForEvaluation = false val downstreamSet: DownstreamSet = DownstreamSet() // TODO: inline DepthTracker? would need to be added to PushNode signature final override val depthTracker = DepthTracker() - @Volatile - var epoch: Long = Long.MIN_VALUE - protected set + val transactionCache = TransactionCache<MuxResult<W, K, V>>() + val epoch + get() = transactionCache.epoch inline fun hasCurrentValueLocked(evalScope: EvalScope): Boolean = epoch == evalScope.epoch - override suspend fun hasCurrentValue(evalScope: EvalScope): Boolean = - mutex.withLock { hasCurrentValueLocked(evalScope) } + override fun hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean = + hasCurrentValueLocked(evalScope) - final override suspend fun addDownstream(downstream: Schedulable) { - mutex.withLock { addDownstreamLocked(downstream) } + final override fun addDownstream(downstream: Schedulable) { + addDownstreamLocked(downstream) } /** @@ -72,135 +67,121 @@ internal sealed class MuxNode<W, K, V, Output>( downstreamSet.add(downstream) } - final override suspend fun removeDownstream(downstream: Schedulable) { + final override fun removeDownstream(downstream: Schedulable) { // TODO: return boolean? - mutex.withLock { downstreamSet.remove(downstream) } + downstreamSet.remove(downstream) } - final override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) { - val deactivate = - mutex.withLock { - downstreamSet.remove(downstream) - downstreamSet.isEmpty() - } + final override fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) { + downstreamSet.remove(downstream) + val deactivate = downstreamSet.isEmpty() if (deactivate) { doDeactivate() } } - final override suspend fun deactivateIfNeeded() { - if (mutex.withLock { downstreamSet.isEmpty() }) { + final override fun deactivateIfNeeded() { + if (downstreamSet.isEmpty()) { doDeactivate() } } /** visit this node from the scheduler (push eval) */ - abstract suspend fun visit(evalScope: EvalScope) + abstract fun visit(logIndent: Int, evalScope: EvalScope) /** perform deactivation logic, propagating to all upstream nodes. */ - protected abstract suspend fun doDeactivate() + protected abstract fun doDeactivate() - final override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) { - if (mutex.withLock { downstreamSet.isEmpty() }) { + final override fun scheduleDeactivationIfNeeded(evalScope: EvalScope) { + if (downstreamSet.isEmpty()) { evalScope.scheduleDeactivation(this) } } - suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) { - mutex.withLock { - if (depthTracker.addDirectUpstream(oldDepth, newDepth)) { - depthTracker.schedule(scheduler, this) - } + fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) { + + if (depthTracker.addDirectUpstream(oldDepth, newDepth)) { + depthTracker.schedule(scheduler, this) } } - suspend fun moveIndirectUpstreamToDirect( + fun moveIndirectUpstreamToDirect( scheduler: Scheduler, oldIndirectDepth: Int, oldIndirectRoots: Set<MuxDeferredNode<*, *, *>>, newDepth: Int, ) { - mutex.withLock { - if ( - depthTracker.addDirectUpstream(oldDepth = null, newDepth) or - depthTracker.removeIndirectUpstream(depth = oldIndirectDepth) or - depthTracker.updateIndirectRoots(removals = oldIndirectRoots) - ) { - depthTracker.schedule(scheduler, this) - } + if ( + depthTracker.addDirectUpstream(oldDepth = null, newDepth) or + depthTracker.removeIndirectUpstream(depth = oldIndirectDepth) or + depthTracker.updateIndirectRoots(removals = oldIndirectRoots) + ) { + depthTracker.schedule(scheduler, this) } } - suspend fun adjustIndirectUpstream( + fun adjustIndirectUpstream( scheduler: Scheduler, oldDepth: Int, newDepth: Int, removals: Set<MuxDeferredNode<*, *, *>>, additions: Set<MuxDeferredNode<*, *, *>>, ) { - mutex.withLock { - if ( - depthTracker.addIndirectUpstream(oldDepth, newDepth) or - depthTracker.updateIndirectRoots( - additions, - removals, - butNot = this as? MuxDeferredNode<*, *, *>, - ) - ) { - depthTracker.schedule(scheduler, this) - } + if ( + depthTracker.addIndirectUpstream(oldDepth, newDepth) or + depthTracker.updateIndirectRoots( + additions, + removals, + butNot = this as? MuxDeferredNode<*, *, *>, + ) + ) { + depthTracker.schedule(scheduler, this) } } - suspend fun moveDirectUpstreamToIndirect( + fun moveDirectUpstreamToIndirect( scheduler: Scheduler, oldDepth: Int, newDepth: Int, newIndirectSet: Set<MuxDeferredNode<*, *, *>>, ) { - mutex.withLock { - if ( - depthTracker.addIndirectUpstream(oldDepth = null, newDepth) or - depthTracker.removeDirectUpstream(oldDepth) or - depthTracker.updateIndirectRoots( - additions = newIndirectSet, - butNot = this as? MuxDeferredNode<*, *, *>, - ) - ) { - depthTracker.schedule(scheduler, this) - } + if ( + depthTracker.addIndirectUpstream(oldDepth = null, newDepth) or + depthTracker.removeDirectUpstream(oldDepth) or + depthTracker.updateIndirectRoots( + additions = newIndirectSet, + butNot = this as? MuxDeferredNode<*, *, *>, + ) + ) { + depthTracker.schedule(scheduler, this) } } - suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int, key: K) { - mutex.withLock { - switchedIn.remove(key) - if (depthTracker.removeDirectUpstream(depth)) { - depthTracker.schedule(scheduler, this) - } + fun removeDirectUpstream(scheduler: Scheduler, depth: Int, key: K) { + switchedIn.remove(key) + if (depthTracker.removeDirectUpstream(depth)) { + depthTracker.schedule(scheduler, this) } } - suspend fun removeIndirectUpstream( + fun removeIndirectUpstream( scheduler: Scheduler, oldDepth: Int, indirectSet: Set<MuxDeferredNode<*, *, *>>, key: K, ) { - mutex.withLock { - switchedIn.remove(key) - if ( - depthTracker.removeIndirectUpstream(oldDepth) or - depthTracker.updateIndirectRoots(removals = indirectSet) - ) { - depthTracker.schedule(scheduler, this) - } + switchedIn.remove(key) + if ( + depthTracker.removeIndirectUpstream(oldDepth) or + depthTracker.updateIndirectRoots(removals = indirectSet) + ) { + depthTracker.schedule(scheduler, this) } } - suspend fun visitCompact(scheduler: Scheduler) = coroutineScope { + fun visitCompact(scheduler: Scheduler) { if (depthTracker.isDirty()) { - depthTracker.applyChanges(coroutineScope = this, scheduler, downstreamSet, this@MuxNode) + depthTracker.applyChanges(scheduler, downstreamSet, this@MuxNode) } } @@ -217,22 +198,23 @@ internal sealed class MuxNode<W, K, V, Output>( val schedulable = Schedulable.N(this) - @Volatile lateinit var upstream: NodeConnection<V> + lateinit var upstream: NodeConnection<V> - override suspend fun schedule(evalScope: EvalScope) { - upstreamData[key] = upstream.directUpstream - this@MuxNode.schedule(evalScope) + override fun schedule(logIndent: Int, evalScope: EvalScope) { + logDuration(logIndent, "MuxBranchNode.schedule") { + if (this@MuxNode is MuxPromptNode && this@MuxNode.name != null) { + logLn("[${this@MuxNode}] scheduling $key") + } + upstreamData[key] = upstream.directUpstream + this@MuxNode.schedule(evalScope) + } } - override suspend fun adjustDirectUpstream( - scheduler: Scheduler, - oldDepth: Int, - newDepth: Int, - ) { + override fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) { this@MuxNode.adjustDirectUpstream(scheduler, oldDepth, newDepth) } - override suspend fun moveIndirectUpstreamToDirect( + override fun moveIndirectUpstreamToDirect( scheduler: Scheduler, oldIndirectDepth: Int, oldIndirectSet: Set<MuxDeferredNode<*, *, *>>, @@ -246,7 +228,7 @@ internal sealed class MuxNode<W, K, V, Output>( ) } - override suspend fun adjustIndirectUpstream( + override fun adjustIndirectUpstream( scheduler: Scheduler, oldDepth: Int, newDepth: Int, @@ -256,7 +238,7 @@ internal sealed class MuxNode<W, K, V, Output>( this@MuxNode.adjustIndirectUpstream(scheduler, oldDepth, newDepth, removals, additions) } - override suspend fun moveDirectUpstreamToIndirect( + override fun moveDirectUpstreamToIndirect( scheduler: Scheduler, oldDirectDepth: Int, newIndirectDepth: Int, @@ -270,11 +252,11 @@ internal sealed class MuxNode<W, K, V, Output>( ) } - override suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int) { + override fun removeDirectUpstream(scheduler: Scheduler, depth: Int) { removeDirectUpstream(scheduler, depth, key) } - override suspend fun removeIndirectUpstream( + override fun removeIndirectUpstream( scheduler: Scheduler, depth: Int, indirectSet: Set<MuxDeferredNode<*, *, *>>, @@ -286,113 +268,110 @@ internal sealed class MuxNode<W, K, V, Output>( } } -internal typealias BranchNode<W, K, V> = MuxNode<W, K, V, *>.BranchNode +internal typealias BranchNode<W, K, V> = MuxNode<W, K, V>.BranchNode /** Tracks lifecycle of MuxNode in the network. Essentially a mutable ref for MuxLifecycleState. */ -internal class MuxLifecycle<A>(@Volatile var lifecycleState: MuxLifecycleState<A>) : TFlowImpl<A> { - val mutex = Mutex() +internal class MuxLifecycle<W, K, V>(var lifecycleState: MuxLifecycleState<W, K, V>) : + EventsImpl<MuxResult<W, K, V>> { - override fun toString(): String = "TFlowLifecycle[$hashString][$lifecycleState][$mutex]" + override fun toString(): String = "MuxLifecycle[$hashString][$lifecycleState]" - override suspend fun activate( + override fun activate( evalScope: EvalScope, downstream: Schedulable, - ): ActivationResult<A>? = - mutex.withLock { - when (val state = lifecycleState) { - is MuxLifecycleState.Dead -> null - is MuxLifecycleState.Active -> { - state.node.addDownstreamLocked(downstream) - ActivationResult( - connection = NodeConnection(state.node, state.node), - needsEval = state.node.hasCurrentValueLocked(evalScope), - ) - } - is MuxLifecycleState.Inactive -> { - state.spec - .activate(evalScope, this@MuxLifecycle) - .also { node -> - lifecycleState = - if (node == null) { - MuxLifecycleState.Dead - } else { - MuxLifecycleState.Active(node) - } - } - ?.let { node -> - node.addDownstreamLocked(downstream) - ActivationResult( - connection = NodeConnection(node, node), - needsEval = false, - ) - } - } + ): ActivationResult<MuxResult<W, K, V>>? = + when (val state = lifecycleState) { + is MuxLifecycleState.Dead -> { + null + } + is MuxLifecycleState.Active -> { + state.node.addDownstreamLocked(downstream) + ActivationResult( + connection = NodeConnection(state.node, state.node), + needsEval = state.node.hasCurrentValueLocked(evalScope), + ) + } + is MuxLifecycleState.Inactive -> { + state.spec + .activate(evalScope, this@MuxLifecycle) + .also { node -> + lifecycleState = + if (node == null) { + MuxLifecycleState.Dead + } else { + MuxLifecycleState.Active(node.first) + } + } + ?.let { (node, postActivate) -> + postActivate?.invoke() + node.addDownstreamLocked(downstream) + ActivationResult(connection = NodeConnection(node, node), needsEval = false) + } } } } -internal sealed interface MuxLifecycleState<out A> { - class Inactive<A>(val spec: MuxActivator<A>) : MuxLifecycleState<A> { +internal sealed interface MuxLifecycleState<out W, out K, out V> { + class Inactive<W, K, V>(val spec: MuxActivator<W, K, V>) : MuxLifecycleState<W, K, V> { override fun toString(): String = "Inactive" } - class Active<A>(val node: MuxNode<*, *, *, A>) : MuxLifecycleState<A> { + class Active<W, K, V>(val node: MuxNode<W, K, V>) : MuxLifecycleState<W, K, V> { override fun toString(): String = "Active(node=$node)" } - data object Dead : MuxLifecycleState<Nothing> + data object Dead : MuxLifecycleState<Nothing, Nothing, Nothing> } -internal interface MuxActivator<A> { - suspend fun activate(evalScope: EvalScope, lifecycle: MuxLifecycle<A>): MuxNode<*, *, *, A>? +internal interface MuxActivator<W, K, V> { + fun activate( + evalScope: EvalScope, + lifecycle: MuxLifecycle<W, K, V>, + ): Pair<MuxNode<W, K, V>, (() -> Unit)?>? } -internal inline fun <A> MuxLifecycle(onSubscribe: MuxActivator<A>): TFlowImpl<A> = - MuxLifecycle(MuxLifecycleState.Inactive(onSubscribe)) +internal inline fun <W, K, V> MuxLifecycle( + onSubscribe: MuxActivator<W, K, V> +): EventsImpl<MuxResult<W, K, V>> = MuxLifecycle(MuxLifecycleState.Inactive(onSubscribe)) -internal fun <K, V> TFlowImpl<MuxResult<MapHolder.W, K, V>>.awaitValues(): TFlowImpl<Map<K, V>> = - mapImpl({ this@awaitValues }) { results -> - results.asMapHolder().unwrapped.mapValues { it.value.getPushEvent(this) } +internal fun <K, V> EventsImpl<MuxResult<MapHolder.W, K, V>>.awaitValues(): EventsImpl<Map<K, V>> = + mapImpl({ this@awaitValues }) { results, logIndent -> + results.asMapHolder().unwrapped.mapValues { it.value.getPushEvent(logIndent, this) } } // activation logic -internal suspend fun <W, K, V, O> MuxNode<W, K, V, O>.initializeUpstream( +internal fun <W, K, V> MuxNode<W, K, V>.initializeUpstream( evalScope: EvalScope, - getStorage: suspend EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>, + getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>, storeFactory: MutableMapK.Factory<W, K>, ) { val storage = getStorage(evalScope) - coroutineScope { - val initUpstream = buildList { - storage.forEach { (key, flow) -> - val branchNode = BranchNode(key) - add( - asyncImmediate(start = CoroutineStart.LAZY) { - flow.activate(evalScope, branchNode.schedulable)?.let { (conn, needsEval) -> - Triple( - key, - branchNode.apply { upstream = conn }, - if (needsEval) conn.directUpstream else null, - ) - } - } - ) - } + val initUpstream = buildList { + storage.forEach { (key, events) -> + val branchNode = BranchNode(key) + add( + events.activate(evalScope, branchNode.schedulable)?.let { (conn, needsEval) -> + Triple( + key, + branchNode.apply { upstream = conn }, + if (needsEval) conn.directUpstream else null, + ) + } + ) } - val results = initUpstream.awaitAll() - switchedIn = storeFactory.create(initUpstream.size) - upstreamData = storeFactory.create(initUpstream.size) - for (triple in results) { - triple?.let { (key, branch, upstream) -> - switchedIn[key] = branch - upstream?.let { upstreamData[key] = upstream } - } + } + switchedIn = storeFactory.create(initUpstream.size) + upstreamData = storeFactory.create(initUpstream.size) + for (triple in initUpstream) { + triple?.let { (key, branch, upstream) -> + switchedIn[key] = branch + upstream?.let { upstreamData[key] = upstream } } } } -internal fun <W, K, V, O> MuxNode<W, K, V, O>.initializeDepth() { +internal fun <W, K, V> MuxNode<W, K, V>.initializeDepth() { switchedIn.forEach { (_, branch) -> val conn = branch.upstream if (conn.depthTracker.snapshotIsDirect) { diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt index 7f40df508fb1..53a6ecabda6a 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt @@ -16,15 +16,17 @@ package com.android.systemui.kairos.internal +import com.android.systemui.kairos.internal.store.MapK import com.android.systemui.kairos.internal.store.MutableArrayMapK import com.android.systemui.kairos.internal.store.MutableMapK import com.android.systemui.kairos.internal.store.SingletonMapK import com.android.systemui.kairos.internal.store.StoreEntry import com.android.systemui.kairos.internal.store.asArrayHolder +import com.android.systemui.kairos.internal.store.asSingle import com.android.systemui.kairos.internal.store.singleOf -import com.android.systemui.kairos.internal.util.Key import com.android.systemui.kairos.internal.util.hashString -import com.android.systemui.kairos.internal.util.mapParallel +import com.android.systemui.kairos.internal.util.logDuration +import com.android.systemui.kairos.internal.util.logLn import com.android.systemui.kairos.util.Just import com.android.systemui.kairos.util.Maybe import com.android.systemui.kairos.util.None @@ -37,41 +39,49 @@ import com.android.systemui.kairos.util.maybeThis import com.android.systemui.kairos.util.merge import com.android.systemui.kairos.util.orError import com.android.systemui.kairos.util.these -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.withLock internal class MuxDeferredNode<W, K, V>( - lifecycle: MuxLifecycle<MuxResult<W, K, V>>, - val spec: MuxActivator<MuxResult<W, K, V>>, + val name: String?, + lifecycle: MuxLifecycle<W, K, V>, + val spec: MuxActivator<W, K, V>, factory: MutableMapK.Factory<W, K>, -) : MuxNode<W, K, V, MuxResult<W, K, V>>(lifecycle, factory), Key<MuxResult<W, K, V>> { +) : MuxNode<W, K, V>(lifecycle, factory) { val schedulable = Schedulable.M(this) - - @Volatile var patches: NodeConnection<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>? = null - @Volatile var patchData: Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>? = null - - override suspend fun visit(evalScope: EvalScope) { - val scheduleDownstream = upstreamData.isNotEmpty() - val result = upstreamData.readOnlyCopy() - upstreamData.clear() - val compactDownstream = depthTracker.isDirty() - if (scheduleDownstream || compactDownstream) { - coroutineScope { - mutex.withLock { - if (compactDownstream) { + var patches: NodeConnection<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>? = null + var patchData: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>? = null + + override fun visit(logIndent: Int, evalScope: EvalScope) { + check(epoch < evalScope.epoch) { "node unexpectedly visited multiple times in transaction" } + logDuration(logIndent, "MuxDeferred[$name].visit") { + val scheduleDownstream: Boolean + val result: MapK<W, K, PullNode<V>> + logDuration("copying upstream data", false) { + scheduleDownstream = upstreamData.isNotEmpty() + result = upstreamData.readOnlyCopy() + upstreamData.clear() + } + if (name != null) { + logLn("[${this@MuxDeferredNode}] result = $result") + } + val compactDownstream = depthTracker.isDirty() + if (scheduleDownstream || compactDownstream) { + if (compactDownstream) { + logDuration("compactDownstream", false) { depthTracker.applyChanges( - coroutineScope = this, evalScope.scheduler, downstreamSet, muxNode = this@MuxDeferredNode, ) } - if (scheduleDownstream) { - epoch = evalScope.epoch - evalScope.setResult(this@MuxDeferredNode, result) - if (!scheduleAll(downstreamSet, evalScope)) { + } + if (scheduleDownstream) { + logDuration("scheduleDownstream") { + if (name != null) { + logLn("[${this@MuxDeferredNode}] scheduling") + } + transactionCache.put(evalScope, result) + if (!scheduleAll(currentLogIndent, downstreamSet, evalScope)) { evalScope.scheduleDeactivation(this@MuxDeferredNode) } } @@ -80,26 +90,26 @@ internal class MuxDeferredNode<W, K, V>( } } - override suspend fun getPushEvent(evalScope: EvalScope): MuxResult<W, K, V> = - evalScope.getCurrentValue(key = this) + override fun getPushEvent(logIndent: Int, evalScope: EvalScope): MuxResult<W, K, V> = + logDuration(logIndent, "MuxDeferred.getPushEvent") { + transactionCache.getCurrentValue(evalScope).also { + if (name != null) { + logLn("[${this@MuxDeferredNode}] getPushEvent = $it") + } + } + } - private suspend fun compactIfNeeded(evalScope: EvalScope) { + private fun compactIfNeeded(evalScope: EvalScope) { depthTracker.propagateChanges(evalScope.compactor, this) } - override suspend fun doDeactivate() { + override fun doDeactivate() { // Update lifecycle - lifecycle.mutex.withLock { - if (lifecycle.lifecycleState !is MuxLifecycleState.Active) return@doDeactivate - lifecycle.lifecycleState = MuxLifecycleState.Inactive(spec) - } + if (lifecycle.lifecycleState !is MuxLifecycleState.Active) return@doDeactivate + lifecycle.lifecycleState = MuxLifecycleState.Inactive(spec) // Process branch nodes - coroutineScope { - switchedIn.forEach { (_, branchNode) -> - branchNode.upstream.let { - launch { it.removeDownstreamAndDeactivateIfNeeded(branchNode.schedulable) } - } - } + switchedIn.forEach { (_, branchNode) -> + branchNode.upstream.removeDownstreamAndDeactivateIfNeeded(branchNode.schedulable) } // Process patch node patches?.removeDownstreamAndDeactivateIfNeeded(schedulable) @@ -108,15 +118,18 @@ internal class MuxDeferredNode<W, K, V>( // MOVE phase // - concurrent moves may be occurring, but no more evals. all depth recalculations are // deferred to the end of this phase. - suspend fun performMove(evalScope: EvalScope) { + fun performMove(logIndent: Int, evalScope: EvalScope) { + if (name != null) { + logLn(logIndent, "[${this@MuxDeferredNode}] performMove (patchData = $patchData)") + } + val patch = patchData ?: return patchData = null - // TODO: this logic is very similar to what's in MuxPromptMoving, maybe turn into an inline - // fun? + // TODO: this logic is very similar to what's in MuxPrompt, maybe turn into an inline fun? // We have a patch, process additions/updates and removals - val adds = mutableListOf<Pair<K, TFlowImpl<V>>>() + val adds = mutableListOf<Pair<K, EventsImpl<V>>>() val removes = mutableListOf<K>() patch.forEach { (k, newUpstream) -> when (newUpstream) { @@ -127,131 +140,112 @@ internal class MuxDeferredNode<W, K, V>( val severed = mutableListOf<NodeConnection<*>>() - coroutineScope { - // remove and sever - removes.forEach { k -> - switchedIn.remove(k)?.let { branchNode: BranchNode -> - val conn = branchNode.upstream - severed.add(conn) - launch { conn.removeDownstream(downstream = branchNode.schedulable) } - depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth) - } + // remove and sever + removes.forEach { k -> + switchedIn.remove(k)?.let { branchNode: BranchNode -> + val conn = branchNode.upstream + severed.add(conn) + conn.removeDownstream(downstream = branchNode.schedulable) + depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth) } + } - // add or replace - adds - .mapParallel { (k, newUpstream: TFlowImpl<V>) -> - val branchNode = BranchNode(k) - k to - newUpstream.activate(evalScope, branchNode.schedulable)?.let { (conn, _) -> - branchNode.apply { upstream = conn } - } - } - .forEach { (k, newBranch: BranchNode?) -> - // remove old and sever, if present - switchedIn.remove(k)?.let { branchNode -> - val conn = branchNode.upstream - severed.add(conn) - launch { conn.removeDownstream(downstream = branchNode.schedulable) } - depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth) - } + // add or replace + adds.forEach { (k, newUpstream: EventsImpl<V>) -> + // remove old and sever, if present + switchedIn.remove(k)?.let { branchNode -> + val conn = branchNode.upstream + severed.add(conn) + conn.removeDownstream(downstream = branchNode.schedulable) + depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth) + } - // add new - newBranch?.let { - switchedIn[k] = newBranch - val branchDepthTracker = newBranch.upstream.depthTracker - if (branchDepthTracker.snapshotIsDirect) { - depthTracker.addDirectUpstream( - oldDepth = null, - newDepth = branchDepthTracker.snapshotDirectDepth, - ) - } else { - depthTracker.addIndirectUpstream( - oldDepth = null, - newDepth = branchDepthTracker.snapshotIndirectDepth, - ) - depthTracker.updateIndirectRoots( - additions = branchDepthTracker.snapshotIndirectRoots, - butNot = this@MuxDeferredNode, - ) - } - } + // add new + val newBranch = BranchNode(k) + newUpstream.activate(evalScope, newBranch.schedulable)?.let { (conn, _) -> + newBranch.upstream = conn + switchedIn[k] = newBranch + val branchDepthTracker = newBranch.upstream.depthTracker + if (branchDepthTracker.snapshotIsDirect) { + depthTracker.addDirectUpstream( + oldDepth = null, + newDepth = branchDepthTracker.snapshotDirectDepth, + ) + } else { + depthTracker.addIndirectUpstream( + oldDepth = null, + newDepth = branchDepthTracker.snapshotIndirectDepth, + ) + depthTracker.updateIndirectRoots( + additions = branchDepthTracker.snapshotIndirectRoots, + butNot = this@MuxDeferredNode, + ) } + } } - coroutineScope { - for (severedNode in severed) { - launch { severedNode.scheduleDeactivationIfNeeded(evalScope) } - } + for (severedNode in severed) { + severedNode.scheduleDeactivationIfNeeded(evalScope) } compactIfNeeded(evalScope) } - suspend fun removeDirectPatchNode(scheduler: Scheduler) { - mutex.withLock { - if ( - depthTracker.removeIndirectUpstream(depth = 0) or - depthTracker.setIsIndirectRoot(false) - ) { - depthTracker.schedule(scheduler, this) - } - patches = null + fun removeDirectPatchNode(scheduler: Scheduler) { + if ( + depthTracker.removeIndirectUpstream(depth = 0) or depthTracker.setIsIndirectRoot(false) + ) { + depthTracker.schedule(scheduler, this) } + patches = null } - suspend fun removeIndirectPatchNode( + fun removeIndirectPatchNode( scheduler: Scheduler, depth: Int, indirectSet: Set<MuxDeferredNode<*, *, *>>, ) { // indirectly connected patches forward the indirectSet - mutex.withLock { - if ( - depthTracker.updateIndirectRoots(removals = indirectSet) or - depthTracker.removeIndirectUpstream(depth) - ) { - depthTracker.schedule(scheduler, this) - } - patches = null + if ( + depthTracker.updateIndirectRoots(removals = indirectSet) or + depthTracker.removeIndirectUpstream(depth) + ) { + depthTracker.schedule(scheduler, this) } + patches = null } - suspend fun moveIndirectPatchNodeToDirect( + fun moveIndirectPatchNodeToDirect( scheduler: Scheduler, oldIndirectDepth: Int, oldIndirectSet: Set<MuxDeferredNode<*, *, *>>, ) { // directly connected patches are stored as an indirect singleton set of the patchNode - mutex.withLock { - if ( - depthTracker.updateIndirectRoots(removals = oldIndirectSet) or - depthTracker.removeIndirectUpstream(oldIndirectDepth) or - depthTracker.setIsIndirectRoot(true) - ) { - depthTracker.schedule(scheduler, this) - } + if ( + depthTracker.updateIndirectRoots(removals = oldIndirectSet) or + depthTracker.removeIndirectUpstream(oldIndirectDepth) or + depthTracker.setIsIndirectRoot(true) + ) { + depthTracker.schedule(scheduler, this) } } - suspend fun moveDirectPatchNodeToIndirect( + fun moveDirectPatchNodeToIndirect( scheduler: Scheduler, newIndirectDepth: Int, newIndirectSet: Set<MuxDeferredNode<*, *, *>>, ) { // indirectly connected patches forward the indirectSet - mutex.withLock { - if ( - depthTracker.setIsIndirectRoot(false) or - depthTracker.updateIndirectRoots(additions = newIndirectSet, butNot = this) or - depthTracker.addIndirectUpstream(oldDepth = null, newDepth = newIndirectDepth) - ) { - depthTracker.schedule(scheduler, this) - } + if ( + depthTracker.setIsIndirectRoot(false) or + depthTracker.updateIndirectRoots(additions = newIndirectSet, butNot = this) or + depthTracker.addIndirectUpstream(oldDepth = null, newDepth = newIndirectDepth) + ) { + depthTracker.schedule(scheduler, this) } } - suspend fun adjustIndirectPatchNode( + fun adjustIndirectPatchNode( scheduler: Scheduler, oldDepth: Int, newDepth: Int, @@ -259,65 +253,73 @@ internal class MuxDeferredNode<W, K, V>( additions: Set<MuxDeferredNode<*, *, *>>, ) { // indirectly connected patches forward the indirectSet - mutex.withLock { - if ( - depthTracker.updateIndirectRoots( - additions = additions, - removals = removals, - butNot = this, - ) or depthTracker.addIndirectUpstream(oldDepth = oldDepth, newDepth = newDepth) - ) { - depthTracker.schedule(scheduler, this) - } + if ( + depthTracker.updateIndirectRoots( + additions = additions, + removals = removals, + butNot = this, + ) or depthTracker.addIndirectUpstream(oldDepth = oldDepth, newDepth = newDepth) + ) { + depthTracker.schedule(scheduler, this) } } - suspend fun scheduleMover(evalScope: EvalScope) { - patchData = - checkNotNull(patches) { "mux mover scheduled with unset patches upstream node" } - .getPushEvent(evalScope) - evalScope.scheduleMuxMover(this) + fun scheduleMover(logIndent: Int, evalScope: EvalScope) { + logDuration(logIndent, "MuxDeferred.scheduleMover") { + patchData = + checkNotNull(patches) { "mux mover scheduled with unset patches upstream node" } + .getPushEvent(currentLogIndent, evalScope) + evalScope.scheduleMuxMover(this@MuxDeferredNode) + } } - override fun toString(): String = "${this::class.simpleName}@$hashString" + override fun toString(): String = + "${this::class.simpleName}@$hashString${name?.let { "[$it]" }.orEmpty()}" } internal inline fun <A> switchDeferredImplSingle( - crossinline getStorage: suspend EvalScope.() -> TFlowImpl<A>, - crossinline getPatches: suspend EvalScope.() -> TFlowImpl<TFlowImpl<A>>, -): TFlowImpl<A> = - mapImpl({ + name: String? = null, + crossinline getStorage: EvalScope.() -> EventsImpl<A>, + crossinline getPatches: EvalScope.() -> EventsImpl<EventsImpl<A>>, +): EventsImpl<A> { + val patches = mapImpl(getPatches) { newFlow, _ -> singleOf(just(newFlow)).asIterable() } + val switchDeferredImpl = switchDeferredImpl( + name = name, getStorage = { singleOf(getStorage()).asIterable() }, - getPatches = { - mapImpl(getPatches) { newFlow -> singleOf(just(newFlow)).asIterable() } - }, + getPatches = { patches }, storeFactory = SingletonMapK.Factory(), ) - }) { map -> - map.getValue(Unit).getPushEvent(this) + return mapImpl({ switchDeferredImpl }) { map, logIndent -> + map.asSingle().getValue(Unit).getPushEvent(logIndent, this).also { + if (name != null) { + logLn(logIndent, "[$name] extracting single mux: $it") + } + } } +} internal fun <W, K, V> switchDeferredImpl( - getStorage: suspend EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>, - getPatches: suspend EvalScope.() -> TFlowImpl<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>, + name: String? = null, + getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>, + getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>, storeFactory: MutableMapK.Factory<W, K>, -): TFlowImpl<MuxResult<W, K, V>> = - MuxLifecycle(MuxDeferredActivator(getStorage, storeFactory, getPatches)) +): EventsImpl<MuxResult<W, K, V>> = + MuxLifecycle(MuxDeferredActivator(name, getStorage, storeFactory, getPatches)) private class MuxDeferredActivator<W, K, V>( - private val getStorage: suspend EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>, + private val name: String?, + private val getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>, private val storeFactory: MutableMapK.Factory<W, K>, - private val getPatches: - suspend EvalScope.() -> TFlowImpl<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>, -) : MuxActivator<MuxResult<W, K, V>> { - override suspend fun activate( + private val getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>, +) : MuxActivator<W, K, V> { + override fun activate( evalScope: EvalScope, - lifecycle: MuxLifecycle<MuxResult<W, K, V>>, - ): MuxNode<W, *, *, MuxResult<W, K, V>>? { + lifecycle: MuxLifecycle<W, K, V>, + ): Pair<MuxNode<W, K, V>, (() -> Unit)?>? { // Initialize mux node and switched-in connections. val muxNode = - MuxDeferredNode(lifecycle, this, storeFactory).apply { + MuxDeferredNode(name, lifecycle, this, storeFactory).apply { initializeUpstream(evalScope, getStorage, storeFactory) // Update depth based on all initial switched-in nodes. initializeDepth() @@ -327,29 +329,34 @@ private class MuxDeferredActivator<W, K, V>( depthTracker.setIsIndirectRoot(true) depthTracker.reset() } - // Setup patches connection; deferring allows for a recursive connection, where - // muxNode is downstream of itself via patches. - var isIndirect = true - evalScope.deferAction { - val (patchesConn, needsEval) = - getPatches(evalScope).activate(evalScope, downstream = muxNode.schedulable) - ?: run { - isIndirect = false - // Turns out we can't connect to patches, so update our depth and - // propagate - muxNode.mutex.withLock { + + // Schedule for evaluation if any switched-in nodes have already emitted within + // this transaction. + if (muxNode.upstreamData.isNotEmpty()) { + muxNode.schedule(evalScope) + } + + return muxNode to + fun() { + // Setup patches connection; deferring allows for a recursive connection, where + // muxNode is downstream of itself via patches. + val (patchesConn, needsEval) = + getPatches(evalScope).activate(evalScope, downstream = muxNode.schedulable) + ?: run { + // Turns out we can't connect to patches, so update our depth and + // propagate if (muxNode.depthTracker.setIsIndirectRoot(false)) { + // TODO: schedules might not be necessary now that we're not + // parallel? muxNode.depthTracker.schedule(evalScope.scheduler, muxNode) } + return } - return@deferAction - } - muxNode.patches = patchesConn + muxNode.patches = patchesConn - if (!patchesConn.schedulerUpstream.depthTracker.snapshotIsDirect) { - // Turns out patches is indirect, so we are not a root. Update depth and - // propagate. - muxNode.mutex.withLock { + if (!patchesConn.schedulerUpstream.depthTracker.snapshotIsDirect) { + // Turns out patches is indirect, so we are not a root. Update depth and + // propagate. if ( muxNode.depthTracker.setIsIndirectRoot(false) or muxNode.depthTracker.addIndirectUpstream( @@ -363,64 +370,64 @@ private class MuxDeferredActivator<W, K, V>( muxNode.depthTracker.schedule(evalScope.scheduler, muxNode) } } + // Schedule mover to process patch emission at the end of this transaction, if + // needed. + if (needsEval) { + muxNode.patchData = patchesConn.getPushEvent(0, evalScope) + evalScope.scheduleMuxMover(muxNode) + } } - // Schedule mover to process patch emission at the end of this transaction, if - // needed. - if (needsEval) { - muxNode.patchData = patchesConn.getPushEvent(evalScope) - evalScope.scheduleMuxMover(muxNode) - } - } - - // Schedule for evaluation if any switched-in nodes have already emitted within - // this transaction. - if (muxNode.upstreamData.isNotEmpty()) { - muxNode.schedule(evalScope) - } - return muxNode.takeUnless { muxNode.switchedIn.isEmpty() && !isIndirect } } } internal inline fun <A> mergeNodes( - crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>, - crossinline getOther: suspend EvalScope.() -> TFlowImpl<A>, - crossinline f: suspend EvalScope.(A, A) -> A, -): TFlowImpl<A> { + crossinline getPulse: EvalScope.() -> EventsImpl<A>, + crossinline getOther: EvalScope.() -> EventsImpl<A>, + name: String? = null, + crossinline f: EvalScope.(A, A) -> A, +): EventsImpl<A> { + val mergedThese = mergeNodes(name, getPulse, getOther) val merged = - mapImpl({ mergeNodes(getPulse, getOther) }) { these -> - these.merge { thiz, that -> f(thiz, that) } - } + mapImpl({ mergedThese }) { these, _ -> these.merge { thiz, that -> f(thiz, that) } } return merged.cached() } -internal fun <T> Iterable<T>.asIterableWithIndex(): Iterable<StoreEntry<Int, T>> = +internal fun <T> Iterable<T>.asIterableWithIndex(): Iterable<Map.Entry<Int, T>> = asSequence().mapIndexed { i, t -> StoreEntry(i, t) }.asIterable() internal inline fun <A, B> mergeNodes( - crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>, - crossinline getOther: suspend EvalScope.() -> TFlowImpl<B>, -): TFlowImpl<These<A, B>> { + name: String? = null, + crossinline getPulse: EvalScope.() -> EventsImpl<A>, + crossinline getOther: EvalScope.() -> EventsImpl<B>, +): EventsImpl<These<A, B>> { val storage = - listOf(mapImpl(getPulse) { These.thiz<A, B>(it) }, mapImpl(getOther) { These.that(it) }) + listOf( + mapImpl(getPulse) { it, _ -> These.thiz<A, B>(it) }, + mapImpl(getOther) { it, _ -> These.that(it) }, + ) .asIterableWithIndex() val switchNode = switchDeferredImpl( + name = name, getStorage = { storage }, getPatches = { neverImpl }, storeFactory = MutableArrayMapK.Factory(), ) val merged = - mapImpl({ switchNode }) { mergeResults -> - val first = mergeResults.getMaybe(0).flatMap { it.getPushEvent(this).maybeThis() } - val second = mergeResults.getMaybe(1).flatMap { it.getPushEvent(this).maybeThat() } + mapImpl({ switchNode }) { it, logIndent -> + val mergeResults = it.asArrayHolder() + val first = + mergeResults.getMaybe(0).flatMap { it.getPushEvent(logIndent, this).maybeThis() } + val second = + mergeResults.getMaybe(1).flatMap { it.getPushEvent(logIndent, this).maybeThat() } these(first, second).orError { "unexpected missing merge result" } } return merged.cached() } internal inline fun <A> mergeNodes( - crossinline getPulses: suspend EvalScope.() -> Iterable<TFlowImpl<A>> -): TFlowImpl<List<A>> { + crossinline getPulses: EvalScope.() -> Iterable<EventsImpl<A>> +): EventsImpl<List<A>> { val switchNode = switchDeferredImpl( getStorage = { getPulses().asIterableWithIndex() }, @@ -428,16 +435,16 @@ internal inline fun <A> mergeNodes( storeFactory = MutableArrayMapK.Factory(), ) val merged = - mapImpl({ switchNode }) { + mapImpl({ switchNode }) { it, logIndent -> val mergeResults = it.asArrayHolder() - mergeResults.map { (_, node) -> node.getPushEvent(this) } + mergeResults.map { (_, node) -> node.getPushEvent(logIndent, this) } } return merged.cached() } internal inline fun <A> mergeNodesLeft( - crossinline getPulses: suspend EvalScope.() -> Iterable<TFlowImpl<A>> -): TFlowImpl<A> { + crossinline getPulses: EvalScope.() -> Iterable<EventsImpl<A>> +): EventsImpl<A> { val switchNode = switchDeferredImpl( getStorage = { getPulses().asIterableWithIndex() }, @@ -445,6 +452,9 @@ internal inline fun <A> mergeNodesLeft( storeFactory = MutableArrayMapK.Factory(), ) val merged = - mapImpl({ switchNode }) { mergeResults -> mergeResults.values.first().getPushEvent(this) } + mapImpl({ switchNode }) { it, logIndent -> + val mergeResults = it.asArrayHolder() + mergeResults.values.first().getPushEvent(logIndent, this) + } return merged.cached() } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt index 839c5a64272a..785a98b105c5 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt @@ -18,207 +18,180 @@ package com.android.systemui.kairos.internal import com.android.systemui.kairos.internal.store.MutableMapK import com.android.systemui.kairos.internal.store.SingletonMapK +import com.android.systemui.kairos.internal.store.asSingle import com.android.systemui.kairos.internal.store.singleOf -import com.android.systemui.kairos.internal.util.Key -import com.android.systemui.kairos.internal.util.launchImmediate -import com.android.systemui.kairos.internal.util.mapParallel +import com.android.systemui.kairos.internal.util.LogIndent +import com.android.systemui.kairos.internal.util.hashString +import com.android.systemui.kairos.internal.util.logDuration import com.android.systemui.kairos.util.Just import com.android.systemui.kairos.util.Maybe import com.android.systemui.kairos.util.None import com.android.systemui.kairos.util.just -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.withLock -private typealias MuxPromptMovingResult<W, K, V> = Pair<MuxResult<W, K, V>, MuxResult<W, K, V>?> - -internal class MuxPromptMovingNode<W, K, V>( - lifecycle: MuxLifecycle<MuxPromptMovingResult<W, K, V>>, - private val spec: MuxActivator<MuxPromptMovingResult<W, K, V>>, +internal class MuxPromptNode<W, K, V>( + val name: String?, + lifecycle: MuxLifecycle<W, K, V>, + private val spec: MuxActivator<W, K, V>, factory: MutableMapK.Factory<W, K>, -) : - MuxNode<W, K, V, MuxPromptMovingResult<W, K, V>>(lifecycle, factory), - Key<MuxPromptMovingResult<W, K, V>> { - - @Volatile var patchData: Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>? = null - @Volatile var patches: PatchNode? = null +) : MuxNode<W, K, V>(lifecycle, factory) { - @Volatile private var reEval: MuxPromptMovingResult<W, K, V>? = null + var patchData: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>? = null + var patches: PatchNode? = null - override suspend fun visit(evalScope: EvalScope) { - val preSwitchNotEmpty = upstreamData.isNotEmpty() - val preSwitchResults: MuxResult<W, K, V> = upstreamData.readOnlyCopy() - upstreamData.clear() + override fun visit(logIndent: Int, evalScope: EvalScope) { + check(epoch < evalScope.epoch) { "node unexpectedly visited multiple times in transaction" } + logDuration(logIndent, "MuxPrompt.visit") { + val patch: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>? = patchData + patchData = null - val patch: Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>? = patchData - patchData = null - - val (reschedule, evalResult) = - reEval?.let { false to it } - ?: if (preSwitchNotEmpty || patch != null) { - doEval(preSwitchNotEmpty, preSwitchResults, patch, evalScope) - } else { - false to null + // If there's a patch, process it. + patch?.let { + val needsReschedule = processPatch(patch, evalScope) + // We may need to reschedule if newly-switched-in nodes have not yet been + // visited within this transaction. + val depthIncreased = depthTracker.dirty_depthIncreased() + if (needsReschedule || depthIncreased) { + if (depthIncreased) { + depthTracker.schedule(evalScope.compactor, this@MuxPromptNode) + } + if (name != null) { + logLn( + "[${this@MuxPromptNode}] rescheduling (reschedule=$needsReschedule, depthIncrease=$depthIncreased)" + ) + } + schedule(evalScope) + return } - reEval = null - - if (reschedule || depthTracker.dirty_depthIncreased()) { - reEval = evalResult - // Can't schedule downstream yet, need to compact first - if (depthTracker.dirty_depthIncreased()) { - depthTracker.schedule(evalScope.compactor, node = this) } - schedule(evalScope) - } else { + val results = upstreamData.readOnlyCopy().also { upstreamData.clear() } + + // If we don't need to reschedule, or there wasn't a patch at all, then we proceed + // with merging pre-switch and post-switch results + val hasResult = results.isNotEmpty() val compactDownstream = depthTracker.isDirty() - if (evalResult != null || compactDownstream) { - coroutineScope { - mutex.withLock { - if (compactDownstream) { - adjustDownstreamDepths(evalScope, coroutineScope = this) - } - if (evalResult != null) { - epoch = evalScope.epoch - evalScope.setResult(this@MuxPromptMovingNode, evalResult) - if (!scheduleAll(downstreamSet, evalScope)) { - evalScope.scheduleDeactivation(this@MuxPromptMovingNode) - } - } + if (hasResult || compactDownstream) { + if (compactDownstream) { + adjustDownstreamDepths(evalScope) + } + if (hasResult) { + transactionCache.put(evalScope, results) + if (!scheduleAll(currentLogIndent, downstreamSet, evalScope)) { + evalScope.scheduleDeactivation(this@MuxPromptNode) } } } } } - private suspend fun doEval( - preSwitchNotEmpty: Boolean, - preSwitchResults: MuxResult<W, K, V>, - patch: Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>?, + // side-effect: this will populate `upstreamData` with any immediately available results + private fun LogIndent.processPatch( + patch: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>, evalScope: EvalScope, - ): Pair<Boolean, MuxPromptMovingResult<W, K, V>?> { - val newlySwitchedIn: MuxResult<W, K, V>? = - patch?.let { - // We have a patch, process additions/updates and removals - val adds = mutableListOf<Pair<K, TFlowImpl<V>>>() - val removes = mutableListOf<K>() - patch.forEach { (k, newUpstream) -> - when (newUpstream) { - is Just -> adds.add(k to newUpstream.value) - None -> removes.add(k) - } - } + ): Boolean { + var needsReschedule = false + // We have a patch, process additions/updates and removals + val adds = mutableListOf<Pair<K, EventsImpl<V>>>() + val removes = mutableListOf<K>() + patch.forEach { (k, newUpstream) -> + when (newUpstream) { + is Just -> adds.add(k to newUpstream.value) + None -> removes.add(k) + } + } - val additionsAndUpdates = mutableListOf<Pair<K, PullNode<V>>>() - val severed = mutableListOf<NodeConnection<*>>() - - coroutineScope { - // remove and sever - removes.forEach { k -> - switchedIn.remove(k)?.let { branchNode: BranchNode -> - val conn: NodeConnection<V> = branchNode.upstream - severed.add(conn) - launchImmediate { - conn.removeDownstream(downstream = branchNode.schedulable) - } - depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth) - } - } + val severed = mutableListOf<NodeConnection<*>>() - // add or replace - adds - .mapParallel { (k, newUpstream: TFlowImpl<V>) -> - val branchNode = BranchNode(k) - k to - newUpstream.activate(evalScope, branchNode.schedulable)?.let { - (conn, _) -> - branchNode.apply { upstream = conn } - } - } - .forEach { (k, newBranch: BranchNode?) -> - // remove old and sever, if present - switchedIn.remove(k)?.let { oldBranch: BranchNode -> - val conn: NodeConnection<V> = oldBranch.upstream - severed.add(conn) - launchImmediate { - conn.removeDownstream(downstream = oldBranch.schedulable) - } - depthTracker.removeDirectUpstream( - conn.depthTracker.snapshotDirectDepth - ) - } - - // add new - newBranch?.let { - switchedIn[k] = newBranch - additionsAndUpdates.add(k to newBranch.upstream.directUpstream) - val branchDepthTracker = newBranch.upstream.depthTracker - if (branchDepthTracker.snapshotIsDirect) { - depthTracker.addDirectUpstream( - oldDepth = null, - newDepth = branchDepthTracker.snapshotDirectDepth, - ) - } else { - depthTracker.addIndirectUpstream( - oldDepth = null, - newDepth = branchDepthTracker.snapshotIndirectDepth, - ) - depthTracker.updateIndirectRoots( - additions = branchDepthTracker.snapshotIndirectRoots, - butNot = null, - ) - } - } - } + // remove and sever + removes.forEach { k -> + switchedIn.remove(k)?.let { branchNode: BranchNode -> + if (name != null) { + logLn("[${this@MuxPromptNode}] removing $k") } + val conn: NodeConnection<V> = branchNode.upstream + severed.add(conn) + conn.removeDownstream(downstream = branchNode.schedulable) + depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth) + } + } - coroutineScope { - for (severedNode in severed) { - launch { severedNode.scheduleDeactivationIfNeeded(evalScope) } - } + // add or replace + adds.forEach { (k, newUpstream: EventsImpl<V>) -> + // remove old and sever, if present + switchedIn.remove(k)?.let { oldBranch: BranchNode -> + if (name != null) { + logLn("[${this@MuxPromptNode}] replacing $k") } + val conn: NodeConnection<V> = oldBranch.upstream + severed.add(conn) + conn.removeDownstream(downstream = oldBranch.schedulable) + depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth) + } - val resultStore = storeFactory.create<PullNode<V>>(additionsAndUpdates.size) - for ((k, node) in additionsAndUpdates) { - resultStore[k] = node + // add new + val newBranch = BranchNode(k) + newUpstream.activate(evalScope, newBranch.schedulable)?.let { (conn, needsEval) -> + newBranch.upstream = conn + if (name != null) { + logLn("[${this@MuxPromptNode}] switching in $k") + } + switchedIn[k] = newBranch + if (needsEval) { + upstreamData[k] = newBranch.upstream.directUpstream + } else { + needsReschedule = true + } + val branchDepthTracker = newBranch.upstream.depthTracker + if (branchDepthTracker.snapshotIsDirect) { + depthTracker.addDirectUpstream( + oldDepth = null, + newDepth = branchDepthTracker.snapshotDirectDepth, + ) + } else { + depthTracker.addIndirectUpstream( + oldDepth = null, + newDepth = branchDepthTracker.snapshotIndirectDepth, + ) + depthTracker.updateIndirectRoots( + additions = branchDepthTracker.snapshotIndirectRoots, + butNot = null, + ) } - resultStore.takeIf { it.isNotEmpty() }?.asReadOnly() } + } - return if (preSwitchNotEmpty || newlySwitchedIn != null) { - (newlySwitchedIn != null) to (preSwitchResults to newlySwitchedIn) - } else { - false to null + for (severedNode in severed) { + severedNode.scheduleDeactivationIfNeeded(evalScope) } + + return needsReschedule } - private fun adjustDownstreamDepths(evalScope: EvalScope, coroutineScope: CoroutineScope) { + private fun adjustDownstreamDepths(evalScope: EvalScope) { if (depthTracker.dirty_depthIncreased()) { // schedule downstream nodes on the compaction scheduler; this scheduler is drained at // the end of this eval depth, so that all depth increases are applied before we advance // the eval step - depthTracker.schedule(evalScope.compactor, node = this@MuxPromptMovingNode) + depthTracker.schedule(evalScope.compactor, node = this@MuxPromptNode) } else if (depthTracker.isDirty()) { // schedule downstream nodes on the eval scheduler; this is more efficient and is only // safe if the depth hasn't increased depthTracker.applyChanges( - coroutineScope, evalScope.scheduler, downstreamSet, - muxNode = this@MuxPromptMovingNode, + muxNode = this@MuxPromptNode, ) } } - override suspend fun getPushEvent(evalScope: EvalScope): MuxPromptMovingResult<W, K, V> = - evalScope.getCurrentValue(key = this) + override fun getPushEvent(logIndent: Int, evalScope: EvalScope): MuxResult<W, K, V> = + logDuration(logIndent, "MuxPrompt.getPushEvent") { + transactionCache.getCurrentValue(evalScope) + } - override suspend fun doDeactivate() { + override fun doDeactivate() { // Update lifecycle - lifecycle.mutex.withLock { - if (lifecycle.lifecycleState !is MuxLifecycleState.Active) return@doDeactivate - lifecycle.lifecycleState = MuxLifecycleState.Inactive(spec) - } + if (lifecycle.lifecycleState !is MuxLifecycleState.Active) return + lifecycle.lifecycleState = MuxLifecycleState.Inactive(spec) // Process branch nodes switchedIn.forEach { (_, branchNode) -> branchNode.upstream.removeDownstreamAndDeactivateIfNeeded( @@ -231,57 +204,54 @@ internal class MuxPromptMovingNode<W, K, V>( } } - suspend fun removeIndirectPatchNode( + fun removeIndirectPatchNode( scheduler: Scheduler, oldDepth: Int, indirectSet: Set<MuxDeferredNode<*, *, *>>, ) { - mutex.withLock { - patches = null - if ( - depthTracker.removeIndirectUpstream(oldDepth) or - depthTracker.updateIndirectRoots(removals = indirectSet) - ) { - depthTracker.schedule(scheduler, this) - } + patches = null + if ( + depthTracker.removeIndirectUpstream(oldDepth) or + depthTracker.updateIndirectRoots(removals = indirectSet) + ) { + depthTracker.schedule(scheduler, this) } } - suspend fun removeDirectPatchNode(scheduler: Scheduler, depth: Int) { - mutex.withLock { - patches = null - if (depthTracker.removeDirectUpstream(depth)) { - depthTracker.schedule(scheduler, this) - } + fun removeDirectPatchNode(scheduler: Scheduler, depth: Int) { + patches = null + if (depthTracker.removeDirectUpstream(depth)) { + depthTracker.schedule(scheduler, this) } } + override fun toString(): String = + "${this::class.simpleName}@$hashString${name?.let { "[$it]" }.orEmpty()}" + inner class PatchNode : SchedulableNode { val schedulable = Schedulable.N(this) - lateinit var upstream: NodeConnection<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>> + lateinit var upstream: NodeConnection<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>> - override suspend fun schedule(evalScope: EvalScope) { - patchData = upstream.getPushEvent(evalScope) - this@MuxPromptMovingNode.schedule(evalScope) + override fun schedule(logIndent: Int, evalScope: EvalScope) { + logDuration(logIndent, "MuxPromptPatchNode.schedule") { + patchData = upstream.getPushEvent(currentLogIndent, evalScope) + this@MuxPromptNode.schedule(evalScope) + } } - override suspend fun adjustDirectUpstream( - scheduler: Scheduler, - oldDepth: Int, - newDepth: Int, - ) { - this@MuxPromptMovingNode.adjustDirectUpstream(scheduler, oldDepth, newDepth) + override fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) { + this@MuxPromptNode.adjustDirectUpstream(scheduler, oldDepth, newDepth) } - override suspend fun moveIndirectUpstreamToDirect( + override fun moveIndirectUpstreamToDirect( scheduler: Scheduler, oldIndirectDepth: Int, oldIndirectSet: Set<MuxDeferredNode<*, *, *>>, newDirectDepth: Int, ) { - this@MuxPromptMovingNode.moveIndirectUpstreamToDirect( + this@MuxPromptNode.moveIndirectUpstreamToDirect( scheduler, oldIndirectDepth, oldIndirectSet, @@ -289,14 +259,14 @@ internal class MuxPromptMovingNode<W, K, V>( ) } - override suspend fun adjustIndirectUpstream( + override fun adjustIndirectUpstream( scheduler: Scheduler, oldDepth: Int, newDepth: Int, removals: Set<MuxDeferredNode<*, *, *>>, additions: Set<MuxDeferredNode<*, *, *>>, ) { - this@MuxPromptMovingNode.adjustIndirectUpstream( + this@MuxPromptNode.adjustIndirectUpstream( scheduler, oldDepth, newDepth, @@ -305,13 +275,13 @@ internal class MuxPromptMovingNode<W, K, V>( ) } - override suspend fun moveDirectUpstreamToIndirect( + override fun moveDirectUpstreamToIndirect( scheduler: Scheduler, oldDirectDepth: Int, newIndirectDepth: Int, newIndirectSet: Set<MuxDeferredNode<*, *, *>>, ) { - this@MuxPromptMovingNode.moveDirectUpstreamToIndirect( + this@MuxPromptNode.moveDirectUpstreamToIndirect( scheduler, oldDirectDepth, newIndirectDepth, @@ -319,98 +289,70 @@ internal class MuxPromptMovingNode<W, K, V>( ) } - override suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int) { - this@MuxPromptMovingNode.removeDirectPatchNode(scheduler, depth) + override fun removeDirectUpstream(scheduler: Scheduler, depth: Int) { + this@MuxPromptNode.removeDirectPatchNode(scheduler, depth) } - override suspend fun removeIndirectUpstream( + override fun removeIndirectUpstream( scheduler: Scheduler, depth: Int, indirectSet: Set<MuxDeferredNode<*, *, *>>, ) { - this@MuxPromptMovingNode.removeIndirectPatchNode(scheduler, depth, indirectSet) + this@MuxPromptNode.removeIndirectPatchNode(scheduler, depth, indirectSet) } } } -internal class MuxPromptEvalNode<W, K, V>( - private val movingNode: PullNode<MuxPromptMovingResult<W, K, V>>, - private val factory: MutableMapK.Factory<W, K>, -) : PullNode<MuxResult<W, K, V>> { - override suspend fun getPushEvent(evalScope: EvalScope): MuxResult<W, K, V> = - movingNode.getPushEvent(evalScope).let { (preSwitchResults, newlySwitchedIn) -> - newlySwitchedIn?.let { - factory - .create(preSwitchResults) - .also { store -> - newlySwitchedIn.forEach { k, pullNode -> store[k] = pullNode } - } - .asReadOnly() - } ?: preSwitchResults - } -} - internal inline fun <A> switchPromptImplSingle( - crossinline getStorage: suspend EvalScope.() -> TFlowImpl<A>, - crossinline getPatches: suspend EvalScope.() -> TFlowImpl<TFlowImpl<A>>, -): TFlowImpl<A> = - mapImpl({ + crossinline getStorage: EvalScope.() -> EventsImpl<A>, + crossinline getPatches: EvalScope.() -> EventsImpl<EventsImpl<A>>, +): EventsImpl<A> { + val switchPromptImpl = switchPromptImpl( getStorage = { singleOf(getStorage()).asIterable() }, getPatches = { - mapImpl(getPatches) { newFlow -> singleOf(just(newFlow)).asIterable() } + mapImpl(getPatches) { newFlow, _ -> singleOf(just(newFlow)).asIterable() } }, storeFactory = SingletonMapK.Factory(), ) - }) { map -> - map.getValue(Unit).getPushEvent(this) + return mapImpl({ switchPromptImpl }) { map, logIndent -> + map.asSingle().getValue(Unit).getPushEvent(logIndent, this) } +} internal fun <W, K, V> switchPromptImpl( - getStorage: suspend EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>, - getPatches: suspend EvalScope.() -> TFlowImpl<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>, + name: String? = null, + getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>, + getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>, storeFactory: MutableMapK.Factory<W, K>, -): TFlowImpl<MuxResult<W, K, V>> { - val moving = MuxLifecycle(MuxPromptActivator(getStorage, storeFactory, getPatches)) - val eval = TFlowCheap { downstream -> - moving.activate(evalScope = this, downstream)?.let { (connection, needsEval) -> - val evalNode = MuxPromptEvalNode(connection.directUpstream, storeFactory) - ActivationResult( - connection = NodeConnection(evalNode, connection.schedulerUpstream), - needsEval = needsEval, - ) - } - } - return eval.cached() -} +): EventsImpl<MuxResult<W, K, V>> = + MuxLifecycle(MuxPromptActivator(name, getStorage, storeFactory, getPatches)) private class MuxPromptActivator<W, K, V>( - private val getStorage: suspend EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>, + private val name: String?, + private val getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>, private val storeFactory: MutableMapK.Factory<W, K>, - private val getPatches: - suspend EvalScope.() -> TFlowImpl<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>, -) : MuxActivator<MuxPromptMovingResult<W, K, V>> { - override suspend fun activate( + private val getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>, +) : MuxActivator<W, K, V> { + override fun activate( evalScope: EvalScope, - lifecycle: MuxLifecycle<MuxPromptMovingResult<W, K, V>>, - ): MuxNode<W, *, *, MuxPromptMovingResult<W, K, V>>? { + lifecycle: MuxLifecycle<W, K, V>, + ): Pair<MuxNode<W, K, V>, (() -> Unit)?>? { // Initialize mux node and switched-in connections. val movingNode = - MuxPromptMovingNode(lifecycle, this, storeFactory).apply { - coroutineScope { - launch { initializeUpstream(evalScope, getStorage, storeFactory) } - // Setup patches connection - val patchNode = PatchNode() - getPatches(evalScope) - .activate(evalScope = evalScope, downstream = patchNode.schedulable) - ?.let { (conn, needsEval) -> - patchNode.upstream = conn - patches = patchNode - if (needsEval) { - patchData = conn.getPushEvent(evalScope) - } + MuxPromptNode(name, lifecycle, this, storeFactory).apply { + initializeUpstream(evalScope, getStorage, storeFactory) + // Setup patches connection + val patchNode = PatchNode() + getPatches(evalScope) + .activate(evalScope = evalScope, downstream = patchNode.schedulable) + ?.let { (conn, needsEval) -> + patchNode.upstream = conn + patches = patchNode + if (needsEval) { + patchData = conn.getPushEvent(0, evalScope) } - } + } // Update depth based on all initial switched-in nodes. initializeDepth() // Update depth based on patches node. @@ -441,6 +383,10 @@ private class MuxPromptActivator<W, K, V>( movingNode.schedule(evalScope) } - return movingNode.takeUnless { it.patches == null && it.switchedIn.isEmpty() } + return if (movingNode.patches == null && movingNode.switchedIn.isEmpty()) { + null + } else { + movingNode to null + } } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt index 79d4b7a843ac..d13cdded7ac3 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt @@ -16,17 +16,17 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.TState +import com.android.systemui.kairos.State import com.android.systemui.kairos.internal.util.HeteroMap +import com.android.systemui.kairos.internal.util.logDuration +import com.android.systemui.kairos.internal.util.logLn import com.android.systemui.kairos.util.Just import com.android.systemui.kairos.util.Maybe import com.android.systemui.kairos.util.just import com.android.systemui.kairos.util.none -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentLinkedDeque -import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicLong import kotlin.coroutines.ContinuationInterceptor +import kotlin.time.measureTime import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred @@ -52,32 +52,41 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope { override val network get() = this - override val compactor = SchedulerImpl() - override val scheduler = SchedulerImpl() - override val transactionStore = HeteroMap() + override val compactor = SchedulerImpl { + if (it.markedForCompaction) false + else { + it.markedForCompaction = true + true + } + } + override val scheduler = SchedulerImpl { + if (it.markedForEvaluation) false + else { + it.markedForEvaluation = true + true + } + } + override val transactionStore = TransactionStore() - private val stateWrites = ConcurrentLinkedQueue<TStateSource<*>>() - private val outputsByDispatcher = - ConcurrentHashMap<ContinuationInterceptor, ConcurrentLinkedQueue<Output<*>>>() - private val muxMovers = ConcurrentLinkedQueue<MuxDeferredNode<*, *, *>>() - private val deactivations = ConcurrentLinkedDeque<PushNode<*>>() - private val outputDeactivations = ConcurrentLinkedQueue<Output<*>>() + private val stateWrites = ArrayDeque<StateSource<*>>() + private val outputsByDispatcher = HashMap<ContinuationInterceptor, ArrayDeque<Output<*>>>() + private val muxMovers = ArrayDeque<MuxDeferredNode<*, *, *>>() + private val deactivations = ArrayDeque<PushNode<*>>() + private val outputDeactivations = ArrayDeque<Output<*>>() private val transactionMutex = Mutex() private val inputScheduleChan = Channel<ScheduledAction<*>>() override fun scheduleOutput(output: Output<*>) { val continuationInterceptor = output.context[ContinuationInterceptor] ?: Dispatchers.Unconfined - outputsByDispatcher - .computeIfAbsent(continuationInterceptor) { ConcurrentLinkedQueue() } - .add(output) + outputsByDispatcher.computeIfAbsent(continuationInterceptor) { ArrayDeque() }.add(output) } override fun scheduleMuxMover(muxMover: MuxDeferredNode<*, *, *>) { muxMovers.add(muxMover) } - override fun schedule(state: TStateSource<*>) { + override fun schedule(state: StateSource<*>) { stateWrites.add(state) } @@ -89,7 +98,7 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope { outputDeactivations.add(output) } - /** Listens for external events and starts FRP transactions. Runs forever. */ + /** Listens for external events and starts Kairos transactions. Runs forever. */ suspend fun runInputScheduler() { val actions = mutableListOf<ScheduledAction<*>>() for (first in inputScheduleChan) { @@ -101,28 +110,37 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope { actions.add(func) } transactionMutex.withLock { - try { - // Run all actions - evalScope { - for (action in actions) { - launch { action.started(evalScope = this@evalScope) } + val e = epoch + val duration = measureTime { + logLn(0, "===starting transaction $e===") + try { + logDuration(1, "init actions") { + // Run all actions + evalScope { + for (action in actions) { + action.started(evalScope = this@evalScope) + } + } + } + // Step through the network + doTransaction(1) + } catch (e: Exception) { + // Signal failure + while (actions.isNotEmpty()) { + actions.removeLast().fail(e) + } + // re-throw, cancelling this coroutine + throw e + } finally { + logDuration(1, "signal completions") { + // Signal completion + while (actions.isNotEmpty()) { + actions.removeLast().completed() + } } - } - // Step through the network - doTransaction() - } catch (e: Exception) { - // Signal failure - while (actions.isNotEmpty()) { - actions.removeLast().fail(e) - } - // re-throw, cancelling this coroutine - throw e - } finally { - // Signal completion - while (actions.isNotEmpty()) { - actions.removeLast().completed() } } + logLn(0, "===transaction $e took $duration===") } } } @@ -139,35 +157,47 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope { onResult.invokeOnCompletion { job.cancel() } } - suspend fun <R> evalScope(block: suspend EvalScope.() -> R): R = deferScope { + inline fun <R> evalScope(block: EvalScope.() -> R): R = deferScope { block(EvalScopeImpl(this@Network, this)) } - /** Performs a transactional update of the FRP network. */ - private suspend fun doTransaction() { + /** Performs a transactional update of the Kairos network. */ + private suspend fun doTransaction(logIndent: Int) { // Traverse network, then run outputs - do { - scheduler.drainEval(this) - } while (evalScope { evalOutputs(this) }) + logDuration(logIndent, "traverse network") { + do { + val numNodes = + logDuration("drainEval") { scheduler.drainEval(currentLogIndent, this@Network) } + logLn("drained $numNodes nodes") + } while (logDuration("evalOutputs") { evalScope { evalOutputs(this) } }) + } // Update states - evalScope { evalStateWriters(this) } + logDuration(logIndent, "update states") { + evalScope { evalStateWriters(currentLogIndent, this) } + } // Invalidate caches // Note: this needs to occur before deferred switches - transactionStore.clear() + logDuration(logIndent, "clear store") { transactionStore.clear() } epoch++ // Perform deferred switches - evalScope { evalMuxMovers(this) } + logDuration(logIndent, "evalMuxMovers") { + evalScope { evalMuxMovers(currentLogIndent, this) } + } // Compact depths - scheduler.drainCompact() - compactor.drainCompact() + logDuration(logIndent, "compact") { + scheduler.drainCompact(currentLogIndent) + compactor.drainCompact(currentLogIndent) + } // Deactivate nodes with no downstream - evalDeactivations() + logDuration(logIndent, "deactivations") { evalDeactivations() } } /** Invokes all [Output]s that have received data within this transaction. */ private suspend fun evalOutputs(evalScope: EvalScope): Boolean { + if (outputsByDispatcher.isEmpty()) { + return false + } // Outputs can enqueue other outputs, so we need two loops - if (outputsByDispatcher.isEmpty()) return false while (outputsByDispatcher.isNotEmpty()) { var launchedAny = false coroutineScope { @@ -176,57 +206,50 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope { launchedAny = true launch(key) { while (outputs.isNotEmpty()) { - val output = outputs.remove() + val output = outputs.removeFirst() launch { output.visit(evalScope) } } } } } } - if (!launchedAny) outputsByDispatcher.clear() + if (!launchedAny) { + outputsByDispatcher.clear() + } } return true } - private suspend fun evalMuxMovers(evalScope: EvalScope) { + private fun evalMuxMovers(logIndent: Int, evalScope: EvalScope) { while (muxMovers.isNotEmpty()) { - coroutineScope { - val toMove = muxMovers.remove() - launch { toMove.performMove(evalScope) } - } + val toMove = muxMovers.removeFirst() + toMove.performMove(logIndent, evalScope) } } - /** Updates all [TState]es that have changed within this transaction. */ - private suspend fun evalStateWriters(evalScope: EvalScope) { - coroutineScope { - while (stateWrites.isNotEmpty()) { - val latch = stateWrites.remove() - launch { latch.updateState(evalScope) } - } + /** Updates all [State]es that have changed within this transaction. */ + private fun evalStateWriters(logIndent: Int, evalScope: EvalScope) { + while (stateWrites.isNotEmpty()) { + val latch = stateWrites.removeFirst() + latch.updateState(logIndent, evalScope) } } - private suspend fun evalDeactivations() { - coroutineScope { - launch { - while (deactivations.isNotEmpty()) { - // traverse in reverse order - // - deactivations are added in depth-order during the node traversal phase - // - perform deactivations in reverse order, in case later ones propagate to - // earlier ones - val toDeactivate = deactivations.removeLast() - launch { toDeactivate.deactivateIfNeeded() } - } - } - while (outputDeactivations.isNotEmpty()) { - val toDeactivate = outputDeactivations.remove() - launch { - toDeactivate.upstream?.removeDownstreamAndDeactivateIfNeeded( - downstream = toDeactivate.schedulable - ) - } - } + private fun evalDeactivations() { + while (deactivations.isNotEmpty()) { + // traverse in reverse order + // - deactivations are added in depth-order during the node traversal phase + // - perform deactivations in reverse order, in case later ones propagate to + // earlier ones + val toDeactivate = deactivations.removeLast() + toDeactivate.deactivateIfNeeded() + } + + while (outputDeactivations.isNotEmpty()) { + val toDeactivate = outputDeactivations.removeFirst() + toDeactivate.upstream?.removeDownstreamAndDeactivateIfNeeded( + downstream = toDeactivate.schedulable + ) } check(deactivations.isEmpty()) { "unexpected lingering deactivations" } check(outputDeactivations.isEmpty()) { "unexpected lingering output deactivations" } @@ -260,4 +283,39 @@ internal class ScheduledAction<T>( } } -internal typealias TransactionStore = HeteroMap +internal class TransactionStore private constructor(private val storage: HeteroMap) { + constructor(capacity: Int) : this(HeteroMap(capacity)) + + constructor() : this(HeteroMap()) + + operator fun <A> get(key: HeteroMap.Key<A>): A = + storage.getOrError(key) { "no value for $key in this transaction" } + + operator fun <A> set(key: HeteroMap.Key<A>, value: A) { + storage[key] = value + } + + fun clear() = storage.clear() +} + +internal class TransactionCache<A> { + private val key = object : HeteroMap.Key<A> {} + @Volatile + var epoch: Long = Long.MIN_VALUE + private set + + fun getOrPut(evalScope: EvalScope, block: () -> A): A = + if (epoch < evalScope.epoch) { + epoch = evalScope.epoch + block().also { evalScope.transactionStore[key] = it } + } else { + evalScope.transactionStore[key] + } + + fun put(evalScope: EvalScope, value: A) { + epoch = evalScope.epoch + evalScope.transactionStore[key] = value + } + + fun getCurrentValue(evalScope: EvalScope): A = evalScope.transactionStore[key] +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt index fbd9689eb1d0..f662f1907069 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt @@ -16,32 +16,6 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.FrpScope -import kotlin.coroutines.Continuation -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.coroutineContext -import kotlin.coroutines.startCoroutine -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.completeWith -import kotlinx.coroutines.job +import com.android.systemui.kairos.KairosScope -internal object NoScope { - private object FrpScopeImpl : FrpScope - - suspend fun <R> runInFrpScope(block: suspend FrpScope.() -> R): R { - val complete = CompletableDeferred<R>(coroutineContext.job) - block.startCoroutine( - FrpScopeImpl, - object : Continuation<R> { - override val context: CoroutineContext - get() = EmptyCoroutineContext - - override fun resumeWith(result: Result<R>) { - complete.completeWith(result) - } - }, - ) - return complete.await() - } -} +internal object NoScope : KairosScope diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NodeTypes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NodeTypes.kt index 7a015d8ca1f6..39b8bfe540d2 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NodeTypes.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NodeTypes.kt @@ -22,18 +22,18 @@ Muxes + Branch */ internal sealed interface SchedulableNode { /** schedule this node w/ given NodeEvalScope */ - suspend fun schedule(evalScope: EvalScope) + fun schedule(logIndent: Int, evalScope: EvalScope) - suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) + fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) - suspend fun moveIndirectUpstreamToDirect( + fun moveIndirectUpstreamToDirect( scheduler: Scheduler, oldIndirectDepth: Int, oldIndirectSet: Set<MuxDeferredNode<*, *, *>>, newDirectDepth: Int, ) - suspend fun adjustIndirectUpstream( + fun adjustIndirectUpstream( scheduler: Scheduler, oldDepth: Int, newDepth: Int, @@ -41,20 +41,20 @@ internal sealed interface SchedulableNode { additions: Set<MuxDeferredNode<*, *, *>>, ) - suspend fun moveDirectUpstreamToIndirect( + fun moveDirectUpstreamToIndirect( scheduler: Scheduler, oldDirectDepth: Int, newIndirectDepth: Int, newIndirectSet: Set<MuxDeferredNode<*, *, *>>, ) - suspend fun removeIndirectUpstream( + fun removeIndirectUpstream( scheduler: Scheduler, depth: Int, indirectSet: Set<MuxDeferredNode<*, *, *>>, ) - suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int) + fun removeDirectUpstream(scheduler: Scheduler, depth: Int) } /* @@ -66,7 +66,7 @@ internal sealed interface PullNode<out A> { * will read from the cache, otherwise it will perform a full evaluation, even if invoked * multiple times within a transaction. */ - suspend fun getPushEvent(evalScope: EvalScope): A + fun getPushEvent(logIndent: Int, evalScope: EvalScope): A } /* @@ -74,19 +74,19 @@ Muxes + DmuxBranch */ internal sealed interface PushNode<A> : PullNode<A> { - suspend fun hasCurrentValue(evalScope: EvalScope): Boolean + fun hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean val depthTracker: DepthTracker - suspend fun removeDownstream(downstream: Schedulable) + fun removeDownstream(downstream: Schedulable) /** called during cleanup phase */ - suspend fun deactivateIfNeeded() + fun deactivateIfNeeded() /** called from mux nodes after severs */ - suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) + fun scheduleDeactivationIfNeeded(evalScope: EvalScope) - suspend fun addDownstream(downstream: Schedulable) + fun addDownstream(downstream: Schedulable) - suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) + fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Output.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Output.kt index 3373de05249c..38d8cf70b36e 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Output.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Output.kt @@ -21,8 +21,8 @@ import kotlin.coroutines.EmptyCoroutineContext internal class Output<A>( val context: CoroutineContext = EmptyCoroutineContext, - val onDeath: suspend () -> Unit = {}, - val onEmit: suspend EvalScope.(A) -> Unit, + val onDeath: () -> Unit = {}, + val onEmit: EvalScope.(A) -> Unit, ) { val schedulable = Schedulable.O(this) @@ -33,23 +33,24 @@ internal class Output<A>( private object NoResult // invoked by network - suspend fun visit(evalScope: EvalScope) { + fun visit(evalScope: EvalScope) { val upstreamResult = result check(upstreamResult !== NoResult) { "output visited with null upstream result" } result = NoResult @Suppress("UNCHECKED_CAST") evalScope.onEmit(upstreamResult as A) } - suspend fun kill() { + fun kill() { onDeath() } - suspend fun schedule(evalScope: EvalScope) { + fun schedule(logIndent: Int, evalScope: EvalScope) { result = - checkNotNull(upstream) { "output scheduled with null upstream" }.getPushEvent(evalScope) + checkNotNull(upstream) { "output scheduled with null upstream" } + .getPushEvent(logIndent, evalScope) evalScope.scheduleOutput(this) } } -internal inline fun OneShot(crossinline onEmit: suspend EvalScope.() -> Unit): Output<Unit> = +internal inline fun OneShot(crossinline onEmit: EvalScope.() -> Unit): Output<Unit> = Output<Unit>(onEmit = { onEmit() }).apply { result = Unit } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt index 43b621fadc67..517e54f57833 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt @@ -16,22 +16,24 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.internal.util.Key -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Deferred +import com.android.systemui.kairos.internal.util.logDuration -internal val neverImpl: TFlowImpl<Nothing> = TFlowCheap { null } +internal val neverImpl: EventsImpl<Nothing> = EventsImplCheap { null } -internal class MapNode<A, B>(val upstream: PullNode<A>, val transform: suspend EvalScope.(A) -> B) : +internal class MapNode<A, B>(val upstream: PullNode<A>, val transform: EvalScope.(A, Int) -> B) : PullNode<B> { - override suspend fun getPushEvent(evalScope: EvalScope): B = - evalScope.transform(upstream.getPushEvent(evalScope)) + override fun getPushEvent(logIndent: Int, evalScope: EvalScope): B = + logDuration(logIndent, "MapNode.getPushEvent") { + val upstream = + logDuration("upstream event") { upstream.getPushEvent(currentLogIndent, evalScope) } + logDuration("transform") { evalScope.transform(upstream, currentLogIndent) } + } } internal inline fun <A, B> mapImpl( - crossinline upstream: suspend EvalScope.() -> TFlowImpl<A>, - noinline transform: suspend EvalScope.(A) -> B, -): TFlowImpl<B> = TFlowCheap { downstream -> + crossinline upstream: EvalScope.() -> EventsImpl<A>, + noinline transform: EvalScope.(A, Int) -> B, +): EventsImpl<B> = EventsImplCheap { downstream -> upstream().activate(evalScope = this, downstream)?.let { (connection, needsEval) -> ActivationResult( connection = @@ -44,19 +46,29 @@ internal inline fun <A, B> mapImpl( } } -internal class CachedNode<A>(val key: Key<Deferred<A>>, val upstream: PullNode<A>) : PullNode<A> { - override suspend fun getPushEvent(evalScope: EvalScope): A { - val deferred = - evalScope.transactionStore.getOrPut(key) { - evalScope.deferAsync(CoroutineStart.LAZY) { upstream.getPushEvent(evalScope) } - } - return deferred.await() - } +internal class CachedNode<A>( + private val transactionCache: TransactionCache<Lazy<A>>, + val upstream: PullNode<A>, +) : PullNode<A> { + override fun getPushEvent(logIndent: Int, evalScope: EvalScope): A = + logDuration(logIndent, "CachedNode.getPushEvent") { + val deferred = + logDuration("CachedNode.getOrPut", false) { + transactionCache.getOrPut(evalScope) { + evalScope.deferAsync { + logDuration("CachedNode.getUpstreamEvent") { + upstream.getPushEvent(currentLogIndent, evalScope) + } + } + } + } + logDuration("await") { deferred.value } + } } -internal fun <A> TFlowImpl<A>.cached(): TFlowImpl<A> { - val key = object : Key<Deferred<A>> {} - return TFlowCheap { +internal fun <A> EventsImpl<A>.cached(): EventsImpl<A> { + val key = TransactionCache<Lazy<A>>() + return EventsImplCheap { it -> activate(this, it)?.let { (connection, needsEval) -> ActivationResult( connection = diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Scheduler.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Scheduler.kt index d046420517fe..0529bcb63c07 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Scheduler.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Scheduler.kt @@ -14,66 +14,76 @@ * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class) - package com.android.systemui.kairos.internal -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.PriorityBlockingQueue -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch +import com.android.systemui.kairos.internal.util.LogIndent +import java.util.PriorityQueue internal interface Scheduler { - fun schedule(depth: Int, node: MuxNode<*, *, *, *>) + fun schedule(depth: Int, node: MuxNode<*, *, *>) - fun scheduleIndirect(indirectDepth: Int, node: MuxNode<*, *, *, *>) + fun scheduleIndirect(indirectDepth: Int, node: MuxNode<*, *, *>) } -internal class SchedulerImpl : Scheduler { - val enqueued = ConcurrentHashMap<MuxNode<*, *, *, *>, Any>() - val scheduledQ = - PriorityBlockingQueue<Pair<Int, MuxNode<*, *, *, *>>>(16, compareBy { it.first }) +internal class SchedulerImpl(private val enqueue: (MuxNode<*, *, *>) -> Boolean) : Scheduler { + private val scheduledQ = PriorityQueue<Pair<Int, MuxNode<*, *, *>>>(compareBy { it.first }) - override fun schedule(depth: Int, node: MuxNode<*, *, *, *>) { - if (enqueued.putIfAbsent(node, node) == null) { + override fun schedule(depth: Int, node: MuxNode<*, *, *>) { + if (enqueue(node)) { scheduledQ.add(Pair(depth, node)) } } - override fun scheduleIndirect(indirectDepth: Int, node: MuxNode<*, *, *, *>) { + override fun scheduleIndirect(indirectDepth: Int, node: MuxNode<*, *, *>) { schedule(Int.MIN_VALUE + indirectDepth, node) } - internal suspend fun drainEval(network: Network) { - drain { runStep -> - runStep { muxNode -> network.evalScope { muxNode.visit(this) } } + internal fun drainEval(logIndent: Int, network: Network): Int = + drain(logIndent) { runStep -> + runStep { muxNode -> + network.evalScope { + muxNode.markedForEvaluation = false + muxNode.visit(currentLogIndent, this) + } + } // If any visited MuxPromptNodes had their depths increased, eagerly propagate those // depth changes now before performing further network evaluation. - network.compactor.drainCompact() + val numNodes = network.compactor.drainCompact(currentLogIndent) + logLn("promptly compacted $numNodes nodes") } - } - internal suspend fun drainCompact() { - drain { runStep -> runStep { muxNode -> muxNode.visitCompact(scheduler = this) } } - } + internal fun drainCompact(logIndent: Int): Int = + drain(logIndent) { runStep -> + runStep { muxNode -> + muxNode.markedForCompaction = false + muxNode.visitCompact(scheduler = this@SchedulerImpl) + } + } - private suspend inline fun drain( + private inline fun drain( + logIndent: Int, crossinline onStep: - suspend ( - runStep: suspend (visit: suspend (MuxNode<*, *, *, *>) -> Unit) -> Unit - ) -> Unit - ): Unit = coroutineScope { + LogIndent.( + runStep: LogIndent.(visit: LogIndent.(MuxNode<*, *, *>) -> Unit) -> Unit + ) -> Unit, + ): Int { + var total = 0 while (scheduledQ.isNotEmpty()) { val maxDepth = scheduledQ.peek()?.first ?: error("Unexpected empty scheduler") - onStep { visit -> runStep(maxDepth, visit) } + LogIndent(logIndent).onStep { visit -> + logDuration("step $maxDepth") { + val subtotal = runStep(maxDepth) { visit(it) } + logLn("visited $subtotal nodes") + total += subtotal + } + } } + return total } - private suspend inline fun runStep( - maxDepth: Int, - crossinline visit: suspend (MuxNode<*, *, *, *>) -> Unit, - ) = coroutineScope { + private inline fun runStep(maxDepth: Int, crossinline visit: (MuxNode<*, *, *>) -> Unit): Int { + var total = 0 + val toVisit = mutableListOf<MuxNode<*, *, *>>() while (scheduledQ.peek()?.first?.let { it <= maxDepth } == true) { val (d, node) = scheduledQ.remove() if ( @@ -82,11 +92,15 @@ internal class SchedulerImpl : Scheduler { ) { scheduledQ.add(node.depthTracker.dirty_directDepth to node) } else { - launch { - enqueued.remove(node) - visit(node) - } + total++ + toVisit.add(node) } } + + for (node in toVisit) { + visit(node) + } + + return total } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TStateImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt index 916f22575b0c..5ba645246b0f 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TStateImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt @@ -20,29 +20,22 @@ import com.android.systemui.kairos.internal.store.ConcurrentHashMapK import com.android.systemui.kairos.internal.store.MutableArrayMapK import com.android.systemui.kairos.internal.store.MutableMapK import com.android.systemui.kairos.internal.store.StoreEntry -import com.android.systemui.kairos.internal.util.Key import com.android.systemui.kairos.internal.util.hashString -import com.android.systemui.kairos.internal.util.launchImmediate import com.android.systemui.kairos.util.Maybe import com.android.systemui.kairos.util.just import com.android.systemui.kairos.util.none import java.util.concurrent.atomic.AtomicLong -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Deferred import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.coroutineScope -internal sealed interface TStateImpl<out A> { +internal sealed interface StateImpl<out A> { val name: String? val operatorName: String - val changes: TFlowImpl<A> + val changes: EventsImpl<A> - suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> + fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> } -internal sealed class TStateDerived<A>(override val changes: TFlowImpl<A>) : - TStateImpl<A>, Key<Deferred<Pair<A, Long>>> { +internal sealed class StateDerived<A>(override val changes: EventsImpl<A>) : StateImpl<A> { @Volatile var invalidatedEpoch = Long.MIN_VALUE @@ -52,12 +45,12 @@ internal sealed class TStateDerived<A>(override val changes: TFlowImpl<A>) : protected var cache: Any? = EmptyCache private set - override suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> = - evalScope.transactionStore - .getOrPut(this) { evalScope.deferAsync(CoroutineStart.LAZY) { pull(evalScope) } } - .await() + private val transactionCache = TransactionCache<Lazy<Pair<A, Long>>>() - suspend fun pull(evalScope: EvalScope): Pair<A, Long> { + override fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> = + transactionCache.getOrPut(evalScope) { evalScope.deferAsync { pull(evalScope) } }.value + + fun pull(evalScope: EvalScope): Pair<A, Long> { @Suppress("UNCHECKED_CAST") return recalc(evalScope)?.also { (a, epoch) -> setCache(a, epoch) } ?: ((cache as A) to invalidatedEpoch) @@ -75,65 +68,66 @@ internal sealed class TStateDerived<A>(override val changes: TFlowImpl<A>) : return if (cache == EmptyCache) none else just(cache as A) } - protected abstract suspend fun recalc(evalScope: EvalScope): Pair<A, Long>? + protected abstract fun recalc(evalScope: EvalScope): Pair<A, Long>? private data object EmptyCache } -internal class TStateSource<A>( +internal class StateSource<A>( override val name: String?, override val operatorName: String, - init: Deferred<A>, - override val changes: TFlowImpl<A>, -) : TStateImpl<A> { + init: Lazy<A>, + override val changes: EventsImpl<A>, +) : StateImpl<A> { constructor( name: String?, operatorName: String, init: A, - changes: TFlowImpl<A>, - ) : this(name, operatorName, CompletableDeferred(init), changes) + changes: EventsImpl<A>, + ) : this(name, operatorName, CompletableLazy(init), changes) lateinit var upstreamConnection: NodeConnection<A> // Note: Don't need to synchronize; we will never interleave reads and writes, since all writes // are performed at the end of a network step, after any reads would have taken place. - @Volatile private var _current: Deferred<A> = init + @Volatile private var _current: Lazy<A> = init + @Volatile var writeEpoch = 0L private set - override suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> = - _current.await() to writeEpoch + override fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> = + _current.value to writeEpoch /** called by network after eval phase has completed */ - suspend fun updateState(evalScope: EvalScope) { + fun updateState(logIndent: Int, evalScope: EvalScope) { // write the latch - _current = CompletableDeferred(upstreamConnection.getPushEvent(evalScope)) + // TODO: deferAsync? + _current = CompletableLazy(upstreamConnection.getPushEvent(logIndent, evalScope)) writeEpoch = evalScope.epoch } - override fun toString(): String = "TStateImpl(changes=$changes, current=$_current)" + override fun toString(): String = "StateImpl(changes=$changes, current=$_current)" @OptIn(ExperimentalCoroutinesApi::class) - fun getStorageUnsafe(): Maybe<A> = - if (_current.isCompleted) just(_current.getCompleted()) else none + fun getStorageUnsafe(): Maybe<A> = if (_current.isInitialized()) just(_current.value) else none } -internal fun <A> constS(name: String?, operatorName: String, init: A): TStateImpl<A> = - TStateSource(name, operatorName, init, neverImpl) +internal fun <A> constState(name: String?, operatorName: String, init: A): StateImpl<A> = + StateSource(name, operatorName, init, neverImpl) -internal inline fun <A> activatedTStateSource( +internal inline fun <A> activatedStateSource( name: String?, operatorName: String, evalScope: EvalScope, - crossinline getChanges: suspend EvalScope.() -> TFlowImpl<A>, - init: Deferred<A>, -): TStateImpl<A> { - lateinit var state: TStateSource<A> - val calm: TFlowImpl<A> = + crossinline getChanges: EvalScope.() -> EventsImpl<A>, + init: Lazy<A>, +): StateImpl<A> { + lateinit var state: StateSource<A> + val calm: EventsImpl<A> = filterImpl(getChanges) { new -> new != state.getCurrentWithEpoch(evalScope = this).first } - return TStateSource(name, operatorName, init, calm).also { + return StateSource(name, operatorName, init, calm).also { state = it evalScope.scheduleOutput( OneShot { @@ -149,9 +143,9 @@ internal inline fun <A> activatedTStateSource( } } -private inline fun <A> TFlowImpl<A>.calm( - crossinline getState: () -> TStateDerived<A> -): TFlowImpl<A> = +private inline fun <A> EventsImpl<A>.calm( + crossinline getState: () -> StateDerived<A> +): EventsImpl<A> = filterImpl({ this@calm }) { new -> val state = getState() val (current, _) = state.getCurrentWithEpoch(evalScope = this) @@ -164,22 +158,28 @@ private inline fun <A> TFlowImpl<A>.calm( } .cached() -internal fun <A, B> TStateImpl<A>.mapCheap( +internal fun <A, B> StateImpl<A>.mapCheap( name: String?, operatorName: String, - transform: suspend EvalScope.(A) -> B, -): TStateImpl<B> = - DerivedMapCheap(name, operatorName, this, mapImpl({ changes }) { transform(it) }, transform) + transform: EvalScope.(A) -> B, +): StateImpl<B> = + DerivedMapCheap( + name, + operatorName, + this, + mapImpl({ changes }) { it, _ -> transform(it) }, + transform, + ) internal class DerivedMapCheap<A, B>( override val name: String?, override val operatorName: String, - val upstream: TStateImpl<A>, - override val changes: TFlowImpl<B>, - private val transform: suspend EvalScope.(A) -> B, -) : TStateImpl<B> { + val upstream: StateImpl<A>, + override val changes: EventsImpl<B>, + private val transform: EvalScope.(A) -> B, +) : StateImpl<B> { - override suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<B, Long> { + override fun getCurrentWithEpoch(evalScope: EvalScope): Pair<B, Long> { val (a, epoch) = upstream.getCurrentWithEpoch(evalScope) return evalScope.transform(a) to epoch } @@ -187,13 +187,13 @@ internal class DerivedMapCheap<A, B>( override fun toString(): String = "${this::class.simpleName}@$hashString" } -internal fun <A, B> TStateImpl<A>.map( +internal fun <A, B> StateImpl<A>.map( name: String?, operatorName: String, - transform: suspend EvalScope.(A) -> B, -): TStateImpl<B> { - lateinit var state: TStateDerived<B> - val mappedChanges = mapImpl({ changes }) { transform(it) }.cached().calm { state } + transform: EvalScope.(A) -> B, +): StateImpl<B> { + lateinit var state: StateDerived<B> + val mappedChanges = mapImpl({ changes }) { it, _ -> transform(it) }.cached().calm { state } state = DerivedMap(name, operatorName, transform, this, mappedChanges) return state } @@ -201,13 +201,13 @@ internal fun <A, B> TStateImpl<A>.map( internal class DerivedMap<A, B>( override val name: String?, override val operatorName: String, - private val transform: suspend EvalScope.(A) -> B, - val upstream: TStateImpl<A>, - changes: TFlowImpl<B>, -) : TStateDerived<B>(changes) { + private val transform: EvalScope.(A) -> B, + val upstream: StateImpl<A>, + changes: EventsImpl<B>, +) : StateDerived<B>(changes) { override fun toString(): String = "${this::class.simpleName}@$hashString" - override suspend fun recalc(evalScope: EvalScope): Pair<B, Long>? { + override fun recalc(evalScope: EvalScope): Pair<B, Long>? { val (a, epoch) = upstream.getCurrentWithEpoch(evalScope) return if (epoch > invalidatedEpoch) { evalScope.transform(a) to epoch @@ -217,17 +217,18 @@ internal class DerivedMap<A, B>( } } -internal fun <A> TStateImpl<TStateImpl<A>>.flatten(name: String?, operator: String): TStateImpl<A> { +internal fun <A> StateImpl<StateImpl<A>>.flatten(name: String?, operator: String): StateImpl<A> { // emits the current value of the new inner state, when that state is emitted - val switchEvents = mapImpl({ changes }) { newInner -> newInner.getCurrentWithEpoch(this).first } + val switchEvents = + mapImpl({ changes }) { newInner, _ -> newInner.getCurrentWithEpoch(this).first } // emits the new value of the new inner state when that state is emitted, or // falls back to the current value if a new state is *not* being emitted this // transaction val innerChanges = - mapImpl({ changes }) { newInner -> + mapImpl({ changes }) { newInner, _ -> mergeNodes({ switchEvents }, { newInner.changes }) { _, new -> new } } - val switchedChanges: TFlowImpl<A> = + val switchedChanges: EventsImpl<A> = switchPromptImplSingle( getStorage = { this@flatten.getCurrentWithEpoch(evalScope = this).first.changes }, getPatches = { innerChanges }, @@ -240,10 +241,10 @@ internal fun <A> TStateImpl<TStateImpl<A>>.flatten(name: String?, operator: Stri internal class DerivedFlatten<A>( override val name: String?, override val operatorName: String, - val upstream: TStateImpl<TStateImpl<A>>, - changes: TFlowImpl<A>, -) : TStateDerived<A>(changes) { - override suspend fun recalc(evalScope: EvalScope): Pair<A, Long> { + val upstream: StateImpl<StateImpl<A>>, + changes: EventsImpl<A>, +) : StateDerived<A>(changes) { + override fun recalc(evalScope: EvalScope): Pair<A, Long> { val (inner, epoch0) = upstream.getCurrentWithEpoch(evalScope) val (a, epoch1) = inner.getCurrentWithEpoch(evalScope) return a to maxOf(epoch0, epoch1) @@ -253,19 +254,19 @@ internal class DerivedFlatten<A>( } @Suppress("NOTHING_TO_INLINE") -internal inline fun <A, B> TStateImpl<A>.flatMap( +internal inline fun <A, B> StateImpl<A>.flatMap( name: String?, operatorName: String, - noinline transform: suspend EvalScope.(A) -> TStateImpl<B>, -): TStateImpl<B> = map(null, operatorName, transform).flatten(name, operatorName) + noinline transform: EvalScope.(A) -> StateImpl<B>, +): StateImpl<B> = map(null, operatorName, transform).flatten(name, operatorName) internal fun <A, B, Z> zipStates( name: String?, operatorName: String, - l1: TStateImpl<A>, - l2: TStateImpl<B>, - transform: suspend EvalScope.(A, B) -> Z, -): TStateImpl<Z> = + l1: StateImpl<A>, + l2: StateImpl<B>, + transform: EvalScope.(A, B) -> Z, +): StateImpl<Z> = zipStateList(null, operatorName, listOf(l1, l2)).map(name, operatorName) { @Suppress("UNCHECKED_CAST") transform(it[0] as A, it[1] as B) } @@ -273,11 +274,11 @@ internal fun <A, B, Z> zipStates( internal fun <A, B, C, Z> zipStates( name: String?, operatorName: String, - l1: TStateImpl<A>, - l2: TStateImpl<B>, - l3: TStateImpl<C>, - transform: suspend EvalScope.(A, B, C) -> Z, -): TStateImpl<Z> = + l1: StateImpl<A>, + l2: StateImpl<B>, + l3: StateImpl<C>, + transform: EvalScope.(A, B, C) -> Z, +): StateImpl<Z> = zipStateList(null, operatorName, listOf(l1, l2, l3)).map(name, operatorName) { @Suppress("UNCHECKED_CAST") transform(it[0] as A, it[1] as B, it[2] as C) } @@ -285,12 +286,12 @@ internal fun <A, B, C, Z> zipStates( internal fun <A, B, C, D, Z> zipStates( name: String?, operatorName: String, - l1: TStateImpl<A>, - l2: TStateImpl<B>, - l3: TStateImpl<C>, - l4: TStateImpl<D>, - transform: suspend EvalScope.(A, B, C, D) -> Z, -): TStateImpl<Z> = + l1: StateImpl<A>, + l2: StateImpl<B>, + l3: StateImpl<C>, + l4: StateImpl<D>, + transform: EvalScope.(A, B, C, D) -> Z, +): StateImpl<Z> = zipStateList(null, operatorName, listOf(l1, l2, l3, l4)).map(name, operatorName) { @Suppress("UNCHECKED_CAST") transform(it[0] as A, it[1] as B, it[2] as C, it[3] as D) } @@ -298,13 +299,13 @@ internal fun <A, B, C, D, Z> zipStates( internal fun <A, B, C, D, E, Z> zipStates( name: String?, operatorName: String, - l1: TStateImpl<A>, - l2: TStateImpl<B>, - l3: TStateImpl<C>, - l4: TStateImpl<D>, - l5: TStateImpl<E>, - transform: suspend EvalScope.(A, B, C, D, E) -> Z, -): TStateImpl<Z> = + l1: StateImpl<A>, + l2: StateImpl<B>, + l3: StateImpl<C>, + l4: StateImpl<D>, + l5: StateImpl<E>, + transform: EvalScope.(A, B, C, D, E) -> Z, +): StateImpl<Z> = zipStateList(null, operatorName, listOf(l1, l2, l3, l4, l5)).map(name, operatorName) { @Suppress("UNCHECKED_CAST") transform(it[0] as A, it[1] as B, it[2] as C, it[3] as D, it[4] as E) @@ -313,8 +314,8 @@ internal fun <A, B, C, D, E, Z> zipStates( internal fun <K, V> zipStateMap( name: String?, operatorName: String, - states: Map<K, TStateImpl<V>>, -): TStateImpl<Map<K, V>> = + states: Map<K, StateImpl<V>>, +): StateImpl<Map<K, V>> = zipStates( name = name, operatorName = operatorName, @@ -326,18 +327,14 @@ internal fun <K, V> zipStateMap( internal fun <V> zipStateList( name: String?, operatorName: String, - states: List<TStateImpl<V>>, -): TStateImpl<List<V>> { + states: List<StateImpl<V>>, +): StateImpl<List<V>> { val zipped = zipStates( name = name, operatorName = operatorName, numStates = states.size, - states = - states - .asSequence() - .mapIndexed { index, tStateImpl -> StoreEntry(index, tStateImpl) } - .asIterable(), + states = states.asIterableWithIndex(), storeFactory = MutableArrayMapK.Factory(), ) // Like mapCheap, but with caching (or like map, but without the calm changes, as they are not @@ -347,7 +344,7 @@ internal fun <V> zipStateList( operatorName = operatorName, transform = { arrayStore -> arrayStore.values.toList() }, upstream = zipped, - changes = mapImpl({ zipped.changes }) { arrayStore -> arrayStore.values.toList() }, + changes = mapImpl({ zipped.changes }) { arrayStore, _ -> arrayStore.values.toList() }, ) } @@ -355,35 +352,31 @@ internal fun <W, K, A> zipStates( name: String?, operatorName: String, numStates: Int, - states: Iterable<Map.Entry<K, TStateImpl<A>>>, + states: Iterable<Map.Entry<K, StateImpl<A>>>, storeFactory: MutableMapK.Factory<W, K>, -): TStateImpl<MutableMapK<W, K, A>> { +): StateImpl<MutableMapK<W, K, A>> { if (numStates == 0) { - return constS(name, operatorName, storeFactory.create(0)) + return constState(name, operatorName, storeFactory.create(0)) } val stateChanges = states.asSequence().map { (k, v) -> StoreEntry(k, v.changes) }.asIterable() lateinit var state: DerivedZipped<W, K, A> // No need for calm; invariant ensures that changes will only emit when there's a difference + val switchDeferredImpl = + switchDeferredImpl( + getStorage = { stateChanges }, + getPatches = { neverImpl }, + storeFactory = storeFactory, + ) val changes = - mapImpl({ - switchDeferredImpl( - getStorage = { stateChanges }, - getPatches = { neverImpl }, - storeFactory = storeFactory, - ) - }) { patch -> + mapImpl({ switchDeferredImpl }) { patch, logIndent -> val store = storeFactory.create<A>(numStates) - coroutineScope { - states.forEach { (k, state) -> - launchImmediate { - store[k] = - if (patch.contains(k)) { - patch.getValue(k).getPushEvent(evalScope = this@mapImpl) - } else { - state.getCurrentWithEpoch(evalScope = this@mapImpl).first - } + states.forEach { (k, state) -> + store[k] = + if (patch.contains(k)) { + patch.getValue(k).getPushEvent(logIndent, evalScope = this@mapImpl) + } else { + state.getCurrentWithEpoch(evalScope = this@mapImpl).first } - } } store.also { state.setCache(it, epoch) } } @@ -404,21 +397,17 @@ internal class DerivedZipped<W, K, A>( override val name: String?, override val operatorName: String, private val upstreamSize: Int, - val upstream: Iterable<Map.Entry<K, TStateImpl<A>>>, - changes: TFlowImpl<MutableMapK<W, K, A>>, + val upstream: Iterable<Map.Entry<K, StateImpl<A>>>, + changes: EventsImpl<MutableMapK<W, K, A>>, private val storeFactory: MutableMapK.Factory<W, K>, -) : TStateDerived<MutableMapK<W, K, A>>(changes) { - override suspend fun recalc(evalScope: EvalScope): Pair<MutableMapK<W, K, A>, Long> { +) : StateDerived<MutableMapK<W, K, A>>(changes) { + override fun recalc(evalScope: EvalScope): Pair<MutableMapK<W, K, A>, Long> { val newEpoch = AtomicLong() val store = storeFactory.create<A>(upstreamSize) - coroutineScope { - for ((key, value) in upstream) { - launchImmediate { - val (a, epoch) = value.getCurrentWithEpoch(evalScope) - newEpoch.accumulateAndGet(epoch, ::maxOf) - store[key] = a - } - } + for ((key, value) in upstream) { + val (a, epoch) = value.getCurrentWithEpoch(evalScope) + newEpoch.accumulateAndGet(epoch, ::maxOf) + store[key] = a } return store to newEpoch.get() } @@ -430,10 +419,10 @@ internal class DerivedZipped<W, K, A>( internal inline fun <A> zipStates( name: String?, operatorName: String, - states: List<TStateImpl<A>>, -): TStateImpl<List<A>> = + states: List<StateImpl<A>>, +): StateImpl<List<A>> = if (states.isEmpty()) { - constS(name, operatorName, emptyList()) + constState(name, operatorName, emptyList()) } else { zipStateList(null, operatorName, states) } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt index 94f94f510d48..b5eec85f1f5f 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt @@ -16,116 +16,65 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.FrpDeferredValue -import com.android.systemui.kairos.FrpStateScope -import com.android.systemui.kairos.FrpStateful -import com.android.systemui.kairos.FrpTransactionScope -import com.android.systemui.kairos.GroupedTFlow -import com.android.systemui.kairos.TFlow -import com.android.systemui.kairos.TFlowInit -import com.android.systemui.kairos.TFlowLoop -import com.android.systemui.kairos.TState -import com.android.systemui.kairos.TStateInit -import com.android.systemui.kairos.emptyTFlow +import com.android.systemui.kairos.DeferredValue +import com.android.systemui.kairos.Events +import com.android.systemui.kairos.EventsInit +import com.android.systemui.kairos.EventsLoop +import com.android.systemui.kairos.GroupedEvents +import com.android.systemui.kairos.State +import com.android.systemui.kairos.StateInit +import com.android.systemui.kairos.StateScope +import com.android.systemui.kairos.Stateful +import com.android.systemui.kairos.emptyEvents import com.android.systemui.kairos.groupByKey import com.android.systemui.kairos.init import com.android.systemui.kairos.internal.store.ConcurrentHashMapK -import com.android.systemui.kairos.internal.util.mapValuesParallel import com.android.systemui.kairos.mapCheap import com.android.systemui.kairos.merge -import com.android.systemui.kairos.switch +import com.android.systemui.kairos.switchEvents import com.android.systemui.kairos.util.Maybe import com.android.systemui.kairos.util.map -import kotlin.coroutines.Continuation -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.startCoroutine -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.completeWith -import kotlinx.coroutines.job -internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: TFlow<Any>) : - StateScope, EvalScope by evalScope { +internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: Events<Any>) : + InternalStateScope, EvalScope by evalScope { - private val endSignalOnce: TFlow<Any> = endSignal.nextOnlyInternal("StateScope.endSignal") + override val endSignalOnce: Events<Any> = endSignal.nextOnlyInternal("StateScope.endSignal") - private fun <A> TFlow<A>.truncateToScope(operatorName: String): TFlow<A> = - if (endSignalOnce === emptyTFlow) { - this - } else { - endSignalOnce.mapCheap { emptyTFlow }.toTStateInternal(operatorName, this).switch() - } - - private fun <A> TFlow<A>.nextOnlyInternal(operatorName: String): TFlow<A> = - if (this === emptyTFlow) { - this - } else { - TFlowLoop<A>().apply { - loopback = - mapCheap { emptyTFlow } - .toTStateInternal(operatorName, this@nextOnlyInternal) - .switch() - } - } - - private fun <A> TFlow<A>.toTStateInternal(operatorName: String, init: A): TState<A> = - toTStateInternalDeferred(operatorName, CompletableDeferred(init)) - - private fun <A> TFlow<A>.toTStateInternalDeferred( - operatorName: String, - init: Deferred<A>, - ): TState<A> { - val changes = this@toTStateInternalDeferred - val name = operatorName - val impl = - activatedTStateSource( - name, - operatorName, - evalScope, - { changes.init.connect(evalScope = this) }, - init, - ) - return TStateInit(constInit(name, impl)) - } - - private fun <R> deferredInternal(block: suspend FrpStateScope.() -> R): FrpDeferredValue<R> = - FrpDeferredValue(deferAsync { runInStateScope(block) }) + override fun <A> deferredStateScope(block: StateScope.() -> A): DeferredValue<A> = + DeferredValue(deferAsync { block() }) - private fun <A> TFlow<A>.toTStateDeferredInternal( - initialValue: FrpDeferredValue<A> - ): TState<A> { - val operatorName = "toTStateDeferred" + override fun <A> Events<A>.holdStateDeferred(initialValue: DeferredValue<A>): State<A> { + val operatorName = "holdStateDeferred" // Ensure state is only collected until the end of this scope return truncateToScope(operatorName) - .toTStateInternalDeferred(operatorName, initialValue.unwrapped) + .toStateInternalDeferred(operatorName, initialValue.unwrapped) } - private fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyInternal( - storage: TState<Map<K, TFlow<V>>> - ): TFlow<Map<K, V>> { - val name = "mergeIncrementally" - return TFlowInit( + override fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( + name: String?, + initialEvents: DeferredValue<Map<K, Events<V>>>, + ): Events<Map<K, V>> { + val storage: State<Map<K, Events<V>>> = foldStateMapIncrementally(initialEvents) + val patches = + mapImpl({ init.connect(this) }) { patch, _ -> + patch + .mapValues { (_, m) -> m.map { events -> events.init.connect(this) } } + .asIterable() + } + return EventsInit( constInit( name, switchDeferredImpl( + name = name, getStorage = { storage.init .connect(this) .getCurrentWithEpoch(this) .first - .mapValuesParallel { (_, flow) -> flow.init.connect(this) } + .mapValues { (_, events) -> events.init.connect(this) } .asIterable() }, - getPatches = { - mapImpl({ init.connect(this) }) { patch -> - patch - .mapValuesParallel { (_, m) -> - m.map { flow -> flow.init.connect(this) } - } - .asIterable() - } - }, + getPatches = { patches }, storeFactory = ConcurrentHashMapK.Factory(), ) .awaitValues(), @@ -133,31 +82,31 @@ internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: ) } - private fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptInternal( - storage: TState<Map<K, TFlow<V>>> - ): TFlow<Map<K, V>> { - val name = "mergeIncrementallyPrompt" - return TFlowInit( + override fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( + initialEvents: DeferredValue<Map<K, Events<V>>>, + name: String?, + ): Events<Map<K, V>> { + val storage: State<Map<K, Events<V>>> = foldStateMapIncrementally(initialEvents) + val patches = + mapImpl({ init.connect(this) }) { patch, _ -> + patch + .mapValues { (_, m) -> m.map { events -> events.init.connect(this) } } + .asIterable() + } + return EventsInit( constInit( name, switchPromptImpl( + name = name, getStorage = { storage.init .connect(this) .getCurrentWithEpoch(this) .first - .mapValuesParallel { (_, flow) -> flow.init.connect(this) } + .mapValues { (_, events) -> events.init.connect(this) } .asIterable() }, - getPatches = { - mapImpl({ init.connect(this) }) { patch -> - patch - .mapValuesParallel { (_, m) -> - m.map { flow -> flow.init.connect(this) } - } - .asIterable() - } - }, + getPatches = { patches }, storeFactory = ConcurrentHashMapK.Factory(), ) .awaitValues(), @@ -165,109 +114,98 @@ internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: ) } - private fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKeyInternal( - init: FrpDeferredValue<Map<K, FrpStateful<B>>>, + override fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey( + init: DeferredValue<Map<K, Stateful<B>>>, numKeys: Int?, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> { - val eventsByKey: GroupedTFlow<K, Maybe<FrpStateful<A>>> = groupByKey(numKeys) - val initOut: Deferred<Map<K, B>> = deferAsync { - init.unwrapped.await().mapValuesParallel { (k, stateful) -> - val newEnd = with(frpScope) { eventsByKey[k].skipNext() } + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> { + val eventsByKey: GroupedEvents<K, Maybe<Stateful<A>>> = groupByKey(numKeys) + val initOut: Lazy<Map<K, B>> = deferAsync { + init.unwrapped.value.mapValues { (k, stateful) -> + val newEnd = eventsByKey[k] val newScope = childStateScope(newEnd) - newScope.runInStateScope(stateful) + newScope.stateful() } } - val changesNode: TFlowImpl<Map<K, Maybe<A>>> = - mapImpl( - upstream = { this@applyLatestStatefulForKeyInternal.init.connect(evalScope = this) } - ) { upstreamMap -> - upstreamMap.mapValuesParallel { (k: K, ma: Maybe<FrpStateful<A>>) -> + val changesNode: EventsImpl<Map<K, Maybe<A>>> = + mapImpl(upstream = { this@applyLatestStatefulForKey.init.connect(evalScope = this) }) { + upstreamMap, + _ -> + upstreamMap.mapValues { (k: K, ma: Maybe<Stateful<A>>) -> reenterStateScope(this@StateScopeImpl).run { ma.map { stateful -> - val newEnd = with(frpScope) { eventsByKey[k].skipNext() } + val newEnd = eventsByKey[k].skipNext() val newScope = childStateScope(newEnd) - newScope.runInStateScope(stateful) + newScope.stateful() } } } } val operatorName = "applyLatestStatefulForKey" val name = operatorName - val changes: TFlow<Map<K, Maybe<A>>> = TFlowInit(constInit(name, changesNode.cached())) - return changes to FrpDeferredValue(initOut) + val changes: Events<Map<K, Maybe<A>>> = EventsInit(constInit(name, changesNode.cached())) + return changes to DeferredValue(initOut) } - private fun <A> TFlow<FrpStateful<A>>.observeStatefulsInternal(): TFlow<A> { - val operatorName = "observeStatefuls" + override fun <A> Events<Stateful<A>>.applyStatefuls(): Events<A> { + val operatorName = "applyStatefuls" val name = operatorName - return TFlowInit( + return EventsInit( constInit( name, - mapImpl( - upstream = { this@observeStatefulsInternal.init.connect(evalScope = this) } - ) { stateful -> - reenterStateScope(outerScope = this@StateScopeImpl) - .runInStateScope(stateful) + mapImpl(upstream = { this@applyStatefuls.init.connect(evalScope = this) }) { + stateful, + _ -> + reenterStateScope(outerScope = this@StateScopeImpl).stateful() } .cached(), ) ) } - override val frpScope: FrpStateScope = FrpStateScopeImpl() - - private inner class FrpStateScopeImpl : - FrpStateScope, FrpTransactionScope by evalScope.frpScope { - - override fun <A> deferredStateScope( - block: suspend FrpStateScope.() -> A - ): FrpDeferredValue<A> = deferredInternal(block) - - override fun <A> TFlow<A>.holdDeferred(initialValue: FrpDeferredValue<A>): TState<A> = - toTStateDeferredInternal(initialValue) + override fun childStateScope(newEnd: Events<Any>) = + StateScopeImpl(evalScope, merge(newEnd, endSignal)) - override fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>> - ): TFlow<Map<K, V>> { - val storage: TState<Map<K, TFlow<V>>> = foldMapIncrementally(initialTFlows) - return mergeIncrementallyInternal(storage) + private fun <A> Events<A>.truncateToScope(operatorName: String): Events<A> = + if (endSignalOnce === emptyEvents) { + this + } else { + endSignalOnce + .mapCheap { emptyEvents } + .toStateInternal(operatorName, this) + .switchEvents() } - override fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly( - initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>> - ): TFlow<Map<K, V>> { - val storage: TState<Map<K, TFlow<V>>> = foldMapIncrementally(initialTFlows) - return mergeIncrementallyPromptInternal(storage) + private fun <A> Events<A>.nextOnlyInternal(operatorName: String): Events<A> = + if (this === emptyEvents) { + this + } else { + EventsLoop<A>().apply { + loopback = + mapCheap { emptyEvents } + .toStateInternal(operatorName, this@nextOnlyInternal) + .switchEvents() + } } - override fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey( - init: FrpDeferredValue<Map<K, FrpStateful<B>>>, - numKeys: Int?, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> = - applyLatestStatefulForKeyInternal(init, numKeys) - - override fun <A> TFlow<FrpStateful<A>>.applyStatefuls(): TFlow<A> = - observeStatefulsInternal() - } - - override suspend fun <R> runInStateScope(block: suspend FrpStateScope.() -> R): R { - val complete = CompletableDeferred<R>(parent = coroutineContext.job) - block.startCoroutine( - frpScope, - object : Continuation<R> { - override val context: CoroutineContext - get() = EmptyCoroutineContext + private fun <A> Events<A>.toStateInternal(operatorName: String, init: A): State<A> = + toStateInternalDeferred(operatorName, CompletableLazy(init)) - override fun resumeWith(result: Result<R>) { - complete.completeWith(result) - } - }, - ) - return complete.await() + private fun <A> Events<A>.toStateInternalDeferred( + operatorName: String, + init: Lazy<A>, + ): State<A> { + val changes = this@toStateInternalDeferred + val name = operatorName + val impl = + activatedStateSource( + name, + operatorName, + evalScope, + { changes.init.connect(evalScope = this) }, + init, + ) + return StateInit(constInit(name, impl)) } - - override fun childStateScope(newEnd: TFlow<Any>) = - StateScopeImpl(evalScope, merge(newEnd, endSignal)) } private fun EvalScope.reenterStateScope(outerScope: StateScopeImpl) = diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TransactionalImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TransactionalImpl.kt index 8647bdd5b7b1..13bd3b005871 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TransactionalImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TransactionalImpl.kt @@ -16,31 +16,25 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.internal.util.Key import com.android.systemui.kairos.internal.util.hashString -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Deferred internal sealed class TransactionalImpl<out A> { - data class Const<out A>(val value: Deferred<A>) : TransactionalImpl<A>() + data class Const<out A>(val value: Lazy<A>) : TransactionalImpl<A>() + + class Impl<A>(val block: EvalScope.() -> A) : TransactionalImpl<A>() { + val cache = TransactionCache<Lazy<A>>() - class Impl<A>(val block: suspend EvalScope.() -> A) : TransactionalImpl<A>(), Key<Deferred<A>> { override fun toString(): String = "${this::class.simpleName}@$hashString" } } @Suppress("NOTHING_TO_INLINE") -internal inline fun <A> transactionalImpl( - noinline block: suspend EvalScope.() -> A -): TransactionalImpl<A> = TransactionalImpl.Impl(block) +internal inline fun <A> transactionalImpl(noinline block: EvalScope.() -> A): TransactionalImpl<A> = + TransactionalImpl.Impl(block) -internal fun <A> TransactionalImpl<A>.sample(evalScope: EvalScope): Deferred<A> = +internal fun <A> TransactionalImpl<A>.sample(evalScope: EvalScope): Lazy<A> = when (this) { is TransactionalImpl.Const -> value is TransactionalImpl.Impl -> - evalScope.transactionStore - .getOrPut(this) { - evalScope.deferAsync(start = CoroutineStart.LAZY) { evalScope.block() } - } - .also { it.start() } + cache.getOrPut(evalScope) { evalScope.deferAsync { evalScope.block() } } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt index 33709a97da8f..4d183481898b 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt @@ -21,13 +21,14 @@ import com.android.systemui.kairos.util.None import com.android.systemui.kairos.util.just import java.util.concurrent.ConcurrentHashMap -internal interface Key<A> - private object NULL -internal class HeteroMap { +internal class HeteroMap private constructor(private val store: ConcurrentHashMap<Key<*>, Any>) { + interface Key<A> {} + + constructor() : this(ConcurrentHashMap()) - private val store = ConcurrentHashMap<Key<*>, Any>() + constructor(capacity: Int) : this(ConcurrentHashMap(capacity)) @Suppress("UNCHECKED_CAST") operator fun <A> get(key: Key<A>): Maybe<A> = diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/MapUtils.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/MapUtils.kt index ebf9a66be0ae..13f884666182 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/MapUtils.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/MapUtils.kt @@ -16,8 +16,6 @@ package com.android.systemui.kairos.internal.util -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.yield @@ -32,7 +30,7 @@ internal suspend inline fun <K, A, B : Any, M : MutableMap<K, B>> Map<K, A> destination.also { coroutineScope { mapValues { - async { + asyncImmediate { yield() block(it) } @@ -53,7 +51,7 @@ internal inline fun <K, A, B : Any, M : MutableMap<K, B>> Map<K, A>.mapValuesNot internal suspend fun <A, B> Iterable<A>.mapParallel(transform: suspend (A) -> B): List<B> = coroutineScope { - map { async(start = CoroutineStart.LAZY) { transform(it) } }.awaitAll() + map { asyncImmediate { transform(it) } }.awaitAll() } internal suspend fun <K, A, B, M : MutableMap<K, B>> Map<K, A>.mapValuesParallelTo( diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt index 6bb7f9f593aa..466a9f83b91f 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt @@ -18,8 +18,13 @@ package com.android.systemui.kairos.internal.util +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext +import kotlin.time.DurationUnit +import kotlin.time.measureTimedValue import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Deferred @@ -31,6 +36,62 @@ import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch import kotlinx.coroutines.newCoroutineContext +private const val LogEnabled = false + +@Suppress("NOTHING_TO_INLINE") +internal inline fun logLn(indent: Int = 0, message: Any?) { + if (!LogEnabled) return + log(indent, message) + println() +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun log(indent: Int = 0, message: Any?) { + if (!LogEnabled) return + printIndent(indent) + print(message) +} + +@JvmInline +internal value class LogIndent(val currentLogIndent: Int) { + @OptIn(ExperimentalContracts::class) + inline fun <R> logDuration(prefix: String, start: Boolean = true, block: LogIndent.() -> R): R { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return logDuration(currentLogIndent, prefix, start, block) + } + + @Suppress("NOTHING_TO_INLINE") + inline fun logLn(message: Any?) = logLn(currentLogIndent, message) +} + +@OptIn(ExperimentalContracts::class) +internal inline fun <R> logDuration( + indent: Int, + prefix: String, + start: Boolean = true, + block: LogIndent.() -> R, +): R { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + if (!LogEnabled) return LogIndent(0).block() + if (start) { + logLn(indent, prefix) + } + val (result, duration) = measureTimedValue { LogIndent(indent + 1).block() } + + printIndent(indent) + print(prefix) + print(": ") + println(duration.toString(DurationUnit.MICROSECONDS)) + return result +} + +@Suppress("NOTHING_TO_INLINE") +private inline fun printIndent(indent: Int) { + for (i in 0 until indent) { + print(" ") + } +} + internal fun <A> CoroutineScope.asyncImmediate( start: CoroutineStart = CoroutineStart.UNDISPATCHED, context: CoroutineContext = EmptyCoroutineContext, diff --git a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt index f3303f697fc9..287d8cb136c4 100644 --- a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt +++ b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt @@ -1,21 +1,3 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalFrpApi::class) - package com.android.systemui.kairos import com.android.systemui.kairos.util.Either @@ -56,11 +38,12 @@ import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Test +@OptIn(ExperimentalCoroutinesApi::class) class KairosTests { @Test fun basic() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() + val emitter = network.mutableEvents<Int>() var result: Int? = null activateSpec(network) { emitter.observe { result = it } } runCurrent() @@ -71,8 +54,8 @@ class KairosTests { } @Test - fun basicTFlow() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() + fun basicEvents() = runFrpTest { network -> + val emitter = network.mutableEvents<Int>() println("starting network") val result = activateSpecWithResult(network) { emitter.nextDeferred() } runCurrent() @@ -85,9 +68,9 @@ class KairosTests { } @Test - fun basicTState() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() - val result = activateSpecWithResult(network) { emitter.hold(0).stateChanges.nextDeferred() } + fun basicState() = runFrpTest { network -> + val emitter = network.mutableEvents<Int>() + val result = activateSpecWithResult(network) { emitter.holdState(0).changes.nextDeferred() } runCurrent() emitter.emit(3) @@ -111,7 +94,7 @@ class KairosTests { fun basicTransactional() = runFrpTest { network -> var value: Int? = null var bSource = 1 - val emitter = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() // Sampling this transactional will increment the source count. val transactional = transactionally { bSource++ } measureTime { @@ -150,24 +133,26 @@ class KairosTests { @Test fun diamondGraph() = runFrpTest { network -> - val flow = network.mutableTFlow<Int>() - val outFlow = + val flow = network.mutableEvents<Int>() + val ouevents = activateSpecWithResult(network) { - // map TFlow like we map Flow + // map Events like we map Flow val left = flow.map { "left" to it }.onEach { println("left: $it") } val right = flow.map { "right" to it }.onEach { println("right: $it") } - // convert TFlows to TStates so that they can be combined + // convert Eventss to States so that they can be combined val combined = - left.hold("left" to 0).combineWith(right.hold("right" to 0)) { l, r -> l to r } - combined.stateChanges // get TState changes + left.holdState("left" to 0).combineWith(right.holdState("right" to 0)) { l, r -> + l to r + } + combined.changes // get State changes .onEach { println("merged: $it") } .toSharedFlow() // convert back to Flow } runCurrent() val results = mutableListOf<Pair<Pair<String, Int>, Pair<String, Int>>>() - backgroundScope.launch { outFlow.toCollection(results) } + backgroundScope.launch { ouevents.toCollection(results) } runCurrent() flow.emit(1) @@ -186,19 +171,19 @@ class KairosTests { fun staticNetwork() = runFrpTest { network -> var finalSum: Int? = null - val intEmitter = network.mutableTFlow<Int>() - val sampleEmitter = network.mutableTFlow<Unit>() + val intEmitter = network.mutableEvents<Int>() + val sampleEmitter = network.mutableEvents<Unit>() activateSpecWithResult(network) { val updates = intEmitter.map { a -> { b: Int -> a + b } } val sumD = - TStateLoop<Int>().apply { + StateLoop<Int>().apply { loopback = updates .sample(this) { f, sum -> f(sum) } .onEach { println("sum update: $it") } - .hold(0) + .holdState(0) } sampleEmitter .onEach { println("sampleEmitter emitted") } @@ -228,10 +213,10 @@ class KairosTests { var wasSold = false var currentAmt: Int? = null - val coin = network.mutableTFlow<Unit>() + val coin = network.mutableEvents<Unit>() val price = 50 - val frpSpec = frpSpec { - val eSold = TFlowLoop<Unit>() + val buildSpec = buildSpec { + val eSold = EventsLoop<Unit>() val eInsert = coin.map { @@ -251,10 +236,10 @@ class KairosTests { val eUpdate = eInsert.mergeWith(eReset) { f, g -> { a -> g(f(a)) } } - val dTotal = TStateLoop<Int>() - dTotal.loopback = eUpdate.sample(dTotal) { f, total -> f(total) }.hold(price) + val dTotal = StateLoop<Int>() + dTotal.loopback = eUpdate.sample(dTotal) { f, total -> f(total) }.holdState(price) - val eAmt = dTotal.stateChanges + val eAmt = dTotal.changes val bAmt = transactionally { dTotal.sample() } eSold.loopback = coin @@ -269,7 +254,7 @@ class KairosTests { eSold.nextDeferred() } - activateSpec(network) { frpSpec.applySpec() } + activateSpec(network) { buildSpec.applySpec() } runCurrent() @@ -313,8 +298,8 @@ class KairosTests { @Test fun promptCleanup() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() - val stopper = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Int>() + val stopper = network.mutableEvents<Unit>() var result: Int? = null @@ -332,19 +317,19 @@ class KairosTests { } @Test - fun switchTFlow() = runFrpTest { network -> + fun switchEvents() = runFrpTest { network -> var currentSum: Int? = null - val switchHandler = network.mutableTFlow<Pair<TFlow<Int>, String>>() - val aHandler = network.mutableTFlow<Int>() - val stopHandler = network.mutableTFlow<Unit>() - val bHandler = network.mutableTFlow<Int>() + val switchHandler = network.mutableEvents<Pair<Events<Int>, String>>() + val aHandler = network.mutableEvents<Int>() + val stopHandler = network.mutableEvents<Unit>() + val bHandler = network.mutableEvents<Int>() val sumFlow = activateSpecWithResult(network) { - val switchE = TFlowLoop<TFlow<Int>>() + val switchE = EventsLoop<Events<Int>>() switchE.loopback = - switchHandler.mapStateful { (intFlow, name) -> + switchHandler.mapStateful { (inevents, name) -> println("[onEach] Switching to: $name") val nextSwitch = switchE.skipNext().onEach { println("[onEach] switched-out") } @@ -352,11 +337,11 @@ class KairosTests { stopHandler .onEach { println("[onEach] stopped") } .mergeWith(nextSwitch) { _, b -> b } - intFlow.takeUntil(stopEvent) + inevents.takeUntil(stopEvent) } - val adderE: TFlow<(Int) -> Int> = - switchE.hold(emptyTFlow).switch().map { a -> + val adderE: Events<(Int) -> Int> = + switchE.holdState(emptyEvents).switchEvents().map { a -> println("[onEach] new number $a") ({ sum: Int -> println("$a+$sum=${a + sum}") @@ -364,13 +349,13 @@ class KairosTests { }) } - val sumD = TStateLoop<Int>() + val sumD = StateLoop<Int>() sumD.loopback = adderE .sample(sumD) { f, sum -> f(sum) } .onEach { println("[onEach] writing sum: $it") } - .hold(0) - val sumE = sumD.stateChanges + .holdState(0) + val sumE = sumD.changes sumE.toSharedFlow() } @@ -494,16 +479,16 @@ class KairosTests { @Test fun switchIndirect() = runFrpTest { network -> - val emitter = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() activateSpec(network) { - emptyTFlow.map { emitter.map { 1 } }.flatten().map { "$it" }.observe() + emptyEvents.map { emitter.map { 1 } }.flatten().map { "$it" }.observe() } runCurrent() } @Test fun switchInWithResult() = runFrpTest { network -> - val emitter = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() val out = activateSpecWithResult(network) { emitter.map { emitter.map { 1 } }.flatten().toSharedFlow() @@ -519,11 +504,11 @@ class KairosTests { fun switchInCompleted() = runFrpTest { network -> val outputs = mutableListOf<Int>() - val switchAH = network.mutableTFlow<Unit>() - val intAH = network.mutableTFlow<Int>() - val stopEmitter = network.mutableTFlow<Unit>() + val switchAH = network.mutableEvents<Unit>() + val intAH = network.mutableEvents<Int>() + val stopEmitter = network.mutableEvents<Unit>() - val top = frpSpec { + val top = buildSpec { val intS = intAH.takeUntil(stopEmitter) val switched = switchAH.map { intS }.flatten() switched.toSharedFlow() @@ -555,13 +540,13 @@ class KairosTests { } @Test - fun switchTFlow_outerCompletesFirst() = runFrpTest { network -> + fun switchEvents_outerCompletesFirst() = runFrpTest { network -> var stepResult: Int? = null - val switchAH = network.mutableTFlow<Unit>() - val switchStopEmitter = network.mutableTFlow<Unit>() - val intStopEmitter = network.mutableTFlow<Unit>() - val intAH = network.mutableTFlow<Int>() + val switchAH = network.mutableEvents<Unit>() + val switchStopEmitter = network.mutableEvents<Unit>() + val intStopEmitter = network.mutableEvents<Unit>() + val intAH = network.mutableEvents<Int>() val flow = activateSpecWithResult(network) { val intS = intAH.takeUntil(intStopEmitter) @@ -609,8 +594,8 @@ class KairosTests { } @Test - fun mapTFlow() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() + fun mapEvents() = runFrpTest { network -> + val emitter = network.mutableEvents<Int>() var stepResult: Int? = null val flow = @@ -644,7 +629,7 @@ class KairosTests { var pullValue = 0 val a = transactionally { pullValue } val b = transactionally { a.sample() * 2 } - val emitter = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() val flow = activateSpecWithResult(network) { val sampleB = emitter.sample(b) { _, b -> b } @@ -668,14 +653,14 @@ class KairosTests { } @Test - fun mapTState() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() + fun mapState() = runFrpTest { network -> + val emitter = network.mutableEvents<Int>() var stepResult: Int? = null val flow = activateSpecWithResult(network) { - val state = emitter.hold(0).map { it + 2 } + val state = emitter.holdState(0).map { it + 2 } val stateCurrent = transactionally { state.sample() } - val stateChanges = state.stateChanges + val stateChanges = state.changes val sampleState = emitter.sample(stateCurrent) { _, b -> b } val merge = stateChanges.mergeWith(sampleState) { a, b -> a + b } merge.toSharedFlow() @@ -696,14 +681,14 @@ class KairosTests { @Test fun partitionEither() = runFrpTest { network -> - val emitter = network.mutableTFlow<Either<Int, Int>>() + val emitter = network.mutableEvents<Either<Int, Int>>() val result = activateSpecWithResult(network) { val (l, r) = emitter.partitionEither() val pDiamond = l.map { it * 2 } .mergeWith(r.map { it * -1 }) { _, _ -> error("unexpected coincidence") } - pDiamond.hold(null).toStateFlow() + pDiamond.holdState(null).toStateFlow() } runCurrent() @@ -719,15 +704,16 @@ class KairosTests { } @Test - fun accumTState() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() - val sampler = network.mutableTFlow<Unit>() + fun accumState() = runFrpTest { network -> + val emitter = network.mutableEvents<Int>() + val sampler = network.mutableEvents<Unit>() var stepResult: Int? = null val flow = activateSpecWithResult(network) { - val sumState = emitter.map { a -> { b: Int -> a + b } }.fold(0) { f, a -> f(a) } + val sumState = + emitter.map { a -> { b: Int -> a + b } }.foldState(0) { f, a -> f(a) } - sumState.stateChanges + sumState.changes .mergeWith(sampler.sample(sumState) { _, sum -> sum }) { _, _ -> error("Unexpected coincidence") } @@ -751,11 +737,11 @@ class KairosTests { } @Test - fun mergeTFlows() = runFrpTest { network -> - val first = network.mutableTFlow<Int>() - val stopFirst = network.mutableTFlow<Unit>() - val second = network.mutableTFlow<Int>() - val stopSecond = network.mutableTFlow<Unit>() + fun mergeEventss() = runFrpTest { network -> + val first = network.mutableEvents<Int>() + val stopFirst = network.mutableEvents<Unit>() + val second = network.mutableEvents<Int>() + val stopSecond = network.mutableEvents<Unit>() var stepResult: Int? = null val flow: SharedFlow<Int> @@ -832,19 +818,19 @@ class KairosTests { secondEmitDuration: ${secondEmitDuration.toString(DurationUnit.MILLISECONDS, 2)} stopFirstDuration: ${stopFirstDuration.toString(DurationUnit.MILLISECONDS, 2)} testDeadEmitFirstDuration: ${ - testDeadEmitFirstDuration.toString( - DurationUnit.MILLISECONDS, - 2, - ) - } + testDeadEmitFirstDuration.toString( + DurationUnit.MILLISECONDS, + 2, + ) + } secondEmitDuration2: ${secondEmitDuration2.toString(DurationUnit.MILLISECONDS, 2)} stopSecondDuration: ${stopSecondDuration.toString(DurationUnit.MILLISECONDS, 2)} testDeadEmitSecondDuration: ${ - testDeadEmitSecondDuration.toString( - DurationUnit.MILLISECONDS, - 2, - ) - } + testDeadEmitSecondDuration.toString( + DurationUnit.MILLISECONDS, + 2, + ) + } """ .trimIndent() ) @@ -852,10 +838,10 @@ class KairosTests { @Test fun sampleCancel() = runFrpTest { network -> - val updater = network.mutableTFlow<Int>() - val stopUpdater = network.mutableTFlow<Unit>() - val sampler = network.mutableTFlow<Unit>() - val stopSampler = network.mutableTFlow<Unit>() + val updater = network.mutableEvents<Int>() + val stopUpdater = network.mutableEvents<Unit>() + val sampler = network.mutableEvents<Unit>() + val stopSampler = network.mutableEvents<Unit>() var stepResult: Int? = null val flow = activateSpecWithResult(network) { @@ -863,7 +849,7 @@ class KairosTests { val samplerS = sampler.takeUntil(stopSamplerFirst) val stopUpdaterFirst = stopUpdater val updaterS = updater.takeUntil(stopUpdaterFirst) - val sampledS = samplerS.sample(updaterS.hold(0)) { _, b -> b } + val sampledS = samplerS.sample(updaterS.holdState(0)) { _, b -> b } sampledS.toSharedFlow() } @@ -894,35 +880,35 @@ class KairosTests { @Test fun combineStates_differentUpstreams() = runFrpTest { network -> - val a = network.mutableTFlow<Int>() - val b = network.mutableTFlow<Int>() + val a = network.mutableEvents<Int>() + val b = network.mutableEvents<Int>() var observed: Pair<Int, Int>? = null - val tState = + val state = activateSpecWithResult(network) { - val state = combine(a.hold(0), b.hold(0)) { a, b -> Pair(a, b) } - state.stateChanges.observe { observed = it } + val state = combine(a.holdState(0), b.holdState(0)) { a, b -> Pair(a, b) } + state.changes.observe { observed = it } state } - assertEquals(0 to 0, network.transact { tState.sample() }) + assertEquals(0 to 0, network.transact { state.sample() }) assertEquals(null, observed) a.emit(5) assertEquals(5 to 0, observed) - assertEquals(5 to 0, network.transact { tState.sample() }) + assertEquals(5 to 0, network.transact { state.sample() }) b.emit(3) assertEquals(5 to 3, observed) - assertEquals(5 to 3, network.transact { tState.sample() }) + assertEquals(5 to 3, network.transact { state.sample() }) } @Test fun sampleCombinedStates() = runFrpTest { network -> - val updater = network.mutableTFlow<Int>() - val emitter = network.mutableTFlow<Unit>() + val updater = network.mutableEvents<Int>() + val emitter = network.mutableEvents<Unit>() val result = activateSpecWithResult(network) { - val bA = updater.map { it * 2 }.hold(0) - val bB = updater.hold(0) - val combineD: TState<Pair<Int, Int>> = bA.combineWith(bB) { a, b -> a to b } + val bA = updater.map { it * 2 }.holdState(0) + val bB = updater.holdState(0) + val combineD: State<Pair<Int, Int>> = bA.combineWith(bB) { a, b -> a to b } val sampleS = emitter.sample(combineD) { _, b -> b } sampleS.nextDeferred() } @@ -943,13 +929,13 @@ class KairosTests { @Test fun switchMapPromptly() = runFrpTest { network -> - val emitter = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() val result = activateSpecWithResult(network) { emitter .map { emitter.map { 1 }.map { it + 1 }.map { it * 2 } } - .hold(emptyTFlow) - .switchPromptly() + .holdState(emptyEvents) + .switchEventsPromptly() .nextDeferred() } runCurrent() @@ -963,8 +949,8 @@ class KairosTests { @Test fun switchDeeper() = runFrpTest { network -> - val emitter = network.mutableTFlow<Unit>() - val e2 = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() + val e2 = network.mutableEvents<Unit>() val result = activateSpecWithResult(network) { val tres = @@ -989,14 +975,14 @@ class KairosTests { @Test fun recursionBasic() = runFrpTest { network -> - val add1 = network.mutableTFlow<Unit>() - val sub1 = network.mutableTFlow<Unit>() + val add1 = network.mutableEvents<Unit>() + val sub1 = network.mutableEvents<Unit>() val stepResult: StateFlow<Int> = activateSpecWithResult(network) { - val dSum = TStateLoop<Int>() + val dSum = StateLoop<Int>() val sAdd1 = add1.sample(dSum) { _, sum -> sum + 1 } val sMinus1 = sub1.sample(dSum) { _, sum -> sum - 1 } - dSum.loopback = sAdd1.mergeWith(sMinus1) { a, _ -> a }.hold(0) + dSum.loopback = sAdd1.mergeWith(sMinus1) { a, _ -> a }.holdState(0) dSum.toStateFlow() } runCurrent() @@ -1018,16 +1004,17 @@ class KairosTests { } @Test - fun recursiveTState() = runFrpTest { network -> - val e = network.mutableTFlow<Unit>() + fun recursiveState() = runFrpTest { network -> + val e = network.mutableEvents<Unit>() var changes = 0 val state = activateSpecWithResult(network) { - val s = TFlowLoop<Unit>() - val deferred = s.map { tStateOf(null) } - val e3 = e.map { tStateOf(Unit) } - val flattened = e3.mergeWith(deferred) { a, _ -> a }.hold(tStateOf(null)).flatten() - s.loopback = emptyTFlow + val s = EventsLoop<Unit>() + val deferred = s.map { stateOf(null) } + val e3 = e.map { stateOf(Unit) } + val flattened = + e3.mergeWith(deferred) { a, _ -> a }.holdState(stateOf(null)).flatten() + s.loopback = emptyEvents flattened.toStateFlow() } @@ -1037,7 +1024,7 @@ class KairosTests { @Test fun fanOut() = runFrpTest { network -> - val e = network.mutableTFlow<Map<String, Int>>() + val e = network.mutableEvents<Map<String, Int>>() val (fooFlow, barFlow) = activateSpecWithResult(network) { val selector = e.groupByKey() @@ -1073,15 +1060,15 @@ class KairosTests { @Test fun fanOutLateSubscribe() = runFrpTest { network -> - val e = network.mutableTFlow<Map<String, Int>>() + val e = network.mutableEvents<Map<String, Int>>() val barFlow = activateSpecWithResult(network) { val selector = e.groupByKey() selector .eventsForKey("foo") .map { selector.eventsForKey("bar") } - .hold(emptyTFlow) - .switchPromptly() + .holdState(emptyEvents) + .switchEventsPromptly() .toSharedFlow() } val stateFlow = barFlow.stateIn(backgroundScope, SharingStarted.Eagerly, null) @@ -1096,9 +1083,9 @@ class KairosTests { } @Test - fun inputFlowCompleted() = runFrpTest { network -> + fun inpueventsCompleted() = runFrpTest { network -> val results = mutableListOf<Int>() - val e = network.mutableTFlow<Int>() + val e = network.mutableEvents<Int>() activateSpec(network) { e.nextOnly().observe { results.add(it) } } runCurrent() @@ -1114,49 +1101,59 @@ class KairosTests { @Test fun fanOutThenMergeIncrementally() = runFrpTest { network -> - // A tflow of group updates, where a group is a tflow of child updates, where a child is a + // A events of group updates, where a group is a events of child updates, where a child is a // stateflow - val e = network.mutableTFlow<Map<Int, Maybe<TFlow<Map<Int, Maybe<StateFlow<String>>>>>>>() + val e = network.mutableEvents<Map<Int, Maybe<Events<Map<Int, Maybe<StateFlow<String>>>>>>>() println("fanOutMergeInc START") val state = activateSpecWithResult(network) { - // Convert nested Flows to nested TFlow/TState - val emitter: TFlow<Map<Int, Maybe<TFlow<Map<Int, Maybe<TState<String>>>>>>> = + // Convert nested Flows to nested Events/State + val emitter: Events<Map<Int, Maybe<Events<Map<Int, Maybe<State<String>>>>>>> = e.mapBuild { m -> m.mapValues { (_, mFlow) -> mFlow.map { it.mapBuild { m2 -> + println("m2: $m2") m2.mapValues { (_, mState) -> - mState.map { stateFlow -> stateFlow.toTState() } + mState.map { stateFlow -> stateFlow.toState() } } } } } } - // Accumulate all of our updates into a single TState - val accState: TState<Map<Int, Map<Int, String>>> = + // Accumulate all of our updates into a single State + val accState: State<Map<Int, Map<Int, String>>> = emitter .mapStateful { - changeMap: Map<Int, Maybe<TFlow<Map<Int, Maybe<TState<String>>>>>> -> + changeMap: Map<Int, Maybe<Events<Map<Int, Maybe<State<String>>>>>> -> changeMap.mapValues { (groupId, mGroupChanges) -> mGroupChanges.map { - groupChanges: TFlow<Map<Int, Maybe<TState<String>>>> -> + groupChanges: Events<Map<Int, Maybe<State<String>>>> -> // New group val childChangeById = groupChanges.groupByKey() - val map: TFlow<Map<Int, Maybe<TFlow<Maybe<TState<String>>>>>> = + val map: Events<Map<Int, Maybe<Events<Maybe<State<String>>>>>> = groupChanges.mapStateful { - gChangeMap: Map<Int, Maybe<TState<String>>> -> + gChangeMap: Map<Int, Maybe<State<String>>> -> + println("gChangeMap: $gChangeMap") gChangeMap.mapValues { (childId, mChild) -> - mChild.map { child: TState<String> -> + mChild.map { child: State<String> -> println("new child $childId in the house") // New child val eRemoved = childChangeById .eventsForKey(childId) .filter { it === None } - .nextOnly() + .onEach { + println( + "removing? (groupId=$groupId, childId=$childId)" + ) + } + .nextOnly( + name = + "eRemoved(groupId=$groupId, childId=$childId)" + ) - val addChild: TFlow<Maybe<TState<String>>> = + val addChild: Events<Maybe<State<String>>> = now.map { mChild } .onEach { println( @@ -1164,7 +1161,7 @@ class KairosTests { ) } - val removeChild: TFlow<Maybe<TState<String>>> = + val removeChild: Events<Maybe<State<String>>> = eRemoved .onEach { println( @@ -1173,23 +1170,28 @@ class KairosTests { } .map { none() } - addChild.mergeWith(removeChild) { _, _ -> + addChild.mergeWith( + removeChild, + name = + "childUpdatesMerged(groupId=$groupId, childId=$childId)", + ) { _, _ -> error("unexpected coincidence") } } } } - val mergeIncrementally: TFlow<Map<Int, Maybe<TState<String>>>> = + val mergeIncrementally: Events<Map<Int, Maybe<State<String>>>> = map.onEach { println("merge patch: $it") } - .mergeIncrementallyPromptly() + .mergeIncrementallyPromptly(name = "mergeIncrementally") mergeIncrementally - .onEach { println("patch: $it") } - .foldMapIncrementally() + .onEach { println("foldmap patch: $it") } + .foldStateMapIncrementally() .flatMap { it.combine() } } } } - .foldMapIncrementally() + .onEach { println("fold patch: $it") } + .foldStateMapIncrementally() .flatMap { it.combine() } accState.toStateFlow() @@ -1198,7 +1200,7 @@ class KairosTests { assertEquals(emptyMap(), state.value) - val emitter2 = network.mutableTFlow<Map<Int, Maybe<StateFlow<String>>>>() + val emitter2 = network.mutableEvents<Map<Int, Maybe<StateFlow<String>>>>() println() println("init outer 0") e.emit(mapOf(0 to just(emitter2.onEach { println("emitter2 emit: $it") }))) @@ -1233,6 +1235,10 @@ class KairosTests { assertEquals(mapOf(0 to mapOf(10 to "(2, 10)")), state.value) + // LogEnabled = true + + println("batch update") + // batch update emitter2.emit( mapOf( @@ -1248,13 +1254,13 @@ class KairosTests { @Test fun applyLatestNetworkChanges() = runFrpTest { network -> - val newCount = network.mutableTFlow<FrpSpec<Flow<Int>>>() + val newCount = network.mutableEvents<BuildSpec<Flow<Int>>>() val flowOfFlows: Flow<Flow<Int>> = activateSpecWithResult(network) { newCount.applyLatestSpec().toSharedFlow() } runCurrent() - val incCount = network.mutableTFlow<Unit>() - fun newFlow(): FrpSpec<SharedFlow<Int>> = frpSpec { + val incCount = network.mutableEvents<Unit>() + fun newFlow(): BuildSpec<SharedFlow<Int>> = buildSpec { launchEffect { try { println("new flow!") @@ -1263,16 +1269,16 @@ class KairosTests { println("cancelling old flow") } } - lateinit var count: TState<Int> + lateinit var count: State<Int> count = incCount .onEach { println("incrementing ${count.sample()}") } - .fold(0) { _, c -> c + 1 } - count.stateChanges.toSharedFlow() + .foldState(0) { _, c -> c + 1 } + count.changes.toSharedFlow() } var outerCount = 0 - val lastFlows: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> = + val laseventss: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> = flowOfFlows .map { it.stateIn(backgroundScope, SharingStarted.Eagerly, null) } .pairwise(MutableStateFlow(null)) @@ -1290,18 +1296,18 @@ class KairosTests { assertEquals(1, outerCount) // assertEquals(1, incCount.subscriptionCount) - assertNull(lastFlows.value.second.value) + assertNull(laseventss.value.second.value) incCount.emit(Unit) runCurrent() println("checking") - assertEquals(1, lastFlows.value.second.value) + assertEquals(1, laseventss.value.second.value) incCount.emit(Unit) runCurrent() - assertEquals(2, lastFlows.value.second.value) + assertEquals(2, laseventss.value.second.value) newCount.emit(newFlow()) runCurrent() @@ -1309,17 +1315,17 @@ class KairosTests { runCurrent() // verify old flow is not getting updates - assertEquals(2, lastFlows.value.first.value) + assertEquals(2, laseventss.value.first.value) // but the new one is - assertEquals(1, lastFlows.value.second.value) + assertEquals(1, laseventss.value.second.value) } @Test fun buildScope_stateAccumulation() = runFrpTest { network -> - val input = network.mutableTFlow<Unit>() + val input = network.mutableEvents<Unit>() var observedCount: Int? = null activateSpec(network) { - val (c, j) = asyncScope { input.fold(0) { _, x -> x + 1 } } + val (c, j) = asyncScope { input.foldState(0) { _, x -> x + 1 } } deferredBuildScopeAction { c.get().observe { observedCount = it } } } runCurrent() @@ -1336,7 +1342,7 @@ class KairosTests { @Test fun effect() = runFrpTest { network -> - val input = network.mutableTFlow<Unit>() + val input = network.mutableEvents<Unit>() var effectRunning = false var count = 0 activateSpec(network) { @@ -1348,7 +1354,7 @@ class KairosTests { effectRunning = false } } - merge(emptyTFlow, input.nextOnly()).observe { + merge(emptyEvents, input.nextOnly()).observe { count++ j.cancel() } @@ -1372,21 +1378,21 @@ class KairosTests { private fun runFrpTest( timeout: Duration = 3.seconds, - block: suspend TestScope.(FrpNetwork) -> Unit, + block: suspend TestScope.(KairosNetwork) -> Unit, ) { runTest(timeout = timeout) { - val network = backgroundScope.newFrpNetwork() + val network = backgroundScope.launchKairosNetwork() runCurrent() block(network) } } - private fun TestScope.activateSpec(network: FrpNetwork, spec: FrpSpec<*>) = + private fun TestScope.activateSpec(network: KairosNetwork, spec: BuildSpec<*>) = backgroundScope.launch { network.activateSpec(spec) } private suspend fun <R> TestScope.activateSpecWithResult( - network: FrpNetwork, - spec: FrpSpec<R>, + network: KairosNetwork, + spec: BuildSpec<R>, ): R = CompletableDeferred<R>() .apply { activateSpec(network) { complete(spec.applySpec()) } } |