[M1-PROF-02] Frame graph / budget report: per-frame records, declared-budget evaluation, --budget-report, exit 3 - #49
Merged
Conversation
…-budget evaluation, --budget-report, exit 3 (#49) - New public header src/laige-sim/include/laige/sim/frame_budget.h (+ frame_budget.cpp): the FR-11.2 per-frame budget report — FrameBudgetRecord (one completed frame's cheap scalars: the tick delta, the frame's sim work ms, the pool-reservations sim-alloc delta, the G-R5 overrun/critical event deltas), the FrameBudgetRecorder fixed 32-frame ring (recordFrame O(1) allocation-free hot path, at(i) oldest-first over the wrapping ring, totalFrames() keeps counting — no silent truncation), and buildFrameBudgetReport (cold format pass, AGENTS 12 field format): every DECLARED budget measured vs declared with a pass/flag — each system's declared SystemDef budget vs its M1-SYS-03 window's p99, sim_tick_avg/sim_tick_p99 (budgets.json) vs the Profiler tick window, sim_heap_allocs (hard zero) vs the per-frame sim-alloc deltas; the M0-CORE-08 budgetCheck blocks embedded verbatim; loud NO_SAMPLES / NO_ENTRY / FAIL; the over-budget systems list; overall=PASS|FAIL. G-R8 exception markers on the raw double tokens (wall-clock diagnostics — never enter sim state, hashes, or replays, ARCH-009). - profiler.h/.cpp: Profiler::tickWindow() (the M1-PROF-02 cold read of the tick-time window for the sim_tick_avg/sim_tick_p99 checks). - engine.h/.cpp: always-on per-frame accumulation in runFrames (two O(1) reads + two O(systemCount) G-R5 counter passes + one O(1) ring write per frame — no allocation, no logging; the run-setup allocation count stays exactly three, HeadlessFramePathAllocatesNothing still pins it); startBudgetReport(path, lastNFrames) (EVERY build; loads the table, builds + CACHES the report at the run's end on every path — readable after shutdown; a budget FAIL never fails the run — CORE-002), budgetReportRequested(), lastBudgetReport(); budget/* structured events (NFR-13.3 5-field grammar); the new members move with the engine (move ctor/assign). - laige-run.cpp: --budget-report [N] (1..32; omitted = all retained; printed to stdout after the profile summary line, even on a failed run), --budgets <path> (flag -> LAIGE_BUDGETS_PATH env -> budgets.json CWD — the laige-bench resolution order), --fail-on-budget (exit 3 when the run COMPLETED but overall=FAIL; run failure stays 1, start/load failure 2); usage text updated for the new flags + exit 3. - Tests: budget_report_tests.cpp (14 tests: the ring semantics, the SYNTHETIC OVER-BUDGET SYSTEM with correct numbers — 2 ms burn vs a 0.1 ms fpx16_16 budget, both G-R5 multipliers exceeded on the nominal floor so runs/warns/errors are exact and preemption-tolerant: the declared budget echoed, measured p99 above it, the over_budget: line, overall=FAIL; the healthy-world PASS; the loud NO_ENTRY/NO_SAMPLES; the engine's cached-after-shutdown report + the G-R5 fold into the per-frame records (10 warns + 10 criticals exact, rate-limiting-off capture sink); the start validation; the record path's zero- allocation, non-sanitizer trees). CTest entries: budget_report (in the TSAN list) + laige_run_budget CLI smoke (every P0 OS job: 30-tick run, --budget-report 4 --budgets <repo>/budgets.json --fail-on-budget, passes iff overall=PASS + exit 0). - Fixture: tests/laige-sim/fixtures/budget_report_sample.txt (a real 0-system run's report — a sample, not a golden; the report carries wall-clock values). - Docs: docs/api/frame_budget.md (new — the declared budgets, the report format + pass/flag semantics, the Engine surface, the CLI + exit 3, Performance, determinism, Testing/CI) + engine.md (CLI flags, exit 3, per-frame cost, misuse, Testing) + profiler.md cross-ref + the docs/README + sim README indexes; laige-api.json regenerated (24 headers); roadmap M1-PROF-02 checked + progress board (M1 22/25) + change log line. - Verified: canonical g++ Debug tree zero-warning, full ctest 91/91 (new budget_report + laige_run_budget entries green; the zero- allocation pin unchanged), clang cross-tree zero-warning, ASan tree budget_report/engine/system_timing green (leak-free), tools/laige-determinism-lint + tools/laige-include-lint green, laige-api-scanner drift check clean.
offdev
force-pushed
the
feat/m1-prof-02-budget-report
branch
from
September 23, 2026 14:55
acda6d2 to
0c7cf5c
Compare
…S per PR cadence)
…, laige-bench precedent)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements M1-PROF-02 · Frame graph / budget report (roadmap
M1-heartbeat.md; PRD FR-11.2, §9.1 S-6, §9.3 G-R5; depends on M1-PROF-01 #47 and M0-CORE-08). Every declared budget — each system's declared time budget, the total tick-time budgets, and the sim allocation-count budget — is measured vs declared with a pass/flag per budget, the over-budget systems are listed (the G-R5 event feed), andlaige-run --budget-reportprints the last-N-frames report in the AGENTS §12 field format with a--fail-on-budgetCI gate (exit 3).1. The report core (
frame_budget.h/frame_budget.cpp, new)FrameBudgetRecord— one completed frame's cheap scalars: the 0-based frame index, the completed tick count, the within-frame tick delta, the frame's sim work ms, the frame's sim allocation count (theWorld::archetypeStats().totalReservationsdelta; 0 = steady state), and the per-frame deltas of the G-R5system/budget_overrunwarn /system/budget_criticalerror counters.FrameBudgetRecorder— fixed 32-frame ring (kFrameBudgetWindow):recordFrameis O(1) and allocates nothing (the hot path); recording beyond the window drops the oldest record andtotalFrames()keeps counting (no silent truncation — CORE-008);at(i)reads oldest-first over the wrapping ring.buildFrameBudgetReport(recorder, profiler, world, budgets, options)— cold format pass (allocates — reporting is never a hot path), the AGENTS §12 field format:SystemDef::budgetMs(fpx16_16, exact — ADR 0002), measured as the M1-SYS-03 rolling window's p99 (the sustained-overrun signal; single recovered overruns stay visible in the per-frame records + thewarns/errorscounters);sim_tick_avg/sim_tick_p99(budgets.json, M0-CORE-08) against theProfiler's tick window (mean/p99) — via the newProfiler::tickWindow()cold accessor;sim_heap_allocs(the PRD §8.1 hard-zero budget) against the retained frames' per-frame sim-alloc deltas (max);budgetCheckblocks embedded verbatim (M0-CORE-08), a per-system section (one line per system, ascending id), theover_budget:list, andoverall=PASS|FAIL.NO_SAMPLES, missing budgets.json entry →NO_ENTRY, and a broken harness foldsoverallto FAIL (a zero-tick run is never silent — CORE-008).2. Engine wiring (
engine.h/engine.cpp)HeadlessFramePathAllocatesNothingstill pins it (the ring is created inEngine::create, engine setup, not run setup).startBudgetReport(budgetsPath, lastNFrames)— EVERY build (diagnostics, not replay state); loads the table at start and builds + caches the report at the run's end on every path (a failed run's report describes what happened; readable after shutdown — the world and the profiler are released in the ordered shutdown). A budget FAIL never fails the run (CORE-002): the caller gates.budgetReportRequested(),lastBudgetReport();budget/*structured events (NFR-13.3 5-field grammar); the new members move with the engine (move ctor/assign).doubletokens — the measured times are wall-clock diagnostics that never enter sim state, hashes, or replays (ARCH-009);tools/laige-determinism-lintgreen.3. CLI (
laige-run)--budget-report [N]— print the last N frames' budget report to stdout at the end of the run (1..32; omitted = all retained), even on a failed run (the run failure dominates the exit code).--budgets <path>— the budgets.json file; resolution: flag →LAIGE_BUDGETS_PATHenv →budgets.jsonin the working directory (thelaige-benchorder).--fail-on-budget— exit 3 when the run completed but the report isoverall=FAIL(the PRD §8.1 CI gate). Exit codes: 0 ok, 1 run failure, 2 usage/IO/config/profile-write/budget-start, 3 budget failure.4. Tests + fixture
budget_reportCTest entry (14 tests, in the TSAN list): the ring semantics; the synthetic over-budget system with correct numbers (a 2 ms burn vs a 0.1 ms fpx16_16 budget — 20×, both G-R5 multipliers exceeded on the nominal floor, soruns=10 warns=10 errors=10and p99 ≥ 2.0 are exact and preemption-tolerant: the declared budget echoed, theover_budget:line,overall=FAIL); the healthy-world PASS; the loud NO_ENTRY / NO_SAMPLES (a zero-tick run); the engine's cached-after-shutdown report + the G-R5 events folded into the per-frame records (10 warns + 10 criticals exact, rate-limiting-off capture sink); the start validation (stopped/empty-path/double/unreadable/malformed); and the record path's zero-allocation (budget-report-recorder-zeroalloc frames=1000 allocs=0, non-sanitizer trees).laige_run_budgetCLI smoke (every P0 OS job): 30-tick run with--budget-report 4 --budgets <repo>/budgets.json --fail-on-budget; passes iff the report printsoverall=PASSand the run exits 0.tests/laige-sim/fixtures/budget_report_sample.txt— a sample, not a golden (the report carries wall-clock values; the suite asserts the machine-greppable structure instead).5. Docs, manifest, roadmap
docs/api/frame_budget.md(declared budgets, report format + pass/flag semantics, Engine surface, CLI + exit 3, Performance, determinism, Testing/CI) +docs/api/engine.md(CLI flags, exit 3, per-frame cost, misuse, Testing) +docs/api/profiler.mdcross-ref + thedocs/README.mdandsrc/laige-sim/README.mdindexes.laige-api.jsonregenerated (24 headers; new budget-surface symbols +Profiler::tickWindow+ the threeEnginemethods); scanner drift check clean.Verification (local)
ctest91/91 (newbudget_report+laige_run_budgetentries green;profiler/engine/system_timingunchanged — the zero-allocation pin intact).budget_report/engine/system_timinggreen (leak-free — the ring and the cached report).tools/laige-determinism-lint+tools/laige-include-lint: green.Found during this step (not fixed — out of scope)
formatProfileSummaryLine(andformatProfileText,profiler.cpp) appendentities_alive=without a leading space, so the profile summary line prints...max=0.004549entities_alive=0(the token runs into the previous field; M1-PROF-01 #47). Cosmetic in the one-line summary only; worth a one-line fix in the next profiler step.