perf(core): make texture renderable-owner tracking O(1) on scroll path#102
Merged
Conversation
During scroll, CoreNode.update sets the IsRenderable bit on nearly every visible node every frame, so updateIsRenderable runs per node per frame and unconditionally called texture.setRenderableOwner. That did an indexOf over renderableOwners: any[] — O(owners) — and SubTexture forwards to the parent atlas texture, whose owner list can hold one entry per sprite/glyph consumer. On a representative trace this was ~165ms of no-op array scans. Two changes: 1. renderableOwners array -> Set<string | number>. setRenderableOwner now uses has/add/delete with early returns on no-op calls; the 0<->1 size transition logic (renderable flag, onChangeIsRenderable, load()) is unchanged. The four .length consumers move to .size. This is a type-level breaking change for anything introspecting renderableOwners (Inspector, external devtools) — ship as a minor bump. 2. Per-node textureOwnership cache so updateTextureOwnership early-returns when ownership hasn't changed. The cache is synced at every site that touches ownership without going through the helper (unloadTexture, texture setter, CoreTextNode direct call) so a stale true can't skip a re-registration that triggers Texture.load(). After this, steady-state scroll makes zero ownership calls. Adds Texture.test.ts (Set semantics) and CoreNode ownership-cache tests covering the cache skip, out-of-bounds release, texture swap reset, and the freed -> reload re-registration cycle. All 243 unit tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
During scroll,
CoreNode.updatesets theIsRenderablebit on nearly every visible node every frame, soupdateIsRenderable(CoreNode.ts:1840) runs per node per frame and unconditionally calledtexture.setRenderableOwner(Texture.ts:276). That did anindexOfoverrenderableOwners: any[]— O(owners) — andSubTexture.setRenderableOwnerforwards to the parent atlas texture, whose owner list can hold one entry per sprite/glyph consumer. On a representative ~13.5s trace this was ~165ms of no-op array scans.What changed
1.
renderableOwnersarray →Set<string | number>(Texture.ts)setRenderableOwnerrewritten withhas/add/deleteand early returns on no-op calls. The 0↔1 size-transition logic (renderableflag,onChangeIsRenderable,load()) is behaviorally unchanged..lengthconsumers move to.size:Texture.ts,SubTexture.ts,TextureMemoryManager.ts,Inspector.ts.Setis supported on the Chrome 38 language floor.2. Per-node
textureOwnershipcache (CoreNode.ts)updateTextureOwnershipearly-returns when ownership hasn't changed, so steady-state scroll makes zero ownership calls.unloadTexture, thetexturesetter (per-(node, texture) reset on swap), and the direct call inCoreTextNode— so a staletruecan't skip a re-registration that triggersTexture.load(). The freed/failed handlers already route through the helper, so the freed → re-add →load()reload cycle keeps working.Tests
Texture.test.ts: Set semantics (double-add no-op + single transition event, remove of non-member, 1→0 fires once, re-add after release re-triggersload(), mixed string/number owners,canBeCleanedUpowner-count behavior).CoreNodeownership-cache tests: the cache skip, out-of-bounds release once, texture-swap cache reset, and the freed → reload re-registration cycle.tsc --buildclean; prettier + eslint clean on changed files.Reviewer notes
renderableOwnersisreadonly any[]in the public type and is now aSet. Anything introspecting it (Inspector does; external devtools might) breaks at the type level. Ship as a minor bump with a changelog note.svg-icons-1snapshot + ~20 tests whose snapshots exist only underchromium-ci) that reproduce identically on cleanmain— verified by stashing. Unrelated to this PR; tracked separately.pnpm linksmoke test on the app the 165ms trace came from.🤖 Generated with Claude Code