Skip to content

perf(reconciler): structural-skip untouched child ranges (positional, P4)#699

Merged
azchohfi merged 7 commits into
mainfrom
azchohfi-reconciler-structural-skip
Jun 27, 2026
Merged

perf(reconciler): structural-skip untouched child ranges (positional, P4)#699
azchohfi merged 7 commits into
mainfrom
azchohfi-reconciler-structural-skip

Conversation

@azchohfi

Copy link
Copy Markdown
Collaborator

What

Makes ChildReconciler.ReconcilePositional O(changed) instead of O(count) when a memoizing producer (UseMemoCellsByIndex) reuses untouched cells reference-equal. This attacks the positional skip-walk floor — the per-render O(count) cost of visiting every cell just to confirm it can be skipped — which dominates low-mutation renders of large keyed grids (e.g. StocksGrid).

This is P4 of the reconciler perf workstream. P1 (#692, self-merge alloc guard) and P3 (#695, automation early-out) are the per-changed-cell levers; P4 is the different, larger lever for low-mutation renders (the all-skip floor).

Mechanism — CWT side-channel hint (mirrors #681's _dirtyAncestorPath bridge)

  • Producer — the UseMemoCellsByIndex reuse branch publishes a ChildDiffHint (ChangedIndices + ThemeSensitiveCount) keyed by reference on the fresh-per-render Element[]. No Element-record widening; AOT-safe (ConditionalWeakTable, no reflection). The theme count is carried forward incrementally so steady-state reuse stays O(changed); a one-time O(count) scan runs only on the first reuse after a full rebuild and as a defensive recompute.
  • ConsumerReconcilePositional engages a fast path that updates only the hinted changed indices and skips the rest, iff all six gates hold:
    1. old/new element counts match;
    2. the live child collection equals that count (no in-flight animation inflated it);
    3. no animation ambient (insert/move/exit reshape the child list);
    4. a hint is present for this array — a CWT hit also proves Filter returned the same reference (no null/EmptyElement shifted the index space), so the hint's indices line up with both filtered arrays;
    5. no cell is theme-sensitive (!hint.AnyThemeSensitive) — the load-bearing safety gate;
    6. the container is not on perf: deferred Yoga layout-cache follow-ups (skip-clean-children, cached min-content, Reset dead-code) + cross-axis measure-cache staleness #681's dirty-ancestor path — else a self-triggered descendant (e.g. a stateful memoized cell) would be unreachable through a structural skip and must take the full walk.

Correctness

  • Untouched indices are reference-equal by construction: the hook reuses prevChildren[i] for unchanged i and rebuilds only changedIndices. The fast path and the full walk share a single UpdateCommonChild helper, so both honour identical skip / update / type-mismatch semantics — the fast path does not re-implement the per-index body.
  • Theme gate is the load-bearing property. The only work the full walk does for an untouched cell that a structural skip would drop is re-resolving ApplyThemeBindings / ApplyResourceOverrides ThemeRefs against the effective theme (which a parent RequestedTheme toggle can change without touching the element tree). Gating on the whole-array AnyThemeSensitive flag is provably safe and sidesteps the subtle dirty-path reasoning that bit P2.

Tests

Headless (Reactor.Tests)

  • UseMemoCellsTests (+8) — producer hint correctness incl. first-render-no-hint, count-change-no-hint, empty-changes, incremental theme-count carry (decrement/increment), and caller-mutation snapshot.
  • ChildDiffHintsTests (+9) — registry publish/get, reference-keying, overwrite, AnyThemeSensitive tracking, IsThemeSensitive for plain / ThemeBindings / ResourceOverrides.ThemeRefs / literals-only.
  • ChildReconcilerStructuralSkipTests (+7) — consumer differential vs full walk, incl. the gate teeth ThemeSensitive_Hint_Forces_Full_Walk (revert gate 5 → it fails), count-mismatch defeat, defensive out-of-range index, empty-changed.

Live selftests (Reactor.AppTests.Host)

  • StructuralSkip_LifecycleParity — an OnUpdateAction cell at a changed index and untouched ref-equal indices: fires for the changed cell, never for untouched — identical to the full walk.
  • StructuralSkip_ThemeRangeParity — a themed ref-equal range under a RequestedTheme toggle renders + re-themes, no cell dropped.

Empirical theme-teeth note (important)

A live color-delta teeth for the theme gate is genuinely impossible: WinUI auto-re-resolves a {ThemeResource} Style setter on any effective-theme change even when Reactor structurally skips the cell (verified — gate reverted → cells skipped, ApplyThemeBindings not re-run, yet brushes still went Light→Dark). The one snapshot a skip truly leaves stale (ApplyResourceOverrides' concrete ThemeRef.Resolve into fe.Resources) does not reliably re-resolve in the reconcile harness. So the headless ThemeSensitive_Hint_Forces_Full_Walk is the gate's authoritative teeth; the live fixture is the end-to-end parity companion. This strengthens the safety story — a skipped themed cell's brush updates anyway.

Measurement

The win shows under the low-mutation skip-floor metric (--percent 0) added to /perf by #693/the skip-floor column — which is now on main, so this PR is measurable. On the default 50%-mutation StocksGrid the effect is within noise (most cells change → little to skip), which is expected and fine data.

File-disjointness

Disjoint from the perf fleet: #692/#695 own Reconciler.Update.cs; this PR touches ChildReconciler.cs / new ChildDiffHints.cs / UseMemoCells.cs + a small parentControl thread-through in Reconciler.cs (lines ~1916–1947) and V1HandlerAdapter.cs — both far from the automation region.

Gate status

DRAFT, held for merge (reactor-perf class — reviewed by the user with /perf numbers). Full Reactor.Tests green (9726), core lib AOT-clean (warnings-as-errors), headless gate teeth verified biting.

🔒 Do not merge until the user GOes with measured deltas.

… P4)

Make `ChildReconciler.ReconcilePositional` O(changed) instead of O(count) when
a memoizing producer (`UseMemoCellsByIndex`) reuses untouched cells reference-
equal. Targets the positional skip-walk FLOOR — the per-render O(count) cost of
visiting every cell to confirm it can be skipped — which dominates low-mutation
renders of large keyed grids (e.g. StocksGrid).

Mechanism (CWT side-channel hint, mirrors #681's _dirtyAncestorPath bridge):
  • Producer: the `UseMemoCellsByIndex` reuse branch publishes a `ChildDiffHint`
    (ChangedIndices + ThemeSensitiveCount) keyed by reference on the fresh-per-
    render Element[]. No Element-record widening; AOT-safe (ConditionalWeakTable,
    no reflection). The theme count is carried forward incrementally so steady-
    state reuse stays O(changed); a one-time O(count) scan runs only on the first
    reuse after a full rebuild and as a defensive recompute.
  • Consumer: `ReconcilePositional` engages a fast path that updates ONLY the
    hinted changed indices and skips the rest, iff ALL hold:
      1. old/new element counts match,
      2. the live child collection equals that count (no in-flight anim inflated it),
      3. no animation ambient,
      4. a hint is present for THIS array (a CWT hit also proves Filter returned
         the same reference — no null/EmptyElement shifted the index space),
      5. no cell is theme-sensitive (`!AnyThemeSensitive`),
      6. the container is not on #681's dirty-ancestor path.

Correctness:
  • Untouched indices are reference-equal BY CONSTRUCTION (the hook reuses
    prevChildren[i] for unchanged i and rebuilds only changedIndices). The changed
    and full-walk paths share a single `UpdateCommonChild` helper, so both honour
    identical skip / update / type-mismatch semantics.
  • The theme gate is the load-bearing safety property: the ONLY work the full walk
    does for an untouched cell that a structural skip would drop is re-resolving
    `ApplyThemeBindings` / `ApplyResourceOverrides` ThemeRefs against the effective
    theme (which a parent RequestedTheme toggle can change WITHOUT touching the
    element tree). Gating on the whole-array `AnyThemeSensitive` flag is provably
    safe and sidesteps the subtle dirty-path reasoning that bit P2.

Tests:
  • Headless (Reactor.Tests): producer hint correctness incl. incremental theme-
    count carry + caller-mutation snapshot (UseMemoCellsTests); hint registry +
    IsThemeSensitive (ChildDiffHintsTests); consumer differential vs full walk incl.
    the gate teeth `ThemeSensitive_Hint_Forces_Full_Walk` (revert the gate → fails),
    count-mismatch, defensive OOB, empty-changed (ChildReconcilerStructuralSkipTests).
  • Live selftests (Reactor.AppTests.Host): LifecycleParity (OnUpdateAction fires for
    a changed index, never for untouched ref-equal — == full walk); ThemeRangeParity
    (themed ref-equal range under a RequestedTheme toggle renders + re-themes, no cell
    dropped). Per the empirical note below, the authoritative gate teeth is the
    headless visited-index assertion, not a live color delta.

Empirical theme note: a LIVE color-delta teeth for the theme gate is impossible —
WinUI auto-re-resolves a `{ThemeResource}` Style setter on any effective-theme
change even when Reactor structurally skips the cell (verified: gate reverted →
cells skipped, ApplyThemeBindings not re-run, yet brushes still went Light→Dark).
The one snapshot a skip truly leaves stale (`ApplyResourceOverrides`' concrete
ThemeRef.Resolve into fe.Resources) does not reliably re-resolve in the reconcile
harness. The headless `ThemeSensitive_Hint_Forces_Full_Walk` is therefore the gate's
load-bearing teeth; the live fixture is the end-to-end parity companion.

Measurement: the win shows under a low-mutation skip-floor metric (PERFVAL's
`--percent 0`); on the default 50%-mutation StocksGrid it is within noise.
File-disjoint from the perf fleet (#692/#695 own Reconciler.Update.cs; this touches
ChildReconciler.cs / ChildDiffHints.cs / UseMemoCells.cs + a parentControl thread-
through in Reconciler.cs / V1HandlerAdapter.cs).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new “structural skip” fast path for positional child reconciliation, allowing ChildReconciler.ReconcilePositional to update only producer-declared changed indices (and skip untouched reference-equal ranges) when UseMemoCellsByIndex reuses previous element instances.

Changes:

  • Add a ChildDiffHint/ChildDiffHints ConditionalWeakTable side-channel published by UseMemoCellsByIndex and consumed by ChildReconciler to make low-mutation positional reconciliation scale with O(changed) rather than O(count).
  • Thread parentControl through reconciler→child reconciler entry points to gate the fast path via the dirty-ancestor-path safety check.
  • Add headless unit tests and live selftest fixtures covering hint publication/consumption and theme gating parity scenarios.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/Reactor.Tests/UseMemoCellsTests.cs Adds unit coverage for UseMemoCellsByIndex hint publication and theme-sensitive count carry.
tests/Reactor.Tests/ChildReconcilerStructuralSkipTests.cs New headless tests asserting the fast path visits only changed indices and honors gating.
tests/Reactor.Tests/ChildDiffHintsTests.cs New unit tests for hint registry behavior and theme-sensitivity predicate.
tests/Reactor.AppTests.Host/SelfTest/SelfTestFixtureRegistry.cs Registers the new live fixtures in the selftest registry.
tests/Reactor.AppTests.Host/SelfTest/Fixtures/StructuralSkipFixtures.cs New live fixtures validating lifecycle parity and theme-range parity end-to-end.
src/Reactor/Hooks/UseMemoCells.cs Publishes ChildDiffHint on reuse renders and maintains theme-sensitive count incrementally.
src/Reactor/Core/V1Protocol/V1HandlerAdapter.cs Threads control through ReconcilePanelChildrenInto to support dirty-path gating.
src/Reactor/Core/Reconciler.cs Threads panel/parentControl into ChildReconciler.Reconcile and updates panel reconcile helper signature.
src/Reactor/Core/ChildReconciler.cs Adds the gated positional structural-skip fast path and refactors per-index logic into UpdateCommonChild.
src/Reactor/Core/ChildDiffHints.cs New side-channel types: ChildDiffHint, ChildDiffHints, and IsThemeSensitive predicate.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Reactor/Hooks/UseMemoCells.cs Outdated
Comment thread src/Reactor/Hooks/UseMemoCells.cs
Comment thread src/Reactor/Core/ChildReconciler.cs Outdated
azchohfi and others added 2 commits June 26, 2026 04:58
Address the internal pr-review skill + GitHub Copilot findings on the
positional structural-skip fast path. No behavior change for the StocksGrid
target workload (all new gates pass on it); each fold tightens correctness or
documents an invariant.

- Hot-reload safety (C1): add `!ForceFullRenderActive` gate so a hot-reload
  force pass never structurally skips an untouched wrapper cell (the dirty
  path is empty during a pure force pass, so the dirty-path gate alone did
  not cover it). Falls back to the full walk, which honours
  ForceRenderThroughWrapper per cell.
- Array-identity guard (S1): the hint now carries a WeakReference to the exact
  previous-render array its ChangedIndices were diffed against; the fast path
  engages only when the reconciler's old array IS that array. A cheap,
  self-documenting sufficient condition for the per-index ref-equality
  invariant; any defensive copy upstream safely falls back to the full walk.
  Weak on purpose -- a strong ref would chain every historical array through
  the reference-keyed CWT and leak.
- Duplicate-index hardening (dedupe): snapshot + sort/compact the caller's
  changedIndices before the theme tally / builder / publish. A duplicated
  themed->plain index could otherwise under-count the incremental
  theme-sensitive tally and wrongly publish AnyThemeSensitive=false, and would
  rebuild + re-update the same cell N times.
- Dirty-path gate (T1): documented as conservative defense-in-depth. Proven by
  experiment that it is behaviorally redundant given the count/CWT/array-id
  gates (the full walk skips a ref-equal self-triggered cell identically via
  CanSkipUpdate), retained as cheap insurance; costs nothing on the target
  workload (cell panel is a descendant, not an ancestor, of the self-triggered
  grid component).
- ResourceOverrides conservatism (C3): documented why the ThemeRef-backed
  ResourceOverrides arm of IsThemeSensitive is intentionally conservative.

Tests: weak-ref round-trip + stale-old-array teeth (gate 8) + chained
theme-count carry (T3) + duplicate-index theme-count/build-once (T4). Full
Reactor.Tests green (9731); StructuralSkip selftests green; core lib Release
AOT-clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fold the three GitHub Copilot review findings on the positional structural-skip
fast path. All are hardening; no behavior change on the StocksGrid target.

- Null cells (findings 1+2): a cell builder may legitimately return null
  (ChildReconciler.Filter drops nulls downstream), but PR-C's theme tally now
  inspects prev/built cells via ChildDiffHints.IsThemeSensitive, which
  dereferenced element.ThemeBindings and would NRE on a null. Widen the
  predicate to accept Element? and treat null as non-theme-sensitive (a null
  has no bindings to re-resolve). Fixes all three call sites in
  UseMemoCellsByIndex (the O(count) CountThemeSensitive scan + both incremental
  tally reads) at the single chokepoint.
- DebugElementsSkipped diagnostic (finding 3): the fast path adjusted the
  skipped-element counter by `common - changed.Length`, but the loop
  defensively ignores out-of-range hint indices, so the raw hint length
  over-counts visited work and the diagnostic could skew (or, with enough
  out-of-range indices, go negative). Track indices ACTUALLY visited and base
  the adjustment on that, making the counter match the full walk exactly.

Tests: null-cell predicate guard + producer null-cell theme-scan teeth; the
out-of-range consumer test now asserts the skipped-element count equals the
full-walk total (4), which the old `common - changed.Length` undercounted.
Full Reactor.Tests green (9733); core lib Release AOT-clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Comment thread src/Reactor/Core/ChildReconciler.cs
Ran the internal pr-review skill on the PR-C HEAD (7 dimensions + a gpt-5.4
multi-model cross-check, a different model family). Fold the actionable findings:

- H1 (test-coverage; multi-model CONFIRMED load-bearing): add a hot-reload gate
  teeth selftest. StructuralSkip_HotReloadWrapperReRender puts a WRAPPER cell
  (Component) inside a UseMemoCellsByIndex range whose body a simulated
  hot-reload edit changes, then drives a real force pass. The fast path's
  !ForceFullRenderActive gate must defer to the full walk (which honours
  ForceRenderThroughWrapper per cell) so the wrapper re-renders its edited body.
  Teeth verified: reverting the gate fails WrapperReRenders + OldBodyGone (the
  structural skip swallows the edit).

- H2 (test-coverage; partially-confirmed): add a headless differential test that
  mirrors the real producer's reference-equal reuse at untouched indices (not
  fresh copies) and asserts the fast-path output == full-walk output (identical
  skip accounting, no structural mutation, visited set a subset).

- M1 (security + correctness; multi-model: real but not a ship-blocker, no cheap
  complete defense) + M3 (docs + api): document the returned array's
  immutability / no-mutation contract, the changedIndices dedupe contract, and
  the theme-sensitive fallback in the UseMemoCellsByIndex XML doc; hand-sync the
  generated reference MD.

Dispositions recorded (no code change):
- H3 / gate 6 (!IsOnDirtyAncestorPath): multi-model DISPUTED the test-coverage
  finding and independently confirmed the gate is behaviorally redundant given
  the count/CWT/array-id gates (a ref-equal untouched cell is skipped
  identically by the full walk via Element.CanSkipUpdate before dirty-path logic
  is consulted). No behavioral teeth is constructible; kept as documented cheap
  defense-in-depth.
- M2: ThemeRangeParity already documents itself in-code as a smoke/parity check,
  not the gate teeth; the authoritative !AnyThemeSensitive teeth is the headless
  ChildReconcilerStructuralSkipTests.ThemeSensitive_Hint_Forces_Full_Walk.
- L1: the ResourceOverrides arm of IsThemeSensitive is intentionally conservative
  (already documented) per the verified theme crux.

Gates: core lib Release AOT 0W/0E; Reactor.Tests 9734 pass / 0 fail;
StructuralSkip selftests 3 fixtures / 14 checks green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comment thread src/Reactor/Core/ChildReconciler.cs
Comment thread src/Reactor/Hooks/UseMemoCells.cs
@azchohfi

Copy link
Copy Markdown
Collaborator Author

/perf

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

⚡ Reactor perf comparison

Workload: StressPerf.ReactorOptimized StocksGrid · --percent 50 --duration 10 · x64 Release · median of 12 paired runs (2 warmup dropped); Δ is the mean change with a 95% CI · PR head and main built and run interleaved on the same runner.

Regression vs main baseline

Metric main (baseline) This PR Δ (95% CI) Status
Renders/sec ↑ 3.72 3.74 -0.8% 95% CI [-3.5, +2.0] ≈ within noise
Avg Reconcile (ms) ↓ 103.8 93.7 -7.7% 95% CI [-11.7, -3.8] ✅ improvement
Avg Diff (ms) ↓ 92.5 84.3 -7.2% 95% CI [-11.4, -3.1] ✅ improvement
Avg Memory (MB) ↓ 290.1 287.3 -1.0% 95% CI [-1.5, -0.5] ✅ improvement

Low-mutation skip-floor (--percent 0)

At --percent 0 the workload mutates few cells per tick (always at least one), so reconcile/diff isolate the O(n) per-tick child skip-walk floor that higher mutation rates dilute — ChildReconciler re-walks every child each tick even when nothing moved. The closer --percent is to 0, the more this floor is the signal, so a structural-skip optimization shows up cleanly where the headline table above buries it. Δ is the mean paired change with a 95% CI.

Metric main (baseline) This PR Δ (95% CI) Status
Renders/sec ↑ 16.63 16.93 +0.9% 95% CI [-4.4, +6.1] ≈ within noise
Avg Reconcile (ms) ↓ 31.0 30.6 -0.2% 95% CI [-6.7, +6.2] ≈ within noise
Avg Diff (ms) ↓ 29.1 28.7 -0.5% 95% CI [-7.0, +5.9] ≈ within noise
Avg Memory (MB) ↓ 269.0 267.5 -0.6% 95% CI [-1.0, -0.3] ✅ improvement

Allocation (Reactor) — lower is better

Metric main (baseline) This PR Δ (95% CI) Status
Alloc bytes/render ↓ 5724791 4782013 -16.5% 95% CI [-17.2, -15.9] ✅ improvement
Gen0 GC / 1k renders ↓ 227.88 162.16 -28.3% 95% CI [-32.4, -24.1] ✅ improvement

Keyed-list workload (StressPerf.KeyedList, --percent 50)

A separate macro workload: a ~500-row stably keyed list whose rows are reordered / inserted / removed each tick. Because every child carries a key, the child reconciler takes its keyed arm (ReconcileKeyedReconcileKeyedMiddle, the LIS-based minimal-move pass) instead of the positional re-walk the StocksGrid tables above measure — so this is the sensitive macro signal for keyed-diff work the positional cells can never reach. Same interleaved paired-Δ 95% CI as the headline table.

Metric main (baseline) This PR Δ (95% CI) Status
Renders/sec ↑ 17.58 17.99 +2.4% 95% CI [-4.5, +9.2] ≈ within noise
Avg Reconcile (ms) ↓ 10.3 10.4 +1.2% 95% CI [-4.3, +6.6] ≈ within noise
Avg Diff (ms) ↓ 10.2 10.3 +1.1% 95% CI [-4.5, +6.7] ≈ within noise
Avg Memory (MB) ↓ 168.3 167.6 -0.1% 95% CI [-0.8, +0.5] ≈ within noise

Allocation (keyed-list) — lower is better

Metric main (baseline) This PR Δ (95% CI) Status
Alloc bytes/render ↓ 314102 314352 0.0% 95% CI [-0.3, +0.3] ≈ within noise
Gen0 GC / 1k renders ↓ 17.05 16.62 -1.3% 95% CI [-7.9, +5.3] ≈ within noise

Reconciler micro-benchmarks (PerfBench.ControlModel)

Production --variant Reactor control-model path, ns-resolution and WinUI-undiluted (spec-047 M1–M13) — ↓ lower is better. Status tracks allocated bytes/op, the authoritative signal here; it is deterministic for structurally-fixed benches, while dispatcher / background-thread benches carry a small process-to-process offset, so a bench is flagged only when its 95% CI clears a ±3% minimum-effect band (real structural alloc changes are several percent to many-x). ns/op is shown for context but is not auto-flagged (its paired CI is rep-interleaved but the flag remains dormant pending a real-CI identical-binary band calibration). Δ is the mean paired change with a 95% CI.

Bench main ns/op Δ ns (95% CI) main B/op Δ alloc (95% CI) Status
M1 Mount_Leaf_NoCallback 93493.9 +0.4% 95% CI [-3.3, +4.1] 1140.9 0.0% 95% CI [0.0, 0.0] ≈ within noise
M2 Mount_Leaf_OneCallback 74037.6 -7.8% 95% CI [-17.0, +1.5] 3383.3 0.0% 95% CI [0.0, 0.0] ≈ within noise
M3 Mount_Leaf_ThreeCallbacks 170400.1 +0.2% 95% CI [-7.3, +7.8] 8760.5 +0.1% 95% CI [-0.1, +0.2] ≈ within noise
M4 Dispatch_Switch_Cold 75742.5 -5.8% 95% CI [-19.1, +7.5] 1767.8 0.0% 95% CI [0.0, 0.0] ≈ within noise
M5 Dispatch_Switch_Warm 76839.5 +1.4% 95% CI [-12.6, +15.5] 1805.9 +1.2% 95% CI [-1.0, +3.3] ≈ within noise
M6 Dispatch_ExternalType 62680.8 +2.6% 95% CI [-11.1, +16.4] 1028.6 +2.2% 95% CI [-1.7, +6.0] ≈ within noise
M7 Update_NoChange 37807.8 +0.1% 95% CI [-2.7, +2.8] 370.1 -0.5% 95% CI [-10.6, +9.6] ≈ within noise
M8 Update_OneLeafChanged 27229.6 +0.2% 95% CI [-1.5, +2.0] 536.0 0.0% 95% CI [0.0, 0.0] ≈ within noise
M9 Update_AllChanged 2509194.4 +1.4% 95% CI [-1.0, +3.9] 184278.1 0.0% 95% CI [0.0, 0.0] ≈ within noise
M10 EventHandlerState_Alloc 50037.7 +3.0% 95% CI [-1.8, +7.7] 3100.3 +1.2% 95% CI [-0.1, +2.5] ≈ within noise
M11 ModifierEHS_Frequency 32246.4 -6.1% 95% CI [-16.4, +4.2] 638.9 +0.2% 95% CI [-0.1, +0.6] ≈ within noise
M12 Pool_Rent_HotPath 76248.4 -4.7% 95% CI [-13.2, +3.9] 1099.9 0.0% 95% CI [0.0, 0.0] ≈ within noise
M13 Setters_Suppression_Scope 101.7 -6.1% 95% CI [-24.7, +12.4] 26.7 0.0% 95% CI [0.0, 0.0] ≈ within noise
M14 Dsl_Rebuild_Cascade 1313412.1 +0.4% 95% CI [-3.7, +4.5] 2231828.9 0.0% 95% CI [0.0, 0.0] ≈ within noise
C207 ChangeHandler_DpRead_Coalesce 1025.2 -1.5% 95% CI [-14.8, +11.7] 0.6 0.0% 95% CI [0.0, 0.0] ≈ within noise
OAlloc Optional_Element_Alloc 215.5 -0.5% 95% CI [-10.4, +9.4] 528.0 0.0% 95% CI [0.0, 0.0] ≈ within noise
OUpdate Optional_Reconciler_Update 9753.6 +8.5% 95% CI [-8.7, +25.7] 2772.3 0.0% 95% CI [0.0, 0.0] ≈ within noise

Cross-framework reference (same StocksGrid workload)

Metric vanilla WinUI3¹ Rust windows-reactor² Reactor (this PR)
Renders/sec ↑ 4.35 6.08 3.74
Avg Reconcile (ms) ↓ n/a 20.0 93.7
Avg Diff (ms) ↓ n/a 17.7 84.3
Avg Memory (MB) ↓ 262.6 195.5 287.3

↑ higher is better · ↓ lower is better. Within noise = the 95% confidence interval of the paired Δ includes 0 (no change resolvable at this sample size); ✅ improvement / ⚠️ regression require the CI to exclude 0.
Allocation metrics (alloc bytes/render, Gen0 GC) are the sensitive signal for allocation-reduction work, where the mean-ms / memory figures are largely flat. They read n/a for a harness built from a revision that predates them (rebase the PR onto main to populate them).
Reconciler micro-benchmarks run PerfBench.ControlModel --variant Reactor (M1–M13) as a headless loop bracketed by per-thread alloc + GC counters — ns-resolution and free of WinUI render / working-set dilution, so they resolve Core/Reconciler allocation deltas the macro StocksGrid workload cannot. main and PR each link their own src/Reactor build and are rep-interleaved (a fresh alternated process per rep); Δ is the paired 95% CI over per-rep means. The Status column tracks allocated bytes/op (deterministic for identical code); ns/op is informational — its paired CI is now unbiased but the flag stays dormant pending a real-CI identical-binary band calibration.
¹ vanilla WinUI3 = StressPerf.Direct (imperative; no virtual-DOM, so it has no reconcile/diff phase — those cells read n/a). Measured live on this runner.
² Rust = test_reactor_perf from microsoft/windows-rs — a port of this harness (same StocksGrid, same --percent/--duration CLI). Built from source and measured live on this runner.
Absolute numbers are runner-dependent — trust the Δ vs main, not the absolute values. Memory (working set) is the noisiest metric.
Runner: CPU: AMD EPYC 9V74 80-Core Processor · 4 logical cores · 16 GB RAM · runner: GitHub Actions 1043006457.
Generated by .github/workflows/perf-compare.yml · PR 71c3a6b vs main e8572a0 · 2026-06-27T07:22:31Z · run log.

@azchohfi

Copy link
Copy Markdown
Collaborator Author

/perf

@azchohfi

Copy link
Copy Markdown
Collaborator Author

/perf

@azchohfi azchohfi marked this pull request as ready for review June 27, 2026 06:04
@azchohfi azchohfi requested a review from Copilot June 27, 2026 06:04
@azchohfi

Copy link
Copy Markdown
Collaborator Author

/perf

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

The existing ChildReconcilerStructuralSkipTests assert the fast path's
VISIT COUNT (which child indices are read) but nothing pins the resulting
allocation cut, so the measured StocksGrid allocation win (#699) could be
silently reverted with every behavioural test still green.

Add Structural_Skip_Pins_PerCell_Read_Elision_As_Allocation_Budget: a
MeasuringChildCollection charges a fixed managed allocation per Get(i),
modeling the per-cell COM read / marshaling the skip elides for untouched
reference-equal cells (the real cost is native and unmeasurable headless).
Fast path (hint published) allocates O(changed); full walk (no hint)
allocates O(count). Asserts the mechanism (5 vs 500 reads/iter) and an 8x
GC-bytes budget. Has teeth: disabling the fast-path gate makes the hinted
path walk every cell, collapsing fast onto full and failing the test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@azchohfi azchohfi requested a review from Copilot June 27, 2026 06:45
@azchohfi

Copy link
Copy Markdown
Collaborator Author

/perf

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Comment thread src/Reactor/Core/ChildDiffHints.cs
Copilot review on #699 flagged that AnyThemeSensitive derives from
ThemeSensitiveCount > 0, so a hypothetical negative count would read as
NOT theme-sensitive and could allow the structural-skip fast path to skip
a theme-sensitive subtree (a missed-update risk).

The only producer (UseMemoCellsByIndex) already clamps its incremental
tally to a >= 0 floor before publishing (UseMemoCells.cs:299-300) and
CountThemeSensitive only counts upward, so a negative is unreachable and
> 0 is correct today. But this is the SAFETY gate for a correctness-
sensitive skip, so harden the type to be fail-safe regardless: test != 0
rather than > 0. Behavior is byte-identical for every value the producer
can emit (all >= 0); the only difference is that an anomalous negative now
BLOCKS the skip (forces the always-correct full walk) instead of silently
allowing it — the correct fail direction for a correctness gate.

Provably perf-neutral: the StocksGrid workload publishes count == 0 every
render, where both > 0 and != 0 yield false identically, so the fast
path engages unchanged. Adds a fail-safe teeth test that goes red if the
guard is reverted to > 0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

@azchohfi azchohfi merged commit 0002f19 into main Jun 27, 2026
19 checks passed
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.

2 participants