Skip to content

Performance Profiling Guide

github-actions[bot] edited this page Oct 18, 2026 · 2 revisions

Performance Profiling Guide

This page explains how to use SparkEngine's built-in profiling tools to identify and resolve performance bottlenecks.

Source: SparkEngine/Source/Utils/Profiler.h, ChromeTracing.h, MemoryDebugger.h, DebugOverlay.h, FrameInspector.h

performance-budget-governance is a required CI job. It validates perf-budgets/v1 and runs the comparator's adversarial and CLI regression tests, including an over-budget result that must exit nonzero. CTest also registers the hardening suite as PerformanceBudget_Hardening. This is policy enforcement: the committed metrics are still pending_measurement, with no certified hardware rows or accepted measured baselines. No runtime performance gate is claimed until a real result producer and certified baseline are available.


Quick Start

Enable the profiler overlay in-game:

auto& profiler = Profiler::GetInstance();
profiler.SetOverlayEnabled(true);

Or via the debug console:

profiler.overlay on
profiler.gpu on

CPU Profiling

Profile Categories

Every profiling sample is tagged with a category:

Category What it covers
Frame Overall frame timing
Render Draw calls, state changes
Physics Jolt Physics simulation step
Audio XAudio2 processing
GameLogic ECS systems, gameplay
Input Input polling and dispatch
Particles Particle system updates
UI ImGui layout and rendering
Custom User-defined sections

Manual Instrumentation

auto& profiler = Profiler::GetInstance();

// Begin/end a named section
profiler.BeginSection("PhysicsStep", ProfileCategory::Physics);
physicsSystem.Update(dt);
profiler.EndSection("PhysicsStep");

Scoped Profiling (RAII)

{
    PROFILE_SCOPE("RenderShadows", ProfileCategory::Render);
    RenderShadowMaps();
}  // Automatically ends when scope exits

Reading Results

float physicsMs = profiler.GetSectionTime("PhysicsStep");
float frameMs = profiler.GetFrameTime();
float fps = 1000.0f / frameMs;

// Get all section timings
auto sections = profiler.GetAllSections();
for (const auto& [name, timing] : sections) {
    // timing.lastMs, timing.avgMs, timing.maxMs, timing.category
}

GPU Profiling

GPU timing uses D3D11 timestamp queries (Windows only):

profiler.SetGPUProfilingEnabled(true);

// In rendering code
profiler.BeginGPUSection("ShadowPass");
RenderShadowMaps();
profiler.EndGPUSection("ShadowPass");

// Read results (available next frame due to GPU latency)
float shadowMs = profiler.GetGPUSectionTime("ShadowPass");

On the Windows D3D11 game path, gfx_benchmark <seconds> (1-300) observes actual successful swap-chain presents while normal gameplay continues. The command returns a start acknowledgement; the final console entry reports the successful present count, elapsed wall-clock FPS, CPU time from BeginFrame() through Present(), and (on D3D11 when timestamp results are valid) the average GPU render interval from up to 60 valid D3D11 timestamp samples, collected two frames after submission. This interval ends before the pre-Present overlay and swap-chain Present(); it is not GPU wall-clock frame time. The result labels GPU timing unavailable when the driver/device returns no valid samples. If rendering stops before the requested interval, the result is explicitly incomplete.

Compare runs only with the same scene/workload, VSync state, resolution, graphics settings, backend, and hardware; the wall-clock Present-call rate is not a cross-configuration GPU benchmark.

gfx_screenshot [filename] likewise acknowledges a queued request, not a saved file. The next D3D11 frame is captured after the game overlay and before Present(); the console then reports the saved path or a failure. A request still queued at renderer shutdown is reported as not saved.


Frame Timing History

The profiler maintains a rolling history of frame times:

// Get the last N frame times
auto history = profiler.GetFrameTimeHistory();  // vector<float>

// Get statistics
float avgFrame = profiler.GetAverageFrameTime();  // ms
float minFrame = profiler.GetMinFrameTime();
float maxFrame = profiler.GetMaxFrameTime();
float p99Frame = profiler.GetPercentileFrameTime(99);

Chrome Tracing Export

Export profiling data to Chrome's chrome://tracing format for detailed timeline analysis:

Source: SparkEngine/Source/Utils/ChromeTracing.h

ChromeTracing tracer;

// Record events
tracer.BeginEvent("Update", "GameLogic");
// ... work ...
tracer.EndEvent("Update", "GameLogic");

// Export to file
tracer.ExportToFile("profile_capture.json");

Open the exported .json file in:

  • Chrome: Navigate to chrome://tracing and load the file
  • Perfetto UI: Drag and drop the file

Capture Workflow

  1. Start capture: profiler.capture start
  2. Play through the problematic scenario
  3. Stop capture: profiler.capture stop
  4. File is saved to profile_capture.json in the working directory

Memory Profiling

Source: SparkEngine/Source/Utils/MemoryDebugger.h

Category Tracking

auto stats = MemoryDebugger::GetInstance().GetCategoryStats();
for (const auto& cat : stats) {
    // cat.name: "Physics", "Rendering", "Audio", etc.
    // cat.currentBytes: currently allocated
    // cat.peakBytes: high-water mark
    // cat.totalAllocations: lifetime count
}

