// Copyright Epic Games, Inc. All Rights Reserved. #include "zencore/stats.h" #include #include "zencore/thread.h" #include "zencore/timer.h" #if ZEN_WITH_TESTS # include #endif // // Derived from https://github.com/dln/medida/blob/master/src/medida/stats/ewma.cc // namespace zen { static constinit int kTickIntervalInSeconds = 5; static constinit double kSecondsPerMinute = 60.0; static constinit int kOneMinute = 1; static constinit int kFiveMinutes = 5; static constinit int kFifteenMinutes = 15; static const double kM1_ALPHA = 1.0 - std::exp(-kTickIntervalInSeconds / kSecondsPerMinute / kOneMinute); static const double kM5_ALPHA = 1.0 - std::exp(-kTickIntervalInSeconds / kSecondsPerMinute / kFiveMinutes); static const double kM15_ALPHA = 1.0 - std::exp(-kTickIntervalInSeconds / kSecondsPerMinute / kFifteenMinutes); static const uint64_t CountPerTick = GetHifreqTimerFrequencySafe() * kTickIntervalInSeconds; static const uint64_t CountPerSecond = GetHifreqTimerFrequencySafe(); ////////////////////////////////////////////////////////////////////////// void RawEWMA::Tick(double Alpha, uint64_t Interval, uint64_t Count, bool IsInitialUpdate) { const double InstantRate = double(Count) / Interval; if (IsInitialUpdate) { m_Rate.store(InstantRate, std::memory_order_release); } else { m_Rate.fetch_add(Alpha * (InstantRate - m_Rate)); } } double RawEWMA::Rate() const { return m_Rate.load(std::memory_order_relaxed) * CountPerSecond; } ////////////////////////////////////////////////////////////////////////// Meter::Meter() : m_StartTick{GetHifreqTimerValue()}, m_LastTick(m_StartTick.load()) { } Meter::~Meter() { } void Meter::TickIfNecessary() { uint64_t OldTick = m_LastTick.load(); const uint64_t NewTick = GetHifreqTimerValue(); const uint64_t Age = NewTick - OldTick; if (Age > CountPerTick) { // Ensure only one thread at a time updates the time. This // works because our tick interval should be sufficiently // long to ensure two threads don't end up inside this block if (m_LastTick.compare_exchange_strong(OldTick, NewTick)) { m_Remain.fetch_add(Age); do { int64_t Remain = m_Remain.load(std::memory_order_relaxed); if (Remain < 0) { return; } if (m_Remain.compare_exchange_strong(Remain, Remain - CountPerTick)) { Tick(); } } while (true); } } } void Meter::Tick() { const uint64_t PendingCount = m_PendingCount.exchange(0); const bool IsFirstTick = m_IsFirstTick; if (IsFirstTick) { m_IsFirstTick = false; } m_RateM1.Tick(kM1_ALPHA, CountPerTick, PendingCount, IsFirstTick); m_RateM5.Tick(kM5_ALPHA, CountPerTick, PendingCount, IsFirstTick); m_RateM15.Tick(kM15_ALPHA, CountPerTick, PendingCount, IsFirstTick); } double Meter::Rate1() { TickIfNecessary(); return m_RateM1.Rate(); } double Meter::Rate5() { TickIfNecessary(); return m_RateM5.Rate(); } double Meter::Rate15() { TickIfNecessary(); return m_RateM15.Rate(); } double Meter::MeanRate() { const uint64_t Count = m_TotalCount.load(std::memory_order_relaxed); if (Count == 0) { return 0.0; } const uint64_t Elapsed = GetHifreqTimerValue() - m_StartTick; return (double(Count) * GetHifreqTimerFrequency()) / Elapsed; } void Meter::Mark(uint64_t Count) { TickIfNecessary(); m_TotalCount.fetch_add(Count); m_PendingCount.fetch_add(Count); } ////////////////////////////////////////////////////////////////////////// #if ZEN_WITH_TESTS TEST_CASE("EWMA") { SUBCASE("Simple_1") { RawEWMA Ewma1; Ewma1.Tick(kM1_ALPHA, CountPerSecond, 5, true); CHECK(fabs(Ewma1.Rate() - 5) < 0.1); for (int i = 0; i < 60; ++i) { Ewma1.Tick(kM1_ALPHA, CountPerSecond, 10, false); } CHECK(fabs(Ewma1.Rate() - 10) < 0.1); for (int i = 0; i < 60; ++i) { Ewma1.Tick(kM1_ALPHA, CountPerSecond, 20, false); } CHECK(fabs(Ewma1.Rate() - 20) < 0.1); } SUBCASE("Simple_10") { RawEWMA Ewma1; RawEWMA Ewma5; RawEWMA Ewma15; Ewma1.Tick(kM1_ALPHA, CountPerSecond, 5, true); Ewma5.Tick(kM5_ALPHA, CountPerSecond, 5, true); Ewma15.Tick(kM15_ALPHA, CountPerSecond, 5, true); CHECK(fabs(Ewma1.Rate() - 5) < 0.1); CHECK(fabs(Ewma5.Rate() - 5) < 0.1); CHECK(fabs(Ewma15.Rate() - 5) < 0.1); auto Tick1 = [&Ewma1](auto Value) { Ewma1.Tick(kM1_ALPHA, CountPerSecond, Value, false); }; auto Tick5 = [&Ewma5](auto Value) { Ewma5.Tick(kM5_ALPHA, CountPerSecond, Value, false); }; auto Tick15 = [&Ewma15](auto Value) { Ewma15.Tick(kM15_ALPHA, CountPerSecond, Value, false); }; for (int i = 0; i < 60; ++i) { Tick1(10); Tick5(10); Tick15(10); } CHECK(fabs(Ewma1.Rate() - 10) < 0.1); for (int i = 0; i < 5 * 60; ++i) { Tick1(20); Tick5(20); Tick15(20); } CHECK(fabs(Ewma1.Rate() - 20) < 0.1); CHECK(fabs(Ewma5.Rate() - 20) < 0.1); for (int i = 0; i < 16 * 60; ++i) { Tick1(100); Tick5(100); Tick15(100); } CHECK(fabs(Ewma1.Rate() - 100) < 0.1); CHECK(fabs(Ewma5.Rate() - 100) < 0.1); CHECK(fabs(Ewma15.Rate() - 100) < 0.5); } } # if 0 // This is not really a unit test, but mildly useful to exercise some code TEST_CASE("Meter") { Meter Meter1; Meter1.Mark(1); Sleep(1000); Meter1.Mark(1); Sleep(1000); Meter1.Mark(1); Sleep(1000); Meter1.Mark(1); Sleep(1000); Meter1.Mark(1); Sleep(1000); Meter1.Mark(1); Sleep(1000); Meter1.Mark(1); Sleep(1000); Meter1.Mark(1); Sleep(1000); Meter1.Mark(1); Sleep(1000); [[maybe_unused]] double Rate = Meter1.MeanRate(); } # endif void stats_forcelink() { } #endif } // namespace zen