Skip to content

Use WorkStealingDispatcher in runtime, behind a flag. #1365

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

Merged
merged 2 commits into from
Jul 23, 2025

Conversation

zach-klippenstein
Copy link
Collaborator

@zach-klippenstein zach-klippenstein commented Jul 9, 2025

Installs the WorkStealingDispatcher introduced in #1364 into the workflow runtime behind a new runtime flag.

@rjrjr
Copy link
Contributor

rjrjr commented Jul 9, 2025

So if this works, use it instead of adding the new Compose dependency?

@zach-klippenstein zach-klippenstein force-pushed the zachklipp/dispatcher-runtime branch from 65a8c7f to c150bc0 Compare July 9, 2025 00:49
@zach-klippenstein zach-klippenstein marked this pull request as ready for review July 9, 2025 00:52
@zach-klippenstein zach-klippenstein requested a review from a team as a code owner July 9, 2025 00:52
@workflow-pr-fixer workflow-pr-fixer bot requested a review from a team as a code owner July 9, 2025 00:53
@zach-klippenstein zach-klippenstein force-pushed the zachklipp/dispatcher-runtime branch from 9c0f379 to 26aab12 Compare July 9, 2025 01:00
@zach-klippenstein zach-klippenstein force-pushed the zachklipp/dispatcher-runtime branch from a2fe965 to ab23b7b Compare July 9, 2025 18:00
@zach-klippenstein zach-klippenstein force-pushed the zachklipp/dispatcher branch 4 times, most recently from e06b394 to 20e7b6b Compare July 9, 2025 18:59
@zach-klippenstein zach-klippenstein force-pushed the zachklipp/dispatcher-runtime branch from f6e81e9 to 468cef1 Compare July 9, 2025 19:40
@zach-klippenstein
Copy link
Collaborator Author

So if this works, use it instead of adding the new Compose dependency?

I would still like to get away from Unconfined if there's an opportunity to do so, but if this lets us move faster without shaving that yak, then awesome.

@zach-klippenstein zach-klippenstein marked this pull request as draft July 9, 2025 19:55
@zach-klippenstein
Copy link
Collaborator Author

zach-klippenstein commented Jul 9, 2025

Changed to draft while I write tests.

