-
Notifications
You must be signed in to change notification settings - Fork 105
1311:Use Compose's AndroidUiDispatcher.Main to conflate actions in runtime. #1353
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: sedwards/move-render-2
Are you sure you want to change the base?
1311:Use Compose's AndroidUiDispatcher.Main to conflate actions in runtime. #1353
Conversation
e6ae168
to
7f98d60
Compare
…ntime If it is not being used on Android, use it for the runtime whenever on Android (requiring Compose)
3276377
to
229688b
Compare
Is it basically a no-op, or actually a no-op? Because if it's not identical, we shouldn't treat it as optional. We already have so many confusing workflow flags, if we start requiring various configurations with caveats about things like dispatchers, the messiness just explodes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok so after reading, I'm still not sure I fully understand what the significant behavior difference is between using Compose's main dispatcher and Main.immediate.
Numbering these assertions/questions so you can respond more easily.
- When the conflation flag is off and multiple actions are available immediately – then with Compose's dispatcher they may result in multiple emissions to the UI layer with redundant updates, but all will still happen before the frame is drawn.
Main.immediate
will process them all immediately but also emit to the UI immediately, so effectively the same as far as the UI is concerned. But that was existing behavior. - When no actions are available, but another action is sent before the next Choreographer frame, then the action is still be guaranteed to be processed before the frame is drawn (either by
Main.immediate
's immediacy or by compose's pre-frame drain). Also existing behavior. - When the flag is on, then we avoid suspending when multiple actions are available, which is theoretically a performance win internally since not suspending is faster than suspending, even with
Main.immediate
?- Externally, the behavior is the same (only emit last conflated rendering to UI) even with
Main.immediate
because we already weren't emitting on the renderings flow until after the (previously suspending) conflation loop?
- Externally, the behavior is the same (only emit last conflated rendering to UI) even with
- If all these are true, then this change only affects behavior for multiple immediate actions when the conflation flag is off? Otherwise the we don't suspend in the conflation loop, so the choice of dispatcher doesn't matter.
@@ -118,58 +138,69 @@ class AndroidDispatchersRenderWorkflowInTest( | |||
val renderings = renderWorkflowIn( | |||
workflow = workflow, | |||
scope = backgroundScope + | |||
Dispatchers.Main.immediate, | |||
AndroidUiDispatcher.Main, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is the only dispatcher we're testing, is it now a hard requirement that apps using workflow UI on Android must use Compose? If so we should probably communicate that very clearly and update the docs too. If not, then can we test both?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And furthermore, does this imply the behavior broke for other dispatchers? If not, can we just add a parameter to the parameterization that specifies the dispatcher to use, so we can continue to validate it?
props = props, | ||
runtimeConfig = setOf(CONFLATE_STALE_RENDERINGS), | ||
runtimeConfig = runtime.runtimeConfig, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah cool, so this change is saying that the behavior this tests validates now happens for all configs, as long as we're using that dispatcher?
@@ -272,9 +277,17 @@ public fun <PropsT, OutputT, RenderingT> renderWorkflowIn( | |||
onOutput: suspend (OutputT) -> Unit = {} | |||
): StateFlow<RenderingT> { | |||
val restoredSnap = savedStateHandle?.get<PickledTreesnapshot>(KEY)?.snapshot | |||
|
|||
// Add in Compose's AndroidUiDispatcher.Main by default if none is specified. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add this new magical behavior to the kdoc as well.
actionResult = actionResult, | ||
) | ||
) { | ||
if (shouldShortCircuitForUnchangedState(actionResult = actionResult)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Do we really need this parameter to be named here? When the value passed in has the same name, it doesn't really add any readability.
val result = subtreeManager.applyNextAvailableChildAction() | ||
|
||
if (result == ActionsExhausted) { | ||
return eventActionsChannel.tryReceive().getOrNull()?.let { action -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please confirm if i understand this right: If we hit this branch, it means either no children had actions to run, or some child had an action but it didn't produce an output?
/** | ||
* Will try to apply any immediately available actions in this action queue or any of our | ||
* children's. | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* | |
* | |
* Contrast this to [onNextAction], which is used to await the next action when none is available right now. | |
* Both call [applyAction] when one is available. |
@@ -207,24 +206,35 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>( | |||
* a re-render, e.g. my state changed or a child state changed. | |||
* | |||
* It is an error to call this method after calling [cancel]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* It is an error to call this method after calling [cancel]. | |
* It is an error to call this method after calling [cancel]. | |
* | |
* Contrast this to [applyNextAvailableAction], which is used to check for an action | |
* that is already available without waiting, and then _immediately_ apply it. | |
* Both call [applyAction] when one is available. |
@@ -83,24 +80,25 @@ internal class WorkflowRunner<PropsT, OutputT, RenderingT>( | |||
* and resume (breaking ties with order of declaration). Guarantees only continuing on the winning | |||
* coroutine and no others. | |||
*/ | |||
@OptIn(WorkflowExperimentalRuntime::class) | |||
suspend fun processAction(waitForAnAction: Boolean = true): ActionProcessingResult { | |||
suspend fun waitAndProcessAction(): ActionProcessingResult { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Since you're renaming this anyway, "await" is the more idiomatic term for "listen while suspended". In contrast, "wait" canonically implies blocking a thread.
ActionsExhausted | ||
} | ||
} | ||
rootNode.onNextAction(this) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Follow-up to nit): Renaming this method would also make this code pretty much self-documenting and the above line comment redundant.
* none immediately available. | ||
*/ | ||
fun applyNextAvailableAction(): ActionProcessingResult { | ||
return rootNode.applyNextAvailableAction() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Maybe name this applyNextAvailableSubtreeAction
to make it more clear when reading callsites that this actually walks the tree? It took me a few read-throughs of the subtree/WN code to remember these were actually depth-first visitors methods (this and onNextAction
).
Note this also includes some adjustments to the runtime loop. After the first suspension to wait and apply an action, with
CONFLATE_STALE_RENDERINGS
we loop without suspending - applying any actions that are already queued up on the action queues.If it is not being used on Android, use it for the runtime whenever on Android (requiring Compose).
As it turns out, Compose's
AndroidUiDispatcher.Main
and its greedy behaviour to drain all dispatches before the next Android Choreographer frame gives us exactly the behaviour we want.In fact,
CONFLATE_STALE_RENDERINGS
is basically a no-op when using theAndroidUiDispatcher.Main
to dispatch the runtime and consume the renderings. The greediness means that the updates all happen before the rendering can be consumed by view code (as well as all happen before the next frame). Because this is not an immediate dispatcher, when we pass a new rendering to the stateflow to be consumed by the view code, it is not immediately picked up (it has to be dispatched, even though theAndroidUiDispatcher.Main
is going to do that greedily and before the next frame). This changes ordering.On Android context where we use this dispatcher, we can likely just not use
CONFLATE_STALE_RENDERINGS
at all. however, for other dispatchers, it could still have an effect. We will want to be careful with how we roll out this change.Still the code used to drain immediately available actions can be used by
DRAIN_EXCLUSIVE_ACTIONS
to avoid render passes.Fixes #1311
This should also take care of #1257 if we start using the Compose AndroidUiDispatcher.