Cleanroom reimplementation of the renderer tree walking#49
Open
maartenbreddels wants to merge 2 commits into
Open
Cleanroom reimplementation of the renderer tree walking#49maartenbreddels wants to merge 2 commits into
maartenbreddels wants to merge 2 commits into
Conversation
d0e21d6 to
6dde459
Compare
The default renderer is unchanged: _RenderContext keeps the original tree-walking (_render/_reconsolidate/_remove_element/_visit_children*), byte-for-byte. A _RenderContextFast(_RenderContext) subclass overrides only those methods with a reimplementation that does work proportional to what changed instead of a full tree walk: - state setters mark needs_render_descendant up the parent chain, so a render pass only descends into subtrees that can contain work; an identical, fully-reconciled, exception-free subtree is skipped in both phases (clean_subtree) and keeps its widgets; - force_update/update/first render walk fully (rc._walk_all); - stale-element removal runs once per context instead of an O(n) set-diff per element; - widget updates are skipped when an identical element reconciles to identical child widgets. Selected with REACTON_FAST=1 (via _render_context_class()); off by default. Side-effect (Layout/Style) widget tracking also moved to ipywidgets' on_widget_constructed hook (shared by both renderers) instead of a per-creation global-dict diff that was O(live widgets). benchmarks/ holds the harness, baseline-vs-fast result JSONs, and README.md documenting the design, the renderer contract both implementations satisfy, the deliberate test-invisible deltas, and known issues found during the work. On a 300-row tree the fast renderer takes a single-leaf update from 13.7ms to 0.62ms (22x) and a memoized-subtree skip from 14.4ms to 1.0ms (14x), behavior-identical to the default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a reacton-fast: ["0", "1"] matrix dimension so the suite runs with the default renderer and with the opt-in fast one (REACTON_FAST=1) on every Python version, keeping the two behavior-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
b7a5d2d to
4edf9c1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a faster renderer as an opt-in, off-by-default variant. The default
_RenderContextkeeps the original tree-walking unchanged (byte-for-byte with master); a_RenderContextFast(_RenderContext)subclass overrides only the tree-walking methods (_render,_reconsolidate,_remove_element,_visit_children,_visit_children_values). Select it with the env varREACTON_FAST=1.CI runs the full suite against both renderers (a
reacton-fast: ["0", "1"]matrix dimension on every Python version), so the two stay behavior-identical.Why
With the default renderer, any state update pays a full tree walk — a single-leaf update, a memoized-subtree "skip", and a full
force_updateall cost the same ~14ms on a 300-row tree — and stale-element removal is accidentally quadratic.How (the fast variant)
needs_render_descendantup the parent chain, so a render pass only descends into subtrees that can contain work. A component subtree whose element is identical to the previous render, fully reconciled, and free of dirty/excepted contexts is skipped in both phases and keeps its previous widgets (clean_subtree).force_update()/update()/first render walk fully (rc._walk_all).on_widget_constructedhook (shared by both renderers) instead of a per-creation global-dict diff that was O(live widgets).Results (300-row tree,
REACTON_FAST=1vs default)Harness (
bench.py/compare.py), result JSONs, the full renderer contract both implementations satisfy, the deliberate test-invisible deltas, and known issues found during the work are documented inbenchmarks/README.md.Verification
pytest reacton/ --ignore=reacton/generate_test.py→ 167 passed, 4 skipped.REACTON_FAST=1 pytest …→ same (167 passed, 4 skipped)._RenderContext's five tree-walking methods are byte-for-byte identical to master (AST-verified), so the default path carries zero behavioral risk; the fast path is exercised entirely by the opt-in CI matrix dimension.🤖 Generated with Claude Code