Performance Diagnostics
This tutorial gives a practical triage path for large scenes, streaming, batching, LOD, texture streaming, and frame-time issues.
Use structured metrics first. Enable subsystem logs only when you need detail.
Enable Metrics
At runtime:
Or from the environment:
Enable periodic frame stats:
Use .verbose when diagnosing streaming, batching, or memory behavior:
Read Snapshots In Code
let metrics = EngineProfiler.shared.snapshot()
print("CPU mean: \(metrics.cpuFrame.meanMs) ms")
print("GPU mean: \(metrics.gpuCommandBuffer.meanMs) ms")
let frameStats = getEngineStatsSnapshot()
print("Frame: \(frameStats.frameIndex)")
print("Render: \(frameStats.timing.renderTotalMs) ms")
Use this for in-app HUDs, automated tests, or regression tracking.
Spatial Debug Overlays
Use overlays to inspect what the engine is doing spatially:
setSpatialDebug(.tileBounds(enabled: true))
setSpatialDebug(.lodLevels(true))
setSpatialDebug(.textureStreamingTiers(true))
For octree leaf bounds:
setSpatialDebug(.octreeLeafBounds(.enabled(
maxLeafNodeCount: 0,
occupiedOnly: true,
colorMode: .residency
)))
Disable overlays when finished:
Streaming Triage
For streamed scenes, start with verbose stats. Look for:
- high
backlog: load slots are saturated - high
avgLoadMs: asset I/O or parsing is slow - high
applyMs: GPU upload/application cost is expensive - frequent
evictions: memory budget is too tight - high visible full-tile triangles far from camera: LOD switching may be late
Read streaming diagnostics programmatically:
let diag = GeometryStreamingSystem.shared.getDiagnosticsSnapshot()
print("workMs: \(diag.updateWorkMs)")
print("candidates: \(diag.loadCandidates)")
print("slots: \(diag.availableLoadSlots)")
print("evictions: \(diag.evictionsPerformed)")
Tune with:
setGeometryStreaming(.tileConcurrency(2))
setGeometryStreaming(.meshConcurrency(3))
setGeometryStreaming(.queryRadius(650.0))
setGeometryStreaming(.velocityLookAhead(time: 0.5, minSpeed: 1.5))
setGeometryStreaming(.candidateSorting(importance: true, occlusion: true))
Static Batching Triage
If draw count is high relative to visible entities, inspect batching:
setLogger(.category(.batching, true))
BatchingSystem.shared.logMaterialDiagnosticsNow()
setLogger(.category(.batching, false))
Common patterns:
| Symptom | Likely Cause |
|---|---|
staticBatch=0 |
Entities were never marked with StaticBatchComponent. |
high uniqueMatLOD |
Too much material diversity. |
cellsBlocked > 0 |
Batch cells are too complex for the runtime guard. |
low resolved |
Entities are transparent, animated, or identity-preserving. |
For always-resident static content:
For tiled streaming scenes, do not call generateBatches() per tile. The engine
updates streamed batching incrementally.
Logger Categories
Enable focused logs only while reproducing an issue:
setLogger(.categories([
.tileStreaming,
.streamingHeartbeat,
.oocStatus,
.oocTiming,
.assetLoader,
], true))
Disable them after capture:
setLogger(.categories([
.tileStreaming,
.streamingHeartbeat,
.oocStatus,
.oocTiming,
.assetLoader,
], false))
Texture diagnostics:
Light portal diagnostics:
setLogger(.category(.lightPortal, true))
LightPortalSystem.shared.logDiagnosticsNow()
setLogger(.category(.lightPortal, false))
Practical Triage Order
- Enable metrics and compact frame stats.
- If frame time is bad, switch to verbose stats.
- Use spatial overlays to inspect tile bounds, LOD, and texture tiers.
- If draw count is high, run batching diagnostics.
- If loading stalls, inspect streaming diagnostics and logs.
- If visual lighting from portals is wrong, run light portal diagnostics.
- Disable high-volume log categories when finished.