Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2f8e79e
spec(047): Phase 3 close-out — Panel<>.PerChildAttachedAfterAll two-p…
codemonkeychris May 28, 2026
53fabfc
spec(047): Phase 3 close-out — TemplatedItems<> strategy + Reconciler…
codemonkeychris May 28, 2026
5e4f73d
spec(047): Phase 3 close-out Port (4) — RelativePanel via PerChildAtt…
codemonkeychris May 28, 2026
e98613e
spec(047): Phase 3 close-out Port (5) G2 — TemplatedListView<T> / Tem…
codemonkeychris May 28, 2026
0ae98ff
spec(047): Phase 3 close-out — docs + perf bench registry
codemonkeychris May 28, 2026
70f8881
spec(047): Phase 3 close-out — advisory perf re-capture (3×5 Cloud PC…
codemonkeychris May 28, 2026
8cd7a38
spec(047): Phase 3 finish — Engine (1) ItemsRepeater arm on erased bi…
codemonkeychris May 28, 2026
5f388ed
spec(047): Phase 3 finish — Engine (5) NumberBox.Min/Max via .Coercin…
codemonkeychris May 28, 2026
0ceac2e
spec(047): Phase 3 finish — Engine (4) .Imperative property escape hatch
codemonkeychris May 28, 2026
78485ce
spec(047): Phase 3 finish — Engines (2)+(3): two-strategy + Target audit
codemonkeychris May 28, 2026
dec110d
spec(047): Phase 3 finish — Carve (12) Expander.HeaderTemplate via .I…
codemonkeychris May 28, 2026
a337a39
spec(047): Phase 3 finish — Port (6) Lazy*Stack G2 (LazyVStack<T> / L…
codemonkeychris May 28, 2026
2cfdad8
spec(047): Phase 3 finish — Carve (14) Path.PathDataString via .Imper…
codemonkeychris May 28, 2026
8aaffe5
spec(047): Phase 3 finish — dispatch consolidation (single is-check arm)
codemonkeychris May 28, 2026
99b3786
spec(047): Phase 3 finish — §14 + tracker close-out narrative
codemonkeychris May 28, 2026
b2d699d
spec(047): Phase 3 finish — advisory perf re-capture (3×5 Cloud PC x64)
codemonkeychris May 28, 2026
68ff66f
spec(047): Phase 3 finish — Ports (8)-(11) G3 TreeView / FlipView / T…
codemonkeychris May 28, 2026
b3073f3
spec(047): Phase 3 finish — §14 + tracker update for G3 ports landing
codemonkeychris May 28, 2026
4aa7347
spec(047): Phase 3 finish — Port (7) ItemsRepeater<T> (closes carry-f…
codemonkeychris May 28, 2026
ae52ee8
spec(047): Phase 3 finish — §14 + tracker close-out (Port (7) landed)
codemonkeychris May 28, 2026
3e28c36
spec(047): Phase 3 finish — honest scope accounting (deferred/not-att…
codemonkeychris May 28, 2026
122e839
spec(047): Phase 3 finish — address PR #437 CR feedback
codemonkeychris May 28, 2026
8a3ceed
build(cli): skip SignaturesGen regen in CI to break the Reactor.Local…
codemonkeychris May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 53 additions & 7 deletions docs/specs/047-extensible-control-model.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Spec 047 §14 Phase 3 close-out, x64 advisory

**This is an advisory x64 capture, NOT authoritative.** Cloud PC
(`CPC-ander-YTZ3O`, AMD EPYC 7763 64-Core Processor, x64), not on
AC/dedicated hardware. Do not cite these numbers in §13 or §14 spec
text. A stable-AC ARM64 re-capture on `LAPTOP-4MEP83VI` should ratify
the matrix before §14 Phase 3 is closed.

## Why this capture exists

This run extends the `2026-05-27-phase3-final-3x5/` matrix with the
four close-out commits landed on `spec/047-phase3-close-out` (off PR
#436 HEAD):

- **Engine (1)** — `Panel<>.PerChildAttachedAfterAll` two-pass shape.
- **Engine (2)** — `TemplatedItems<>` strategy + `Reconciler.BindKeyedItemsSource`,
plus the T-erased `TemplatedItemsErased<>` + `BindErasedKeyedItemsSource`
variant.
- **Port (4)** — RelativePanel descriptor uses the new after-all callback.
- **Port (5) G2** — `TemplatedListView<T>` / `TemplatedGridView<T>` via
base-derived registration. Adds a base-walk to
`V1HandlerRegistry.TryGet` (exact-type entries always win; base-derived
entries cached per concrete type for O(1) steady state).

`DescriptorVariantFactory` now registers **52 ported controls** (50
from prior + 2 new via `RegisterHandlerForDerivedTypes` for the typed
templated lists). The bench matrix detects (a) dispatch-table shape
change from the +2 entries and the new strategy pattern-match arms in
`V1HandlerAdapter`, (b) the registry's base-walk fallback cost on
non-derived lookups, and (c) the `DescriptorHandler.Children` switch
gaining the two new templated-items cases.

## Capture environment

`CPC-ander-YTZ3O`, x64 (AMD EPYC 7763 64-Core Processor), Release,
.NET 10.0.8, Windows 11 26200. **Cloud PC — not on AC/dedicated
hardware**. 3 process launches × 5 reps × 13 benches × 4 variants =
780 measurements across `launch-1.jsonl` + `launch-2.jsonl` +
`launch-3.jsonl`.

## Headline — V1 ON (descriptors, post close-out) vs V1 OFF (today)

Median of n=15 (3 launches × 5 reps) per cell. Compared against the
prior `2026-05-27-phase3-final-3x5/` headline (50 controls) to surface
the close-out delta.

| Bench | This capture (V1 ON vs V1 OFF) | Prior `phase3-final-3x5` | Delta vs prior | Notes |
|---|---:|---:|---:|---|
| M1 Mount_Leaf_NoCallback | **+21.2%** | +14.9% | +6.3pp regression | New strategy pattern-match arms in `V1HandlerAdapter.DispatchChildrenMount` add two upfront `is`-checks (`ITemplatedItemsStrategy`, `IErasedTemplatedItemsStrategy`). |
| M2 Mount_Leaf_OneCallback | -0.1% | -1.7% | within noise | |
| M3 Mount_Leaf_ThreeCallbacks | -1.0% | +3.3% | improvement | |
| M4 Dispatch_Switch_Cold | **-20.8%** | -21.2% | held | Dispatch wins persist — the +2 entries don't push past the inflection. |
| M5 Dispatch_Switch_Warm | **-23.9%** | -24.3% | held | Same. |
| M6 Dispatch_ExternalType | -0.5% | +0.2% | within noise | The new base-walk fallback in `V1HandlerRegistry.TryGet` is gated on `_baseEntries.Count == 0` so external-type lookups skip it. |
| M7 Update_NoChange | +6.3% | +7.4% | minor improvement | |
| M8 Update_OneLeafChanged | **+18.9%** | +25.5% | improvement (-6.6pp) | Largest movement. `DescriptorHandler.Children` switch refactor (added `ITemplatedItemsStrategy` / `IErasedTemplatedItemsStrategy` arms returning `null` so dispatch happens inline) shortens the non-ItemsHost Update path. |
| M9 Update_AllChanged | +4.5% | +3.6% | within noise | |
| M10 EventHandlerState_Alloc | -1.7% | +8.7% | improvement (-10.4pp) | Volatile run-to-run; not load-bearing. |
| M11 ModifierEHS_Frequency | +9.7% | +8.5% | within judgment band | |
| M12 Pool_Rent_HotPath | **+18.5%** | +20.9% | held | Descriptor-interpreter pool-rent overhead persists; known regression. |
| M13 Setters_Suppression_Scope | -2.1% | -0.9% | within noise | |

**Net signal**:

- **M1 regressed +6.3pp** from the prior advisory — directly attributable
to the strategy pattern-match arms added in
`V1HandlerAdapter.DispatchChildrenMount`. Two `is`-checks fire before
the pattern switch on every Mount, even for leaves that don't use
templated items. Worth folding into the prior `case` switch in a
Phase 4 perf-tuning pass; not load-bearing for correctness.
- **M8 improved -6.6pp** — the `DescriptorHandler.Children` switch
short-circuit for ItemsHost / templated-items strategies is a
structural win.
- **M4 / M5 dispatch wins held**.
- **M12 Pool_Rent_HotPath +18.5%** carry-over — same descriptor-rent
overhead the prior capture already documented; nothing in this branch
intersects.

## Q1 decision matrix — for completeness

Per §13 Q1's pre-committed decision matrix applied to
ReactorDescriptors vs ReactorV2:

| Bench | vs ReactorV2 ns | Q1 band |
|---|---:|---|
| M1 | +20.2% | exceeds 15% — judgment call vs LOC/readability |
| M2 | -0.5% | ship descriptors |
| M5 | -19.5% | ship descriptors (improvement) |
| M7 | +7.3% | judgment-call band |
| M10 | +5.2% | judgment-call band |

**Verdict:** No reopen condition for Q1 — Q1's reopen is gated on
source-gen (§7) landing, not advisory perf noise. The close-out scope's
M1 +21.2% and M12 +18.5% should be confirmed on stable-AC ARM64 before
any spec-text change.

## Caveats

- **Cloud PC noise.** Per the prior README: "noise-prone, advisory.
Do not cite in §13/§14 spec text."
- **ARM64 stable-AC re-capture on `LAPTOP-4MEP83VI` is deferred** for
the §14 ratification gate.

## Reproduce

```powershell
cd C:\Users\andersonch\Code\reactor2
dotnet build tests/perf_bench/PerfBench.ControlModel -c Release -p:Platform=x64
$exe = "tests\perf_bench\PerfBench.ControlModel\bin\x64\Release\net10.0-windows10.0.22621.0\PerfBench.ControlModel.exe"
$out = "docs\specs\047\phase3-results\CPC-ander-YTZ3O-x64-advisory\2026-05-27-phase3-closeout-3x5"
$results = "tests\perf_bench\PerfBench.ControlModel\bin\x64\Release\net10.0-windows10.0.22621.0\results.jsonl"
for ($i = 1; $i -le 3; $i++) {
Remove-Item $results -ErrorAction SilentlyContinue
Start-Process -FilePath $exe -Wait -NoNewWindow # Start-Process -Wait is required;
# `& $exe` does not block on this WinUI app.
Copy-Item $results "$out\launch-$i.jsonl"
}
python "$out\aggregate.py" > "$out\summary.md"
```

See `summary.md` for the full per-bench table.
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Spec 047 §14 Phase 2 (Q1 spike) — aggregate launch-N.jsonl into a means
+ 95% CI table per (bench, variant), and emit the Q1 decision-matrix deltas
(ReactorDescriptors vs ReactorV2, ReactorDescriptors vs ReactorToday).

Usage: python aggregate.py # reads launch-*.jsonl in CWD
"""
import glob
import json
import math
import statistics
from collections import defaultdict


def main():
rows = []
for path in sorted(glob.glob("launch-*.jsonl")):
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
row = json.loads(line)
if row.get("status") != "ok":
continue
rows.append(row)

# Group by (benchId, variant).
buckets = defaultdict(list)
for r in rows:
buckets[(r["benchId"], r["variant"])].append(r)

benches = sorted({b for (b, _) in buckets}, key=_bench_key)
variants = ["ReactorToday", "ReactorV2", "ReactorDescriptors"]

def summarize(rs, key):
vals = [r[key] for r in rs]
if not vals:
return (math.nan, math.nan, 0)
mean = statistics.mean(vals)
if len(vals) > 1:
stdev = statistics.stdev(vals)
# 95% CI half-width for a t-distribution. For n=15 dof=14, t ≈ 2.145.
# Approximate with 1.96 for simplicity — close enough at n≥10.
ci_half = 1.96 * stdev / math.sqrt(len(vals))
else:
ci_half = math.nan
return mean, ci_half, len(vals)

# ── Per-(bench, variant) summary table. ──
print("# Per-(bench, variant) means")
print()
print(f"| Bench | Variant | n | Mean ns | 95% CI ±ns | Mean alloc B | 95% CI ±B |")
print(f"|---|---|---:|---:|---:|---:|---:|")
for b in benches:
for v in variants:
rs = buckets.get((b, v), [])
mean_ns, ci_ns, n = summarize(rs, "meanNs")
mean_b, ci_b, _ = summarize(rs, "allocBytes")
if n == 0:
print(f"| {b} | {v} | 0 | — | — | — | — |")
else:
print(
f"| {b} | {v} | {n} | {mean_ns:,.0f} | {ci_ns:,.0f} "
f"| {mean_b:,.0f} | {ci_b:,.0f} |"
)
print(f"| | | | | | | |")

# ── Q1 decision-matrix deltas. ──
print()
print("# Q1 head-to-head — ReactorDescriptors deltas")
print()
print(
"| Bench | vs ReactorV2 ns | vs ReactorV2 alloc | vs ReactorToday ns | vs ReactorToday alloc | Q1 band |"
)
print("|---|---:|---:|---:|---:|---|")
for b in benches:
ds = buckets.get((b, "ReactorDescriptors"), [])
v2 = buckets.get((b, "ReactorV2"), [])
today = buckets.get((b, "ReactorToday"), [])
d_ns, _, _ = summarize(ds, "meanNs")
d_b, _, _ = summarize(ds, "allocBytes")
v_ns, _, _ = summarize(v2, "meanNs")
v_b, _, _ = summarize(v2, "allocBytes")
t_ns, _, _ = summarize(today, "meanNs")
t_b, _, _ = summarize(today, "allocBytes")

def pct(a, base):
if base and not math.isnan(base) and not math.isnan(a):
return (a - base) / base * 100.0
return math.nan

vs_v2_ns = pct(d_ns, v_ns)
vs_v2_b = pct(d_b, v_b)
vs_t_ns = pct(d_ns, t_ns)
vs_t_b = pct(d_b, t_b)

# §13 Q1 matrix bands keyed off the worst of ns vs V2.
worst = vs_v2_ns
if math.isnan(worst):
band = "-"
elif abs(worst) <= 5:
band = "<=5%: ship descriptors"
elif abs(worst) <= 15:
band = "5-15%: judgment call"
else:
band = ">15%: ship hand-coded"

print(
f"| {b} | {vs_v2_ns:+.1f}% | {vs_v2_b:+.1f}% | {vs_t_ns:+.1f}% | {vs_t_b:+.1f}% | {band} |"
)


def _bench_key(s):
# M1, M2, ..., M13 — sort numerically.
try:
return int(s.lstrip("M"))
except ValueError:
return 999


if __name__ == "__main__":
main()
Loading
Loading