From 10fe7807806891dba62b6f532155a04dd8b3fe53 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Mon, 11 May 2026 10:40:32 -0400 Subject: [PATCH] perf(coreNode): split children update loop on childUpdateType MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inner loop had a per-iter `if (childUpdateType !== 0)` compare even though that value is invariant across the loop. Split into two specialized loops based on whether childUpdateType is zero — saves one compare per child per frame, which adds up for nodes with many children. Behavior is identical to the previous combined loop; two unit tests lock in the contract for both branches: - inherited childUpdateType cascades into children whose updateType was 0 - with no inherited bits, only children with pending work are walked Co-Authored-By: Claude Opus 4.7 --- src/core/CoreNode.test.ts | 69 +++++++++++++++++++++++++++++++++++++++ src/core/CoreNode.ts | 30 ++++++++++++----- 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/src/core/CoreNode.test.ts b/src/core/CoreNode.test.ts index 2ca7598..6478aa3 100644 --- a/src/core/CoreNode.test.ts +++ b/src/core/CoreNode.test.ts @@ -902,4 +902,73 @@ describe('set color()', () => { expect(node._hasContainResize).toBe(false); }); }); + + describe('children update loop branches', () => { + it('inherited childUpdateType is applied even to children with updateType=0', () => { + const root = new CoreNode(stage, defaultProps()); + root.globalTransform = Matrix3d.identity(); + root.worldAlpha = 1; + + const parent = new CoreNode(stage, defaultProps({ parent: root })); + parent.alpha = 1; + parent.w = 100; + parent.h = 100; + + const child = new CoreNode(stage, defaultProps({ parent })); + child.alpha = 1; + child.w = 100; + child.h = 100; + + // Bring both to steady state then clear pending work on the child. + parent.update(0, clippingRect); + child.updateType = 0; + + // Mark parent dirty with WorldAlpha — it should cascade into the + // child via the `childUpdateType !== 0` loop branch, even though + // the child started this frame with no pending work. + parent.alpha = 0.5; + parent.update(1, clippingRect); + + expect(child.worldAlpha).toBeCloseTo(0.5, 5); + }); + + it('skips children with no pending work when there is nothing to inherit', () => { + const root = new CoreNode(stage, defaultProps()); + root.globalTransform = Matrix3d.identity(); + root.worldAlpha = 1; + + const parent = new CoreNode(stage, defaultProps({ parent: root })); + parent.alpha = 1; + parent.w = 100; + parent.h = 100; + + const childA = new CoreNode(stage, defaultProps({ parent })); + childA.alpha = 1; + childA.w = 100; + childA.h = 100; + const childB = new CoreNode(stage, defaultProps({ parent })); + childB.alpha = 1; + childB.w = 100; + childB.h = 100; + + parent.update(0, clippingRect); + childA.updateType = 0; + childB.updateType = 0; + + // Mark only childB dirty. Force parent into the Children branch + // without seeding any inherited bits, so the loop should take the + // `childUpdateType === 0` branch and only walk dirty children. + childB.setUpdateType(UpdateType.Local); + parent.updateType |= UpdateType.Children; + parent.childUpdateType = 0; + + const spyA = vi.spyOn(childA, 'update'); + const spyB = vi.spyOn(childB, 'update'); + + parent.update(1, clippingRect); + + expect(spyA).not.toHaveBeenCalled(); + expect(spyB).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 55ebe83..2411a28 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -1469,18 +1469,30 @@ export class CoreNode extends EventEmitter { childClippingRect = NO_CLIPPING_RECT; } - for (let i = 0, length = this.children.length; i < length; i++) { - const child = this.children[i] as CoreNode; - - if (childUpdateType !== 0) { + const children = this.children; + const length = children.length; + if (childUpdateType !== 0) { + // Specialized loop: OR-in the inherited update bits for every child, + // then update if non-zero. Avoids the per-iter `childUpdateType !== 0` + // compare. + for (let i = 0; i < length; i++) { + const child = children[i] as CoreNode; child.updateType |= childUpdateType; + if (child.updateType === 0) { + continue; + } + child.update(delta, childClippingRect); } - - if (child.updateType === 0) { - continue; + } else { + // Specialized loop: nothing to inherit, so only walk children that + // already have pending work of their own. + for (let i = 0; i < length; i++) { + const child = children[i] as CoreNode; + if (child.updateType === 0) { + continue; + } + child.update(delta, childClippingRect); } - - child.update(delta, childClippingRect); } }