Files
natsdotnet/dottrace.md
Joseph Doherty 5de4962bd3 Improve docs coverage and refresh profiling parser artifacts
Add domain-specific XML documentation across src server components to satisfy CommentChecker, and update dotTrace parsing outputs used for diagnostics.
2026-03-14 04:06:04 -04:00

9.5 KiB

dotTrace Command-Line Profiler

Installation

Installed as a .NET global tool:

dotnet tool install --global JetBrains.dotTrace.GlobalTools

Update to latest:

dotnet tool update --global JetBrains.dotTrace.GlobalTools

Current version: 2025.3.3

Quick Start

Profile the NATS server (sampling, 30 seconds)

dottrace start --framework=NetCore --profiling-type=Sampling \
  --timeout=30s --save-to=./snapshots/nats-sampling.dtp \
  -- dotnet run --project src/NATS.Server.Host -- -p 14222

Profile the NATS server (timeline, with async/TPL info)

dottrace start --framework=NetCore --profiling-type=Timeline \
  --timeout=30s --save-to=./snapshots/nats-timeline.dtt \
  -- dotnet run --project src/NATS.Server.Host -- -p 14222

Attach to a running server by PID

dottrace attach <PID> --profiling-type=Sampling \
  --timeout=30s --save-to=./snapshots/nats-attach.dtp

Attach by process name

dottrace attach NATS.Server.Host --profiling-type=Sampling \
  --timeout=30s --save-to=./snapshots/nats-attach.dtp

Profiling Types

Type Flag Snapshot Extension Use Case
Sampling --profiling-type=Sampling .dtp Low overhead, CPU hotspots (default)
Timeline --profiling-type=Timeline .dtt Thread activity, async/await, TPL tasks
Tracing --profiling-type=Tracing .dtp Exact call counts, higher overhead
Line-by-Line --profiling-type=LineByLine .dtp Per-line timing (not available for attach)

Sampling options

# Use thread time instead of CPU instructions
--time-measurement=ThreadTime

# Default (CPU instruction count)
--time-measurement=CpuInstruction

Timeline options

# Disable TPL data collection for better performance
--disable-tpl

Common Options

Option Description
--framework=NetCore Required for .NET Core / .NET 5+ apps
--save-to=<path> Snapshot output path (file or directory)
--overwrite Overwrite existing snapshot files
--timeout=<duration> Auto-stop after duration (e.g., 30s, 5m, 1h)
--propagate-exit-code Return the profiled app's exit code instead of dotTrace's
--profile-child Also profile child processes
--profile-child=<mask> Profile matching child processes (e.g., dotnet)
--work-dir=<path> Set working directory for the profiled app
--collect-data-from-start=off Don't collect until explicitly started via service messages

Interactive Profiling with Service Messages

For fine-grained control over when data is collected, use --service-input=stdin:

dottrace start --framework=NetCore --service-input=stdin \
  --save-to=./snapshots/nats-interactive.dtp \
  -- dotnet run --project src/NATS.Server.Host -- -p 14222

Then type these commands into stdin (each must start on a new line and end with a carriage return):

Command Effect
##dotTrace["start"] Start collecting performance data
##dotTrace["get-snapshot"] Save snapshot and stop collecting
##dotTrace["drop"] Discard collected data and stop
##dotTrace["disconnect"] Detach/stop profiler

Stdout will emit status messages like:

##dotTrace["ready"]
##dotTrace["connected", {pid: 1234, path:"dotnet"}]
##dotTrace["started", {pid: 1234, path:"dotnet"}]
##dotTrace["snapshot-saved", {pid: 1234, filename:"./snapshots/nats-interactive.dtp"}]

Example Workflows

Profile a benchmark run

dottrace start --framework=NetCore --profiling-type=Sampling \
  --save-to=./snapshots/bench.dtp \
  -- dotnet run --project tests/NATS.Server.Benchmarks -c Release

Profile tests

dottrace start --framework=NetCore --profiling-type=Sampling \
  --timeout=2m --save-to=./snapshots/tests.dtp \
  -- dotnet test tests/NATS.Server.Core.Tests --filter "FullyQualifiedName~PubSub"

Profile with child processes (e.g., server spawns workers)

dottrace start --framework=NetCore --profile-child \
  --timeout=30s --save-to=./snapshots/nats-children.dtp \
  -- dotnet run --project src/NATS.Server.Host

Exporting Reports

dotTrace's XML report tool (Reporter.exe) is Windows-only. On macOS, use dotnet-trace for profiling with exportable formats:

# Install dotnet-trace
dotnet tool install --global dotnet-trace

# Collect a trace from a running process (nettrace format)
dotnet-trace collect --process-id <PID> --duration 00:00:30

