353 lines
11 KiB
C++
353 lines
11 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "DerivedDataCacheStats.h"
|
|
#include "DerivedDataLegacyCacheStore.h"
|
|
|
|
#include "Algo/BinarySearch.h"
|
|
|
|
namespace UE::DerivedData
|
|
{
|
|
|
|
void FRequestCounter::AddRequest(const ERequestType Type, const ERequestOp Op, const EStatus Status)
|
|
{
|
|
switch (Type)
|
|
{
|
|
case ERequestType::Record:
|
|
switch (Op)
|
|
{
|
|
case ERequestOp::Put:
|
|
++(Status == EStatus::Ok ? PutRecord.Hits : PutRecord.Misses);
|
|
break;
|
|
case ERequestOp::Get:
|
|
++(Status == EStatus::Ok ? GetRecord.Hits : GetRecord.Misses);
|
|
break;
|
|
case ERequestOp::GetChunk:
|
|
++(Status == EStatus::Ok ? GetRecordChunk.Hits : GetRecordChunk.Misses);
|
|
break;
|
|
}
|
|
break;
|
|
case ERequestType::Value:
|
|
switch (Op)
|
|
{
|
|
case ERequestOp::Put:
|
|
++(Status == EStatus::Ok ? PutValue.Hits : PutValue.Misses);
|
|
break;
|
|
case ERequestOp::Get:
|
|
++(Status == EStatus::Ok ? GetValue.Hits : GetValue.Misses);
|
|
break;
|
|
case ERequestOp::GetChunk:
|
|
++(Status == EStatus::Ok ? GetValueChunk.Hits : GetValueChunk.Misses);
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void FTimeAveragedStat::Add(FMonotonicTimePoint StartTime, FMonotonicTimePoint EndTime, double Value)
|
|
{
|
|
checkf(StartTime <= EndTime, TEXT("StartTime %.3f is later than EndTime %.3f."), StartTime.ToSeconds(), EndTime.ToSeconds());
|
|
|
|
TUniqueLock Lock(Mutex);
|
|
|
|
if (EndTime <= LastTime - Period)
|
|
{
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
const int32 Index = Algo::LowerBoundBy(EndValues, EndTime, &FValue::Time);
|
|
EndValues.Insert({EndTime, Value}, Index);
|
|
}
|
|
|
|
if (StartTime < LastTime)
|
|
{
|
|
AccumulatedValue += Value;
|
|
++AccumulatedValueCount;
|
|
}
|
|
else
|
|
{
|
|
const int32 Index = Algo::LowerBoundBy(StartValues, StartTime, &FValue::Time);
|
|
StartValues.Insert({StartTime, Value}, Index);
|
|
}
|
|
|
|
const int32 Count = ActiveRanges.Num();
|
|
const int32 StartIndex = Algo::LowerBoundBy(ActiveRanges, StartTime, &FRange::EndTime);
|
|
if (StartIndex == Count)
|
|
{
|
|
ActiveRanges.Add({StartTime, EndTime});
|
|
}
|
|
else
|
|
{
|
|
FRange& StartRange = ActiveRanges[StartIndex];
|
|
if (EndTime < StartRange.StartTime)
|
|
{
|
|
ActiveRanges.Insert({StartTime, EndTime}, StartIndex);
|
|
}
|
|
else
|
|
{
|
|
if (StartTime < StartRange.StartTime)
|
|
{
|
|
StartRange.StartTime = StartTime;
|
|
}
|
|
|
|
const int32 EndIndex = Algo::LowerBoundBy(ActiveRanges, EndTime, &FRange::StartTime);
|
|
StartRange.EndTime = FMath::Max(EndTime, ActiveRanges[EndIndex - 1].EndTime);
|
|
|
|
ActiveRanges.RemoveAt(StartIndex + 1, EndIndex - StartIndex - 1, EAllowShrinking::No);
|
|
}
|
|
}
|
|
|
|
// Avoid unbounded growth by updating when behind by more than one period.
|
|
if (LastTime + Period < EndTime)
|
|
{
|
|
Update(EndTime);
|
|
}
|
|
}
|
|
|
|
double FTimeAveragedStat::GetRate(FMonotonicTimePoint Time)
|
|
{
|
|
TUniqueLock Lock(Mutex);
|
|
if (LastTime < Time)
|
|
{
|
|
Update(Time);
|
|
}
|
|
return AverageRate;
|
|
}
|
|
|
|
double FTimeAveragedStat::GetValue(FMonotonicTimePoint Time)
|
|
{
|
|
TUniqueLock Lock(Mutex);
|
|
if (LastTime < Time)
|
|
{
|
|
Update(Time);
|
|
}
|
|
return AverageValue;
|
|
}
|
|
|
|
void FTimeAveragedStat::Update(FMonotonicTimePoint Time)
|
|
{
|
|
LastTime = Time;
|
|
|
|
const FMonotonicTimePoint StartTime = Time - Period;
|
|
const FMonotonicTimePoint EndTime = Time;
|
|
|
|
// NOTE: Values are not being prorated when entering or exiting their time range.
|
|
// While this is technically inaccurate, the error will likely be small due
|
|
// to the use case being requests of one second averaged over one minute.
|
|
|
|
// Add values entering the current period.
|
|
int32 StartCount = 0;
|
|
for (const FValue& Value : StartValues)
|
|
{
|
|
if (EndTime < Value.Time)
|
|
{
|
|
break;
|
|
}
|
|
AccumulatedValue += Value.Value;
|
|
++AccumulatedValueCount;
|
|
++StartCount;
|
|
}
|
|
StartValues.RemoveAt(0, StartCount, EAllowShrinking::No);
|
|
|
|
// Remove values exiting the current period.
|
|
int32 EndCount = 0;
|
|
for (const FValue& Value : EndValues)
|
|
{
|
|
if (StartTime <= Value.Time)
|
|
{
|
|
break;
|
|
}
|
|
AccumulatedValue -= Value.Value;
|
|
--AccumulatedValueCount;
|
|
++EndCount;
|
|
}
|
|
EndValues.RemoveAt(0, EndCount, EAllowShrinking::No);
|
|
|
|
// Remove ranges before the current period.
|
|
int32 RangeCount = 0;
|
|
for (const FRange& Range : ActiveRanges)
|
|
{
|
|
if (StartTime < Range.EndTime)
|
|
{
|
|
break;
|
|
}
|
|
++RangeCount;
|
|
}
|
|
ActiveRanges.RemoveAt(0, RangeCount, EAllowShrinking::No);
|
|
|
|
// Accumulate active time in the current period.
|
|
FMonotonicTimeSpan ActiveTime;
|
|
for (const FRange& Range : ActiveRanges)
|
|
{
|
|
const FMonotonicTimePoint RangeStart = FMath::Max(Range.StartTime, StartTime);
|
|
const FMonotonicTimePoint RangeEnd = FMath::Min(Range.EndTime, EndTime);
|
|
ActiveTime += RangeEnd - RangeStart;
|
|
}
|
|
|
|
if (AccumulatedValueCount)
|
|
{
|
|
AverageRate = !ActiveTime.IsZero() ? AccumulatedValue / ActiveTime.ToSeconds() : 0.0;
|
|
AverageValue = AccumulatedValue / AccumulatedValueCount;
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
FCacheStoreStats::FCacheStoreStats(FCacheStats& InCacheStats, ECacheStoreFlags InFlags, FStringView InType, FStringView InName, FStringView InPath)
|
|
: Type(InType)
|
|
, Name(InName)
|
|
, Path(InPath)
|
|
, Flags(InFlags)
|
|
, CacheStats(InCacheStats)
|
|
{
|
|
const FMonotonicTimeSpan Period = FMonotonicTimeSpan::FromSeconds(60.0);
|
|
AverageLatency.SetPeriod(Period);
|
|
AveragePhysicalReadSize.SetPeriod(Period);
|
|
AveragePhysicalWriteSize.SetPeriod(Period);
|
|
}
|
|
|
|
void FCacheStoreStats::SetFlags(ECacheStoreFlags InFlags)
|
|
{
|
|
TUniqueLock Lock(Mutex);
|
|
Flags = InFlags;
|
|
}
|
|
|
|
void FCacheStoreStats::SetStatus(ECacheStoreStatusCode InStatusCode, const FText& InStatus)
|
|
{
|
|
TUniqueLock Lock(Mutex);
|
|
Status = InStatus;
|
|
StatusCode = InStatusCode;
|
|
}
|
|
|
|
void FCacheStoreStats::SetAttribute(FStringView Key, FStringView Value)
|
|
{
|
|
TUniqueLock Lock(Mutex);
|
|
Attributes.Emplace(Key, Value);
|
|
}
|
|
|
|
void FCacheStoreStats::AddRequest(const FCacheStoreRequestStats& Stats)
|
|
{
|
|
checkf(Stats.Bucket.IsValid(), TEXT("Stats for request '%s' did not set a bucket."), *Stats.Name);
|
|
|
|
#if ENABLE_COOK_STATS
|
|
using FCallStats = FCookStats::CallStats;
|
|
using EHitOrMiss = FCallStats::EHitOrMiss;
|
|
using EStatType = FCallStats::EStatType;
|
|
|
|
const EHitOrMiss HitOrMiss = Stats.Status == EStatus::Ok ? EHitOrMiss::Hit : EHitOrMiss::Miss;
|
|
const bool bIsCanceled = Stats.Status == EStatus::Canceled;
|
|
const bool bIsGet = Stats.Op != ECacheStoreRequestOp::Put;
|
|
const bool bIsInGameThread = IsInGameThread();
|
|
#endif
|
|
|
|
{
|
|
// Accumulate only physical size and time from each cache store.
|
|
// Request count and logical size are tracked by the cache hierarchy.
|
|
|
|
FCacheBucketStats& BucketStats = CacheStats.GetBucket(Stats.Bucket);
|
|
TUniqueLock Lock(BucketStats.Mutex);
|
|
|
|
BucketStats.PhysicalReadSize += Stats.PhysicalReadSize;
|
|
BucketStats.PhysicalWriteSize += Stats.PhysicalWriteSize;
|
|
BucketStats.MainThreadTime += Stats.MainThreadTime;
|
|
BucketStats.OtherThreadTime += Stats.OtherThreadTime;
|
|
|
|
#if ENABLE_COOK_STATS
|
|
// FCallStats is a legacy stat codepath that isn't aware of cancellations. In order to avoid skewing miss% rates
|
|
// by accumulating cancellation requests as misses, we simply ignore cancellation requests for stat tracking
|
|
if (!bIsCanceled)
|
|
{
|
|
FCallStats& CallStats = bIsGet ? BucketStats.GetStats : BucketStats.PutStats;
|
|
CallStats.Accumulate(HitOrMiss, EStatType::Cycles, int64(Stats.MainThreadTime.ToSeconds() / FPlatformTime::GetSecondsPerCycle64()), /*bIsInGameThread*/ true);
|
|
CallStats.Accumulate(HitOrMiss, EStatType::Cycles, int64(Stats.OtherThreadTime.ToSeconds() / FPlatformTime::GetSecondsPerCycle64()), /*bIsInGameThread*/ false);
|
|
CallStats.Accumulate(HitOrMiss, EStatType::Bytes, bIsGet ? Stats.PhysicalReadSize : Stats.PhysicalWriteSize, bIsInGameThread);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
TUniqueLock Lock(Mutex);
|
|
|
|
LogicalReadSize += Stats.LogicalReadSize;
|
|
LogicalWriteSize += Stats.LogicalWriteSize;
|
|
PhysicalReadSize += Stats.PhysicalReadSize;
|
|
PhysicalWriteSize += Stats.PhysicalWriteSize;
|
|
|
|
MainThreadTime += Stats.MainThreadTime;
|
|
OtherThreadTime += Stats.OtherThreadTime;
|
|
|
|
RequestCount.AddRequest(Stats.Type, Stats.Op, Stats.Status);
|
|
|
|
if (!Stats.Latency.IsInfinity())
|
|
{
|
|
UE_LOG(LogDerivedDataCache, VeryVerbose, TEXT("%s: Adding request latency of %.2fms from %s on %s with status %s"),
|
|
*Name, Stats.Latency.ToMilliseconds(), LexToString(Stats.Op), LexToString(Stats.Type), *WriteToString<32>(Stats.Status));
|
|
AverageLatency.Add(Stats.StartTime, Stats.EndTime, Stats.Latency.ToSeconds());
|
|
}
|
|
|
|
if (Stats.PhysicalReadSize && !Stats.LogicalWriteSize)
|
|
{
|
|
// Skip physical reads for put operations because they pull down the read rate in time periods with few gets.
|
|
AveragePhysicalReadSize.Add(Stats.StartTime, Stats.EndTime, double(Stats.PhysicalReadSize));
|
|
}
|
|
|
|
if (Stats.PhysicalWriteSize && !Stats.LogicalReadSize)
|
|
{
|
|
// Skip physical writes for get operations because they pull down the write rate in time periods with few puts.
|
|
AveragePhysicalWriteSize.Add(Stats.StartTime, Stats.EndTime, double(Stats.PhysicalWriteSize));
|
|
}
|
|
|
|
#if ENABLE_COOK_STATS
|
|
// FCallStats is a legacy stat codepath that isn't aware of cancellations. In order to avoid skewing miss% rates
|
|
// by accumulating cancellation requests as misses, we simply ignore cancellation requests for stat tracking
|
|
if (!bIsCanceled)
|
|
{
|
|
FCookStats::CallStats& CallStats = bIsGet ? GetStats : PutStats;
|
|
CallStats.Accumulate(HitOrMiss, EStatType::Counter, 1, bIsInGameThread);
|
|
CallStats.Accumulate(HitOrMiss, EStatType::Cycles, int64(Stats.MainThreadTime.ToSeconds() / FPlatformTime::GetSecondsPerCycle64()), /*bIsInGameThread*/ true);
|
|
CallStats.Accumulate(HitOrMiss, EStatType::Cycles, int64(Stats.OtherThreadTime.ToSeconds() / FPlatformTime::GetSecondsPerCycle64()), /*bIsInGameThread*/ false);
|
|
CallStats.Accumulate(HitOrMiss, EStatType::Bytes, bIsGet ? Stats.PhysicalReadSize : Stats.PhysicalWriteSize, bIsInGameThread);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void FCacheStoreStats::AddLatency(FMonotonicTimePoint StartTime, FMonotonicTimePoint EndTime, FMonotonicTimeSpan Latency)
|
|
{
|
|
if (!Latency.IsInfinity())
|
|
{
|
|
UE_LOG(LogDerivedDataCache, VeryVerbose, TEXT("%s: Adding non-request latency of %.2fms"),
|
|
*Name, Latency.ToMilliseconds());
|
|
AverageLatency.Add(StartTime, EndTime, Latency.ToSeconds());
|
|
}
|
|
}
|
|
|
|
double FCacheStoreStats::GetAverageLatency()
|
|
{
|
|
TUniqueLock Lock(Mutex);
|
|
return AverageLatency.GetValue(FMonotonicTimePoint::Now());
|
|
}
|
|
|
|
void FCacheStoreStats::SetTotalPhysicalSize(uint64 InTotalPhysicalSize)
|
|
{
|
|
TUniqueLock Lock(Mutex);
|
|
TotalPhysicalSize = InTotalPhysicalSize;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
FCacheBucketStats& FCacheStats::GetBucket(FCacheBucket Bucket)
|
|
{
|
|
TUniqueLock Lock(Mutex);
|
|
const int32 Index = Algo::LowerBoundBy(BucketStats, Bucket, &FCacheBucketStats::Bucket);
|
|
if (!BucketStats.IsValidIndex(Index) || BucketStats[Index]->Bucket != Bucket)
|
|
{
|
|
FCacheBucketStats* Stats = new FCacheBucketStats;
|
|
Stats->Bucket = Bucket;
|
|
BucketStats.EmplaceAt(Index, Stats);
|
|
}
|
|
return *BucketStats[Index];
|
|
}
|
|
|
|
} // UE::DerivedData
|