C++ profiling: from a slow action to its cause
CPU/GPU, symbols, correct process, sampling/tracing and result comparison.
Start with one slow action: a frame at a particular location, resource import or level load. Use a C++ profiler to inspect native engine/extension work. C# logic also needs a managed profiler; native stacks alone do not expose all managed execution.
CPU, GPU or waiting
| Observation | Investigate |
|---|---|
| A thread is executing computations | CPU samples/stacks, expensive functions and their callers |
| A thread is waiting | Timeline, synchronization, I/O and frame waits; wall time is not equivalent CPU work |
| Cost grows with resolution/effects | Separate GPU timing/render capture rather than CPU samples |
| Only the first open is slow | Import, loading and warmup separately from steady-state gameplay |
| Editor slow but standalone game fine | Capture the editor workload rather than a neighboring game process |
FPS alone does not identify a cause. Record resolution, renderer, VSync/frame cap and camera position with a reproducible action. Headless execution establishes neither GPU nor editor-window cost.
Sampling and tracing
Sampling periodically collects stacks to locate CPU work. It is statistical: short captures/rare functions may contain few samples. Tracing shows explicitly instrumented event intervals; a missing zone proves no absence of work. Filtering to a render thread does not turn a native CPU profiler into a GPU profiler.
Windows-specific guides cover Very Sleepy and Superluminal. For other systems, see Hotspot and Instruments. Select by required data/platform rather than expecting identical workflows in every application.
Binaries and symbols
Capturing addresses can work without debug symbols. Function/source resolution needs symbols matching the exact executable/native libraries. A shared product version of 1.0.0 is insufficient: retain binary hashes, symbols and source revision together. C# PDB files do not replace native symbols.
For a separate build, debug_symbols=yes enables symbols without automatically disabling optimization. production=yes debug_symbols=yes preserves the explicit symbol request, but production also changes defaults including LTO. Compare equal compile/link settings. optimize=speed_trace changes optimization relative to speed; do not transfer its measurements to release without checking.
Current MSVC symbol flags include /Zi and /DEBUG:FULL; other toolchains use DWARF flags. Retain separated symbols when applicable and configure profiler symbol search. Matching PDB/source filenames alone proves no binary match. Obtain readable stacks in your module before interpreting source lines.
Select the correct process
| Target | Launch mode |
|---|---|
| Standalone project | Engine executable with --path pointing to a directory containing project.reckfell |
| Project editor | Also add --editor |
| Minimal-process startup/shutdown | Add --quit to the chosen mode; it covers the first iteration only |
Profiler working directory is no reliable replacement for explicit --path. Project Manager, editor and Play can use different PIDs. If a capture contains project selection/idle only, inspect executable path, arguments and target PID. Future child processes require explicit profiler support; attach need not follow them automatically.
--quit exits after the first main-loop iteration. It guarantees neither complete level loading, finished deferred/asynchronous work nor a reproducible steady-state frame. Launch short-lived startup targets from the profiler because attach can miss them. For gameplay, omit --quit, reach the intended state and capture the same interval.
Interpret and repeat
- Verify target process, resolved symbols and absence of project-load errors first.
- For steady-state, use equal warmup and repeat the same action/capture several times. Measure cold starts separately without accidental warmup.
- Distinguish inclusive time including callees from exclusive time in a function body. Do not sum inclusive rows as independent frame costs.
- Inspect the complete process/threads before filtering modules/intervals. Hiding driver/system frames does not remove their real cost.
- Fix a specific cause, repeat the scenario and compare absolute time under equal conditions. A function percentage can fall because other work increased; percentages alone are insufficient.
Use a microbenchmark after locating a hotspot: measure a concrete operation with a consumed result and equal inputs. Empty loops or writes to unused variables can disappear under optimization. static alone does not prevent elimination. Microbenchmarks do not replace full-scenario validation or require empty engine-source tests.
Instrumented zones
Source supports profiler=tracy, profiler=perfetto and profiler=instruments, defaulting to none. Without a backend the profile macros are stubs. A project setting cannot add these zones to an existing binary. A separately configured build is needed; none was built here.
See Tracy, Perfetto and Instruments. Tracy/Perfetto build scripts validate SDK paths; callstack/memory options differ. A zone macro in source proves no recorded trace. Available prebuilt instrumented Reckfell downloads are not established here.
Verification limits
Build options, symbols and tracing gates were reviewed against Reckfell 1.0.0 source. A separate installed-Runtime headless CLI check executed explicit --path game targeting and --quit after the first iteration. This is no CPU/GPU measurement. Third-party profiler capture, symbol resolution, optimized builds and demonstrated speedups remain untested. Engine source, Runtime and SDK unchanged.