diff --git a/include/secsgem/gem/ept_state.hpp b/include/secsgem/gem/ept_state.hpp index 621f97f..ec98a60 100644 --- a/include/secsgem/gem/ept_state.hpp +++ b/include/secsgem/gem/ept_state.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -65,10 +66,26 @@ class EptStateMachine { // doesn't need a parallel timestamp tracker. bool on_event(EptEvent e); + // --- E116 time-bucket accounting -------------------------------------- + // Total accumulated time spent in `s` since the FSM was constructed + // or since reset_history() was last called. Time spent in the + // *current* state up to now is included. + std::chrono::milliseconds accumulated(EptState s) const; + + // Total time across all states (= time since reset_history()). Useful + // as the denominator for utilization / availability ratios. + std::chrono::milliseconds total_elapsed() const; + + // S2F43-style history clear. Sets every bucket to zero and re-anchors + // the current-state timer at "now". + void reset_history(); + private: EptTable table_; EptState state_ = EptState::NonScheduledTime; std::chrono::steady_clock::time_point entered_; + std::chrono::steady_clock::time_point reset_anchor_; + std::array buckets_{}; // indexed by EptState 0..5 StateChangeHandler on_change_; }; diff --git a/src/gem/ept_state.cpp b/src/gem/ept_state.cpp index e3fc548..a635601 100644 --- a/src/gem/ept_state.cpp +++ b/src/gem/ept_state.cpp @@ -78,7 +78,8 @@ EptTable default_ept_table() { EptStateMachine::EptStateMachine() : table_(default_ept_table()), - entered_(std::chrono::steady_clock::now()) {} + entered_(std::chrono::steady_clock::now()), + reset_anchor_(entered_) {} bool EptStateMachine::on_event(EptEvent e) { const auto* row = table_.find(state_, e); @@ -88,6 +89,7 @@ bool EptStateMachine::on_event(EptEvent e) { auto now = std::chrono::steady_clock::now(); auto dwell = std::chrono::duration_cast( now - entered_); + buckets_[static_cast(prev)] += dwell; state_ = *row->to; entered_ = now; if (on_change_) on_change_(prev, state_, e, dwell); @@ -95,4 +97,27 @@ bool EptStateMachine::on_event(EptEvent e) { return true; } +std::chrono::milliseconds EptStateMachine::accumulated(EptState s) const { + const auto idx = static_cast(s); + if (idx >= buckets_.size()) return std::chrono::milliseconds{0}; + auto base = buckets_[idx]; + if (state_ == s) { + const auto now = std::chrono::steady_clock::now(); + base += std::chrono::duration_cast(now - entered_); + } + return base; +} + +std::chrono::milliseconds EptStateMachine::total_elapsed() const { + return std::chrono::duration_cast( + std::chrono::steady_clock::now() - reset_anchor_); +} + +void EptStateMachine::reset_history() { + buckets_.fill(std::chrono::milliseconds{0}); + const auto now = std::chrono::steady_clock::now(); + entered_ = now; + reset_anchor_ = now; +} + } // namespace secsgem::gem diff --git a/tests/test_ept.cpp b/tests/test_ept.cpp index 3e90cf4..c3bff00 100644 --- a/tests/test_ept.cpp +++ b/tests/test_ept.cpp @@ -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); +}