Allocation Hotspots

Find the code locations that allocate most frequently:

auto hotspots = MemoryDebugger::GetInstance().GetHotSpots(10);
for (const auto& spot : hotspots) {
    // spot.location: "Physics/RigidBody.cpp:42"
    // spot.count: number of allocations from this site
    // spot.totalBytes: total bytes allocated
}

Leak Detection

At shutdown:

SPARK_PRINT_LEAK_REPORT();
// Output:
//   [LEAK] 0x7fff1234: 256 bytes (Physics) at PhysicsBody.cpp:87
//   [LEAK] 0x7fff5678: 1024 bytes (Rendering) at Mesh.cpp:142

Debug Overlay

The debug overlay renders real-time statistics as an ImGui window:

DebugOverlay::GetInstance().SetEnabled(true);

Displayed Information

Section Metrics
Frame FPS, frame time (ms), min/max/avg
CPU Per-category breakdown bar chart
GPU Per-pass timing (shadow, geometry, post-process)
Memory Total allocated, per-category breakdown
Draw Calls Total draws, triangles, state changes
Physics Active bodies, collision pairs, simulation time

Console Commands

All profiling tools are accessible via the debug console:

Command Description
profiler.overlay on/off Toggle the profiler overlay
profiler.gpu on/off Toggle GPU timing queries
profiler.capture start Begin Chrome Tracing capture
profiler.capture stop End capture and save to file
profiler.memory Print memory category summary
profiler.leaks Print leak report
profiler.hotspots [N] Show top N allocation hotspots
profiler.reset Clear accumulated statistics

Common Bottleneck Patterns

High Frame Time (> 16.6 ms for 60 FPS)

  1. Check the CPU category breakdown — which system dominates?
  2. If Render is high: check draw call count, reduce overdraw, enable frustum culling
  3. If Physics is high: reduce active body count, increase fixed timestep, simplify collision shapes
  4. If GameLogic is high: profile individual ECS systems, check for O(n^2) algorithms

GPU Bound

  1. Enable GPU profiling: profiler.gpu on
  2. Check which pass is slowest (shadow, geometry, post-process)
  3. Reduce resolution, disable expensive effects (SSAO, bloom), lower shadow quality

Memory Growth

  1. Run profiler.memory periodically to track category growth
  2. Check profiler.hotspots 20 for excessive allocation sites
  3. Consider object pooling for frequently allocated types
  4. Run profiler.leaks at shutdown to catch leaks

Headless NullRHI Soak (PERF-100)

tools/perf-budget/run_nullrhi_soak.py runs the real headless host (-headless -game <module> -require-game -test-frames N) for --duration seconds and watches it from outside. It fails on a crash, a hang, a memory slope above the provisional --max-leak-bytes-per-hour ceiling, or NullRHI resources still live at device shutdown (the host's single SPARK_HEADLESS_NULLRHI_RESOURCES live=N record must say live=0).

Host Memory series Main-thread heartbeat Output
Linux VmRSS from /proc voluntary context switches and CPU time --report, and --out for a >= 1 h run on the linux-nullrhi-ci row
Windows PrivateUsage from K32GetProcessMemoryInfo QueryThreadCycleTime of the earliest-created thread --report only; no Windows soak metric row is defined yet

A main thread blocked on a lock advances neither heartbeat and is declared hung after --heartbeat-timeout. Unbounded queue growth inside the host shows up only as a rising memory slope. ctest -L nullrhi-soak runs the 120 s Soak_NullRHIHeadlessSmoke on both hosts; its budgets are provisional harness guards, and nullrhi.soak.* stay pending_measurement in perf-budgets/v1.

Frame Spikes

  1. Start a Chrome Tracing capture around the spike
  2. Look for single-frame anomalies: GC pauses, asset loads, physics explosions
  3. Use async loading for assets, spread heavy work across frames

Integration with External Tools

RenderDoc

SparkEngine's D3D11 renderer is compatible with RenderDoc for GPU debugging:

  1. Launch the engine through RenderDoc
  2. Press F12 (default) to capture a frame
  3. Inspect draw calls, shader state, and GPU resources

Visual Studio Profiler

For detailed CPU profiling on Windows:

  1. Open the solution in Visual Studio
  2. Debug > Performance Profiler > CPU Usage
  3. Run the scenario and analyze the call tree

Valgrind (Linux)

For memory analysis on Linux:

valgrind --leak-check=full --track-origins=yes ./bin/SparkEngine

Best Practices

  1. Profile before optimizing. Measure first, then fix the actual bottleneck.
  2. Use Release builds for profiling. Debug builds have different performance characteristics.
  3. Profile representative scenarios. Test with real game content, not empty scenes.
  4. Track frame time, not FPS. Frame time is linear and easier to reason about.
  5. Watch for spikes, not just averages. A 1% spike can cause visible stuttering.
  6. Use Chrome Tracing for complex issues. The timeline view reveals ordering and overlap.

See Profiler and Debugging for the full API reference.

SparkEngine Wiki

Website Entry Points

Getting Started

Engine Subsystems

Gameplay & Tools

Platform Support

Graphics

Advanced

Development & Process

Research & Analysis

Engineering Notes & Audits

Specifications

Reference

Clone this wiki locally