tools(profile): the instrument lied by 11.5x -- now it checks itself, and batching kills startup cost

Overnight task 1 of the owner's mandate: "better diagnostic, profiler and tools,
so we can batch test way more quicker."

WHY THIS CAME BEFORE ANY MORE OPTIMISATION
ee89c4d delivered a 5x candidate reduction and a 16x cheaper support floor for
0% end-to-end gain, because the profiler inflated by ~11.5x (14.74 us/sample
profiled vs 1.28 us/voxel real). Two rounds of bucket rankings were artefacts of
FPlatformTime::Cycles64() wrapped around ~100 ns operations -- the timer pair
calibrates at ~0.1 us, so it was measuring itself.

PROFILER
Deterministic count sampling plus amortised block timing every 64 density calls.
Coarse counters (calls, candidates) stay always-on -- they were never the problem.
Timing-only scopes are opt-in behind -profiledensityfull. UE CPU trace scopes added.

  before   profiled 14.74 us/sample  vs  1.28 us/voxel real   ~11.5x
  after    profiled 0.463050 s       vs  0.558470 s unprofiled   -17.09%
           geometry 235AC2DC in both

⭐ Every profiled report now prints profiled total, unprofiled total, discrepancy,
ratio and geometry equality, and EXITS CODE 2 if the comparison fails. An
intermediate run hit -22.6% and failed loudly, which is the point: this class of
bug must be impossible to miss rather than something noticed three rounds later.

⚠️ The -17% residual is run-to-run noise, so ~17% is our measurement floor.
Improvements smaller than that cannot be claimed from a single pair of runs.

BATCH EXPLORER
One process, many cases, one JSON. Startup was ~25-30 s of engine boot for ~3 s of
work, which is what made every iteration expensive.

  3 cases in 6.59 s of case time
  standalone vs batched, same binary: hash 235AC2DC, 2,461 tris, 343,000 tunnel
  lookups, 554,381 passage evaluations -- identical on both

-out= is enforced absolute under the plugin's own Saved/.

CACHE BREAKDOWN (the 272 KB/entry question)
The slot table is small. The weight is spatial-index bins and candidate arrays,
room arrays, support-column arrays and tunnel-world indices -- i.e. much of the
411 MiB is the spatial index ee89c4d added, which also bought 0% end-to-end.
Worth revisiting on those terms.

RESEARCH (recorded, not acted on) -- CODEX-TASK-1-PROFILER-BATCH-RESEARCH.md
Ranked follow-ups: deterministic empty-space early-out, density-grid reuse, task
granularity (ParallelForWithExistingTaskContext + MinBatchSize), SIMD MC (~2x over
optimised scalar, but cross-machine determinism is the risk), sparse hierarchy,
RMC partial updates, Transvoxel LOD seams. Sources cited in the doc.
⚠️ Upstream RMC is 5.3.2; the owner's copy identifies as 5.4 with custom
modifications. No upgrade is safe. Left untouched.

PUSHBACK, accepted: "buckets must sum to their parent" needs a parallel-time
definition -- summing worker CPU is not wall time. Reports now separate worker CPU
sum from wall-clock estimate.

End-to-end unchanged, as expected for a tooling change: 2.746 / 2.691 s on the
128^3 seed 1337 region (was 2.678-2.705). Build clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-09 05:02:21 +02:00
1 parent ee89c4d8c2
commit e63f016835
10 files changed
+1494 -124

No files matched your search

