Skip to content

Compose runtime: refresh workflow implementation when its instance changes [CLF-654] - #1591

Merged
zach-klippenstein merged 3 commits into
zachklipp/clf-646-cache-render-contextfrom
zachklipp/clf-654-compose-runtime-refresh-workflow-instance
Oct 29, 2026
Merged

zach-klippenstein merged 3 commits into
zachklipp/clf-646-cache-render-contextfrom
zachklipp/clf-654-compose-runtime-refresh-workflow-instance

Conversation

@zach-klippenstein

@zach-klippenstein zach-klippenstein commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #1588, which should merge first. After rebasing, the cached renderContext from #1588 stays as it is when the workflow instance changes: RenderContext(this, interceptedWorkflow) only uses the workflow argument for type inference and wraps nothing but this. The benchmark comparison in the comments was measured against main, before this rebase.

Fixes a finding from review of #1442 (comment).

ComposeRenderContext is remembered per workflow identity, but it kept the first workflow and interceptedWorkflow it was created with. When a parent rendered a new instance of the same workflow type and key, the compose runtime still called render, onPropsChanged and snapshotState on the old instance, so it could hold on to stale constructor dependencies. The legacy runtime (StatefulWorkflowNode.maybeUpdateCachedWorkflowInstance()) switches to the new instance, re-intercepts it, and keeps the session's state.

Tests (first commit)

Added to RenderWorkflowInTest. In each one, the parent renders a new child instance on every render pass:

  • new_child_workflow_instance_is_used_to_render_without_resetting_state (ported from Add tests for Compose runtime review feedback [CLF-524] #1577): render uses the new instance, and the child keeps its state.
  • new_child_workflow_instance_is_used_for_onPropsChanged: onPropsChanged is called on the new instance.
  • new_child_workflow_instance_is_used_for_snapshotState: a snapshot saved after the instance changed was written by the new instance.
  • interceptor_renders_new_child_workflow_instance: an interceptor's proceed renders the new instance.

Before the fix, all 4 failed on COMPOSE_RUNTIME_NON_SKIPPING and COMPOSE_RUNTIME_SKIPPING (for example expected:<v[1] props=1 state=1> but was:<v[0] props=1 state=1>), and passed on all 64 legacy runtime configs.

Fix (second commit)

renderWorkflow now calls ComposeRenderContext.updateWorkflow(workflow) on every pass. If the instance changed (identity check), it stores the new instance and re-intercepts it. Session state is left alone. The call happens before the skip check, so a child that skips this pass still uses the new instance for its next render and its snapshots.

With the fix, all :workflow-runtime:jvmTest tests pass.

🤖 Generated with Claude Code

@zach-klippenstein

Copy link
Copy Markdown
Collaborator Author

🤖 Compose runtime microbenchmarks, main (0cf0569) vs this branch (0b4d885)

I ran :benchmarks:runtime-microbenchmark:connectedReleaseAndroidTest on a Galaxy A25 (SM-A256U1, Android 16), plugged in, thermal status 0. Only the 4 compose classes ran ({ShallowBushyTree, SquareishTree} × {ComposeNoSkip, ComposeSkipping}), so there's no AllOptimizations control group in this run. Each branch got one back-to-back run, main first.

Allocations: unchanged in every benchmark. The ±1 differences are median noise. The fix adds an identity check per child render and allocates only when the instance actually changes, which none of these benchmarks do.

Time: geometric mean of branch/main median time.

Config Geomean Δ time Range
ShallowBushyTree_ComposeNoSkip -0.7% -6.5% … +8.2%
ShallowBushyTree_ComposeSkipping +0.7% -1.9% … +2.9%
SquareishTree_ComposeNoSkip +0.8% -4.4% … +5.1%
SquareishTree_ComposeSkipping +3.3% -2.1% … +10.6%
All +1.0%

Deltas go both ways within each config. SquareishTree_ComposeSkipping leans slower. Allocations are identical, and there's no control group to show whether the phone drifted between the two runs, so I can't tell that apart from noise in a single run.

Full results (median time and allocation count)
Config Benchmark main time (ms) branch time (ms) Δ time main allocs branch allocs Δ allocs
ShallowBushyTree_ComposeNoSkip initialRenderAllChildren 2.230 2.236 +0.3% 10864 10863 -0.0%
ShallowBushyTree_ComposeNoSkip initialRenderNewSibling 0.741 0.725 -2.1% 2582 2582 +0.0%
ShallowBushyTree_ComposeNoSkip rememberManyEntries_changingInputs 0.503 0.496 -1.3% 2865 2865 -0.0%
ShallowBushyTree_ComposeNoSkip rememberManyEntries_mixedInputTypes 0.542 0.550 +1.5% 3241 3241 +0.0%
ShallowBushyTree_ComposeNoSkip rememberManyEntries_sameInputs 0.419 0.414 -1.0% 2733 2733 +0.0%
ShallowBushyTree_ComposeNoSkip rerenderAllChildrenByPropsChange 0.587 0.635 +8.1% 1931 1931 -0.0%
ShallowBushyTree_ComposeNoSkip rerenderAllChildrenViaStateChange 0.530 0.524 -1.1% 1828 1828 -0.0%
ShallowBushyTree_ComposeNoSkip rerenderSingleSiblingByPropsChange 0.500 0.503 +0.6% 1914 1914 +0.0%
ShallowBushyTree_ComposeNoSkip rerenderSingleSiblingViaStateChange 0.472 0.472 +0.0% 1820 1820 -0.0%
ShallowBushyTree_ComposeNoSkip stableHandlers_manyCallbacks_propChange 0.406 0.411 +1.2% 2733 2733 +0.0%
ShallowBushyTree_ComposeNoSkip tearDownAllChildren 1.699 1.589 -6.5% 5036 5036 +0.0%
ShallowBushyTree_ComposeNoSkip tearDownSingleSibling 1.304 1.304 +0.1% 2674 2674 -0.0%
ShallowBushyTree_ComposeNoSkip wideSiblingKeys_initialRenderAllChildren 3.333 3.215 -3.6% 15855 15855 +0.0%
ShallowBushyTree_ComposeNoSkip wideSiblingKeys_rerenderSingleSiblingByPropsChange 0.586 0.557 -4.9% 1533 1533 +0.0%
ShallowBushyTree_ComposeSkipping initialRenderAllChildren 2.420 2.402 -0.8% 11619 11620 +0.0%
ShallowBushyTree_ComposeSkipping initialRenderNewSibling 0.533 0.534 +0.2% 1190 1190 +0.0%
ShallowBushyTree_ComposeSkipping rememberManyEntries_changingInputs 0.501 0.511 +2.1% 2867 2867 -0.0%
ShallowBushyTree_ComposeSkipping rememberManyEntries_mixedInputTypes 0.553 0.548 -0.8% 3243 3243 -0.0%
ShallowBushyTree_ComposeSkipping rememberManyEntries_sameInputs 0.443 0.444 +0.3% 2743 2743 -0.0%
ShallowBushyTree_ComposeSkipping rerenderAllChildrenByPropsChange 0.667 0.679 +1.8% 2085 2085 +0.0%
ShallowBushyTree_ComposeSkipping rerenderAllChildrenViaStateChange 0.639 0.652 +2.0% 2063 2063 +0.0%
ShallowBushyTree_ComposeSkipping rerenderSingleSiblingByPropsChange 0.288 0.286 -0.9% 514 514 +0.0%
ShallowBushyTree_ComposeSkipping rerenderSingleSiblingViaStateChange 0.251 0.257 +2.2% 423 423 +0.0%
ShallowBushyTree_ComposeSkipping stableHandlers_manyCallbacks_propChange 0.431 0.435 +0.9% 2743 2743 +0.0%
ShallowBushyTree_ComposeSkipping tearDownAllChildren 1.820 1.827 +0.4% 5041 5041 +0.0%
ShallowBushyTree_ComposeSkipping tearDownSingleSibling 1.162 1.180 +1.5% 1275 1275 -0.0%
ShallowBushyTree_ComposeSkipping wideSiblingKeys_initialRenderAllChildren 4.766 4.675 -1.9% 18358 18359 +0.0%
ShallowBushyTree_ComposeSkipping wideSiblingKeys_rerenderSingleSiblingByPropsChange 0.655 0.674 +3.0% 1296 1296 -0.0%
SquareishTree_ComposeNoSkip initialRenderAllChildren 10.707 10.512 -1.8% 40446 40446 +0.0%
SquareishTree_ComposeNoSkip initialRenderNewSibling 2.562 2.615 +2.1% 8586 8586 +0.0%
SquareishTree_ComposeNoSkip rememberManyEntries_changingInputs 0.504 0.528 +4.8% 2865 2865 +0.0%
SquareishTree_ComposeNoSkip rememberManyEntries_mixedInputTypes 0.554 0.535 -3.5% 3241 3241 -0.0%
SquareishTree_ComposeNoSkip rememberManyEntries_sameInputs 0.427 0.428 +0.3% 2733 2733 +0.0%
SquareishTree_ComposeNoSkip rerenderAllChildrenByPropsChange 2.556 2.655 +3.9% 8453 8453 -0.0%
SquareishTree_ComposeNoSkip rerenderAllChildrenViaStateChange 2.311 2.320 +0.4% 8344 8344 -0.0%
SquareishTree_ComposeNoSkip rerenderSingleSiblingByPropsChange 2.361 2.353 -0.3% 8446 8447 +0.0%
SquareishTree_ComposeNoSkip rerenderSingleSiblingViaStateChange 2.141 2.129 -0.6% 8331 8331 -0.0%
SquareishTree_ComposeNoSkip stableHandlers_manyCallbacks_propChange 0.418 0.422 +1.0% 2733 2733 -0.0%
SquareishTree_ComposeNoSkip tearDownAllChildren 6.895 7.017 +1.8% 25351 25350 -0.0%
SquareishTree_ComposeNoSkip tearDownSingleSibling 2.664 2.799 +5.1% 8535 8536 +0.0%
SquareishTree_ComposeNoSkip wideSiblingKeys_initialRenderAllChildren 3.216 3.321 +3.3% 15855 15855 -0.0%
SquareishTree_ComposeNoSkip wideSiblingKeys_rerenderSingleSiblingByPropsChange 0.617 0.590 -4.4% 1533 1533 +0.0%
SquareishTree_ComposeSkipping initialRenderAllChildren 11.427 11.719 +2.6% 43122 43122 -0.0%
SquareishTree_ComposeSkipping initialRenderNewSibling 1.941 1.977 +1.9% 4243 4244 +0.0%
SquareishTree_ComposeSkipping rememberManyEntries_changingInputs 0.520 0.521 +0.2% 2867 2867 -0.0%
SquareishTree_ComposeSkipping rememberManyEntries_mixedInputTypes 0.558 0.602 +7.8% 3243 3243 +0.0%
SquareishTree_ComposeSkipping rememberManyEntries_sameInputs 0.446 0.465 +4.3% 2743 2743 -0.0%
SquareishTree_ComposeSkipping rerenderAllChildrenByPropsChange 3.081 3.196 +3.7% 9184 9184 -0.0%
SquareishTree_ComposeSkipping rerenderAllChildrenViaStateChange 2.950 2.963 +0.4% 9444 9444 -0.0%
SquareishTree_ComposeSkipping rerenderSingleSiblingByPropsChange 1.698 1.689 -0.5% 4095 4095 -0.0%
SquareishTree_ComposeSkipping rerenderSingleSiblingViaStateChange 0.245 0.271 +10.5% 336 336 -0.0%
SquareishTree_ComposeSkipping stableHandlers_manyCallbacks_propChange 0.439 0.460 +4.6% 2743 2743 -0.0%
SquareishTree_ComposeSkipping tearDownAllChildren 7.381 7.692 +4.2% 26324 26325 +0.0%
SquareishTree_ComposeSkipping tearDownSingleSibling 2.097 2.053 -2.1% 4185 4185 +0.0%
SquareishTree_ComposeSkipping wideSiblingKeys_initialRenderAllChildren 4.743 5.023 +5.9% 18358 18358 -0.0%
SquareishTree_ComposeSkipping wideSiblingKeys_rerenderSingleSiblingByPropsChange 0.686 0.706 +2.9% 1296 1296 +0.0%

zach-klippenstein and others added 2 commits October 28, 2026 16:15
When a parent renders a new instance of the same workflow type and key, the
compose runtime keeps using the first instance for render, onPropsChanged,
snapshotState and interceptors. The legacy runtime switches to the new instance
and keeps the session's state.

The render test is ported from #1577.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ew one

ComposeRenderContext now swaps in the latest workflow instance, and
re-intercepts it, whenever the parent passes a different instance of the
same workflow. Session state stays as it was, which matches
StatefulWorkflowNode.maybeUpdateCachedWorkflowInstance in the traditional
runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zach-klippenstein
zach-klippenstein force-pushed the zachklipp/clf-654-compose-runtime-refresh-workflow-instance branch from 0b4d885 to 9172d05 Compare October 28, 2026 23:16
@zach-klippenstein
zach-klippenstein changed the base branch from main to zachklipp/clf-646-cache-render-context October 28, 2026 23:16
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zach-klippenstein
zach-klippenstein marked this pull request as ready for review October 28, 2026 23:25
@zach-klippenstein
zach-klippenstein requested a review from a team as a code owner October 28, 2026 23:25
@zach-klippenstein
zach-klippenstein merged commit bafbfbf into zachklipp/clf-646-cache-render-context Oct 29, 2026
73 checks passed
@zach-klippenstein
zach-klippenstein deleted the zachklipp/clf-654-compose-runtime-refresh-workflow-instance branch October 29, 2026 02:15
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.

2 participants