AuroraRuntime/Include/Aurora/Utility/ThroughputCalculator.hpp
Reece Wilson 1ff9feb303 [+] Initial heap stat counter API
[+] Upcoming failure categories
[+] Updated SysPushXXX prototypes
[*] Restore bandwidth OnTick position for extrapolate (using current frame stats, ref to last) fractional lerps into the future with ref to average
[*] Compression.cpp AuList<AuUInt8> upgrade was incomplete & could've been improved with modern apis
2022-12-08 19:34:15 +00:00

143 lines
6.1 KiB
C++

/***
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<AuUInt64>(AuSToMS<AuUInt64>(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<double>(/*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<double>(/*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<AuUInt64>(AuSToMS<AuUInt64>(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 {};
};
}