/*** Copyright (C) 2022 J Reece Wilson (a/k/a "Reece"). All rights reserved. File: ThroughputCalculator.hpp Date: 2022-12-06 Author: Reece ***/ #pragma once namespace Aurora::Utility { struct ThroughputCalculator { // call me arbitrarily double inline OnUpdate(AuUInt uUnit) { OnTick(); this->uTotal += uUnit; this->uTotalLifetime += uUnit; return this->dCurFreq; } // or: void inline AddData(AuUInt uUnit) { this->uTotal += uUnit; this->uTotalLifetime += uUnit; } void inline AddSampleSampleTick() // at around 1 tick/sec { OnTick(); } // then call me arbitrarily: double inline GetEstimatedHertz() const { auto uNow = Aurora::Time::SteadyClockNS(); auto uDelta = uNow - this->uLast; // we cannot do anything on frame zero if (!this->dCurFreq) { return 0; } static const auto kOneSecond = AuMSToNS(AuSToMS(1)); auto dDeltaWeight = ((double)uDelta / (double)kOneSecond); // is overshooting over 1s? if (uDelta > kOneSecond) { if (uDelta > this->uLastDelta) { // we're in uncharted territory. delaying til frame +1 and overshooting by +1 should be fine. return 0; } // # otherwise return constant last tick freqency of this->dCurFreq //return this->dCurFreq; // ## dNextFactor: we are at-least the amount of bytes in pending frame since last tick, over the time period of at least 1 second return AuMax(/*dNextFactor: (effectively unit/time)*/ double(this->uTotal) / dDeltaWeight, /* ordinarily a large unit-frame within the tick wouldn't jitter the throughput bc OnTick would normalized the value. however, once over a second, it makes sense to account for pending bytes/`this->uTotal` / time into frame as a baseline. if we're under a second since the last tick, we can simply extrapolate from the normalized frequency, to the current frames `this->uTotal` / uDelta, in a "linear" manner. ok, not so linear, ubytes can change, but it should give us realistic smoothed out bandwidth statistics. it's more like two fractions of time where one is inverted. see: the last expression */ this->dCurFreq /* use the normalized frequency. a single small frame shouldn't significantly jitter the throughput. */); } // otherwise return live frequency... if (uDelta > this->uLastDelta) // last normalized this->dCurFreq, if not current frame if suddenly peaking { return this->dCurFreq; return AuMax(/*dNextFactor: (effectively unit/time)*/ double(this->uTotal) / dDeltaWeight, this->dCurFreq); } else // fractional lerp { //return this->dCurFreq; double dNextFactor = double(this->uTotal) * dDeltaWeight; // ...by calculating the weight of the current tick in terms of delta between now and the previous frame. // the bit we don't know will be the last frames throughput freqency added to the current throughput * delta. // combined, we get a transition to this->uTotal whose starting point is accelerated by this->dCurFreq return (dNextFactor) + (this->dCurFreq * (double(1.f) - dDeltaWeight) /*last frame units/s extrapolated forward by the unit of time that hasn't passed*/); } } AuUInt64 inline GetTotalStats() { return this->uTotalLifetime; } AuUInt64 inline GetLastFrameTimeSteady() { return this->uLast; } AuInt64 inline GetLastFrameTimeWall() { return this->uLastWall; } private: void inline OnTick() { auto uNow = Aurora::Time::SteadyClockNS(); auto uDelta = uNow - this->uLast; this->uLastDelta = uDelta; this->uLast = uNow; this->uLastWall = Aurora::Time::CurrentClockMS(); auto dAbsMax = double(this->uTotal) / (double(uDelta) / (double)AuMSToNS(AuSToMS(1))); auto dAbsMin = this->dCurFreq * (double)0.5f; dAbsMax = (dAbsMax * 0.5) + (dAbsMin * 0.5);// without this, our numbers are too noisey and far ahead of ThroughputCalculator.hpp.buffered. // in either scenario, ThroughputCalculator.hpp.buffered matches system specs on IO tests more closely but it still massively overshoots. // adding this one line of normalization gets this approximation down to system monitoring utilities somewhat accurately. // we still overshoot. the buffered variant could beat us at some point, but this original implementation seems to be much more lightweight. // id question if its worth a switch over. i think this is close enough & justifys its cheapness this->dCurFreq = AuMax(dAbsMin, dAbsMax); this->uTotal = 0; } AuUInt64 uTotal {}; AuUInt64 uTotalLifetime {}; AuUInt64 uLastDelta {}; AuInt64 uLastWall {}; AuUInt64 uLast {}; double dCurFreq {}; }; }