perf: remove eager expandNode from ArrayView#5605
Open
Karavil wants to merge 1 commit intorocicorp:mainfrom
Open
perf: remove eager expandNode from ArrayView#5605Karavil wants to merge 1 commit intorocicorp:mainfrom
Karavil wants to merge 1 commit intorocicorp:mainfrom
Conversation
Remove the recursive eager expansion of relationship thunks that was introduced in rocicorp#5462. Instead of buffering expanded changes and applying them in batch at flush time, apply each change immediately in push() using the immutable applyChange. This preserves correctness (thunks are evaluated while source state is current) and eliminates the O(tree) recursive expansion overhead. The getChildNodes() helper in view-apply-change.ts already handles lazy thunks natively, so no changes are needed there. * Remove expandNode and expandChange functions * Remove mapValues import (only used by expandNode) * Remove change buffering (#pendingChanges array, #applyPendingChanges) * Apply changes immediately in push() via applyChange (immutable) * Update test: singular format error now throws at push time, not flush * All 2212 ZQL tests pass (132 files, 2 pre-existing skips)
|
Someone is attempting to deploy a commit to the Rocicorp Team on Vercel. A member of the Team first needs to authorize it. |
arv
approved these changes
Feb 25, 2026
Contributor
arv
left a comment
There was a problem hiding this comment.
LGTM
When looking at the previous PR I was also looking into removing the data getter but it turned out to be a larger change. I still believe we should get rid of that but in a separate PR.
Karavil
pushed a commit
to Karavil/mono
that referenced
this pull request
Feb 26, 2026
Adds comprehensive tests verifying that the immutable applyChange pipeline preserves object identity for unchanged nodes (enabling React.memo) while correctly bubbling new references up for changed descendants. NOTE: Several tests fail on main due to expandNode eagerly materializing all relationships (breaking identity). These pass with PR rocicorp#5605 which removes expandNode. The two PRs should be merged together. * ArrayView flat list: edit/add/remove preserve sibling identity * Relationship propagation: child edit/add/remove bubble new refs to parent * 3-level deep: grandchild edit bubbles through entire ancestor chain * React snapshot identity: getSnapshot stability, sentinel reuse * React.memo render counting: only changed row children re-render * Data flash prevention: no empty snapshots between data updates
3 tasks
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.
Problem
PR #5462 introduced
expandNode/expandChangeto support change buffering in ArrayView. These functions eagerly materialize ALL relationship thunks recursively before passing nodes toapplyChange. For queries with wide/deep relationships, this turned into the single most expensive operation in the IVM pipeline.Profiled on a page with 45 parent rows, ~200 related rows per parent, and 135 IVM pipelines (3 queries per parent row),
expandNodebecame the dominant cost at 60.7% inclusive time (10,672ms), a 4.3x regression over the pre-#5462 baseline.The root cause:
expandNoderecursively callsArray.from(skipYields(v()), expandNode)on every relationship key of every node, eagerly walking entire relationship trees. Combined withmapValues/mapEntriesobject allocation and generator overhead throughskipYields, this created O(tree_size * pipeline_count) work on everypush()and#hydrate()call.Profiled numbers (Chrome DevTools, React DEV mode)
Test scenario: page rendering 45 parent rows with ~200 related rows each, 135 IVM pipelines (3 queries per parent row).
Before (0.26-canary.8 with expandNode) vs After (this PR)
Function-level breakdown (inclusive time)
Self-time (leaf functions)
Production estimate
The
runfunction (React DEV mode scheduler) accounts for ~48% of total time but disappears in production builds. Subtracting dev-only overhead:Data flow: before vs after
The key insight:
getChildNodes()inview-apply-change.tsalready handles both expanded arrays AND lazy thunks natively. It usesArray.isArray(children)to distinguish the two cases and evaluates thunks on demand. The eagerexpandNodewas doing redundant work thatgetChildNodeswould do anyway, but recursively across the ENTIRE tree instead of just the nodes being changed.Fix
Remove
expandNode/expandChangeand the change buffering (#pendingChanges,#applyPendingChanges, auto-flush in.data). Apply each change immediately inpush()using the immutableapplyChange, exactly as the original code did before #5462 but preserving the immutable return semantics that #5462 introduced.This is safe because:
push()evaluates thunks immediately while source state is current. No stale captures.getChildNodes()handles lazy thunks natively. It usesArray.isArray(children)to distinguish expanded arrays from lazy generator thunks. No expansion needed.flush(), so the push/flush batching contract is maintained.applyChangepreserved. Returns new Entry objects, preserves identity for unchanged nodes.React.memo/ shallow comparison on view data still works correctly.Changes
expandNodeandexpandChangefunctionsmapValuesimport (only used byexpandNode)ExpandedNodetype import (no longer needed in array-view)#pendingChanges,#applyPendingChanges, auto-flush in.data)this.#root = applyChange(...)inpush()Test results
All 2,212 ZQL tests pass across 132 test files (2 pre-existing skips unrelated to this change).
Follows up on #5462.