Skip to content

Fix Windows/macOS CI: profiler MSVC /WX warnings; drift-proof profiler-cost gate - #48

Merged
offdev merged 3 commits into
masterfrom
fix-ci-windows-macos-profiler
Sep 22, 2026
Merged

offdev merged 3 commits into
masterfrom
fix-ci-windows-macos-profiler

Conversation

@offdev

@offdev offdev commented Sep 22, 2026

Copy link
Copy Markdown
Owner

Fixes the two P0 jobs broken by the M1-PROF-01 merge (#47): windows-msvc (build) and macOS arm64 (test). All other jobs in the failed run were green.

1. windows-msvc — build failure in the new profiler code

profiler.cpp (new in #47) had never been compiled under MSVC's /W4 /WX. The CI log's exact warning set:

  • C4244 (treated as error): the seven uint64_t counters implicitly converted to double at JsonValue::fromNumber in formatProfileJson (ticks, frames, draw_calls, texture_binds, net_bytes, sim_allocs, system runs; the uint32_t fields convert exactly and are left alone). Fixed with explicit static_cast<double> + the per-line LAIGE-DETERM-EXCEPTION marker for the double token (determinism-lint policy; the existing stats.n cast precedent).
  • C4996 (treated as error): plain std::fopen in writeProfile. Fixed with a portable openProfileFile helper following the _fsopen(..., _SH_DENYNO) precedent of replay.cpp / config.cpp / logging.cpp (plain-fopen sharing semantics — fopen_s would open _SH_SECURE and deny re-open).
  • Latent, same class: profiler_tests.cpp used plain std::fopen in readFile/fileExists — it never compiled on Windows in the failed run because the build stopped at profiler.cpp. Same precedent via an openReportFile helper (replay_record_tests.cpp precedent).

2. macos-arm64 — ProfilerCost.EnabledCostBoundedToOnePercent at 2.14% vs the 1% gate

on_p50=0.260291 off_p50=0.254833, while the canonical baseline of the same measurement is 0.28% — the profiler's fixed per-tick cost (two steady_clock reads + one O(1) ring write) is far below 2% of any supported platform's 10k-entity tick. Root cause: the original BLOCKED arm order (all ON runs before all OFF runs) measured the two arms in two different phases of the CI runner's frequency/thermal drift under sustained load — the drift itself showed up as overhead, one direction only.

Verified locally on the baseline's own CPU (Ryzen 7950X3D, sustained load): the blocked order spread -1.9% … +1.3% across 10 runs while the true overhead is ~0.03–0.08%. (Per-run best-of-N interleaving was tried first and still spread ±1% — the phase noise lives on a seconds scale, not a run-pair scale.)

Fix: per-tick A/B interleaving — the profiler is enabled on every other tick (runtime setEnabled, the DBG-002 switch), so each ON sample is adjacent (~1 ms) to an OFF sample; both subsequences sample the same machine-state trajectory and the phase cancels at the adjacent-sample level. A transient CI stall slows at most one tick = one sample of 2000 in one window — no p50 effect, so no best-of-N repetition is needed. After the fix: 10/10 runs at -0.01% … +0.08% (12–30× inside the gate). The 1% gate, the workload, and the machine-greppable profiler-cost line format are unchanged.

Verification (local)

  • canonical g++ and clang++ trees: zero-warning builds, full ctest 89/89 green (incl. profiler, api-real-tree, determinism-lint-real-tree, laige_run_smoke)
  • ASan tree: zero-warning build, profiler suite green (the ProfilerCost gate stays excluded from sanitizer trees — the LAIGE_ALLOC_COUNTER gate, unchanged)
  • include-lint + determinism-lint OK; no public API surface change (laige-api.json untouched)
  • the 12 hello_* failures of the first local run were a race on the shared samples/hello/bin/ binaries (three local trees building the same source-tree path in parallel — CI checkouts are isolated); sequential per-tree runs are green
  • MSVC and AppleClang cannot run here — verified through this PR's ci:windows / ci:macos jobs (labelled in order; see run history)

…r-cost gate

The M1-PROF-01 merge (#47) broke two P0 jobs on master:

- windows-msvc (BUILD): profiler.cpp (new in the merge) had never been
  compiled under MSVC's /W4 /WX. The CI log's exact warning set:
  * C4244 (treated as error): the seven uint64_t counters implicitly
    converted to double at JsonValue::fromNumber in formatProfileJson
    (ticks, frames, draw_calls, texture_binds, net_bytes, sim_allocs,
    system runs; the uint32_t fields convert exactly and are left
    alone). Fixed with explicit static_cast<double> + the per-line
    LAIGE-DETERM-EXCEPTION marker for the double token (the
    determinism-lint policy; the existing stats.n cast precedent).
  * C4996 (treated as error): plain std::fopen in writeProfile. Fixed
    with a portable openProfileFile helper following the
    _fsopen(..., _SH_DENYNO) precedent of replay.cpp / config.cpp /
    logging.cpp (plain-fopen sharing semantics).

- windows-msvc (TEST, latent): profiler_tests.cpp used plain
  std::fopen in readFile/fileExists — uncompiled in the failed run
  because the build stopped at profiler.cpp. Same _fsopen(_SH_DENYNO)
  precedent via an openReportFile helper (replay_record_tests.cpp
  precedent).

- macos-arm64 (TEST): ProfilerCost.EnabledCostBoundedToOnePercent
  measured 2.14% against the 1% gate (on_p50=0.260291
  off_p50=0.254833) while the canonical baseline of the same
  measurement is 0.28% — the profiler's fixed per-tick cost (two
  steady_clock reads + one O(1) ring write) is far below 2% of any
  supported platform's 10k-entity tick. Root cause: the original
  BLOCKED arm order (all ON runs before all OFF runs) measured the
  two arms in two different phases of the CI runner's frequency/
  thermal drift under sustained load — the drift itself showed up
  as overhead, one direction only. Verified locally on the
  baseline's own CPU (Ryzen 7950X3D, sustained load): the blocked
  order spread -1.9%...+1.3% over 10 runs while the true overhead is
  ~0.03-0.08%. Fix: per-tick A/B interleaving — the profiler is
  enabled on every other tick (a runtime setEnabled, the DBG-002
  switch), so each ON sample is adjacent (~1 ms) to an OFF sample;
  both subsequences sample the same machine-state trajectory, and
  the phase cancels at the adjacent-sample level. A transient CI
  stall slows at most one tick = one sample of 2000 in one window
  (no p50 effect — no best-of-N repetition needed). After the fix:
  10/10 runs at -0.01%...+0.08% (12-30x inside the gate). The 1%
  gate, the workload, and the machine-greppable line format are
  unchanged.

Verified: canonical g++ and clang++ trees build zero-warning, full
ctest 89/89 green (incl. the profiler entry, api-real-tree,
determinism-lint-real-tree, laige_run_smoke); the ASan tree builds
zero-warning and the profiler suite passes there (the ProfilerCost
gate stays excluded from sanitizer trees — the LAIGE_ALLOC_COUNTER
gate, unchanged). include-lint + determinism-lint OK; no public API
surface change (laige-api.json untouched). The 12 hello_* failures of
the first local run were a race on the shared samples/hello/bin/
binaries (three local trees building the same source-tree path in
parallel — CI checkouts are isolated); sequential per-tree runs are
green. MSVC and AppleClang verified through the PR's ci:windows /
ci:macos jobs.
Every non-instrumented P0 CI job enforces the 1% enabled-cost gate
(the linux-gcc and linux-clang jobs AND the windows-msvc and both
macOS jobs' ctest alike) — the macOS arm64 job's run of the gate is
exactly what exposed the blocked-arm-order drift in #47's merge;
the old 'the CI linux-gcc and linux-clang P0 jobs' wording understated
the live scope.
@offdev
offdev merged commit b5940bc into master Sep 22, 2026
11 checks passed
offdev added a commit that referenced this pull request Sep 23, 2026
offdev added a commit that referenced this pull request Sep 23, 2026
offdev added a commit that referenced this pull request Sep 23, 2026
…-budget evaluation, --budget-report, exit 3 (#49)

* [M1-PROF-02] Frame graph / budget report: per-frame records, declared-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.

* Record commit 0c7cf5c in the M1-PROF-02 changelog row (PR #49)

* Regenerate laige-api.json: FrameBudgetRecorder::at doc summary (0..count() - 1)

* Fix MSVC /WX in budget_report tests: _fsopen via openBudgetsFile (the #48 C4996 precedent)

* No change: retrigger PR CI with the ci:windows P0 selection (one P0 OS per PR cadence)

* Fix MSVC /WX in laige-run: envValue via getenv_s (the #48 C4996 class, laige-bench precedent)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant