perf(reconciler): structural-skip untouched child ranges (positional, P4)#699
Conversation
… 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>
There was a problem hiding this comment.
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/ChildDiffHintsConditionalWeakTable side-channel published byUseMemoCellsByIndexand consumed byChildReconcilerto make low-mutation positional reconciliation scale withO(changed)rather thanO(count). - Thread
parentControlthrough 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.
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>
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>
|
/perf |
⚡ Reactor perf comparisonWorkload: Regression vs
|
| 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 (ReconcileKeyed → ReconcileKeyedMiddle, 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 /
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.
|
/perf |
|
/perf |
|
/perf |
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>
|
/perf |
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>
What
Makes
ChildReconciler.ReconcilePositionalO(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
_dirtyAncestorPathbridge)UseMemoCellsByIndexreuse branch publishes aChildDiffHint(ChangedIndices+ThemeSensitiveCount) keyed by reference on the fresh-per-renderElement[]. NoElement-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.ReconcilePositionalengages a fast path that updates only the hinted changed indices and skips the rest, iff all six gates hold:Filterreturned the same reference (nonull/EmptyElementshifted the index space), so the hint's indices line up with both filtered arrays;!hint.AnyThemeSensitive) — the load-bearing safety gate;Correctness
prevChildren[i]for unchangediand rebuilds onlychangedIndices. The fast path and the full walk share a singleUpdateCommonChildhelper, so both honour identical skip / update / type-mismatch semantics — the fast path does not re-implement the per-index body.ApplyThemeBindings/ApplyResourceOverridesThemeRefs against the effective theme (which a parentRequestedThemetoggle can change without touching the element tree). Gating on the whole-arrayAnyThemeSensitiveflag 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,AnyThemeSensitivetracking,IsThemeSensitivefor plain /ThemeBindings/ResourceOverrides.ThemeRefs/ literals-only.ChildReconcilerStructuralSkipTests(+7) — consumer differential vs full walk, incl. the gate teethThemeSensitive_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— anOnUpdateActioncell 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 aRequestedThemetoggle 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,ApplyThemeBindingsnot re-run, yet brushes still went Light→Dark). The one snapshot a skip truly leaves stale (ApplyResourceOverrides' concreteThemeRef.Resolveintofe.Resources) does not reliably re-resolve in the reconcile harness. So the headlessThemeSensitive_Hint_Forces_Full_Walkis 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/perfby #693/the skip-floor column — which is now onmain, 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 touchesChildReconciler.cs/ newChildDiffHints.cs/UseMemoCells.cs+ a smallparentControlthread-through inReconciler.cs(lines ~1916–1947) andV1HandlerAdapter.cs— both far from the automation region.Gate status
DRAFT, held for merge (reactor-perf class — reviewed by the user with
/perfnumbers). FullReactor.Testsgreen (9726), core lib AOT-clean (warnings-as-errors), headless gate teeth verified biting.🔒 Do not merge until the user GOes with measured deltas.