@@ -161,6 +169,8 @@ public enum class RuntimeConfigOptions {
// DRAIN_EXCLUSIVE_ACTIONS,
// )
// ),

ALL(RuntimeConfigOptions.entries.toSet())
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me like it would be a good idea to have a config that turns on all the flags (for testing, but also for green-field uses of the runtime), but if you'd rather I add explicit configs for this new flag I can do that too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment indicating the difference in intention between this and other enum entries that might happen to also contain all entries when they're introduced.

@@ -1494,7 +1501,9 @@ class RenderWorkflowInTest(

@Test
fun for_conflate_we_do_not_conflate_stacked_actions_into_one_rendering_if_output() {
if (runtimeConfig.contains(CONFLATE_STALE_RENDERINGS)) {
if (CONFLATE_STALE_RENDERINGS in runtimeConfig &&
WORK_STEALING_DISPATCHER !in runtimeConfig
Copy link
Collaborator Author

@zach-klippenstein zach-klippenstein Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tested behavior intentionally changes with WSD on.

@zach-klippenstein
Copy link
Collaborator Author

I added a single test for the new ordering behavior. I don't know if we need more than that since WorkStealingDispatcher itself is tested extensively in isolation. Happy to add more tests if anyone can think up any other interesting edge cases though.

@zach-klippenstein zach-klippenstein marked this pull request as ready for review July 10, 2025 00:42
Copy link
Contributor

@steve-the-edwards steve-the-edwards left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize the latest version of it is not merged yet - but where I would really like to test this is with the tests in AndroidDispatchersRenderWorkflowInTest workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/AndroidDispatchersRenderWorkflowInTest.kt

Currently in the branch from this PR - #1355

We could merge your WSD and then I could rebase and test it with that I guess?

@zach-klippenstein zach-klippenstein force-pushed the zachklipp/dispatcher branch 3 times, most recently from 7568824 to c660617 Compare July 11, 2025 19:03
@zach-klippenstein zach-klippenstein force-pushed the zachklipp/dispatcher branch 2 times, most recently from 446b1d8 to 9cb6cfe Compare July 14, 2025 14:01
@zach-klippenstein zach-klippenstein force-pushed the zachklipp/dispatcher-runtime branch from 68c7b69 to 13b4104 Compare July 14, 2025 14:47
// We advance the dispatcher first to allow any coroutines that were launched by the last
// render pass to start up and potentially enqueue actions.
dispatcher?.let {
workflowTracer.trace("AdvancingWorkflowDispatcher") {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@steve-the-edwards I just added this. I figure since this is intended to improve performance, might as well add a trace section to measure the new work preemptively.

@zach-klippenstein zach-klippenstein force-pushed the zachklipp/dispatcher-runtime branch from 13b4104 to 96b655a Compare July 14, 2025 14:51
@zach-klippenstein zach-klippenstein force-pushed the zachklipp/dispatcher-runtime branch from 96b655a to 095c5a1 Compare July 14, 2025 15:48
@zach-klippenstein zach-klippenstein force-pushed the zachklipp/dispatcher-runtime branch from 095c5a1 to d6512f6 Compare July 14, 2025 15:59
Base automatically changed from zachklipp/dispatcher to main July 14, 2025 17:56
@zach-klippenstein
Copy link
Collaborator Author

Ownership ended with @zach-klippenstein, @steve-the-edwards is this PR's owner now.

@steve-the-edwards steve-the-edwards force-pushed the zachklipp/dispatcher-runtime branch 2 times, most recently from 1f49159 to 94d3855 Compare July 22, 2025 20:11
# Conflicts:
#	workflow-core/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt
#	workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt
#	workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt
@steve-the-edwards steve-the-edwards force-pushed the zachklipp/dispatcher-runtime branch from 94d3855 to 28db854 Compare July 23, 2025 14:13
@@ -239,7 +239,7 @@ jobs:
uses: mikepenz/action-junit-report@5f47764eec0e1c1f19f40c8e60a5ba47e47015c5 # v4
if: always() # always run even if the previous step fails
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
report_paths: '**/build/test-results/jvmTest/TEST-*.xml'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was just a bug with the reporting paths that I found while looking at this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is report_paths?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what files it tries to pick up test results from to show in the PR directly. (this was just not working before).

- name: Check with Gradle
uses: ./.github/actions/gradle-task
with:
task: jvmTest --continue -Pworkflow.runtime=all
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't test the work stealing dispatcher combinations separately, just adding the 'all' config here for the jvm tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which should be fine b/c we plan to ship that way?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'eventually', yes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but good point that we should make sure each combination we roll out with has been tested here.

@@ -578,7 +599,7 @@ jobs:
### <start-connected-check-shards>
shardNum: [ 1, 2, 3 ]
### <end-connected-check-shards>
runtime: [ conflate, stateChange, drainExclusive, conflate-stateChange, partial, conflate-partial, stable, conflate-drainExclusive, stateChange-drainExclusive, partial-drainExclusive, conflate-partial-drainExclusive ]
runtime: [ conflate, stateChange, drainExclusive, conflate-stateChange, partial, conflate-partial, stable, conflate-drainExclusive, stateChange-drainExclusive, partial-drainExclusive, conflate-partial-drainExclusive, all ]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add 'all' the runtime test shard.

* Note that this is *not effective* when using an immediate or unconfined dispatcher as the
* handling of an action happens synchronously after the coroutine sending it into the sink
* is resumed. This means it never dispatches multiple coroutines that send actions into the sink
* before handling the first, so there is never multiple actions to apply!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added notes here about how CSR and DEA won't work with an immediate or unconfined dispatcher.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But WORK_STEALING_DISPATCHER fixes that, right? If so should say so. If not, what's our actual plan? Seems like we should suggest to the reader whatever it is that we're planning to do in POS.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it does not actually fix that. We thought it might until we realized that immediate dispatchers prevent the dispatch of the coroutines for the other actions from happening, so there is nothing to drain after processing just the first action.

The way to fix that would be to use a non immediate dispatcher - e.g. Dispatchers.Main - along with a frame clock listener that explicitly calls advanceUntilIdle() for each frame so that we can maintain the guarantee of stably updating the runtime each frame.

I implemented that and tested it here with these tests, but in discussion with @zach-klippenstein we decided that it was not helpful.

  1. If you are using Compose, then you should use AndroidUiDispatcher.Main for this, otherwise the timing of the two frame callbacks might not be right.
  2. If you are not using Compose, we don't really know this and we are OK with just not being able to use CSR or DEA.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated it to say this:

   * Note further that [WORK_STEALING_DISPATCHER] does not fix this for immediate dispatchers. In
   * order to use this optimization, a non-immediate dispatcher must be used. If using a non-
   * immediate dispatcher, we recommend that you ensure that the Workflow runtime completes all
   * known updates before the next 'frame.' This can be done using a dispatcher that is integrated
   * with the frame clock on your platform. E.g., on Android we recommend using Compose UI's
   * `AndroidUiDispatcher.Main` in order to access these optimizations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes me think we should just drop CSR then, since it's already pretty redundant with what's built into ComposeView et al.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought so at first too, but if the view runner work is not cancellable it does save view runner work by producing fewer renderings when they are stale.

@@ -239,7 +239,7 @@ jobs:
uses: mikepenz/action-junit-report@5f47764eec0e1c1f19f40c8e60a5ba47e47015c5 # v4
if: always() # always run even if the previous step fails
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
report_paths: '**/build/test-results/jvmTest/TEST-*.xml'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is report_paths?

- name: Check with Gradle
uses: ./.github/actions/gradle-task
with:
task: jvmTest --continue -Pworkflow.runtime=all
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which should be fine b/c we plan to ship that way?

* Note that this is *not effective* when using an immediate or unconfined dispatcher as the
* handling of an action happens synchronously after the coroutine sending it into the sink
* is resumed. This means it never dispatches multiple coroutines that send actions into the sink
* before handling the first, so there is never multiple actions to apply!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But WORK_STEALING_DISPATCHER fixes that, right? If so should say so. If not, what's our actual plan? Seems like we should suggest to the reader whatever it is that we're planning to do in POS.

@@ -98,69 +115,349 @@ public enum class RuntimeConfigOptions {
enum class RuntimeOptions(
val runtimeConfig: RuntimeConfig
) {
DEFAULT(RENDER_PER_ACTION),
NONE(RENDER_PER_ACTION),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holy combinatorial explosion.

@steve-the-edwards steve-the-edwards force-pushed the zachklipp/dispatcher-runtime branch from 28db854 to d9bbcff Compare July 23, 2025 15:13
@steve-the-edwards steve-the-edwards merged commit 96997a2 into main Jul 23, 2025
130 of 134 checks passed
@steve-the-edwards steve-the-edwards deleted the zachklipp/dispatcher-runtime branch July 23, 2025 16:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants