Skip to content

Only reset descendant layout hashes when there is one to clear - #4124

Open
T4rk1n wants to merge 2 commits into
devfrom
fix/4121-reset-dispatch
Open

T4rk1n wants to merge 2 commits into
devfrom
fix/4121-reset-dispatch

Conversation

@T4rk1n

@T4rk1n T4rk1n commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #4121.

Since #3570, every wrapper in a re-rendered callback subtree dispatched resetComponentState from its _newRender effect, and each dispatch re-runs every mounted wrapper's selector. Re-rendering a subtree was O(n^2) in its size: 400 rows (1200 components) took 1219 store updates per click.

Renderer

  • resetDescendantHashes only dispatches the reset when a layoutHashes key is at or under the wrapper's path. With no hashes to clear the old dispatch only produced an equal copy of the state, so nothing changes except the extra notification.
  • The reducer returns the same object when nothing matches, and matches whole path segments: resetting children,1 no longer clears children,10.

Benchmarks

full_children_replace already showed this (p90 4.7 s, growth 16x) but was written off as "O(total) by design" with a 12 s fail ceiling. It is linear once fixed.

  • Tightened full_children_replace to warn 400 ms / fail 1500 ms.
  • Added a max_growth gate: fail when per-click cost grows faster than the workload. Linear is ~3x here, the bug was 16-20x.
  • Added rerender_existing_subtree: re-renders the same-size subtree each click, the case from the issue. The existing scenario mostly creates new components, which the [BUG] dbc Tabs not showing default first tab anymore with Dash 4.2.0 and higher #3929 first-render guard skips.
  • Removed the stale full_children_replace baseline entry. After merge, run the regenerate_baseline dispatch to measure both scenarios on CI.
before after
store updates per click (400 rows) 1219 19
full_children_replace p90 / growth 2733 ms / 20x 77 ms / 2.5x
rerender_existing_subtree p90 1232 ms 58 ms

Both scenarios fail on the old renderer and pass with the fix (local runs).

No overlap with #4055 or #3984. #4103 touches other sections of scenarios.py and .ai/PERFORMANCE.md; only the CHANGELOG [UNRELEASED] header will conflict.

Contributor Checklist

  • This PR targets dev (fix or chore) or next (feature, new API, breaking change), see Which branch to target
  • I have run the tests locally and they passed. (refer to testing section in contributing)
  • I have added tests, or extended existing tests, to cover any new features or bugs fixed in this PR
    • test_rdraw005 counts store updates per re-render (319 before, under 40 after)
    • layoutHashes.test.js covers the reducer's segment match and no-op case

optionals

  • I have added entry in the CHANGELOG.md

Every wrapper in a re-rendered callback subtree dispatched
resetComponentState, and each dispatch re-ran every wrapper's selector,
so updates were O(n^2) in the subtree size (#4121). Skip the dispatch
when no hash is under the path, and match whole path segments so
resetting children,1 no longer clears children,10.

The benchmark missed this because full_children_replace was written off
as slow by design with a 12 s ceiling. Tighten it, add a growth gate and
a rerender_existing_subtree scenario, and drop its stale baseline entry.
@sonarqubecloud

sonarqubecloud Bot commented Nov 6, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Dash performance benchmarks

✅ all within thresholds

scenario metric p90 (ms) median growth baseline p90 note
✅ callback_chain chain_ms 467.5 459.1 1.03x 499.1
✅ callback_chain graph_ms 2.4 2.4 1.0x 2.4
✅ callback_fanout fanout_ms 89.9 83.8 0.88x 92.5
✅ deep_nesting render_ms 54.6 51.4 0.95x 56.8
✅ full_children_replace replace_ms 184.4 105.6 3.49x -
✅ initial_render_large render_ms 630.4 566.2 0.91x 694.4
✅ initial_render_small render_ms 100.0 94.1 1.06x 104.0
✅ patch_append_nested append_ms 123.9 87.5 2.14x 192.3
✅ patch_append_toplevel append_ms 128.3 88.0 2.42x 140.2
✅ patch_scalar_update_large update_ms 159.7 143.5 0.85x 202.9
✅ rerender_existing_subtree rerender_ms 131.5 114.9 0.83x -
✅ wildcard_all_resolve wildcard_ms 320.9 306.8 0.98x 313.6
✅ wildcard_all_resolve graph_ms 0.9 0.9 1.0x 1.3

growth = late-third / early-third per-op time; ~1 is flat, a large value means the per-op cost scales with accumulated state.

machine scale vs baseline: 0.98x - divided out of the baseline ratios so they compare like for like (the absolute warn/fail ceilings are left un-scaled); calibrated on initial_render_small.

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.

[BUG] Since 4.2.0, callback-returned children dispatch one store update per descendant (O(N²)); not fixed by #3881

1 participant