# Collect directly in speedscope format
dotnet-trace collect --process-id <PID> --format speedscope --duration 00:00:30

# Convert an existing .nettrace file to speedscope
dotnet-trace convert --format speedscope trace.nettrace

Speedscope files can be visualized at speedscope.app — a web-based flame graph viewer that works on any platform.

dotnet-trace output formats

Format Extension Viewer
nettrace (default) .nettrace PerfView, Visual Studio, Rider
speedscope .speedscope.json speedscope.app
chromium .chromium.json Chrome DevTools (chrome://tracing)

Example: profile NATS server and export flame graph

# Start the server
dotnet run --project src/NATS.Server.Host -- -p 14222 &
SERVER_PID=$!

# Collect a 30-second trace in speedscope format
dotnet-trace collect --process-id $SERVER_PID --format speedscope \
  --duration 00:00:30 --output ./snapshots/nats-trace

# Open the flame graph
open ./snapshots/nats-trace.speedscope.json  # opens in default browser at speedscope.app

Viewing Snapshots

Open .dtp / .dtt snapshot files in:

  • dotTrace GUI (/Users/dohertj2/Applications/dotTrace.app)
  • JetBrains Rider (built-in profiler viewer)
open /Users/dohertj2/Applications/dotTrace.app --args ./snapshots/nats-sampling.dtp

Parsing Raw .dtp Snapshots To JSON

The repository includes a Python-first parser for raw dotTrace sampling and tracing snapshots:

The parser starts from the raw .dtp snapshot family and emits machine-readable JSON for call-tree and hotspot analysis. It uses the locally installed dotTrace assemblies to decode the snapshot format.

Prerequisites

  • python3
  • .NET 10 SDK
  • dotTrace installed at /Users/dohertj2/Applications/dotTrace.app

If dotTrace is installed elsewhere, set DOTTRACE_APP_DIR to the Contents/DotFiles directory:

export DOTTRACE_APP_DIR="/path/to/dotTrace.app/Contents/DotFiles"

Print JSON to stdout

python3 tools/dtp_parse.py snapshots/js-ordered-consume.dtp --stdout

Useful flags:

  • --top N limits hotspot and flat-path output. Default: 200
  • --filter TEXT keeps only call-tree paths and hotspots whose method names match TEXT
  • --flat or --paths adds a hotPaths section with the heaviest flat call chains
  • --include-idle keeps idle and wait methods in hotspot/path rankings. Idle exclusion is on by default.

Write JSON to a file

python3 tools/dtp_parse.py snapshots/js-ordered-consume.dtp \
  --out /tmp/js-ordered-consume-calltree.json
python3 tools/dtp_parse.py snapshots/js-ordered-consume.dtp \
  --filter Microsoft.DotNet.Cli.Program \
  --flat \
  --top 25 \
  --out /tmp/js-ordered-consume-calltree.json

Output shape

The generated JSON contains:

  • snapshot — source path, payload type, time unit, thread count, node count, and reader diagnostics
  • summary — wall time, active time, total samples, and top exclusive method summary
  • threadRoots — top-level thread roots with inclusive time
  • callTree — nested call tree rooted at a synthetic <root>
  • hotspots — flat inclusive and exclusive method lists
  • hotPaths — optional flat call-path list when --flat is used

Hotspot entries are method-first. Synthetic frames such as thread roots are excluded from hotspot rankings, and idle wait frames are excluded by default so the output is easier to feed into an LLM for slowdown analysis.

Typical analysis workflow

  1. Capture a snapshot with dottrace.
  2. Convert the raw .dtp snapshot to JSON:
python3 tools/dtp_parse.py snapshots/nats-sampling.dtp \
  --out /tmp/nats-sampling-calltree.json
  1. Inspect the top hotspots:
python3 - <<'PY'
import json
with open('/tmp/nats-sampling-calltree.json') as f:
    data = json.load(f)
print('Top inclusive:', data['hotspots']['inclusive'][0]['name'])
print('Top exclusive:', data['hotspots']['exclusive'][0]['name'])
PY
  1. Feed the JSON into downstream tooling or an LLM to walk the call tree and identify expensive paths.

Verification

Run the parser test with:

python3 -m unittest tools.tests.test_dtp_parser -v

Exit Codes

Code Meaning
0 Success
65 Profiling failure

Notes

  • Snapshots consist of multiple files: *.dtp, *.dtp.0000, *.dtp.0001, etc. Keep them together.
  • Attach on macOS requires .NET 5 or later.
  • Use -- before the executable path if arguments start with -.
  • The snapshots/ directory is not tracked in git. Create it before profiling:
    mkdir -p snapshots
    
  • The parser currently targets raw .dtp snapshots. Timeline .dtt snapshots are still intended for the GUI viewer.