Integrated CPU Sampling (Statistical Call Tree)
See exactly where CPU cycles burn, down to the method and source line, with no external profiler.
Status: โ Implemented ยท Visibility: Public ยท Level: ๐ฃ Advanced ยท Category: Profiler
๐ฏ What it solves
Typed spans tell you how long an instrumented operation took, but not why โ the time inside an
uninstrumented callee, a BCL method, or a hot loop is invisible to span-based tracing. Answering "where does
MovementSystem.Execute actually spend its CPU?" normally means reaching for a separate tool with its own
launch ritual, its own output file, and no way to line results up against
the transaction/B+Tree/page-cache timeline you already captured. CPU sampling closes that gap in-process: it
captures statistical call stacks for the whole session and folds them into the same trace file, correlated
against everything else your run recorded.
โ๏ธ How it works (in brief)
When enabled, the engine opens an in-process EventPipe session against its own process and lets the .NET runtime's
own ~1 kHz sampler capture managed call stacks for the
session lifetime. Nothing is parsed while it runs: the raw stream is buffered and only decoded, symbol-resolved
against portable PDBs, and embedded as a trailer section of the .typhon-trace file once the session stops.
The Workbench renders the result as a dotTrace-style Call Tree โ self/total time per method, hot-path
highlight, and a "Subsystems" breakdown by engine category โ scoped to any time range, span, or system you pick,
with click-to-source on every frame.
๐ป Usage
// typhon.telemetry.json next to your executable โ CPU sampling is opt-in, alongside its profiler siblings:
// {
// "Typhon": {
// "Telemetry": { "Enabled": true },
// "Profiler": {
// "Enabled": true,
// "Trace": "session.typhon-trace",
// "CpuSampling": { "Enabled": true }
// }
// }
// }
using Typhon.Engine;
// Zero-code path โ the bootstrap starts the sampler before session metadata is captured and stops/parses
// it as part of ordinary trace teardown. No sampler-specific calls are needed in application code.
var runtime = TyphonRuntime.Create(dbe, schedule =>
{
schedule.PublicTrack.DeclareDag("Sim").CallbackSystem("Tick", ctx => RunGameLogic(ctx));
});
// ...run the workload โ every managed thread is sampled for the session's lifetime...
TyphonProfiler.Stop(); // session stops, stream is parsed off-thread, samples embedded in the trace file
| Option | Default | Effect |
|---|---|---|
Typhon:Profiler:CpuSampling:Enabled |
false |
Starts the in-process EventPipe sampler; embeds resolved stacks in the trace on stop |
TYPHON__PROFILER__CPUSAMPLING__ENABLED |
โ | Environment override for the flag above |
โ ๏ธ Guarantees & limits
- No external tool, no special launch โ a managed NuGet-backed session opened from inside the process;
no
dotnet-traceglobal tool, no pre-CLR environment variable, no machine-installed dependency. - No hot-path cost โ sampling runs on the runtime's own sampler thread at a fixed ~1 kHz cadence; it never touches the typed-event producer path. Disabled entirely when the config flag is off.
- File mode only (v1) โ samples are resolved and embedded at session stop; live-TCP streaming does not yet carry CPU samples.
- Managed stacks only โ native (C/C++) frames are not captured; BCL and dynamically-generated frames
resolve name-only (no source line) where no local PDB exists. Engine and application code with a portable
PDB resolves to
file:line:methodwith click-to-source. - Statistical, not exhaustive โ a 1 kHz sampler; short-lived or rare code paths may be under-represented. Wider time ranges give more samples (more accurate) but blend more distinct behavior together โ the Call Tree panel shows the active scope's sample count and a density sparkline so you can judge that trade-off.
- Thread-time, classified โ the sampler catches every managed thread whether or not it's actually on a core. Samples are classified on-CPU / voluntary-wait / involuntary-stall (GC suspension, OS preemption); the view you pick decides which classes are folded into the tree versus collapsed into a labelled aggregate.
- Best-effort โ if the runtime diagnostics server is unavailable (e.g.
DOTNET_EnableDiagnostics=0) or the session fails to start, the engine logs one line and continues without CPU samples; a sampling failure never aborts or corrupts the rest of the trace. - No persisted intermediate file โ the raw EventPipe stream lives in a transient temp file only while the session is running; it is deleted once parsed, never a trace-adjacent deliverable.
๐งช Tests
- CpuSamplerSessionTests โ session lifecycle idempotency, QPC anchor, graceful-degrade when EventPipe diagnostics are unavailable
- CpuSampleParserTests โ
.nettraceparse + symbol resolution driven against a real in-process capture of a CPU-burning workload - CpuSampleSectionRoundTripTests โ CPU-sample trailer section write/read, absent-section case, and hard rejection of pre-v11 traces
๐ Related
- Sibling features: Configuration & Performance Tuning, Profiler Session Lifecycle & Zero-Code Bootstrap, GC Event Tracing
- Source:
src/Typhon.Engine/Profiler/internals/CpuSamplerSession.cs,src/Typhon.Engine/Profiler/internals/CpuSample.cs,src/Typhon.Engine/Profiler/internals/CpuSampleParser.cs,src/Typhon.Engine/Profiler/internals/CpuSampleSectionEncoder.cs,src/Typhon.Engine/Profiler/public/ProfilerLauncher.cs