Repository navigation
Conversation
Blocks, connections and groups containers keep their children in a world-space TileIndex. Each frame a container walks the children whose bounds intersect the camera viewport (plus a margin), children that requested a render, newly appended children and children that just left the view, in the same zIndex/renderOrder order as a full walk. It falls back to a full walk when most children are near the viewport. - performRender() marks the branch pending in culling ancestors, so pending props/state, first iterate and anchor updates of off-screen components are still processed in the next frame. - renderOrder is assigned to every child when the z-order is rebuilt, and isIterated() is true for children skipped by culling, so point hit tests keep today's results. - EXP_VISIBILITY_CULLING setting (default true), VISIBILITY_TILE_SIZE and VISIBILITY_CULLING_MARGIN constants. - Fix Tree.updateZIndex leaving the node in its previous zIndex group (a raised block was walked and drawn twice) and removeZIndex keeping removed nodes in the cached order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lets GraphStressTest and NirvanaMaxGraphTest be compared with EXP_VISIBILITY_CULLING on and off on the preview stand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Prepare only new and requested nodes before painting, then publish viewport visibility incrementally: a camera move rechecks nodes in the strips between the previous and current viewport plus nodes with new bounds, and a structural change runs one complete selection. A single flat pass over the cached paint order visits published and prepared nodes; clean components update inside their own iterate(). This removes the per-frame preparation pass, linear selection and the dense/sparse paint paths that made zoomed-out views slower than v2. Connections compute geometry once per real input change instead of up to three times on mount. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A culled layer root now runs each frame in three steps owned by TreeCulling: new and requested nodes prepare, viewport visibility is published incrementally, and one flat pass over the cached paint order calls iterate(prepared, visible) only for visible and prepared nodes. - ITree.iterate(prepared, visible) replaces the per-node viewport callbacks; components veto painting through isRenderAllowed(). - Coordinator node sets are arrays with slots stored on the nodes, and the paint order marks eligible nodes for publication. - Tree.setChildren() with unchanged children no longer invalidates the scope. - Connection batches keep base-sized chunks in registration order and skip chunks outside the viewport; connections cache line and arrow paths. Tree and Component internals change without compatibility shims: ITree.iterate and the tree iterator take (prepared, visible); onViewportChange, isSkippedByCulling, getWalkToken, preparesOnIterate, invalidateVisibility, getChildrenArray, activeInViewport and Tree.setVisible are removed; override isRenderAllowed() instead of isVisible(); batch items return cached geometry from getPath() and report getPathBounds(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
node scripts/bench/culling/run.mjs builds several versions of packages/graph (git refs or the working tree) and measures GraphLayer update time while panning and zooming the Nirvana and Playground stress scenes, plus the first two frames after mounting, and compares their screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Preview is ready. |
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.
Why
#428 is fast when zoomed in but slower than v2 when zoomed out (scales 0.01–0.03), most of all on connection-heavy scenes such as Nirvana Max Graph, and on the first frames after mounting. The costs that grew with the number of visible nodes:
updatePointsrunning three times per connection on mount.Behavior and architecture
Each culled layer root has one
TreeCullingcoordinator that runs a frame in three steps:prepareUpdate(), ancestors first. Clean nodes are not touched.TileIndexonce per frame, right before publication.iterate(prepared, visible)only for visible and already prepared nodes. A clean visible node applies its own pending work insideiterate(). Clean offscreen nodes get no call; their visible descendants are still reached through them.A frame therefore costs the visible nodes plus the work that changed.
iterate(). A component can only veto painting throughisRenderAllowed()(hidden blocks, connections without geometry).isVisible()combines that veto withisInViewport()for queries.isEligible()per node.Tree.setChildren()with unchanged children no longer invalidates the scope. Before this,Block.updateChildren()returned a new anchor array on every update and rebuilt the whole scope's structure, including on every drag frame.isInViewport()inafterRender.Internal API changes
Compatibility of Tree and Component internals was intentionally not preserved. What to restore, and how, is to be discussed separately.
ITree.iterate(prepared, visible); the tree and scheduler iterator is(node, prepared, visible).onViewportChange,isSkippedByCulling, thegetWalkTokenoption,preparesOnIterate,invalidateVisibility,getChildrenArray,activeInViewport,Tree.setVisible.isRenderAllowed()instead ofisVisible()to veto painting.getPath(). New:BlockConnection.getArrowPath(),batch.markBoundsDirty(item). A chunk containing an item withoutgetPathBounds()is never culled.Results
GraphLayer
scheduler.updatetime, ms, measured withscripts/bench/culling(headless Chromium, ANGLE Metal, Apple Silicon, DPR 2). Both versions run the same zoom history before measuring, and screenshots are compared after the camera moves.Panning,
nearview (1600×1000):Zooming and the
fullview (1600×1600) follow the same pattern.First two frames after mounting, scale 0.01:
Screenshots match v2 at every measured point. At scale 0.01, GPU rasterization varies by a few grey levels between page loads, even for the same version; the benchmark reports the largest per-channel difference to tell this noise from culling errors.
Benchmark
Scenes, options and caveats are described in
scripts/bench/culling/README.md.Not verified
packages/graphtypecheck, lint and all 569 tests pass.🤖 Generated with Claude Code