# Phase D Stage 2 — Manifest Builder: Result **Date**: 2026-05-18 **Outcome**: **LANDED.** Python builder distills Stage-1's cvar-ON canary JSONL into a replay-ready `contention_manifest.json`. 9/9 unit tests pass. ## Source change | file | LOC | purpose | |---|---|---| | [xenia-rs/tools/diff-events/build_contention_manifest.py](../../tools/diff-events/build_contention_manifest.py) | 175 | the builder itself (parser + filter + sort + dedupe + sha256 + summary) | | [xenia-rs/tools/diff-events/test_build_manifest.py](../../tools/diff-events/test_build_manifest.py) | 240 | 9 tests: basic extract, kind filter, contended=false filter, sort by (tid,idx), dedupe, missing-field skip, bad-json skip, summary rendering, empty input | | **Total** | **~415 LOC tooling, zero engine LOC** | | Engine source UNCHANGED. This is pure Python tooling. ## Manifest schema (v1) ```json { "version": 1, "source_canary_jsonl": "", "source_canary_sha256": "", "built_at_host_unix": , "summary": { "total_input_events": , "total_contention_events_kept": , "skipped_bad_lines": , "skipped_duplicate_keys": , "per_tid_counts": { "": , ... } }, "entries": [ { "tid": , "tid_event_idx": , "site_sid": "", "cs_ptr": "0xHHHHHHHH", "contended": true }, ... ] } ``` Entries sorted by `(tid asc, tid_event_idx asc)`. Stage 3's ours-side loader keys on `(tid, tid_event_idx)` for O(1) lookup. The `site_sid` field is the cross-engine identity (C+18 shared-global recipe); the `cs_ptr` is the guest VA (also identical across engines because the guest manages the struct). ## Tests ``` $ python3 xenia-rs/tools/diff-events/test_build_manifest.py PASS test_basic_extract PASS test_filters_non_contention_kinds PASS test_filters_contended_false PASS test_sorts_by_tid_then_idx PASS test_deduplicates_same_tid_idx PASS test_skips_missing_fields PASS test_handles_bad_json_lines PASS test_render_summary_human_readable PASS test_empty_input_yields_zero_kept ALL 9 TESTS PASS ``` ## End-to-end run against Stage 1's cvar-ON trace ``` $ python3 build_contention_manifest.py \ --canary-jsonl xenia-rs/audit-runs/phase-d-stage1/canary-cvaron-trunc.jsonl \ --out xenia-rs/audit-runs/phase-d-stage1/contention_manifest.json contention manifest built from source sha256: 80b9b1901c6b95461d7702c1923f79c44e34778d1f716431d0a8ce99f5945115 total input events scanned: 569,360 contention events kept: 807 bad/skipped lines: 0 duplicate (tid,idx) skipped: 0 per-tid counts: tid= 6 276 <- main, ↔ ours tid=1 tid= 9 109 tid= 10 34 tid= 11 7 tid= 13 180 tid= 14 8 tid= 16 35 tid= 17 27 tid= 18 22 tid= 22 2 tid= 26 18 tid= 29 89 ``` Manifest file size: 122,846 bytes (122 KB), trivial to load in Stage 3. ## Critical entry preserved The plan calls for the manifest to include a `(tid=6, idx≈104,605)` entry near the 104,607 cap. Verified: ```python >>> hit = [e for e in manifest['entries'] ... if e['tid']==6 and e['tid_event_idx']==104664] >>> hit [{'tid': 6, 'tid_event_idx': 104664, 'site_sid': 'c26a128bf45411f7', 'cs_ptr': '0xbc65c890', 'contended': True}] ``` This is the entry Stage 3's `rtl_enter_critical_section` will key on: when ours's tid=1 reaches per-tid ordinal 104,664 on a CS at `0xbc65c890`, force a park via `BlockReason::CriticalSection`. ## Manifest distribution observations 20 distinct cs_ptrs / 20 distinct site_sids (1-to-1, as expected from the deterministic SID recipe). The first 3 tid=6 entries all target the same CS `0xbc65c890`: | idx | cs_ptr | |---|---| | 102,788 | 0xbc65c890 | | 104,664 | 0xbc65c890 | | 106,368 | 0xbc65c890 | So `0xbc65c890` is a hot CS contended 3 times across tid=6's first ~106k events. The last 3 tid=6 entries are on DIFFERENT CSes: | idx | cs_ptr | |---|---| | 248,056 | 0xbca44fc8 | | 249,124 | 0xbccc5508 | | 249,671 | 0xbccc5508 | Manifest is therefore non-trivial — not a single-CS pattern. Stage 3's loader must consult the manifest by `(tid, idx)` and NOT assume any particular CS is "the contended one." ## Numbers vs plan estimate Plan estimated "<100 contention entries across the whole boot given the wait-light profile." Actual = 807 in the truncated trace (250k tid=6 events / 20k per sister). Full untruncated trace = 7,135 events. Plan was ~7× off; the larger manifest is fine — JSON parses in <1ms, lookups are O(1) via dict. ## Phase B image hash `image_loaded_sha256 = ea8d160e…` — UNCHANGED (no Phase B touchpoints). ## Reading-error class No new class. Pure-Python tooling, no engine sources touched. ## Artifacts - [build_contention_manifest.py](../../tools/diff-events/build_contention_manifest.py) - [test_build_manifest.py](../../tools/diff-events/test_build_manifest.py) - [contention_manifest.json](../phase-d-stage1/contention_manifest.json) — built from Stage 1's cvar-ON trace ## Next session **Stage 3** = ours-side `OrderMode::ContentionReplay` mode + manifest loader + forced-park branch in `rtl_enter_critical_section` (~250 LOC across `scheduler.rs`, new `contention_manifest.rs` module in `xenia-kernel`, `exports.rs:2886-2946`). Acceptance: main matched-prefix advances past 104,607 (target ≥106,000) with stable digest × 3 cold runs under replay mode. Default mode (no manifest passed) byte-identical to current `ba5b5e07…`. Stage 4 (diff-tool ENGINE_LOCAL_KINDS for `contention.observed`) lands before any cross-engine diff over a cvar-ON trace.