diff --git a/docs/api/system_timing.md b/docs/api/system_timing.md index b1283e2..fb11cef 100644 --- a/docs/api/system_timing.md +++ b/docs/api/system_timing.md @@ -38,6 +38,15 @@ the window — the measurement is the system's work, not the scheduler's. The sample is handed to the system's rolling window and the budget check in schedule order. +A run shorter than the platform's `steady_clock` tick measures as +exactly `0.0` ms (the start and end reads land on the same tick — +e.g. a sub-tick run on a platform whose clock tick is tens of +nanoseconds or more). `0.0` is a legitimate reading, not a failure +state: it records in the window like any sample, and the budget check +simply sees a run far under budget. Resolution is platform-sensitive +(ARCH-009), so consumers must not treat a `0.0` measurement as +"not measured". + ## The rolling window One `Histogram` per system (M0-CORE-08): fixed capacity diff --git a/laige-api.json b/laige-api.json index a3440f6..706b8ff 100644 --- a/laige-api.json +++ b/laige-api.json @@ -520,9 +520,9 @@ {"name": "laige::SystemInfo::declaresWrite", "kind": "method", "header": "src/laige-sim/include/laige/sim/system.h", "line": 514, "signature": "[[nodiscard]] bool declaresWrite(ComponentTypeId componentId) const noexcept", "summary": "True when the system declares `componentId` for writing.", "budget": "O(1); no allocation.", "experimental": false}, {"name": "laige::SystemTimingStats", "kind": "struct", "header": "src/laige-sim/include/laige/sim/system.h", "line": 536, "signature": "struct SystemTimingStats", "summary": "One system's measured-run scalars (M1-SYS-03; PRD §9.3 G-R5): the cheap per-frame snapshot the profiler (M1-PROF-01) and the frame graph (M1-PROF-02) pull through World::systemTimingStats(id) — O(1), no allocation, no side effects. The window SAMPLES are not here (the rolling histogram is read cold through World::systemTimingWindow(id) — its stats() is O(n log n)). All counters are since-construction; the window rolls across ticks (kSystemTimingWindowSamples, not per-frame).", "budget": null, "experimental": false}, {"name": "laige::SystemTimingStats::runs", "kind": "variable", "header": "src/laige-sim/include/laige/sim/system.h", "line": 538, "signature": "std::uint64_t runs{}", "summary": "Measured runs of the system since world construction.", "budget": null, "experimental": false}, - {"name": "laige::SystemTimingStats::lastMs", "kind": "variable", "header": "src/laige-sim/include/laige/sim/system.h", "line": 541, "signature": "double lastMs{}", "summary": "The measured time (ms) of the most recent run (0 before the first run).", "budget": null, "experimental": false}, - {"name": "laige::SystemTimingStats::warns", "kind": "variable", "header": "src/laige-sim/include/laige/sim/system.h", "line": 543, "signature": "std::uint32_t warns{}", "summary": "The system/budget_overrun warns issued since construction.", "budget": null, "experimental": false}, - {"name": "laige::SystemTimingStats::errors", "kind": "variable", "header": "src/laige-sim/include/laige/sim/system.h", "line": 546, "signature": "std::uint32_t errors{}", "summary": "The system/budget_critical error events issued since construction.", "budget": null, "experimental": false}, - {"name": "LAIGE_SYSTEM", "kind": "macro", "header": "src/laige-sim/include/laige/sim/system.h", "line": 570, "signature": "#define LAIGE_SYSTEM(Name, budget_ms, ...)", "summary": "Declare a system (FR-1.3): at namespace scope, directly above the plain system function's definition. `Name` is both the C++ function name and the system's registration name (stringified); `budget_ms` is the declared per-tick time budget in milliseconds (a numeric literal, e.g. 1 or 0.5 — converted to the exact fpx16_16 once, at program start); the optional trailing `Dep...` names are the depends_on spec (M1-SYS-02): the registration names of the systems `Name` must run after, stringified verbatim into the def's `dependsOn` field (comma-separated, as written). Expands to the function declaration plus", "budget": null, "experimental": false} + {"name": "laige::SystemTimingStats::lastMs", "kind": "variable", "header": "src/laige-sim/include/laige/sim/system.h", "line": 544, "signature": "double lastMs{}", "summary": "The measured time (ms) of the most recent run (0 before the first run). A run shorter than the platform's steady_clock tick measures as exactly 0.0 ms — a legitimate sub-resolution reading (wall-clock resolution is platform-sensitive; ARCH-009), not a failure state.", "budget": null, "experimental": false}, + {"name": "laige::SystemTimingStats::warns", "kind": "variable", "header": "src/laige-sim/include/laige/sim/system.h", "line": 546, "signature": "std::uint32_t warns{}", "summary": "The system/budget_overrun warns issued since construction.", "budget": null, "experimental": false}, + {"name": "laige::SystemTimingStats::errors", "kind": "variable", "header": "src/laige-sim/include/laige/sim/system.h", "line": 549, "signature": "std::uint32_t errors{}", "summary": "The system/budget_critical error events issued since construction.", "budget": null, "experimental": false}, + {"name": "LAIGE_SYSTEM", "kind": "macro", "header": "src/laige-sim/include/laige/sim/system.h", "line": 573, "signature": "#define LAIGE_SYSTEM(Name, budget_ms, ...)", "summary": "Declare a system (FR-1.3): at namespace scope, directly above the plain system function's definition. `Name` is both the C++ function name and the system's registration name (stringified); `budget_ms` is the declared per-tick time budget in milliseconds (a numeric literal, e.g. 1 or 0.5 — converted to the exact fpx16_16 once, at program start); the optional trailing `Dep...` names are the depends_on spec (M1-SYS-02): the registration names of the systems `Name` must run after, stringified verbatim into the def's `dependsOn` field (comma-separated, as written). Expands to the function declaration plus", "budget": null, "experimental": false} ] } diff --git a/src/laige-sim/include/laige/sim/system.h b/src/laige-sim/include/laige/sim/system.h index b6b5377..b86a74c 100644 --- a/src/laige-sim/include/laige/sim/system.h +++ b/src/laige-sim/include/laige/sim/system.h @@ -537,7 +537,10 @@ struct SystemTimingStats { // Measured runs of the system since world construction. std::uint64_t runs{}; // The measured time (ms) of the most recent run (0 before the first - // run). + // run). A run shorter than the platform's steady_clock tick + // measures as exactly 0.0 ms — a legitimate sub-resolution reading + // (wall-clock resolution is platform-sensitive; ARCH-009), not a + // failure state. double lastMs{}; // The system/budget_overrun warns issued since construction. std::uint32_t warns{}; @@ -597,7 +600,8 @@ struct SystemTimingRecord { // constructor, so the record holds it as a unique_ptr — the one // level of indirection is bounded by kMaxSystems). std::unique_ptr window; - double lastMs{}; // most recent measured run (0 before first) + double lastMs{}; // most recent measured run (0 before first; a run + // shorter than the steady_clock tick reads 0.0) std::uint64_t runs{}; // measured runs since construction std::uint32_t warns{}; // budget_overrun warns issued std::uint32_t errors{}; // budget_critical errors issued diff --git a/tests/laige-sim/system_timing_tests.cpp b/tests/laige-sim/system_timing_tests.cpp index 17e6b5e..9b5d956 100644 --- a/tests/laige-sim/system_timing_tests.cpp +++ b/tests/laige-sim/system_timing_tests.cpp @@ -122,7 +122,10 @@ void burnMs(double ms) { const auto start = std::chrono::steady_clock::now(); while (std::chrono::duration_cast>( std::chrono::steady_clock::now() - start).count() < ms) { - gBurnSink += 1; + // Ordinary assignment: compound assignment to a volatile is + // deprecated in C++20 (P1152R2) and a hard error under + // -Werror,-Wdeprecated-volatile (AppleClang on the macos-14 job). + gBurnSink = gBurnSink + 1; } } @@ -306,8 +309,13 @@ TEST(SystemTiming, HealthyTicksLogNothingAndTrackStats) { EXPECT_EQ(st.value().warns, 0u); EXPECT_EQ(st.value().errors, 0u); // A noop system on an empty world runs in microseconds — well - // under its 1 ms budget, with margin for a slow machine. - EXPECT_GT(st.value().lastMs, 0.0); + // under its 1 ms budget, with margin for a slow machine. The lower + // bound is >= 0.0, not > 0.0: a run shorter than the platform's + // steady_clock tick measures as exactly 0.0 ms (the start and end + // reads land on the same tick), and that sub-resolution reading is + // a legitimate value, not a failure (the tracked-state check is + // the runs counter above; lastMs is a diagnostic). + EXPECT_GE(st.value().lastMs, 0.0); EXPECT_LT(st.value().lastMs, 1.0); const laige::Histogram* win = w.systemTimingWindow(SystemId{1});