Table of Contents

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-trace global 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:method with 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 โ€” .nettrace parse + 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