Skip to content

perf(core): make texture renderable-owner tracking O(1) on scroll path#102

Merged
chiefcll merged 1 commit into
mainfrom
perf/renderable-owner-o1
Jun 13, 2026
Merged

perf(core): make texture renderable-owner tracking O(1) on scroll path#102
chiefcll merged 1 commit into
mainfrom
perf/renderable-owner-o1

Conversation

@chiefcll

Copy link
Copy Markdown
Contributor

Why

During scroll, CoreNode.update sets the IsRenderable bit on nearly every visible node every frame, so updateIsRenderable (CoreNode.ts:1840) runs per node per frame and unconditionally called texture.setRenderableOwner (Texture.ts:276). That did an indexOf over renderableOwners: any[] — O(owners) — and SubTexture.setRenderableOwner forwards 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. renderableOwners array → Set<string | number> (Texture.ts)

  • setRenderableOwner rewritten with has/add/delete and early returns on no-op calls. The 0↔1 size-transition logic (renderable flag, onChangeIsRenderable, load()) is behaviorally unchanged.
  • The four .length consumers move to .size: Texture.ts, SubTexture.ts, TextureMemoryManager.ts, Inspector.ts.
  • Set is supported on the Chrome 38 language floor.

2. Per-node textureOwnership cache (CoreNode.ts)

  • updateTextureOwnership early-returns when ownership hasn't changed, so steady-state scroll makes zero ownership calls.
  • The cache is synced at every site that touches ownership without going through the helper — unloadTexture, the texture setter (per-(node, texture) reset on swap), and the direct call in CoreTextNode — so a stale true can't skip a re-registration that triggers Texture.load(). The freed/failed handlers already route through the helper, so the freed → re-add → load() reload cycle keeps working.

Tests

  • New 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-triggers load(), mixed string/number owners, canBeCleanedUp owner-count behavior).
  • New CoreNode ownership-cache tests: the cache skip, out-of-bounds release once, texture-swap cache reset, and the freed → reload re-registration cycle.
  • All 243 unit tests pass; tsc --build clean; prettier + eslint clean on changed files.

Reviewer notes

  • ⚠️ Type-surface breaking change: renderableOwners is readonly any[] in the public type and is now a Set. Anything introspecting it (Inspector does; external devtools might) breaks at the type level. Ship as a minor bump with a changelog note.
  • Visual regression: this change produces zero new diffs. The local suite has ~21 pre-existing failures (one stale local svg-icons-1 snapshot + ~20 tests whose snapshots exist only under chromium-ci) that reproduce identically on clean main — verified by stashing. Unrelated to this PR; tracked separately.
  • Not run here: the consuming app's suite against a linked build of this renderer (that repo isn't in this session) — worth a pnpm link smoke test on the app the 165ms trace came from.

🤖 Generated with Claude Code

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>
@chiefcll chiefcll merged commit ffb945e into main Jun 13, 2026
1 check passed
@chiefcll chiefcll deleted the perf/renderable-owner-o1 branch June 13, 2026 02:36
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