EM_Task/TraceInsights/Private/Insights/ViewModels/CounterAggregation.cpp

481 lines
16 KiB
C++
Raw Permalink Normal View History

2026-02-13 16:18:33 +08:00
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CounterAggregation.h"
#include "TraceServices/Model/AnalysisSession.h"
#include "TraceServices/Model/Counters.h"
namespace Insights
{
////////////////////////////////////////////////////////////////////////////////////////////////////
// TAggregatedStatsEx
////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename Type>
struct TAggregatedStatsEx
{
static constexpr int32 HistogramLen = 100; // number of buckets per histogram
uint64 Count;
TAggregatedStats<Type> BaseStats;
// Histogram for computing median and lower/upper quartiles.
int32 Histogram[HistogramLen];
Type DT; // bucket size
TAggregatedStatsEx()
{
FMemory::Memzero(Histogram, sizeof(int32) * HistogramLen);
DT = Type(1);
}
};
////////////////////////////////////////////////////////////////////////////////////////////////////
// TCounterAggregationHelper
////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename Type>
class TCounterAggregationHelper
{
public:
TCounterAggregationHelper<Type>() = default;
void Reset();
void SetTimeInterval(double InIntervalStartTime, double InIntervalEndTime)
{
IntervalStartTime = InIntervalStartTime;
IntervalEndTime = InIntervalEndTime;
}
void Update(uint32 CounterId, const Trace::ICounter& Counter)
{
EnumerateValues(CounterId, Counter, UpdateMinMax);
}
void PrecomputeHistograms();
void UpdateHistograms(uint32 CounterId, const Trace::ICounter& Counter)
{
EnumerateValues(CounterId, Counter, UpdateHistogram);
}
void PostProcess(bool bComputeMedian);
void ApplyResultsTo(const TMap<uint32, FStatsNodePtr>& StatsNodesIdMap) const;
private:
template <typename CallbackType>
void EnumerateValues(uint32 CounterId, const Trace::ICounter& Counter, CallbackType Callback);
static void UpdateMinMax(TAggregatedStatsEx<Type>& Stats, Type Value);
static void UpdateHistogram(TAggregatedStatsEx<Type>& StatsEx, Type Value);
static void PostProcess(TAggregatedStatsEx<Type>& StatsEx, bool bComputeMedian);
double IntervalStartTime = 0.0;
double IntervalEndTime = 0.0;
TMap<uint32, TAggregatedStatsEx<Type>> StatsMap;
};
////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename Type>
void TCounterAggregationHelper<Type>::Reset()
{
IntervalStartTime = 0.0;
IntervalEndTime = 0.0;
StatsMap.Reset();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// specialization for Type = double
template <>
template <typename CallbackType>
void TCounterAggregationHelper<double>::EnumerateValues(uint32 CounterId, const Trace::ICounter& Counter, CallbackType Callback)
{
TAggregatedStatsEx<double>* StatsExPtr = StatsMap.Find(CounterId);
if (!StatsExPtr)
{
StatsExPtr = &StatsMap.Add(CounterId);
StatsExPtr->Count = 0;
StatsExPtr->BaseStats.Min = +MAX_dbl;
StatsExPtr->BaseStats.Max = -MAX_dbl;
}
TAggregatedStatsEx<double>& StatsEx = *StatsExPtr;
Counter.EnumerateFloatValues(IntervalStartTime, IntervalEndTime, false, [this, &StatsEx, Callback](double Time, double Value)
{
Callback(StatsEx, Value);
});
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// specialization for Type = int64
template <>
template <typename CallbackType>
void TCounterAggregationHelper<int64>::EnumerateValues(uint32 CounterId, const Trace::ICounter& Counter, CallbackType Callback)
{
TAggregatedStatsEx<int64>* StatsExPtr = StatsMap.Find(CounterId);
if (!StatsExPtr)
{
StatsExPtr = &StatsMap.Add(CounterId);
StatsExPtr->Count = 0;
StatsExPtr->BaseStats.Min = MAX_int64;
StatsExPtr->BaseStats.Max = MIN_int64;
}
TAggregatedStatsEx<int64>& StatsEx = *StatsExPtr;
Counter.EnumerateValues(IntervalStartTime, IntervalEndTime, false, [this, &StatsEx, Callback](double Time, int64 Value)
{
Callback(StatsEx, Value);
});
}
////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename Type>
void TCounterAggregationHelper<Type>::UpdateMinMax(TAggregatedStatsEx<Type>& StatsEx, Type Value)
{
TAggregatedStats<Type>& Stats = StatsEx.BaseStats;
Stats.Sum += Value;
if (Value < Stats.Min)
{
Stats.Min = Value;
}
if (Value > Stats.Max)
{
Stats.Max = Value;
}
StatsEx.Count++;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// specialization for Type = double
template <>
void TCounterAggregationHelper<double>::PrecomputeHistograms()
{
for (auto& KV: StatsMap)
{
TAggregatedStatsEx<double>& StatsEx = KV.Value;
const TAggregatedStats<double>& Stats = StatsEx.BaseStats;
// Each bucket (Histogram[i]) will be centered on a value.
// I.e. First bucket (bucket 0) is centered on Min value: [Min-DT/2, Min+DT/2)
// and last bucket (bucket N-1) is centered on Max value: [Max-DT/2, Max+DT/2).
if (Stats.Max == Stats.Min)
{
StatsEx.DT = 1.0; // single large bucket
}
else
{
StatsEx.DT = (Stats.Max - Stats.Min) / (TAggregatedStatsEx<double>::HistogramLen - 1);
if (StatsEx.DT == 0.0)
{
StatsEx.DT = 1.0;
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// specialization for Type = int64
template <>
void TCounterAggregationHelper<int64>::PrecomputeHistograms()
{
for (auto& KV: StatsMap)
{
TAggregatedStatsEx<int64>& StatsEx = KV.Value;
const TAggregatedStats<int64>& Stats = StatsEx.BaseStats;
// Each bucket (Histogram[i]) will be centered on a value.
// I.e. First bucket (bucket 0) is centered on Min value: [Min-DT/2, Min+DT/2)
// and last bucket (bucket N-1) is centered on Max value: [Max-DT/2, Max+DT/2).
if (Stats.Max == Stats.Min)
{
StatsEx.DT = 1; // single bucket
}
else
{
// DT = Ceil[(Max - Min) / (N - 1)]
StatsEx.DT = (Stats.Max - Stats.Min + TAggregatedStatsEx<int64>::HistogramLen - 2) / (TAggregatedStatsEx<int64>::HistogramLen - 1);
if (StatsEx.DT == 0)
{
StatsEx.DT = 1;
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename Type>
void TCounterAggregationHelper<Type>::UpdateHistogram(TAggregatedStatsEx<Type>& StatsEx, Type Value)
{
const TAggregatedStats<Type>& Stats = StatsEx.BaseStats;
// Index = (Value - Min + DT/2) / DT
int32 Index = static_cast<int32>((Value - Stats.Min + StatsEx.DT / 2) / StatsEx.DT);
ensure(Index >= 0);
if (Index < 0)
{
Index = 0;
}
ensure(Index < TAggregatedStatsEx<Type>::HistogramLen);
if (Index >= TAggregatedStatsEx<Type>::HistogramLen)
{
Index = TAggregatedStatsEx<Type>::HistogramLen - 1;
}
StatsEx.Histogram[Index]++;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename Type>
void TCounterAggregationHelper<Type>::PostProcess(TAggregatedStatsEx<Type>& StatsEx, bool bComputeMedian)
{
TAggregatedStats<Type>& Stats = StatsEx.BaseStats;
// Compute average value.
if (StatsEx.Count > 0)
{
Stats.Average = Stats.Sum / static_cast<Type>(StatsEx.Count);
if (bComputeMedian)
{
const int32 HalfCount = StatsEx.Count / 2;
// Compute median value.
int32 Count = 0;
for (int32 HistogramIndex = 0; HistogramIndex < TAggregatedStatsEx<Type>::HistogramLen; HistogramIndex++)
{
Count += StatsEx.Histogram[HistogramIndex];
if (Count > HalfCount)
{
Stats.Median = Stats.Min + HistogramIndex * StatsEx.DT;
if (HistogramIndex > 0 &&
StatsEx.Count % 2 == 0 &&
Count - StatsEx.Histogram[HistogramIndex] == HalfCount)
{
const Type PrevMedian = Stats.Min + (HistogramIndex - 1) * StatsEx.DT;
Stats.Median = (Stats.Median + PrevMedian) / 2;
}
break;
}
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// specialization for Type = double
template <>
void TCounterAggregationHelper<double>::PostProcess(bool bComputeMedian)
{
for (auto& KV: StatsMap)
{
PostProcess(KV.Value, bComputeMedian);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// specialization for Type = int64
template <>
void TCounterAggregationHelper<int64>::PostProcess(bool bComputeMedian)
{
for (auto& KV: StatsMap)
{
PostProcess(KV.Value, bComputeMedian);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// specialization for Type = double
template <>
void TCounterAggregationHelper<double>::ApplyResultsTo(const TMap<uint32, FStatsNodePtr>& StatsNodesIdMap) const
{
for (const auto& KV: StatsMap)
{
// Update the stats node.
FStatsNodePtr NodePtr = StatsNodesIdMap.FindRef(KV.Key);
if (NodePtr != nullptr)
{
NodePtr->SetAggregatedStatsDouble(KV.Value.Count, KV.Value.BaseStats);
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// specialization for Type = int64
template <>
void TCounterAggregationHelper<int64>::ApplyResultsTo(const TMap<uint32, FStatsNodePtr>& StatsNodesIdMap) const
{
for (const auto& KV: StatsMap)
{
// Update the stats node.
FStatsNodePtr NodePtr = StatsNodesIdMap.FindRef(KV.Key);
if (NodePtr != nullptr)
{
NodePtr->SetAggregatedStatsInt64(KV.Value.Count, KV.Value.BaseStats);
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// FCounterAggregationWorker
////////////////////////////////////////////////////////////////////////////////////////////////////
class FCounterAggregationWorker: public IStatsAggregationWorker
{
public:
FCounterAggregationWorker(TSharedPtr<const Trace::IAnalysisSession> InSession, double InStartTime, double InEndTime)
: Session(InSession), StartTime(InStartTime), EndTime(InEndTime)
{
}
virtual ~FCounterAggregationWorker() {}
virtual void DoWork() override;
void ApplyResultsTo(const TMap<uint32, FStatsNodePtr>& StatsNodesIdMap) const;
void ResetResults();
private:
TSharedPtr<const Trace::IAnalysisSession> Session;
double StartTime;
double EndTime;
const bool bComputeMedian = true;
TCounterAggregationHelper<double> CalculationHelperDbl;
TCounterAggregationHelper<int64> CalculationHelperInt;
};
////////////////////////////////////////////////////////////////////////////////////////////////////
void FCounterAggregationWorker::DoWork()
{
CalculationHelperDbl.SetTimeInterval(StartTime, EndTime);
CalculationHelperInt.SetTimeInterval(StartTime, EndTime);
if (Session.IsValid())
{
// Suspend analysis in order to avoid write locks (ones blocked by the read lock below) to further block other read locks.
// Trace::FAnalysisSessionSuspensionScope SessionPauseScope(*Session.Get());
Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get());
const Trace::ICounterProvider& CountersProvider = Trace::ReadCounterProvider(*Session.Get());
// Compute instance count and total/min/max inclusive/exclusive times for each counter.
// Iterate through all counters.
CountersProvider.EnumerateCounters([this](uint32 CounterId, const Trace::ICounter& Counter)
{
if (Counter.IsFloatingPoint())
{
CalculationHelperDbl.Update(CounterId, Counter);
}
else
{
CalculationHelperInt.Update(CounterId, Counter);
}
});
// Now, as we know min/max inclusive/exclusive times for counter, we can compute histogram and median values.
if (bComputeMedian)
{
// Update bucket size (DT) for computing histogram.
CalculationHelperDbl.PrecomputeHistograms();
CalculationHelperInt.PrecomputeHistograms();
// Compute histogram.
// Iterate again through all counters.
CountersProvider.EnumerateCounters([this](uint32 CounterId, const Trace::ICounter& Counter)
{
if (Counter.IsFloatingPoint())
{
CalculationHelperDbl.UpdateHistograms(CounterId, Counter);
}
else
{
CalculationHelperInt.UpdateHistograms(CounterId, Counter);
}
});
}
}
// Compute average and median inclusive/exclusive times.
CalculationHelperDbl.PostProcess(bComputeMedian);
CalculationHelperInt.PostProcess(bComputeMedian);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void FCounterAggregationWorker::ApplyResultsTo(const TMap<uint32, FStatsNodePtr>& StatsNodesIdMap) const
{
CalculationHelperDbl.ApplyResultsTo(StatsNodesIdMap);
CalculationHelperInt.ApplyResultsTo(StatsNodesIdMap);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void FCounterAggregationWorker::ResetResults()
{
CalculationHelperDbl.Reset();
CalculationHelperInt.Reset();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// FCounterAggregator
////////////////////////////////////////////////////////////////////////////////////////////////////
IStatsAggregationWorker* FCounterAggregator::CreateWorker(TSharedPtr<const Trace::IAnalysisSession> InSession)
{
return new FCounterAggregationWorker(InSession, GetIntervalStartTime(), GetIntervalEndTime());
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void FCounterAggregator::ApplyResultsTo(const TMap<uint32, FStatsNodePtr>& StatsNodesIdMap) const
{
// It can only be called from OnFinishedCallback.
check(IsFinished());
FCounterAggregationWorker* Worker = (FCounterAggregationWorker*)GetWorker();
check(Worker != nullptr);
// Apply aggregation stats to tree nodes.
Worker->ApplyResultsTo(StatsNodesIdMap);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void FCounterAggregator::ResetResults()
{
// It can only be called from OnFinishedCallback.
check(IsFinished());
FCounterAggregationWorker* Worker = (FCounterAggregationWorker*)GetWorker();
check(Worker != nullptr);
Worker->ResetResults();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
} // namespace Insights