J: E116 time-bucket accounting

EptStateMachine now retains per-state cumulative dwell time so the
host can read it as SVIDs.  The implementation is zero-overhead while
the FSM is idle (no timers, no background work) — on every transition
we add the prior state's dwell to its bucket and reset the entered_
timestamp.  Live dwell in the current state is included in
accumulated() via a now-vs-entered_ delta at read time.

New public API:
  accumulated(EptState)   per-state cumulative ms (incl. live dwell)
  total_elapsed()         denominator for utilization ratios
  reset_history()         S2F43-style history clear

This closes the gap I called out: previously we emitted CEIDs on
transition but didn't accumulate the bucket the host actually queries
for utilization metrics.  Wiring these into specific SVIDs is the
application's job (equipment.yaml declares SVIDs against any read
callable); the runtime data is now there.

4 new test cases cover accumulation, live-dwell inclusion, and reset.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 08:41:09 +02:00
parent 28dac8e9c8
commit 06f664dfab
3 changed files with 85 additions and 1 deletions
+42
View File
@@ -58,3 +58,45 @@ TEST_CASE("EPT: same-state event is a no-op (no handler call)") {
CHECK(ept.on_event(EptEvent::EnterStandby)); // already in Standby
CHECK(calls == 0);
}
TEST_CASE("EPT: time buckets accumulate per state") {
EptStateMachine ept;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
ept.on_event(EptEvent::EnterStandby);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
ept.on_event(EptEvent::EnterProductive);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
ept.on_event(EptEvent::EnterStandby);
// Each bucket should have at least the slept time.
CHECK(ept.accumulated(EptState::NonScheduledTime).count() >= 5);
CHECK(ept.accumulated(EptState::Standby).count() >= 5);
CHECK(ept.accumulated(EptState::Productive).count() >= 5);
// Total = sum of buckets (modulo current-state in-progress dwell).
CHECK(ept.total_elapsed().count() >= 25);
}
TEST_CASE("EPT: current-state dwell is counted in accumulated()") {
EptStateMachine ept;
ept.on_event(EptEvent::EnterProductive);
std::this_thread::sleep_for(std::chrono::milliseconds(15));
// No transition out; accumulated should still include the live dwell.
CHECK(ept.accumulated(EptState::Productive).count() >= 10);
}
TEST_CASE("EPT: reset_history zeroes all buckets") {
EptStateMachine ept;
ept.on_event(EptEvent::EnterStandby);
std::this_thread::sleep_for(std::chrono::milliseconds(5));
ept.on_event(EptEvent::EnterProductive);
REQUIRE(ept.accumulated(EptState::Standby).count() > 0);
ept.reset_history();
for (auto s : {EptState::NonScheduledTime, EptState::ScheduledDowntime,
EptState::UnscheduledDowntime, EptState::Engineering,
EptState::Standby}) {
CHECK(ept.accumulated(s).count() == 0);
}
// Current state (Productive) starts re-accumulating from zero.
CHECK(ept.accumulated(EptState::Productive).count() < 5);
}