Skip to content

Reject unrelated paths early in determineChangedProps - #4103

Open
maoxor wants to merge 2 commits into
plotly:devfrom
maoxor:renderer-children-update-index
Open

maoxor wants to merge 2 commits into
plotly:devfrom
maoxor:renderer-children-update-index

Conversation

@maoxor

@maoxor maoxor commented Oct 30, 2026

Copy link
Copy Markdown

Addresses #4032 (it makes this much cheaper, but it's still a scan, so I'm leaving the issue open for you to decide).

Components whose type sets dashChildrenUpdate (dcc.Tabs, and several DMC types) walk all of layoutHashes on every store update, and isFirstLevelPropsChild split both paths for each entry. As the issue says, the cost is flagged components x changed paths x store updates.

This keeps the scan and makes it cheap:

  • isFirstLevelPropsChild rejects unrelated paths with startsWith before splitting anything, and returns a shared constant for them.
  • determineChangedProps walks Object.keys instead of building an entry pair for each path.

The selector returns the same thing as before.

Numbers

I added a benchmark scenario, callback_fanout_children_update: one Input driving N Outputs with T dcc.Tabs mounted. Median of 3 runs of python -m benchmarks.run, production bundle, headless Chrome on an Apple Silicon Mac:

Outputs Tabs dev this PR
300 1 28.7 ms 28.1 ms
300 50 67.0 ms 32.9 ms
300 200 245.7 ms 58.7 ms
1000 1 231.4 ms 225.3 ms
1000 50 339.7 ms 244.8 ms
1000 200 787.7 ms 281.6 ms

For reference, callback_fanout (300 outputs, no Tabs) is about 30 ms on both. So with 300 outputs and 200 Tabs, the extra cost of the Tabs goes from about 216 ms to about 29 ms. The 1000 x 1 row comes from 4 rounds alternating the two builds, because my first pass ran dev first and showed a 4% difference that turned out to be run order.

In the profile of the new scenario, isFirstLevelPropsChild + determineChangedProps go from 51% of CPU to 9%. The rest of the suite stays within thresholds. I didn't touch baseline.json: it comes from CI hardware, so I'd rather not mix my laptop's numbers into it. Happy to change that if you prefer.

Why not an index

The issue suggests indexing layoutHashes by parent, so I tried it in a micro benchmark. One store update, with 1000 entries:

Flagged components current scan prefix check index rebuilt when layoutHashes changes
1 0.54 ms 0.06 ms 1.10 ms
50 24.7 ms 2.3 ms 1.05 ms
200 99.4 ms 10.6 ms 1.05 ms

A rebuilt index wins with many flagged components, but it's about twice as slow as today with a single dcc.Tabs, which I'd guess is the most common case. The prefix check is never slower and adds no state. An index kept up to date in the reducer could get the best of both, if the remaining cost still matters for apps like the one in the issue. I noted this in .ai/PERFORMANCE.md.

Testing

  • New tests/selectors.test.js: 5 tests through selectDashProps. They also pass on the old code, since behavior shouldn't change. The sibling case (...,4 vs ...,40) fails if the segment boundary check is removed.
  • Differential check (not committed): the old and new determineChangedProps side by side over 3000 random store histories, with nested flagged paths and removed entries. Identical on 284160 calls.
  • Renderer unit tests: 129 passed. test_descendant_listening, test_redraw, test_render_type and the dcc tab tests pass.
  • tests/integration/renderer: 88 passed, 3 failed. test_rdps015, 016 and 017 fail the same way on dev on my machine (the input isn't cleared before typing, so the text becomes initialedited), so they aren't related to this.

Contributor Checklist

  • I have broken down my PR scope into the following TODO tasks
    • Reject unrelated paths before allocating in isFirstLevelPropsChild
    • Add a benchmark scenario and unit tests
  • 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

optionals

  • I have added entry in the CHANGELOG.md

A component whose type sets dashChildrenUpdate (dcc.Tabs, several DMC
types) walks all of layoutHashes on every store update, and
isFirstLevelPropsChild split both paths for each entry. With many such
components mounted, this was the top of the CPU profile.

Check the path prefix with startsWith before allocating anything, and
walk the keys instead of building an entry pair for each one. The result
of the selector does not change.

Add a benchmark scenario for this case and unit tests for the selector.
@sonarqubecloud

Copy link
Copy Markdown

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.

1 participant