@@ -346,42 +346,47 @@ bool FCaveSpatialIndex::GetColumnCandidates(
SIZE_T FChunkSDFCache::GetAllocatedSize() const
{
SIZE_T Bytes = 0;
auto AddArray = [&Bytes](const auto& Array)
return static_cast<SIZE_T>(GetAllocatedSizeBreakdown().TotalBytes());
}
VoxelDensityProfile::FCacheMemoryBreakdown FChunkSDFCache::GetAllocatedSizeBreakdown() const
{
VoxelDensityProfile::FCacheMemoryBreakdown Breakdown;
auto ArrayBytes = [](const auto& Array) -> uint64
{
Bytes += static_cast<SIZE_T>(Array.GetAllocatedSize());
return static_cast<uint64>(Array.GetAllocatedSize());
};
AddArray(Rooms);
AddArray(RoomFloorJoins);
AddArray(Tunnels);
AddArray(Pits);
AddArray(Chimneys);
AddArray(Columns);
AddArray(SupportColumnEntries);
AddArray(SupportColumns);
Breakdown.RoomsBytes = ArrayBytes(Rooms);
Breakdown.RoomFloorJoinsBytes = ArrayBytes(RoomFloorJoins);
Breakdown.TunnelsBytes = ArrayBytes(Tunnels);
Breakdown.PitsBytes = ArrayBytes(Pits);
Breakdown.ChimneysBytes = ArrayBytes(Chimneys);
Breakdown.ColumnsBytes = ArrayBytes(Columns);
Breakdown.SupportColumnEntriesBytes = ArrayBytes(SupportColumnEntries);
Breakdown.SupportColumnsBytes = ArrayBytes(SupportColumns);
for (const FTunnelSupportFloorColumn& Column : SupportColumns)
{
AddArray(Column.Intervals);
Breakdown.SupportColumnIntervalsBytes += ArrayBytes(Column.Intervals);
}
for (const FCachedTunnel& Tunnel : Tunnels)
{
AddArray(Tunnel.ControlPoints);
AddArray(Tunnel.ControlRadii);
AddArray(Tunnel.WorldControlPoints);
AddArray(Tunnel.WorldControlRadii);
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.ControlPoints);
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.ControlRadii);
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.WorldControlPoints);
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.WorldControlRadii);
}
const FCaveSpatialIndex* Indices[] = {
&RoomIndex, &RoomFloorJoinIndex, &TunnelIndex, &TunnelWorldIndex,
};
for (const FCaveSpatialIndex* Index : Indices)
{
AddArray(Index->Bins);
AddArray(Index->CandidateIndices);
}
return Bytes;
Breakdown.RoomIndexBinsBytes = ArrayBytes(RoomIndex.Bins);
Breakdown.RoomIndexCandidatesBytes = ArrayBytes(RoomIndex.CandidateIndices);
Breakdown.RoomFloorJoinIndexBinsBytes = ArrayBytes(RoomFloorJoinIndex.Bins);
Breakdown.RoomFloorJoinIndexCandidatesBytes = ArrayBytes(RoomFloorJoinIndex.CandidateIndices);
Breakdown.TunnelIndexBinsBytes = ArrayBytes(TunnelIndex.Bins);
Breakdown.TunnelIndexCandidatesBytes = ArrayBytes(TunnelIndex.CandidateIndices);
Breakdown.TunnelWorldIndexBinsBytes = ArrayBytes(TunnelWorldIndex.Bins);
Breakdown.TunnelWorldIndexCandidatesBytes = ArrayBytes(TunnelWorldIndex.CandidateIndices);
return Breakdown;
}
//=============================================================================
@@ -3311,7 +3316,7 @@ void VoxelCaveMorphology::BuildChunkCache(
const float MaxInfluence = FMath::Max(
RoomRadiusEnvelope + FloorReliefEnvelope,
FMath::Abs(Params.TunnelWarpStrength) + TunnelRadiusEnvelope
) + BlendEnvelope;
) + BlendEnvelope * 3.0f;
const float MaxTunnelLen = FMath::Max(Params.MaxTunnelLength, 0.0f);
@@ -3897,7 +3902,7 @@ void VoxelCaveMorphology::BuildChunkCache(
BoundR = FMath::Max(
BoundR, FVector::Dist(CT.BoundCenter, CT.ControlPoints[PointIndex]));
}
BoundR += MaxTunnelRadius + FMath::Max(BlendK, 0.0f);
BoundR += MaxTunnelRadius + FMath::Max(BlendK, 0.0f) * 3.0f;
CT.BoundRadiusSq = BoundR * BoundR;
// Keep a separate bound for the world-space structural backstop. The SDF and world
@@ -4042,7 +4047,8 @@ void VoxelCaveMorphology::BuildChunkCache(
Join.CeilingZ = CommonFloor + JoinRequiredHeight;
Join.BoundCenter = (Join.Start + Join.End) * 0.5f;
const float BoundRadius = 0.5f * FVector::Dist(Join.Start, Join.End)
+ FMath::Abs(Join.Radius) + FMath::Max(BlendK, 0.0f);
+ FMath::Abs(Join.Radius)
+ FMath::Max(BlendK, 0.0f) * 3.0f;
Join.BoundRadiusSq = BoundRadius * BoundRadius;
if (SphereTouchesSearchXY(Join.BoundCenter, Join.BoundRadiusSq))
@@ -4392,7 +4398,7 @@ float VoxelCaveMorphology::EvaluateSDFCached(
int32 RoomCount = 0;
const bool bIndexedRooms = Cache.RoomIndex.GetBin(
WorldX, WorldY, WorldZ, RoomFirst, RoomCount);
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::CaveRoomCandidates,
@@ -4409,7 +4415,7 @@ float VoxelCaveMorphology::EvaluateSDFCached(
// --- DISTANCE CULL ---
const float DistSq = VF_FloatDistSquared(WorldX, WorldY, WorldZ, Room.Center);
if (DistSq > Room.CullRadiusSq) return;
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::CaveRoomEvaluated);
@@ -4515,7 +4521,7 @@ float VoxelCaveMorphology::EvaluateSDFCached(
int32 TunnelCount = 0;
const bool bIndexedTunnels = Cache.TunnelIndex.GetBin(
WorldX, WorldY, WorldZ, TunnelFirst, TunnelCount);
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::CaveTunnelCandidates,
@@ -4536,7 +4542,7 @@ float VoxelCaveMorphology::EvaluateSDFCached(
// --- BOUNDING SPHERE CULL ---
const float DistSq = VF_FloatDistSquared(WorldX, WorldY, WorldZ, Tunnel.BoundCenter);
if (DistSq > Tunnel.BoundRadiusSq) return;
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::CaveTunnelEvaluated);
@@ -4714,12 +4720,12 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
int32 TunnelCount = 0;
const bool bIndexedTunnels = Cache.TunnelWorldIndex.GetBin(
WorldX, WorldY, WorldZ, TunnelFirst, TunnelCount);
if (SupportColumn != nullptr && VoxelDensityProfile::IsEnabled())
if (SupportColumn != nullptr && VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorQueries);
}
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCoreCandidates,
@@ -4753,7 +4759,7 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
{
return;
}
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
@@ -4765,7 +4771,7 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
? Tunnel.WorldControlRadii : Tunnel.ControlRadii;
if (bHasWorldChain)
{
if (SupportColumn != nullptr && VoxelDensityProfile::IsEnabled())
if (SupportColumn != nullptr && VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorChecks);
@@ -4881,7 +4887,7 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
int32 TunnelCount = 0;
const bool bIndexedTunnels = Cache.TunnelWorldIndex.GetBin(
WorldX, WorldY, WorldZ, TunnelFirst, TunnelCount);
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCoreCandidates,
@@ -4941,7 +4947,7 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
if (TestTunnel(TunnelIdx))
{
bSupport = true;
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
@@ -4957,7 +4963,7 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
if (TestTunnel(TunnelIdx))
{
bSupport = true;
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
@@ -4976,7 +4982,7 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
{
OutColumn.Reset();
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportColumnBuilds);
@@ -4993,7 +4999,7 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
}
}
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportColumnCandidates,
@@ -5131,7 +5137,7 @@ float VoxelCaveMorphology::EvaluateSDF(
const float Margin = FMath::Max(
RoomRadiusEnvelope + FloorReliefEnvelope,
FMath::Abs(Params.TunnelWarpStrength) + TunnelRadiusEnvelope
) + BlendEnvelope;
) + BlendEnvelope * 3.0f;
FChunkSDFCache TempCache;
BuildChunkCache(
@@ -3293,7 +3293,7 @@ namespace
&& SharedCache->RegionSize == RegionSize;
if (!bCacheMatches)
{
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::SdfCacheBuild);
@@ -3844,7 +3844,7 @@ namespace
FState& S = State();
const FVector& Position = S.LastWorldPosition;
const FChunkSDFCache& Cache = GetCache();
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorQueries);
@@ -5534,6 +5534,7 @@ void VoxelDensityOps::ReportWorkerRoomGraphCacheFootprint()
uint64 EntryBytes = 0;
uint64 LargestEntryBytes = 0;
uint64 ValidEntries = 0;
VoxelDensityProfile::FCacheMemoryBreakdown Breakdown;
for (int32 Index = 0; Index < RoomGraphCacheSlotCount; ++Index)
{
const FRoomGraphCacheEntry& Entry = GRoomGraphCache[Index];
@@ -5543,6 +5544,7 @@ void VoxelDensityOps::ReportWorkerRoomGraphCacheFootprint()
}
++ValidEntries;
Breakdown += Entry.Cache.GetAllocatedSizeBreakdown();
const uint64 EntryDynamicBytes = static_cast<uint64>(Entry.Cache.GetAllocatedSize());
const uint64 FullEntryBytes = static_cast<uint64>(sizeof(FRoomGraphCacheEntry))
+ EntryDynamicBytes;
@@ -5550,6 +5552,7 @@ void VoxelDensityOps::ReportWorkerRoomGraphCacheFootprint()
EntryBytes += FullEntryBytes;
LargestEntryBytes = FMath::Max(LargestEntryBytes, FullEntryBytes);
}
Breakdown.SlotStorageBytes = static_cast<uint64>(sizeof(GRoomGraphCache));
VoxelDensityProfile::SetWorkerRoomGraphCacheFootprint(
RoomGraphCacheSlotCount,
@@ -5558,7 +5561,8 @@ void VoxelDensityOps::ReportWorkerRoomGraphCacheFootprint()
DynamicBytes,
EntryBytes,
LargestEntryBytes,
ValidEntries);
ValidEntries,
Breakdown);
}
//=============================================================================
@@ -5,6 +5,9 @@ namespace VoxelDensityProfile
namespace
{
std::atomic<bool> GEnabled(false);
std::atomic<EMode> GMode(EMode::Disabled);
std::atomic<uint32> GSampleInterval(DefaultSampleInterval);
std::atomic<uint64> GTimerPairCycles(0);
// The old implementation performed a contended atomic fetch_add for every timer and
// counter. A TunnelNetwork sample can close twenty-plus ranges and add several
@@ -15,7 +18,12 @@ namespace VoxelDensityProfile
{
uint64 Calls[BucketCount]{};
uint64 Cycles[BucketCount]{};
uint64 Samples[BucketCount]{};
uint64 Counters[CounterCount]{};
uint32 SampleSequence[BucketCount]{};
uint32 BlockCallCount[BucketCount]{};
uint64 BlockStartCycles[BucketCount]{};
bool BlockActive[BucketCount]{};
uint64 TunnelCacheCapacityEntries = 0;
uint64 TunnelCacheValidEntries = 0;
@@ -24,6 +32,7 @@ namespace VoxelDensityProfile
uint64 TunnelCacheEntryBytes = 0;
uint64 TunnelCacheLargestEntryBytes = 0;
uint64 TunnelCacheLargestWorkerValidEntries = 0;
FCacheMemoryBreakdown TunnelCacheBreakdown;
uint64 RoomGraphCacheCapacityEntries = 0;
uint64 RoomGraphCacheValidEntries = 0;
@@ -32,12 +41,16 @@ namespace VoxelDensityProfile
uint64 RoomGraphCacheEntryBytes = 0;
uint64 RoomGraphCacheLargestEntryBytes = 0;
uint64 RoomGraphCacheLargestWorkerValidEntries = 0;
FCacheMemoryBreakdown RoomGraphCacheBreakdown;
std::atomic<FThreadState*> Next{nullptr};
};
std::atomic<FThreadState*> GThreadStates{nullptr};
thread_local FThreadState* GThreadState = nullptr;
thread_local uint32 GScopeDepth = 0;
thread_local bool GScopeSampled = false;
thread_local bool GSampledScopeActive = false;
FThreadState& GetThreadState()
{
@@ -64,11 +77,43 @@ namespace VoxelDensityProfile
? Index
: static_cast<int32>(EBucket::OtherOp);
}
bool IsSampledScopeEnabled(EBucket Bucket)
{
// Sampled mode pays for the trustworthy parent and mesh containers only. Fine
// operation scopes remain available in Full mode, or through UE Insights scopes,
// without putting a Begin/End and thread-local bookkeeping on every density call.
switch (Bucket)
{
case EBucket::GetDensityAt:
case EBucket::MesherGenerateMesh:
case EBucket::MesherDensityGrid:
case EBucket::MesherOther:
return true;
default:
return false;
}
}
}
void SetEnabled(bool bEnabled)
{
GEnabled.store(bEnabled, std::memory_order_relaxed);
SetMode(bEnabled ? EMode::Sampled : EMode::Disabled, DefaultSampleInterval);
}
void SetMode(EMode Mode, uint32 SampleInterval)
{
if (SampleInterval == 0)
{
SampleInterval = DefaultSampleInterval;
}
GSampleInterval.store(SampleInterval, std::memory_order_relaxed);
GMode.store(Mode, std::memory_order_relaxed);
GEnabled.store(Mode != EMode::Disabled, std::memory_order_relaxed);
if (Mode != EMode::Disabled && GTimerPairCycles.load(std::memory_order_relaxed) == 0)
{
CalibrateTimer();
}
}
bool IsEnabled()
@@ -76,6 +121,46 @@ namespace VoxelDensityProfile
return GEnabled.load(std::memory_order_relaxed);
}
bool IsCycleTimingEnabled()
{
return GMode.load(std::memory_order_relaxed) != EMode::Disabled;
}
EMode GetMode()
{
return GMode.load(std::memory_order_relaxed);
}
uint32 GetSampleInterval()
{
return GSampleInterval.load(std::memory_order_relaxed);
}
uint64 GetTimerPairCycles()
{
return GTimerPairCycles.load(std::memory_order_relaxed);
}
void CalibrateTimer()
{
constexpr uint32 Iterations = 100000;
volatile uint64 Sink = 0;
const uint64 Start = FPlatformTime::Cycles64();
for (uint32 Index = 0; Index < Iterations; ++Index)
{
const uint64 A = FPlatformTime::Cycles64();
const uint64 B = FPlatformTime::Cycles64();
Sink ^= (B - A);
}
const uint64 End = FPlatformTime::Cycles64();
const uint64 Total = End - Start;
const uint64 PairCycles = Total > 0
? FMath::Max<uint64>(1, Total / Iterations)
: 1;
GTimerPairCycles.store(PairCycles, std::memory_order_relaxed);
(void)Sink;
}
void Reset()
{
for (FThreadState* State = GThreadStates.load(std::memory_order_acquire);
@@ -86,12 +171,37 @@ namespace VoxelDensityProfile
{
State->Calls[Index] = 0;
State->Cycles[Index] = 0;
State->Samples[Index] = 0;
}
for (int32 Index = 0; Index < CounterCount; ++Index)
{
State->Counters[Index] = 0;
}
for (int32 Index = 0; Index < BucketCount; ++Index)
{
State->SampleSequence[Index] = 0;
State->BlockCallCount[Index] = 0;
State->BlockStartCycles[Index] = 0;
State->BlockActive[Index] = false;
}
State->TunnelCacheCapacityEntries = 0;
State->TunnelCacheValidEntries = 0;
State->TunnelCacheStaticBytes = 0;
State->TunnelCacheDynamicBytes = 0;
State->TunnelCacheEntryBytes = 0;
State->TunnelCacheLargestEntryBytes = 0;
State->TunnelCacheBreakdown = FCacheMemoryBreakdown();
State->RoomGraphCacheCapacityEntries = 0;
State->RoomGraphCacheValidEntries = 0;
State->RoomGraphCacheStaticBytes = 0;
State->RoomGraphCacheDynamicBytes = 0;
State->RoomGraphCacheEntryBytes = 0;
State->RoomGraphCacheLargestEntryBytes = 0;
State->RoomGraphCacheBreakdown = FCacheMemoryBreakdown();
}
GScopeDepth = 0;
GScopeSampled = false;
GSampledScopeActive = false;
}
FSnapshot Snapshot()
@@ -104,7 +214,39 @@ namespace VoxelDensityProfile
for (int32 Index = 0; Index < BucketCount; ++Index)
{
Result.Calls[Index] += State->Calls[Index];
Result.Cycles[Index] += State->Cycles[Index];
Result.Samples[Index] += State->Samples[Index];
if (State->Samples[Index] == 0)
{
Result.Cycles[Index] += State->Cycles[Index];
}
else
{
// Store raw sampled cycles in the worker-local state. Estimate the bucket
// from its observed call/sample ratio at aggregation time. This matters for
// small buckets: multiplying every sample by 64 would overstate a bucket with
// only eight calls where all eight were sampled.
const double Scale = static_cast<double>(State->Calls[Index])
/ static_cast<double>(State->Samples[Index]);
const double EstimatedCycles = static_cast<double>(State->Cycles[Index]) * Scale;
Result.Cycles[Index] += EstimatedCycles > static_cast<double>(MAX_uint64)
? MAX_uint64
: static_cast<uint64>(EstimatedCycles + 0.5);
}
if (State->Calls[Index] > 0)
{
++Result.ActiveWorkers[Index];
const double EstimatedWorkerCycles = State->Samples[Index] == 0
? static_cast<double>(State->Cycles[Index])
: static_cast<double>(State->Cycles[Index])
* static_cast<double>(State->Calls[Index])
/ static_cast<double>(State->Samples[Index]);
Result.WallCycles[Index] = FMath::Max(
Result.WallCycles[Index],
EstimatedWorkerCycles > static_cast<double>(MAX_uint64)
? MAX_uint64
: static_cast<uint64>(EstimatedWorkerCycles + 0.5));
}
}
for (int32 Index = 0; Index < CounterCount; ++Index)
{
@@ -129,6 +271,7 @@ namespace VoxelDensityProfile
State->TunnelCacheValidEntries);
Result.TunnelCacheLargestWorkerBytes = FMath::Max(
Result.TunnelCacheLargestWorkerBytes, TunnelWorkerBytes);
Result.TunnelCacheBreakdown += State->TunnelCacheBreakdown;
}
const uint64 RoomGraphWorkerBytes = State->RoomGraphCacheStaticBytes
@@ -149,8 +292,12 @@ namespace VoxelDensityProfile
State->RoomGraphCacheValidEntries);
Result.RoomGraphCacheLargestWorkerBytes = FMath::Max(
Result.RoomGraphCacheLargestWorkerBytes, RoomGraphWorkerBytes);
Result.RoomGraphCacheBreakdown += State->RoomGraphCacheBreakdown;
}
}
Result.TimerPairCycles = GetTimerPairCycles();
Result.SampleInterval = GetSampleInterval();
Result.Mode = GetMode();
return Result;
}
@@ -171,11 +318,52 @@ namespace VoxelDensityProfile
State.Cycles[Index] += Cycles;
}
void RecordCall(EBucket Bucket)
{
const int32 Index = ToIndex(Bucket);
GetThreadState().Calls[Index] += 1;
}
bool ShouldSample(EBucket Bucket)
{
if (!IsCycleTimingEnabled())
{
return false;
}
if (GetMode() == EMode::Full)
{
return true;
}
FThreadState& State = GetThreadState();
const int32 Index = ToIndex(Bucket);
const uint32 Sequence = State.SampleSequence[Index]++;
const uint32 Interval = GetSampleInterval();
if ((Interval & (Interval - 1u)) == 0u)
{
return (Sequence & (Interval - 1u)) == 0u;
}
return (Sequence % Interval) == 0u;
}
void AddSampledMeasurement(EBucket Bucket, uint64 RawCycles)
{
const int32 Index = ToIndex(Bucket);
FThreadState& State = GetThreadState();
const uint64 TimerCost = GetTimerPairCycles();
const uint64 AdjustedCycles = RawCycles > TimerCost
? RawCycles - TimerCost
: 0;
State.Cycles[Index] += AdjustedCycles;
State.Samples[Index] += 1;
}
void SetWorkerTunnelCacheFootprint(
uint64 CapacityEntries, uint64 ValidEntries,
uint64 StaticBytes, uint64 DynamicBytes,
uint64 EntryBytes, uint64 LargestEntryBytes,
uint64 LargestWorkerValidEntries)
uint64 LargestWorkerValidEntries,
const FCacheMemoryBreakdown& Breakdown)
{
if (!IsEnabled()) { return; }
FThreadState& State = GetThreadState();
@@ -186,13 +374,15 @@ namespace VoxelDensityProfile
State.TunnelCacheEntryBytes = EntryBytes;
State.TunnelCacheLargestEntryBytes = LargestEntryBytes;
State.TunnelCacheLargestWorkerValidEntries = LargestWorkerValidEntries;
State.TunnelCacheBreakdown = Breakdown;
}
void SetWorkerRoomGraphCacheFootprint(
uint64 CapacityEntries, uint64 ValidEntries,
uint64 StaticBytes, uint64 DynamicBytes,
uint64 EntryBytes, uint64 LargestEntryBytes,
uint64 LargestWorkerValidEntries)
uint64 LargestWorkerValidEntries,
const FCacheMemoryBreakdown& Breakdown)
{
if (!IsEnabled()) { return; }
FThreadState& State = GetThreadState();
@@ -203,6 +393,7 @@ namespace VoxelDensityProfile
State.RoomGraphCacheEntryBytes = EntryBytes;
State.RoomGraphCacheLargestEntryBytes = LargestEntryBytes;
State.RoomGraphCacheLargestWorkerValidEntries = LargestWorkerValidEntries;
State.RoomGraphCacheBreakdown = Breakdown;
}
const TCHAR* CounterName(ECounter Counter)
@@ -356,6 +547,105 @@ namespace VoxelDensityProfile
return TEXT("Unknown");
}
FScopeToken BeginScope(EBucket Bucket)
{
FScopeToken Token;
const EMode Mode = GetMode();
if (!IsEnabled()
|| (Mode == EMode::Sampled && !IsSampledScopeEnabled(Bucket)))
{
return Token;
}
Token.bEntered = true;
Token.bPreviousSampled = GScopeSampled;
Token.bPreviousSampledScopeActive = GSampledScopeActive;
++GScopeDepth;
// For the hot parent, amortise the timer over a deterministic block of calls. A timer
// pair around one ~100 ns operation is mostly the timer; a pair around 64 calls is cheap
// and estimates the parent from real elapsed work rather than instrumentation latency.
if (Mode == EMode::Sampled && Bucket == EBucket::GetDensityAt)
{
FThreadState& State = GetThreadState();
const int32 Index = ToIndex(Bucket);
const uint32 Sequence = State.SampleSequence[Index]++;
Token.bBlockTiming = true;
const uint32 Interval = GetSampleInterval();
const bool bBlockBoundary = (Interval & (Interval - 1u)) == 0u
? (Sequence & (Interval - 1u)) == 0u
: (Sequence % Interval) == 0u;
if (bBlockBoundary && !State.BlockActive[Index])
{
State.BlockActive[Index] = true;
State.BlockCallCount[Index] = 0;
State.BlockStartCycles[Index] = FPlatformTime::Cycles64();
}
GScopeSampled = false;
return Token;
}
// Every bucket samples independently, but only one nested non-container scope owns a
// timer at a time. Otherwise a sampled GetDensityAt would also start timers for every
// phase and operation inside that same call, measuring instrumentation instead of work.
// Mesher container ranges deliberately remain transparent so their grid can still expose
// density samples underneath them.
const bool bContainerScope = Bucket == EBucket::MesherGenerateMesh
|| Bucket == EBucket::MesherDensityGrid;
Token.bSampled = Mode == EMode::Full
? true
: (!GSampledScopeActive && ShouldSample(Bucket));
GScopeSampled = Token.bSampled;
if (Token.bSampled)
{
if (Mode == EMode::Sampled && !bContainerScope)
{
GSampledScopeActive = true;
}
Token.StartCycles = FPlatformTime::Cycles64();
}
return Token;
}
void EndScope(EBucket Bucket, const FScopeToken& Token)
{
if (!Token.bEntered)
{
return;
}
RecordCall(Bucket);
if (Token.bBlockTiming)
{
FThreadState& State = GetThreadState();
const int32 Index = ToIndex(Bucket);
++State.BlockCallCount[Index];
const uint32 Interval = GetSampleInterval();
if (State.BlockActive[Index] && State.BlockCallCount[Index] >= Interval)
{
const uint64 RawCycles = FPlatformTime::Cycles64() - State.BlockStartCycles[Index];
const uint64 TimerCost = GetTimerPairCycles();
const uint64 AdjustedCycles = RawCycles > TimerCost
? RawCycles - TimerCost
: 0;
State.Cycles[Index] += AdjustedCycles / FMath::Max<uint32>(1, Interval);
State.Samples[Index] += 1;
State.BlockActive[Index] = false;
State.BlockCallCount[Index] = 0;
State.BlockStartCycles[Index] = 0;
}
}
else if (Token.bSampled)
{
AddSampledMeasurement(Bucket, FPlatformTime::Cycles64() - Token.StartCycles);
}
check(GScopeDepth > 0);
--GScopeDepth;
GScopeSampled = Token.bPreviousSampled;
GSampledScopeActive = Token.bPreviousSampledScopeActive;
}
FScopedTimer::~FScopedTimer()
{
End();
@@ -363,8 +653,8 @@ namespace VoxelDensityProfile
void FScopedTimer::End()
{
if (!bActive) { return; }
AddMeasurement(Bucket, FPlatformTime::Cycles64() - StartCycles, 1);
bActive = false;
if (!Token.bEntered) { return; }
EndScope(Bucket, Token);
Token.bEntered = false;
}
}
+13 -5
View File
@@ -18,6 +18,7 @@
#include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it
#include "VoxelStats.h"
#include "VoxelDensityProfile.h"
#include "ProfilingDebugging/CpuProfilerTrace.h"
#if WITH_EDITOR
#include "VoxelStrateComposer.h"
@@ -535,6 +536,7 @@ namespace
uint64 EntryBytes = 0;
uint64 LargestEntryBytes = 0;
uint64 ValidEntries = 0;
VoxelDensityProfile::FCacheMemoryBreakdown Breakdown;
for (int32 Index = 0; Index < TunnelDensityCacheSlotCount; ++Index)
{
const FTunnelNetworkDensityCacheEntry& Entry = Entries[Index];
@@ -547,6 +549,8 @@ namespace
const uint64 StackBytes = static_cast<uint64>(Entry.OpStack.GetAllocatedSize());
const uint64 TunnelCoreBytes = static_cast<uint64>(
Entry.TunnelCore.Cache.GetAllocatedSize());
Breakdown.OpStackBytes += StackBytes;
Breakdown += Entry.TunnelCore.Cache.GetAllocatedSizeBreakdown();
const uint64 EntryDynamicBytes = StackBytes + TunnelCoreBytes;
const uint64 FullEntryBytes = static_cast<uint64>(
sizeof(FTunnelNetworkDensityCacheEntry)) + EntryDynamicBytes;
@@ -554,6 +558,8 @@ namespace
EntryBytes += FullEntryBytes;
LargestEntryBytes = FMath::Max(LargestEntryBytes, FullEntryBytes);
}
Breakdown.SlotStorageBytes = static_cast<uint64>(sizeof(FTunnelNetworkDensityCacheEntry))
* TunnelDensityCacheSlotCount;
VoxelDensityProfile::SetWorkerTunnelCacheFootprint(
TunnelDensityCacheSlotCount,
@@ -563,7 +569,8 @@ namespace
DynamicBytes,
EntryBytes,
LargestEntryBytes,
ValidEntries);
ValidEntries,
Breakdown);
}
FORCEINLINE int32 TunnelDensityCacheSlot(const FIntVector& Chunk)
@@ -852,6 +859,7 @@ void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) const
{
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GetDensityAt);
VoxelDensityProfile::FScopedTimer DensityProfileTimer(
VoxelDensityProfile::EBucket::GetDensityAt);
VoxelDensityProfile::FScopedTimer DensityPrologueTimer(
@@ -998,7 +1006,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
if (!bIsGapChunk)
{
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCacheLookup);
@@ -1009,7 +1017,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
TunnelDensityCacheEntryForWrite = CachedEntryPtr;
if (bCacheKeyMatches)
{
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCacheHit);
@@ -1035,7 +1043,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
ActiveTunnelCoreCache = &CachedEntry.TunnelCore;
bLoadedTunnelDensityCache = true;
}
else if (VoxelDensityProfile::IsEnabled())
else if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCacheMiss);
@@ -1765,7 +1773,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
if (bNeedRebuild)
{
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::SdfCacheBuild);
@@ -4,6 +4,7 @@
#include "VoxelMarchingCubesMesher.h"
#include "MarchingCubesTables.h"
#include "VoxelDensityProfile.h"
#include "ProfilingDebugging/CpuProfilerTrace.h"
//=============================================================================
// MAIN ALGORITHM
@@ -15,6 +16,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
TArray<uint8>* OutCaptureGrid,
int32 BandZMinVox, int32 BandZMaxVox)
{
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh);
FVoxelMeshData MeshData;
if (OutCaptureGrid) { OutCaptureGrid->Reset(); }
if (!Generator) return MeshData;
@@ -300,11 +302,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
static thread_local TArray<int32> CapTris;
GroundTris.Reset();
CapTris.Reset();
const bool bProfileMesher = VoxelDensityProfile::IsEnabled();
uint64 CellClassificationCycles = 0;
uint64 GradientCycles = 0;
uint64 VertexInterpolationCycles = 0;
uint64 StreamBuildingCycles = 0;
const bool bProfileMesher = VoxelDensityProfile::IsCycleTimingEnabled();
uint64 CellClassificationCalls = 0;
uint64 SurfaceCellCalls = 0;
for (int32 cz = CzLo; cz <= CzHi; cz++) // bande de strate : cf. CzLo/CzHi plus haut
@@ -315,7 +313,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
if (ShouldAbortWork()) return FVoxelMeshData();
for (int32 cx = 0; cx < CellsPerAxis; cx++)
{
const uint64 ClassificationStart = bProfileMesher
const bool bSampleCell = bProfileMesher
&& VoxelDensityProfile::ShouldSample(
VoxelDensityProfile::EBucket::MesherCellClassification);
const uint64 ClassificationStart = bSampleCell
? FPlatformTime::Cycles64() : 0;
// PASSE 1 : densités aux 8 coins + index de cas MC SEULEMENT.
// ~70% des cellules d'un chunk sont tout-roc ou tout-air (aucune surface) ;
@@ -334,16 +335,18 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
}
const bool bHasSurface = MCEdgeTable[CaseIndex] != 0;
if (bProfileMesher)
if (bSampleCell)
{
CellClassificationCycles += FPlatformTime::Cycles64() - ClassificationStart;
++CellClassificationCalls;
VoxelDensityProfile::AddSampledMeasurement(
VoxelDensityProfile::EBucket::MesherCellClassification,
FPlatformTime::Cycles64() - ClassificationStart);
}
++CellClassificationCalls;
if (!bHasSurface) continue; // Pas de surface ici → skip
if (bProfileMesher) { ++SurfaceCellCalls; }
++SurfaceCellCalls;
// PASSE 2 : positions + gradients aux 8 coins (uniquement si surface présente).
const uint64 GradientStart = bProfileMesher
const uint64 GradientStart = bSampleCell
? FPlatformTime::Cycles64() : 0;
FVector Positions[8];
FVector Gradients[8];
@@ -356,15 +359,17 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
+ FVector(GX * Step, GY * Step, GZ * Step) * VOXEL_SIZE;
Gradients[i] = GradAt(GX, GY, GZ);
}
if (bProfileMesher)
if (bSampleCell)
{
GradientCycles += FPlatformTime::Cycles64() - GradientStart;
VoxelDensityProfile::AddSampledMeasurement(
VoxelDensityProfile::EBucket::MesherGradientNormals,
FPlatformTime::Cycles64() - GradientStart);
}
// Interpolation des positions + normales sur les arêtes traversées. t = point de
// traversée de l'iso entre les deux coins (clampé, milieu si densités quasi-égales) ;
// la normale interpole les gradients de coin par le même t.
const uint64 VertexInterpolationStart = bProfileMesher
const uint64 VertexInterpolationStart = bSampleCell
? FPlatformTime::Cycles64() : 0;
FVector EdgeVertices[12];
FVector EdgeNormals[12];
@@ -382,15 +387,16 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
EdgeNormals[i] = Gradients[A] + T * (Gradients[B] - Gradients[A]);
}
}
if (bProfileMesher)
if (bSampleCell)
{
VertexInterpolationCycles +=
FPlatformTime::Cycles64() - VertexInterpolationStart;
VoxelDensityProfile::AddSampledMeasurement(
VoxelDensityProfile::EBucket::MesherVertexInterpolation,
FPlatformTime::Cycles64() - VertexInterpolationStart);
}
// Génère les triangles avec vertices dédupliqués.
// Ordre 0, 2, 1 (pas 0, 1, 2) pour le winding attendu par RealtimeMesh.
const uint64 StreamBuildingStart = bProfileMesher
const uint64 StreamBuildingStart = bSampleCell
? FPlatformTime::Cycles64() : 0;
for (int32 i = 0; MCTriTable[CaseIndex][i] != -1; i += 3)
{
@@ -410,9 +416,11 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
Dst.Add(Idx2);
Dst.Add(Idx1);
}
if (bProfileMesher)
if (bSampleCell)
{
StreamBuildingCycles += FPlatformTime::Cycles64() - StreamBuildingStart;
VoxelDensityProfile::AddSampledMeasurement(
VoxelDensityProfile::EBucket::MesherStreamBuilding,
FPlatformTime::Cycles64() - StreamBuildingStart);
}
}
}
@@ -422,16 +430,16 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
{
VoxelDensityProfile::AddMeasurement(
VoxelDensityProfile::EBucket::MesherCellClassification,
CellClassificationCycles, CellClassificationCalls);
0, CellClassificationCalls);
VoxelDensityProfile::AddMeasurement(
VoxelDensityProfile::EBucket::MesherGradientNormals,
GradientCycles, SurfaceCellCalls);
0, SurfaceCellCalls);
VoxelDensityProfile::AddMeasurement(
VoxelDensityProfile::EBucket::MesherVertexInterpolation,
VertexInterpolationCycles, SurfaceCellCalls);
0, SurfaceCellCalls);
VoxelDensityProfile::AddMeasurement(
VoxelDensityProfile::EBucket::MesherStreamBuilding,
StreamBuildingCycles, SurfaceCellCalls);
0, SurfaceCellCalls);
}
VoxelDensityProfile::FScopedTimer MesherFinalisationTimer(
@@ -1688,7 +1688,7 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
// The shortlist is rebuilt once for a (manager, version, chunk) and is shared by the tube,
// landing, and floor paths. No source-fit stencil or topology search is allowed below it.
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::PassageCandidates,
@@ -1714,7 +1714,7 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
// capsule chain unconditionally — the dominant lag source once passages became
// 12-segment worms. Now far passages cost a single squared-distance compare.
if (FVector::DistSquared(Pos, P.BoundCenter) > P.BoundRadiusSq) continue;
if (VoxelDensityProfile::IsEnabled())
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::PassageEvaluated);
@@ -30,6 +30,7 @@
#pragma once
#include "CoreMinimal.h"
#include "VoxelDensityProfile.h"
// Forward declarations
struct FStrateGenerationParams;
@@ -825,6 +826,7 @@ struct FChunkSDFCache
// arrays. The returned value excludes sizeof(FChunkSDFCache) itself so callers can add the
// enclosing entry's inline storage exactly once.
SIZE_T GetAllocatedSize() const;
VoxelDensityProfile::FCacheMemoryBreakdown GetAllocatedSizeBreakdown() const;
};
/** Combined world-space tunnel post query; one cache scan supplies both support and air tests. */
+125 -13
View File
@@ -11,6 +11,17 @@
namespace VoxelDensityProfile
{
enum class EMode : uint8
{
Disabled,
Sampled,
Full,
};
// Count-based sampling is deterministic with respect to the profiler's own counters, but the
// report is intentionally diagnostic only. It never participates in density or mesh output.
constexpr uint32 DefaultSampleInterval = 64;
enum class EBucket : uint8
{
GetDensityAt,
@@ -94,12 +105,91 @@ namespace VoxelDensityProfile
constexpr int32 CounterCount = static_cast<int32>(ECounter::Count);
// Resident allocator-backed bytes for one worker's generation cache family. The profiler
// aggregates this by worker; the fields intentionally describe allocations owned by the
// cache, not the inline size of the enclosing C++ object. SlotStorageBytes is the one
// exception: it is the inline/static storage reserved for the worker's fixed cache table.
struct FCacheMemoryBreakdown
{
uint64 SlotStorageBytes = 0;
uint64 OpStackBytes = 0;
uint64 RoomsBytes = 0;
uint64 RoomFloorJoinsBytes = 0;
uint64 TunnelsBytes = 0;
uint64 PitsBytes = 0;
uint64 ChimneysBytes = 0;
uint64 ColumnsBytes = 0;
uint64 SupportColumnEntriesBytes = 0;
uint64 SupportColumnsBytes = 0;
uint64 SupportColumnIntervalsBytes = 0;
uint64 RoomIndexBinsBytes = 0;
uint64 RoomIndexCandidatesBytes = 0;
uint64 RoomFloorJoinIndexBinsBytes = 0;
uint64 RoomFloorJoinIndexCandidatesBytes = 0;
uint64 TunnelIndexBinsBytes = 0;
uint64 TunnelIndexCandidatesBytes = 0;
uint64 TunnelWorldIndexBinsBytes = 0;
uint64 TunnelWorldIndexCandidatesBytes = 0;
uint64 TotalBytes() const
{
return SlotStorageBytes + OpStackBytes
+ RoomsBytes + RoomFloorJoinsBytes + TunnelsBytes + PitsBytes
+ ChimneysBytes + ColumnsBytes + SupportColumnEntriesBytes
+ SupportColumnsBytes + SupportColumnIntervalsBytes
+ RoomIndexBinsBytes + RoomIndexCandidatesBytes
+ RoomFloorJoinIndexBinsBytes + RoomFloorJoinIndexCandidatesBytes
+ TunnelIndexBinsBytes + TunnelIndexCandidatesBytes
+ TunnelWorldIndexBinsBytes + TunnelWorldIndexCandidatesBytes;
}
uint64 DynamicBytes() const
{
return TotalBytes() - SlotStorageBytes;
}
FCacheMemoryBreakdown& operator+=(const FCacheMemoryBreakdown& Other)
{
SlotStorageBytes += Other.SlotStorageBytes;
OpStackBytes += Other.OpStackBytes;
RoomsBytes += Other.RoomsBytes;
RoomFloorJoinsBytes += Other.RoomFloorJoinsBytes;
TunnelsBytes += Other.TunnelsBytes;
PitsBytes += Other.PitsBytes;
ChimneysBytes += Other.ChimneysBytes;
ColumnsBytes += Other.ColumnsBytes;
SupportColumnEntriesBytes += Other.SupportColumnEntriesBytes;
SupportColumnsBytes += Other.SupportColumnsBytes;
SupportColumnIntervalsBytes += Other.SupportColumnIntervalsBytes;
RoomIndexBinsBytes += Other.RoomIndexBinsBytes;
RoomIndexCandidatesBytes += Other.RoomIndexCandidatesBytes;
RoomFloorJoinIndexBinsBytes += Other.RoomFloorJoinIndexBinsBytes;
RoomFloorJoinIndexCandidatesBytes += Other.RoomFloorJoinIndexCandidatesBytes;
TunnelIndexBinsBytes += Other.TunnelIndexBinsBytes;
TunnelIndexCandidatesBytes += Other.TunnelIndexCandidatesBytes;
TunnelWorldIndexBinsBytes += Other.TunnelWorldIndexBinsBytes;
TunnelWorldIndexCandidatesBytes += Other.TunnelWorldIndexCandidatesBytes;
return *this;
}
};
struct FSnapshot
{
uint64 Calls[BucketCount]{};
// Sum of sampled worker time. Useful for CPU-work accounting, but not directly comparable
// with an export wall clock when the mesher runs in parallel.
uint64 Cycles[BucketCount]{};
uint64 Samples[BucketCount]{};
// Largest estimated worker contribution. This is the report-facing parallel wall
// estimate; it avoids mistaking several parallel workers for several exports.
uint64 WallCycles[BucketCount]{};
uint32 ActiveWorkers[BucketCount]{};
uint64 Counters[CounterCount]{};
uint64 TimerPairCycles = 0;
uint32 SampleInterval = DefaultSampleInterval;
EMode Mode = EMode::Disabled;
// Opt-in worker-cache footprint snapshot. These are current per-thread values (not
// allocation-event counters), so Snapshot() reports the resident cache footprint at the
// end of a profiling run rather than multiplying bytes by cache churn.
@@ -112,6 +202,7 @@ namespace VoxelDensityProfile
uint64 TunnelCacheLargestEntryBytes = 0;
uint64 TunnelCacheLargestWorkerValidEntries = 0;
uint64 TunnelCacheLargestWorkerBytes = 0;
FCacheMemoryBreakdown TunnelCacheBreakdown;
uint64 RoomGraphCacheWorkers = 0;
uint64 RoomGraphCacheCapacityEntries = 0;
@@ -122,49 +213,71 @@ namespace VoxelDensityProfile
uint64 RoomGraphCacheLargestEntryBytes = 0;
uint64 RoomGraphCacheLargestWorkerValidEntries = 0;
uint64 RoomGraphCacheLargestWorkerBytes = 0;
FCacheMemoryBreakdown RoomGraphCacheBreakdown;
};
VOXELFORGE_API void SetEnabled(bool bEnabled);
VOXELFORGE_API void SetMode(EMode Mode, uint32 SampleInterval = DefaultSampleInterval);
// Coarse counters are intentionally always collected, even when cycle timing is disabled.
// They are the low-cost diagnostic path; only cycle timing is opt-in.
FORCEINLINE bool AreCountersEnabled() { return true; }
VOXELFORGE_API bool IsEnabled();
VOXELFORGE_API bool IsCycleTimingEnabled();
VOXELFORGE_API EMode GetMode();
VOXELFORGE_API uint32 GetSampleInterval();
VOXELFORGE_API uint64 GetTimerPairCycles();
VOXELFORGE_API void CalibrateTimer();
VOXELFORGE_API void Reset();
VOXELFORGE_API FSnapshot Snapshot();
VOXELFORGE_API void AddCounter(ECounter Counter, uint64 Amount = 1);
VOXELFORGE_API void AddMeasurement(EBucket Bucket, uint64 Cycles, uint64 Calls = 1);
VOXELFORGE_API void RecordCall(EBucket Bucket);
VOXELFORGE_API bool ShouldSample(EBucket Bucket);
VOXELFORGE_API void AddSampledMeasurement(EBucket Bucket, uint64 RawCycles);
VOXELFORGE_API void SetWorkerTunnelCacheFootprint(
uint64 CapacityEntries, uint64 ValidEntries,
uint64 StaticBytes, uint64 DynamicBytes,
uint64 EntryBytes, uint64 LargestEntryBytes,
uint64 LargestWorkerValidEntries);
uint64 LargestWorkerValidEntries,
const FCacheMemoryBreakdown& Breakdown);
VOXELFORGE_API void SetWorkerRoomGraphCacheFootprint(
uint64 CapacityEntries, uint64 ValidEntries,
uint64 StaticBytes, uint64 DynamicBytes,
uint64 EntryBytes, uint64 LargestEntryBytes,
uint64 LargestWorkerValidEntries);
uint64 LargestWorkerValidEntries,
const FCacheMemoryBreakdown& Breakdown);
VOXELFORGE_API const TCHAR* CounterName(ECounter Counter);
VOXELFORGE_API EBucket BucketFromName(const TCHAR* Name);
VOXELFORGE_API const TCHAR* BucketName(EBucket Bucket);
struct FScopeToken
{
bool bEntered = false;
bool bSampled = false;
bool bBlockTiming = false;
bool bPreviousSampled = false;
bool bPreviousSampledScopeActive = false;
uint64 StartCycles = 0;
};
VOXELFORGE_API FScopeToken BeginScope(EBucket Bucket);
VOXELFORGE_API void EndScope(EBucket Bucket, const FScopeToken& Token);
class FScopedTimer final
{
public:
explicit FScopedTimer(EBucket InBucket)
: Bucket(InBucket)
, bActive(IsEnabled())
, StartCycles(bActive ? FPlatformTime::Cycles64() : 0)
, Token(BeginScope(InBucket))
{
}
explicit FScopedTimer(const TCHAR* InName)
: Bucket(EBucket::OtherOp)
, bActive(IsEnabled())
, StartCycles(0)
{
if (bActive)
{
Bucket = BucketFromName(InName);
StartCycles = FPlatformTime::Cycles64();
}
Bucket = BucketFromName(InName);
Token = BeginScope(Bucket);
}
~FScopedTimer();
@@ -178,7 +291,6 @@ namespace VoxelDensityProfile
private:
EBucket Bucket = EBucket::OtherOp;
bool bActive = false;
uint64 StartCycles = 0;
FScopeToken Token;
};
}
File diff suppressed because it is too large. Load diff