dotnet-diagnostics-cli is the human-facing counterpart to the dotnet-diagnostics-mcp server. Both
ship from this repository and run the same Core diagnostics engine, but they target different consumers:
dotnet-diagnostics-cli (this doc) |
dotnet-diagnostics-mcp (the server) |
|
|---|---|---|
| Consumer | A human, a shell script, a CI job | An LLM, via an MCP client |
| Surface | Sub-commands you type | MCP tools the model calls |
| Transport | None — in-process, one-shot or REPL | Streamable HTTP (bearer auth) or stdio |
| State | One-shot inline results, or a session REPL holding queryable handles |
MCP session holding handles |
| Install | dotnet tool install -g dotnet-diagnostics-cli |
dotnet tool install -g dotnet-diagnostics-mcp |
If you want an LLM to drive diagnostics, use the server — see client-setup.md and
tool-reference.md. If you want to run diagnostics yourself, read on.
The CLI references Core only — it never starts an HTTP server, reads a bearer token, or runs a daemon.
# .NET global tool (requires the .NET 10 SDK)
dotnet tool install -g dotnet-diagnostics-cli
dotnet-diagnostics-cli --helpOther distributions:
- Self-contained binary (no SDK): per-OS archives are attached to every
Release as
dotnet-diagnostics-cli-<version>-<rid>(linux-x64,linux-arm64,osx-arm64,win-x64,win-arm64). The downloaded archive uses that name; the extracted executable inside it isdotnet-diagnostics-cli(dotnet-diagnostics-cli.exeon Windows). - In the sidecar container: the diagnostics sidecar image bundles the CLI on
PATH, sokubectl exec -it <pod> -c diagnostics-mcp -- dotnet-diagnostics-cli processesworks against the co-located workload.
The CLI can emit static completion scripts for bash, zsh and PowerShell. The generated scripts include
sub-commands, top-level flags and enum-valued options such as collect --kind, inspect-heap --source,
dump --dump-type and get-bytes --kind.
# bash (system-wide)
dotnet-diagnostics-cli completion bash | sudo tee /etc/bash_completion.d/dotnet-diagnostics >/dev/null
# bash (current shell only)
source <(dotnet-diagnostics-cli completion bash)# zsh: write the generated function into a directory on fpath, then reload completions.
mkdir -p ~/.zsh/completions
dotnet-diagnostics-cli completion zsh > ~/.zsh/completions/_dotnet-diagnostics
print -r 'fpath=(~/.zsh/completions $fpath)' >> ~/.zshrc
autoload -Uz compinit && compinit# PowerShell: source once for the current session.
dotnet-diagnostics-cli completion pwsh | Out-String | Invoke-Expression
# To load it every time, add the generated script to your profile.
dotnet-diagnostics-cli completion pwsh > "$HOME/.dotnet-diagnostics-completion.ps1"
Add-Content -Path $PROFILE -Value '. "$HOME/.dotnet-diagnostics-completion.ps1"'If you run the self-contained executable directly, keep using dotnet-diagnostics-cli
(dotnet-diagnostics-cli.exe on Windows). The generated scripts also register the historical
dotnet-diagnostics command name.
These apply to every command:
| Option | Meaning |
|---|---|
-p, --pid <pid|name> |
Target OS process id, or a case-insensitive prefix of the visible .NET process entrypoint/name. Purely numeric values are always treated as literal PIDs. Auto-resolved when exactly one .NET process is visible and --pid is omitted. |
--json |
Emit the raw DiagnosticResult<T> envelope as JSON instead of the human table. JSON is never colorized. |
--explain-risk |
Print the resolved Core safety descriptor and exit without executing the command. With --json, emits a machine-readable safety explanation. |
--acknowledge-risk <high|critical> |
Deliberately authorize a high/critical operation in one-shot or other non-interactive use. The value must exactly match the resolved level. |
--launch -- <app> [args] |
Dev mode. Launch <app> as a child of the CLI so live attach works under kernel.yama.ptrace_scope=1 with no privilege — see the Linux note. Supported by capabilities, collect, dump, inspect, inspect-heap (live), get-bytes (module) and session. Mutually exclusive with --pid; the child is terminated on exit. Because process launch/termination is high risk, place the exact resolved --acknowledge-risk before the -- delimiter. |
--suspend-startup |
Cold-start capture (#446). With --launch, launches the target suspended on a reverse-connect DOTNET_DiagnosticPorts=…,suspend port, arms the EventPipe session before any managed code runs, then resumes — capturing non-replayed DependencyInjection call-site activity that an attach after startup misses, plus any loader events emitted by the runtime provider. Applies to collect --kind startup. The MCP equivalent is gated collect_events(kind="startup", launch=...) over stdio. Default off. |
-h, --help |
Show the global usage screen, or a focused screen for <command> --help. |
Exit codes: 0 success (a dump preview is also a success), 1 a structured failure envelope
(e.g. NotSupported, PermissionDenied), 2 a usage / validation or safety-acknowledgement error.
The CLI and MCP server resolve the same descriptor from the Core safety registry. The CLI applies it before launch, attach, capture, export, or mutation:
The canonical generated matrix and production operating profiles are in
production-safety.md. The observe, investigate,
and privileged-response profiles do not override the shared Core safety
registry; every CLI invocation still resolves its concrete descriptor.
- low — executes without extra output;
- moderate — prints one concise
SAFETY warning [moderate] ...line tostderr, then executes; - high / critical — prints
reason,targetImpact,dataExposure,sideEffects, and the artifact path when applicable. A real interactivesessionasks the operator to type the exact level. One-shot commands and sessions whose stdin is redirected never prompt and require--acknowledge-risk <exact-level>.
All warnings and acknowledgement failures go to stderr; stdout remains valid JSON under --json.
Prompts contain only the canonical descriptor and destination path, not provider values, symptoms,
credentials, target payloads, or other command data.
Use --explain-risk to inspect the resolved descriptor without executing:
dotnet-diagnostics-cli inspect-heap --source live --pid 1234 --explain-risk
dotnet-diagnostics-cli dump --pid 1234 --out ./dumps --confirm --explain-risk --jsonStable CI/non-interactive pattern:
# Inspect first; "executed" is false.
dotnet-diagnostics-cli collect --kind exceptions --pid 1234 --explain-risk --json
# The acknowledgement is deliberately pinned to the currently resolved level.
dotnet-diagnostics-cli inspect-heap --source live --pid 1234 \
--acknowledge-risk high --json > heap.json
dotnet-diagnostics-cli dump --pid 1234 --out ./dumps --confirm \
--acknowledge-risk critical --json > dump.jsonIf a future option changes the resolved level, an old acknowledgement does not silently authorize the
new operation: the CLI exits 2 before execution and tells the caller which level is required.
Progress. Long one-shot collections (
collect,inspect-heap,dump) print an elapsed-time spinner to stderr while they run, on an interactive terminal only. It is suppressed under--jsonand whenever stderr is redirected/piped, so machine-readable output (stdout) and captured logs stay clean.Color. Human output uses ANSI color for headlines, section headers, severities and verdicts only when stdout is an interactive terminal. Color is disabled automatically for redirected stdout, whenever
--jsonis used, or whenNO_COLORis set to any value.
Bootstrap a local Docker sidecar for an already-running target container. It can either print the matching central configuration or apply it to a compatible Dockerized central automatically.
This command still keeps the CLI on the "outside": it shells out to the local
dockerCLI, and it never gives either MCP server process access to/var/run/docker.sock.
| Option | Type | Default | Description |
|---|---|---|---|
--target-container |
string |
required | Running Docker container name or id to diagnose. |
--central-container |
string? |
none | Running central MCP container name or id. Enables central-aware private routing and inspection. |
--sidecar-name |
string? |
<target>-dotnet-diagnostics |
Explicit Docker container name for the sidecar. |
--sidecar-image |
string? |
version-compatible GHCR image | Explicit sidecar image override. Released CLIs select ghcr.io/pedrosakuma/dotnet-diagnostics:<cli-version>; repository/source builds select :edge. |
--profile-name |
string? |
sanitized target name | Profile key emitted under Orchestrator:ExternalMcpProfiles. Restricted to ^[A-Za-z0-9][A-Za-z0-9_-]*$ so the env-var form stays usable. |
--profile-url |
string? |
http://127.0.0.1:<host-port>/mcp |
Exact URL the central will dial for this profile. Must be absolute http/https, path exactly /mcp, no query/fragment/userinfo. |
--allow-cidr |
string[] |
derived from --profile-url when it is an IP literal / localhost |
Repeatable allowlist entries for AllowedCidrs. Required when --profile-url uses another hostname (for example host.docker.internal). |
--host-port |
int? |
host-central: 18891; central-aware: none |
Host port published for the sidecar's internal 8080 listener. In central-aware mode publication is opt-in. |
--bearer-token |
string? |
generated | Operator-supplied sidecar bearer token. When omitted, the CLI generates a random 32-byte hex value and prints it in the output. |
--delegation-key |
string? |
generated | Operator-supplied MCP_INTERNAL_SCOPE_DELEGATION_KEY. When omitted, the CLI generates a random 32-byte hex value and prints it in the output. |
--wait |
int? |
90 |
Seconds to wait for the sidecar health check to report healthy. |
--no-sys-ptrace |
flag | off | Skip --cap-add SYS_PTRACE. Leave this off unless you knowingly want EventPipe-only coverage. |
--apply |
flag | off | Require --central-container, atomically write a mode-0600 bootstrap-owned profile file through docker exec stdin, restart the existing central container, and wait for health. |
--replace |
flag | off | With --apply, replace a different profile only when the existing file carries bootstrap ownership metadata. |
Implementation details:
- with
--central-container, inspects both containers and the central's networks using the operator-owned Docker CLI. It prefers a user-defined, local bridge network already shared by target and central, then falls back deterministically to the lexicographically first eligible central network. The generateddocker runattaches the sidecar only to that selected network; - emits
http://ddmcp-<sidecar-name-hash>:8080/mcpusing a fixed 30-character network alias (avoiding Docker's 63-character DNS-label limit) and derivesAllowedCidrsas the resolved sidecar address with a/32(or/128for IPv6), not the whole bridge subnet. Docker DNS may return another container alias, but the server's DNS-resolution/CIDR gate can connect only to the inspected sidecar address, and the validated IP is used directly to prevent rebinding; - publishes no sidecar host port on the automatic route unless
--host-portis explicitly supplied. An explicit--profile-urland--allow-cidrwin; a non-sidecar URL also requires explicit--host-portbecause bootstrap cannot otherwise prove how it reaches the sidecar. Loopback URLs are rejected for a non-host-network central because they address the central itself; - with
--apply, checks an image compatibility marker before starting the sidecar, writes secrets only through redirected stdin (never Docker command arguments), atomically renames the completed mode-0600file, and usesdocker restartrather than reconstructing the container. Mounts, ports, labels, health checks, restart policy, capabilities, security options, and network aliases therefore remain Docker-owned and unchanged. The central loads the file at startup and the CLI waits for its existing health check before reporting success; - rejects stopped/recreated central containers during bootstrap, central
network=hostfor the automatic route, unsupported/default-only networks, sidecar name collisions, and network-connect failures. Multiple candidates are deterministic; - reports whether it created the network attachment and prints both bootstrap-owned cleanup actions. Cleanup disconnects only the selected attachment and removes only the generated sidecar; repeating those commands is idempotent even when Docker reports that the attachment/container is already absent;
- embeds the exact release version as the default image tag in official CLI packages and
self-contained binaries. Stable and prerelease CLIs therefore select the corresponding exact
semver tag published by the container workflow; they never silently fall forward to
:edge; - uses
ghcr.io/pedrosakuma/dotnet-diagnostics:edgefor repository/source builds, whose normal SDK version is not an official release identity. This is a deterministic, useful development default; pass--sidecar-image dotnet-diagnostics-mcp:devwhen testing local sidecar changes; - attaches the sidecar to the target with
docker run --pid container:<target> ...; - runs a short-lived probe from the sidecar image with
--pid host, reads the target's effective UID/GID and inner namespace PID from/proc/<host-pid>/status, and mirrors the identity onto the persistent sidecar's--user; - sets the sidecar's
TMPDIR=/proc/<target-namespace-pid>/root/tmp, so the target diagnostic socket is visible through the shared PID namespace without a host/procbind mount or pre-authored shared volume; - sets
DOTNET_EnableDiagnostics=0on the sidecar so the sidecar's own socket is suppressed.
Docker constraint: the daemon must allow both --pid host for the constrained
read-only probe and --pid container:<target> for the persistent sidecar, and
the sidecar image must contain /bin/cat. The
client shell does not need access to Linux /proc, so Docker Desktop works
from native Windows PowerShell and WSL2. If the daemon rejects the namespace
probe while the target remains running, the CLI returns
kind=HostProcNotAccessible; use the manual shared-volume / compose topology
instead.
Without --apply, output remains configuration-only for host-process centrals and operators who manage
configuration themselves. Automatic apply is intentionally limited to a compatible Dockerized central;
the CLI fails with ApplyUnsupported rather than guessing how to mutate a host process or an unrelated
image. Identical managed content is a no-op. Different bootstrap-owned content requires --replace;
an unowned file is never overwritten. If restart/health verification fails, the prior file is restored
(or the new file removed), restart is retried, and the newly created sidecar is cleaned up.
# Installed release: pulls the exact matching GHCR semver tag when absent locally.
dotnet-diagnostics-cli docker-bootstrap --target-container api --acknowledge-risk high
# Dockerized central: private container-DNS route, no sidecar host port.
dotnet-diagnostics-cli docker-bootstrap \
--target-container api \
--central-container diagnostics-central \
--apply \
--acknowledge-risk high
# Repository development with local MCP changes:
docker build -t dotnet-diagnostics-mcp:dev -f deploy/Dockerfile .
dotnet run --project src/DotnetDiagnostics.Cli -c Release -- \
docker-bootstrap --target-container api --sidecar-image dotnet-diagnostics-mcp:dev \
--acknowledge-risk high
dotnet-diagnostics-cli docker-bootstrap --target-container api --profile-name api-sidecar \
--host-port 18892 --acknowledge-risk high
dotnet-diagnostics-cli docker-bootstrap \
--target-container api \
--host-port 18892 \
--profile-url http://host.docker.internal:18892/mcp \
--allow-cidr 172.17.0.1/32 \
--acknowledge-risk highList attachable .NET processes (pid, runtime, OS/arch, entrypoint).
| Option | Type | Default | Description |
|---|---|---|---|
--command-line-contains |
string? |
none | Case-insensitive substring filter against each process's command line, to disambiguate among several candidates spawned by a wrapper you don't control (e.g. several testhost.exe under dotnet test) — issue #665 part B, mirroring the MCP inspect_process(view="list") filter. |
dotnet-diagnostics-cli processes
dotnet-diagnostics-cli processes --json
dotnet-diagnostics-cli processes --command-line-contains testhostProbe a target's diagnostic capability matrix — CoreCLR vs NativeAOT, whether CPU sampling / gcdump / live attach are available.
dotnet-diagnostics-cli capabilities --pid 1234Diagnose the environment (not the workload) and print the exact fix. Target-optional and
remediation-first: each blocked/degraded check carries a copy-pasteable docker flag / k8s
securityContext snippet / sysctl. Reuses the cheap host probes (ptrace, perf) plus a
/proc/*/status UID read — no EventPipe session, no privilege.
dotnet-diagnostics-cli doctor # host-only readiness check
dotnet-diagnostics-cli doctor --pid 1234 # also verify the diagnostic-socket UID vs the target
dotnet-diagnostics-cli doctor --jsonChecks: socket-uid (UID mismatch blocks all tools), clrmd-attach (ptrace — blocks
thread snapshot / heap / dump), offcpu-perf, native-alloc, and native-lock-contention
(optional samplers — native-lock-contention reports Degraded on Windows since there is no
Windows backend for it in this release, unlike native-alloc). Status
ladder: Ok < Degraded < Blocked.
Exit code: doctor exits non-zero (1) when a hard blocker (Blocked) is present and
0 otherwise, so it can gate a CI job:
dotnet-diagnostics-cli doctor --pid "$APP_PID" || { echo "environment not ready"; exit 1; }One-call process inspector exposing three views (--view required):
| View | What it does |
|---|---|
triage |
Collects counters for --duration seconds (default 5), reports threshold-backed observed signals separately from evidence-backed hypotheses, retains host-normalized CPU plus estimated effective-core consumption, and evaluates in-window GC heap / LOH / working-set growth before returning neutral drill-down hints. |
runtime-config |
Reads the process's effective runtime configuration: GC mode and heap count, ThreadPool worker/IOCP bounds, tiered-compilation flags, filtered runtime env vars, and AppContext switches. |
container |
Reads cgroup/container CPU quota + throttling, memory limits / OOM counters, PSI, pid limits and oom_score for the target process. Linux/cgroup-v2-first; returns partial signals plus notes when the host lacks a container envelope or PSI. |
dotnet-diagnostics-cli inspect --view triage --pid 1234
dotnet-diagnostics-cli inspect --view triage --pid 1234 --duration 10
dotnet-diagnostics-cli inspect --view runtime-config --pid 1234
dotnet-diagnostics-cli inspect --view container --pid 1234
dotnet-diagnostics-cli inspect --view triage --jsonHuman-readable output leads with Assessment, then prints Observed signals, Hypotheses
(confidence, supporting/contradicting evidence, and next step), and ranked indicators. JSON uses
the same modelVersion=2 contract as MCP. Hypotheses are ordered by confidence and then the
strongest supporting observed-signal level. A low-CPU snapshot with a small transient queue is
inconclusive; it is not labeled io-bound.
When request latency remains elevated but the ThreadPool queue no longer crosses the backlog
threshold, human output adds a Signal separation line: the starvation signal is absent in this
window, while the remaining latency must be checked against workload/SLO expectations. This is a
CLI rendering clarification only; it does not change the Core triage assessment or infer that a
latency value is expected without operator context.
Topology-adjusted CPU uses the target's one-shot System.Runtime/ProcessorCount event, never the CLI
process's topology, and emits cpu.effective-core-consumption at approximately one busy core even
when host-normalized CPU is small. cpuTopologyStatus is unknown when the target event is
unavailable. Material first-to-last GC heap, LOH, or working-set growth emits
memory.intra-window-growth; the bounded memory.footprint-growth hypothesis describes the shape
without claiming a leak or prescribing a solution. Memory-growth indicators apply the same 20% and
1 MB materiality floor as the observed signal.
For compatibility, JSON continues to serialize verdict, secondaryVerdicts, severity,
evidence, and topIndicators. verdict and secondaryVerdicts are deprecated for removal in
v1.0; migrate automation to assessment, observedSignals, and hypotheses.
Open an EventPipe session and collect a window of events. --kind is required.
For --kind networking, latency population v2 includes failed completions in
the existing percentiles and HTTP operation groups. correlation.byKind counts
carry the version, all/failed/no-observed-failure sample denominators and HTTP
status-error response counts (503 is not a transport failure). Missing version
means legacy/unknown; zero pairs means unavailable, not measured zero.
latencyAvailability labels HTTP/queue/DNS/TLS independently on snapshots and
all five query views. Existing duration scalars remain compatible: only
measured makes zero a real measurement. percentileSamples (and
queuePercentileSamples/queueSamples in HTTP counts) distinguishes reservoir
retention from accepted samples and capture gaps. Operation groups retain their
own nullable percentileSamples and availability; missing legacy metadata stays
unknown. Human summaries label unavailable p95 instead of printing 0.0ms.
Latencies describe accepted observed pairs, not all
requests. JSON and session queries retain correlation.byKind exclusion
accounting; legacy artifacts without it have unknown coverage. TPL activity
flow can remain enabled after capture. captureQuality separately reports drain
completion, transport loss (null when unknown), stream-read elapsed time and
payload parsing errors. duration remains requested time, not proof of observed
coverage; partial data and its qualifications survive every networking query.
See networking correlation and capture quality.
| Option | Meaning |
|---|---|
--kind <kind> |
One of counters, exceptions, crash-guard, gc, datas, catalog, event_source, activities, gc-activities, logs, jit, threadpool, contention, db, kestrel, networking, requests, startup, sweep, cpu, allocation, off_cpu (alias off-cpu), native-alloc, native-lock-contention, thread-snapshot, cpu-efficiency. |
-d, --duration <int> |
Window in seconds (default: counters 5, datas 15, sweep 6, others 10). |
--depth <level> |
Verbosity: summary, detail (default), raw. |
--top <n> |
Top-N cap for sampler kinds (cpu, allocation, off_cpu, native-alloc, native-lock-contention) and session query pages/ranked views. |
--max-events <int> |
Per-kind cap (events / exceptions / activities / catalog occurrence sample). |
--max-gc-events <int> |
gc-activities only: independent GC collection-row, heap-stat and suspension-interval detail cap (default 200, 1–10000). Does not affect activity retention or GC pairing-state limits. |
--interval <int> |
Refresh interval in seconds (counters, db, kestrel, networking). Default 1. |
--watch <seconds> |
Re-run the command every N seconds, clear/redraw the human output, and stop cleanly on Ctrl-C. Not compatible with --json. With --capture-when it is reinterpreted as the metric sample interval for the bounded gated watch (no redraw loop). |
--capture-when <pred> |
Threshold-gated capture (--kind counters). Arm a bounded watch and capture when a single metric predicate <metric><op><value> trips — e.g. cpu>85, gcHeapMb>=1500, rssMb>2000, threadCount>400, activeTimerCount>1000. Operators: > >= < <=. |
--capture <kind> |
What to capture on trip: dump, cpu-sample, heap, thread-snapshot. Required with --capture-when. |
--window <seconds> |
Required with --capture-when. Hard upper bound on how long the watch is armed (1–300). |
--symbol-path <path> |
NT_SYMBOL_PATH-style search path for cpu, off_cpu and thread-snapshot symbol resolution. Remote symbol servers remain allowlist-gated just like inspect-heap. |
--export-trace |
cpu: keep the raw .nettrace under the artifact root and surface its relative path (default off — the trace is deleted after parsing). Fetch it later with get-bytes --kind trace. |
--resolve-source-lines / --no-resolve-source-lines |
cpu: enable/disable source file:line resolution for the top hotspots. Default on. |
--resolve-method-instantiations |
cpu: opt in to a second ClrMD attach after sampling to recover closed generic method signatures for the hottest managed frames. |
--native-aot-map <file> |
cpu (and gated --capture cpu-sample) against a NativeAOT target: resolve method names from a .map.xml file (the AOT compiler's symbol map) so hot frames show managed method identities instead of raw addresses. Ignored for CoreCLR targets, which resolve symbols from runtime metadata. |
--native-alloc-sample-period <n> |
native-alloc: perf sample period (default 1000) — record one call chain per this many allocator hits. Higher values reduce output volume at the cost of resolution. |
--native-lock-contention-sample-period <n> |
native-lock-contention on Linux only (no Windows backend): perf sample period (default 5000, higher than native-alloc's default because mutex fast-path calls are typically far more frequent than allocator calls) — record one call chain per this many pthread_mutex_lock/unlock hits. |
--dump-file <path> |
thread-snapshot: inspect a previously-captured dump instead of attaching to a live pid. Mutually exclusive with --pid. |
--max-frames-per-thread <n> |
thread-snapshot: cap captured frames per thread (default 64, hard cap 512). |
--include-runtime-frames |
thread-snapshot: include CLR/runtime helper frames. Default off. |
--include-native-frames |
thread-snapshot: include native frames ClrMD cannot map to managed methods. Default off. |
--max-captures <int> |
Stop after N captures (default 1, max 10). |
--provider <name> |
counters: EventCounter provider (repeatable); catalog: EventPipe provider (repeatable; replaces broad defaults); event_source: required provider name. |
--meter <name> |
counters: Meter name (repeatable). |
--source <name> |
activities / gc-activities: ActivitySource filter (repeatable, * / ? globs). |
--trace-id <32-hex> |
activities / gc-activities: optional non-zero W3C trace ID. Core trims surrounding whitespace and normalizes casing before targeted retention. Also retains its existing session query --view trace meaning. |
--max-matched-activities <int> |
activities / gc-activities with --trace-id: independent matching stop-event cap (default 200, minimum 1). Requires targeted collection; rejected for other commands/kinds or without --trace-id. |
--include-http-destination |
activities / gc-activities: opt in to redacted HTTP scheme/host/port joined by real W3C IDs; false by default. Separate from unchanged native tags. Valid in one-shot and session; rejected on other kinds/commands. |
--category <glob> |
logs: ILogger category filter (repeatable). |
--min-level <level> |
logs: minimum level (default Information). |
--unsafe-provider |
event_source: opt in to a non-allowlisted provider. |
--save <file> |
Save a comparable snapshot JSON. Supported collect kinds: counters, datas (gc-datas), gc (gc-events), contention, threadpool. |
CLI activity collection is exploratory by default: --max-events retains the first N
completed stop events after source filtering (default 200). With --trace-id, it instead
retains only matching spans up to the independent --max-matched-activities budget;
unrelated traffic never spends that budget. --max-events keeps its exploratory meaning,
and does not reduce the targeted budget. Invalid IDs and nonpositive budgets return the
normal Core InvalidArgument CLI error envelope; unsupported option combinations are
explicit usage errors. Collection-time --trace-id is accepted only on activities and gc-activities.
Both one-shot commands and session REPL collection use the same Core-only pipeline;
the CLI does not provide distributed/Pod orchestration. JSON capture and activity drilldown
preserve retention provenance, including the effective cap and matching/drop counts.
Missing fields in older captures mean unknown, not verified zero loss. Completed-window
captures cannot establish trace completeness; missing children can inflate residual timings
and change trace rankings. Query's --trace-id only projects an existing artifact; it cannot
recover spans discarded during exploratory collection.
For outbound HttpClient (System.Net.Http / System.Net.Http.HttpRequestOut),
.NET 8 can return valid IDs and timings with tags: {} because the runtime
does not populate HTTP tags. Optional target instrumentation can supply them;
the CLI does not add it. Backend attribution is unavailable without captured
destination metadata, irrespective of timing accuracy. query --view trace
intentionally omits URL/host tags even if present in the full capture; collection
JSON and query --view activities preserve captured tags. See
HTTP Activity tag provenance for .NET 9/10 differences
and the controlled evidence boundary.
--include-http-destination enables the authority-only DiagnosticSource bridge
without target changes. Capture/list/trace expose a separate destination with
availability and provenance, plus httpDestinationCorrelation quality/cap counts.
Missing starts, ambiguous IDs, loss, or caps do not invent a backend. Configured
redaction applies to structured authority, including collection JSON; summaries
never include it. This does not scrub existing native URL tags or expand the
trace tag allowlist. See the linked provenance guide for exact caps and limits.
dotnet-diagnostics-cli collect --kind activities --pid 1234 \
--trace-id abcdef0123456789abcdef0123456789 --max-matched-activities 1000 --jsonInside a session, use the same collect options, then
query --latest-of-kind activities --view trace --trace-id abcdef0123456789abcdef0123456789
without recollecting.
Sequential collect --kind gc and collect --kind activities do not establish
overlapping observation windows. Use the Core-only gc-activities workflow to resolve one
target once and start both collectors concurrently:
dotnet-diagnostics-cli collect --kind gc-activities --pid 1234 \
--duration 10 --source 'MyApp.*' \
--trace-id abcdef0123456789abcdef0123456789 \
--max-events 200 --max-matched-activities 1000 --max-gc-events 500 --top 20 --jsonOne-shot JSON includes the acquired GC and activity evidence, per-side status/reason,
requested windows, actual observed windows, their intersection, signed startup skew
(activity start minus GC start), and a useful inline overlay. The human output prints
the handles, measurement/retention summaries and ranked overlapping spans. A one-shot
handle expires with the CLI process; it is not needed to interpret the inline result.
Inside dotnet-diagnostics-cli session --pid 1234, run:
collect --kind gc-activities --duration 10 --source MyApp.* --trace-id abcdef0123456789abcdef0123456789 --json
query --handle <data.activities.handle.id> --view gc-overlay --gc-handle <data.gc.handle.id> --json
query --latest-of-kind activities --view trace --trace-id abcdef0123456789abcdef0123456789
Replace the placeholders with the two handles returned by that collection. Both actual artifacts are retained for 10 minutes (subject to the session store's normal capacity/TTL), even after target exit following a successful capture. No injected handles, MCP server, HTTP transport, bearer token, daemon, Pod orchestration, or raw-trace disk spill is involved.
The coordinated workflow bounds --duration to 1–300 seconds, each retention budget to
1–10000, and overlay --top to 1–100 (default 20). The normal exploratory activity default
and targeted matching cap remain independent of --max-gc-events. GC collection elapsed
is not pause time: the overlay reuses v2 fully-suspended intervals and per-span interval
union. The same suspension can legitimately overlap multiple activities. Dropped matching
activities, dropped GC intervals, dropped collection rows, transport/pairing uncertainty,
and output top-N/detail omissions remain separate; filtering is not loss.
Starting two tasks is not simultaneous stream readiness. Requested windows start at
dispatch, while actual windows reflect EventPipe startup and drain. The intersection may
exclude either end of a span; retained spans do not prove complete tracing. No matching
retained span does not prove absence of GC or tracing. Each collector has its existing
30-second startup budget and bounded stop/drain; a shared duration-plus-65-second deadline
cancels pending work, and both collector tasks are awaited through cleanup. Failure of one
side does not discard the other's successful evidence. status=partial/failed,
per-side unavailableReason and a null overlay disclose failed, early-ended, unknown,
nonoverlapping or incompatible evidence rather than measured zero. Inspect these fields
even when the CLI returned a diagnostic response successfully. Missing/changed target
lifetime prevents registration and attribution; unknown capture lifetime prevents attribution.
For the real CoreClrSample, source CoreClrSample.Activities emits /activity spans.
The optional sample-only /activity?delayMs=20&collectGc=true performs one bounded,
blocking compacting GC inside the child span. Live acceptance drives a finite unrelated
prefix and asserts an observed event from each collector's own stream before sending
a known-trace request; a fixed startup sleep is not readiness evidence.
dotnet-diagnostics-cli collect --kind counters --pid 1234 --duration 5
dotnet-diagnostics-cli collect --kind counters --pid CoreClrSample --watch 2
dotnet-diagnostics-cli collect --kind counters --pid CoreClrSample --capture-when 'cpu>85' --capture cpu-sample --window 60
dotnet-diagnostics-cli collect --kind counters --pid CoreClrSample --capture-when 'rssMb>2000' --capture dump --window 120 --confirm --acknowledge-risk critical
dotnet-diagnostics-cli collect --kind cpu --pid 1234 --top 20 --export-trace
dotnet-diagnostics-cli collect --kind allocation --pid 1234 --top 15
dotnet-diagnostics-cli collect --kind off_cpu --pid 1234 --top 10 --symbol-path /symbols --acknowledge-risk high
dotnet-diagnostics-cli collect --kind native-alloc --pid 1234 --native-alloc-sample-period 500 --acknowledge-risk high
dotnet-diagnostics-cli collect --kind native-lock-contention --pid 1234 --native-lock-contention-sample-period 2000 --acknowledge-risk high
dotnet-diagnostics-cli collect --kind thread-snapshot --pid 1234 --max-frames-per-thread 128 --acknowledge-risk high
dotnet-diagnostics-cli collect --kind thread-snapshot --dump-file ./app.dmp
dotnet-diagnostics-cli collect --kind datas --pid 1234 --duration 30 --save ./before.json
dotnet-diagnostics-cli collect --kind catalog --pid 1234 --json
dotnet-diagnostics-cli collect --kind event_source --provider System.Net.Http --pid 1234
dotnet-diagnostics-cli collect --kind startup --suspend-startup --launch --acknowledge-risk high -- dotnet App.dll # cold startFor --kind logs, human output places retained entries under an explicit
UNTRUSTED TARGET EVIDENCE heading. Messages, exception text, category/event
names, and scope keys/values are displayed as evidence only and must never be
executed or followed as commands, links, paths, tool requests, or approval
claims. Human rendering uses JSON string literals so embedded newlines and
control characters cannot create fake output sections. --json exposes the
same rule as the machine-readable data.untrustedDataBoundary object while
preserving the captured strings (subject only to documented sensitive-data
redaction and byte caps).
Human thread-snapshot --depth detail output includes bounded decisive evidence inline: up to five
blocked stack groups (six frames each), five contended locks with owner/waiter identities, and the
captured ThreadPool queue/worker summary. Use --json for the complete typed payload. Summary depth
keeps the compact headline.
The standalone CLI now exposes the same Core-only sampler families the MCP server drives:
| Kind | What it captures | Key flags | Summary shape |
|---|---|---|---|
cpu |
CPU stack observations via EventPipe SampleProfiler (CoreCLR default) or explicit OS-backed on-CPU perf/ETW sampling (--cpu-backend os; required automatically for NativeAOT). Explicit backend selection never falls back. CoreCLR's SampleProfiler can include blocked/waiting threads and does not establish scheduler state. |
--top, --cpu-backend, --symbol-path, --export-trace, --resolve-source-lines, --resolve-method-instantiations, --native-aot-map |
evidence metadata; top inclusive/exclusive stack frequencies; selfSamples.runningSamples (OS-backed only), waitingSamples (heuristic), and unknownSamples; symbol-degradation notes; timings; and cpu.self-time.* signals only for OS-backed evidence |
allocation |
Managed allocation samples (GCAllocationTick) with top types by bytes/count and call-tree drilldown. |
--top |
top types by bytes/count, plus signals[] such as allocations.by-type / allocations.by-site |
off_cpu / off-cpu |
Off-CPU stacks (where threads wait / block) via perf or ETW backend. Linux keeps system-wide sched_switch DWARF stacks and records target-scoped stackless raw syscalls separately for labels; if the syscall companion fails, stacks still return with an explicit note and no syscall breakdown. nativeContentionEvidence reports confirmed-blocking only for closed futex/native-sync waits, probable-blocking for censored/degraded native-sync evidence, and none for ambiguous frames without syscall correlation. |
--top, --symbol-path |
top blocking stacks ranked by off-CPU time + native sync evidence classification |
native-alloc |
Native allocator-call hotspots (malloc / calloc / realloc) via perf/ETW backend. Counts are sampled calls, not bytes. |
--top, --native-alloc-sample-period |
top allocator stacks + shared call-tree handle |
native-lock-contention |
Native mutex-call hotspots (pthread_mutex_lock / pthread_mutex_unlock) via a Linux uprobe (no Windows backend in this release). Its nativeContentionEvidence.level is always activity: counts are sampled calls, not confirmed blocking waits. Corroborate with off_cpu to find probable/confirmed blocking. |
--top, --native-lock-contention-sample-period |
top mutex-call activity stacks + shared call-tree handle |
cpu-efficiency |
Aggregate, whole-window CPU microarchitecture-efficiency snapshot (IPC, cache/branch/TLB miss rates, stalled-cycle breakdown, page faults, context-switches/cpu-migrations) via perf stat counting mode (Linux) or ETW PMC sampling (Windows). Every metric is independently nullable — unsupported metrics on this host surface as null + a notes entry, never a failure. |
none (aggregate only; no --top/--symbol-path) |
headline IPC / cache-miss-rate / branch-miss-rate plus the full metric set; no call-tree drilldown (aggregate, not per-method) |
thread-snapshot |
Point-in-time managed threads + lock graph from a live pid or dump. | --dump-file, --max-frames-per-thread, --include-runtime-frames, --include-native-frames, --symbol-path |
decisive threads inline (summary) or bounded thread/lock pages (detail/raw); continue through the session query command |
Examples:
# CPU hotspots + raw trace for offline PerfView / Speedscope:
dotnet-diagnostics-cli collect --kind cpu --pid 1234 --top 20 --export-trace
# Genuine on-CPU CoreCLR evidence (requires perf access on Linux or elevated ETW on Windows):
dotnet-diagnostics-cli collect --kind cpu --pid 1234 --cpu-backend os --top 20
# Managed allocation pressure:
dotnet-diagnostics-cli collect --kind allocation --pid 1234 --top 15 --json
# Off-CPU blocking stacks:
dotnet-diagnostics-cli collect --kind off_cpu --pid 1234 --top 10 --acknowledge-risk high
# Native allocation hotspots (calls, not bytes):
dotnet-diagnostics-cli collect --kind native-alloc --pid 1234 --native-alloc-sample-period 500 --acknowledge-risk high
# Native mutex contention hotspots (Linux only; calls, not confirmed waits):
dotnet-diagnostics-cli collect --kind native-lock-contention --pid 1234 --native-lock-contention-sample-period 2000 --acknowledge-risk high
# Live or dump-based thread snapshot:
dotnet-diagnostics-cli collect --kind thread-snapshot --pid 1234 --max-frames-per-thread 128 --acknowledge-risk high
dotnet-diagnostics-cli collect --kind thread-snapshot --dump-file ./app.dmp --include-runtime-frames--export-trace and --resolve-method-instantiations are EventPipe-only. The CLI
rejects either option with --cpu-backend os rather than silently ignoring it.
Cold-start capture (
--suspend-startup).collect --kind startupattaching to an already-running pid only sees loader/DI activity emitted after attach — the initial cold start (especially non-replayed DI call-site construction/resolution activity) is gone. Pair--suspend-startupwith--launch -- <app>to launch the target suspended on a reverse-connectDOTNET_DiagnosticPorts=…,suspendport, arm EventPipe before any managed code runs, then resume. This mirrors dotnet-monitor reverse-connect. The MCP equivalent iscollect_events(kind="startup", launch=...), available only over stdio whenDiagnostics:AllowProcessLaunch=true. The suspended child + reverse-connect socket are cleaned up on exit and on Ctrl-C.
Timing. EventPipe sessions take ~500 ms–1 s to start, and
counterspayloads arrive on--intervalboundaries — givecountersat least ~6 s. Forexceptions/gc, the collection window must overlap the load that generates the events.
Threshold-gated capture (
--capture-when). A bounded, one-shot watch (the human/CI equivalent of DebugDiagcollect) — not a daemon. It polls oneSystem.RuntimeEventCounter (rssMb=working-set,threadCount=threadpool-thread-count) every--watchseconds (default 2) for at most--windowseconds and captures--captureup to--max-capturestimes the moment the predicate trips, then returns.--capture dumprequires--confirmplus critical-risk acknowledgement and writes the dump to disk (the path is in the result). Forcpu-sample/heap/thread-snapshot, the result records carry headline capture stats plus a drilldown handle. That handle is only reachable by a laterquerywithin the samesession(the in-memory handle store is disposed when a one-shot command exits) — run gated capture inside thesessionREPL when you need to drill into the captured artifact afterward.
Less-obvious kinds.
crash-guardarms an unhandled-exception / crash watch and reports the managed exception that would fault the process.requestsenumerates in-flight ASP.NET Core requests (no ptrace — readsMicrosoft.AspNetCore.Hostingdiagnostics; query-string values are dropped to avoid leaking PII).startupcaptures cold-start activity — pair it with--suspend-startup --launch(below) to see everything before the first managed instruction.sweepruns a bounded parallel triage sweep across several collectors at once and returns a single consolidated verdict, the fastest "what's wrong right now" one-shot.
No arbitrary multi-kind batch (unlike MCP
collect_batch). The MCP server has acollect_batchtool (issue #665 Part C) that fans out a caller-chosen set ofcollect_sample/collect_eventskinds concurrently against one process/window, avoiding the process-exit race of issuing them as separate sequential tool calls with LLM decision latency in between. The CLI's answer to "several signals over one shared window" is the fixed-shapesweepkind above (sameTask.WhenAllfan-out, just a pre-selected collector set); its answer to "the target might not survive a second round-trip" is--capture-when/--capture/--window(a single threshold-gated capture, not N kinds at once). An arbitrary-kind CLI batch was discussed and declined (issue #677): thesessionREPL already keeps the target's handle/pid bound across commands typed back-to-back (no LLM round-trip latency to race), and a one-shot batch verb would need new human-mode rendering for N heterogeneous results with no clear net win oversweep+ a follow-upcollect --kind.
Walk the managed heap of a live process or a .dmp.
| Option | Meaning |
|---|---|
--source <live|dump|gcdump> |
Snapshot source. Inferred: dump when --dump-file is set, else live. gcdump needs no ptrace or dump file, but it induces a blocking Gen2 GC that can pause the target. It returns observed per-type node/byte totals only; object edges, roots, retention and ClrMD-only properties are explicitly unavailable, not observed empty. |
--dump-file <path> |
--source dump: path to a previously-captured .dmp. |
--top-types <int> |
Top-N type count (default 20). |
--include-retention-paths |
Walk a short GC retention chain for the top types. |
--retention-path-limit <int> |
Cap retention-chain depth (default 8). |
--include-static-fields |
Rank static reference fields by referenced object size. |
--include-delegate-targets |
Group MulticastDelegate invocation lists by (target, method). |
--include-duplicate-strings |
Rank duplicate strings by aggregate retained bytes. |
--symbol-path <path> |
NT_SYMBOL_PATH-style search path (remote servers off by default). |
--export-trace |
--source gcdump: keep the raw .nettrace under the artifact root and print its relative path (default off — the trace is deleted after parsing). Fetch it later with get-bytes --kind trace. |
dotnet-diagnostics-cli inspect-heap --pid 1234 --top-types 30 --acknowledge-risk high
dotnet-diagnostics-cli inspect-heap --source dump --dump-file ./app.dmp
dotnet-diagnostics-cli inspect-heap --source gcdump --pid 1234 --acknowledge-risk high # induced GC, no ptrace
dotnet-diagnostics-cli inspect-heap --source gcdump --pid 1234 --export-trace --acknowledge-risk high # keep raw .nettrace
dotnet-diagnostics-cli inspect-heap --launch --acknowledge-risk high -- dotnet App.dll # ptrace_scope=1, no privilegeHuman output includes the gcdump evidence policy and bounded limitation rows.
--json, session queries, saved comparisons and BenchmarkDotNet artifacts preserve
the same structured quality and completion status. A missing quality object on a
legacy artifact means unknown, not clean. Because EventPipe lost-event counts are
not observable on this path, absence, exhaustive-count, regression and
healthy-control claims remain inconclusive even when GC stop and stream completion
were observed. Captured positive per-type evidence remains usable.
--source live attaches via ptrace(2) — see the Linux note, which also
documents the --launch zero-privilege dev mode.
Write a process dump to disk. Without --confirm, a preview is returned (and still exits 0);
because no dump is written, the preview does not require risk acknowledgement. Actual execution
requires both --confirm and critical-risk approval.
| Option | Meaning |
|---|---|
--dump-type <type> |
Mini (default), Triage, WithHeap, Full. |
--out <dir> |
Directory to write into (default: temp artifact root). |
--confirm |
Required to actually write. In one-shot/non-interactive use, combine it with --acknowledge-risk critical. In an interactive session, it enables the single safety prompt; there is no second dump-specific prompt. |
dotnet-diagnostics-cli dump --pid 1234 --dump-type WithHeap --out ./dumps \
--confirm --acknowledge-risk criticalScripting. Parse
--jsonto tell a preview apart from a written dump:data.kind == "confirmation_required"(preview) vsdata.kind == "dump_written".The preview discloses the resolved artifact directory the dump would be written to (
would write to : <dir>) so you can confirm the destination before re-running with--confirmand--acknowledge-risk critical. In an interactivesession, use--confirmand typecriticalat the safety prompt.
Materialise a module (PE/PDB), a dump file, or a raw .nettrace to disk.
| Option | Meaning |
|---|---|
--kind <module|dump|trace> |
Required. Artifact to materialise. |
--out <file> |
Required. Destination file. |
--mvid <guid> |
--kind module: module version id (GUID) to fetch. |
--asset <pe|pdb> |
--kind module: artifact within the module (default pe). |
--dump-file <path> |
--kind dump|trace: path to the source .dmp / .nettrace to copy out. |
dotnet-diagnostics-cli get-bytes --kind module --pid 1234 --mvid <guid> --out ./app.dll --acknowledge-risk critical
dotnet-diagnostics-cli get-bytes --kind dump --dump-file ./app.dmp --out ./copy.dmp --acknowledge-risk critical
dotnet-diagnostics-cli get-bytes --kind trace --dump-file ./cpu.nettrace --out ./cpu.copy.nettrace --acknowledge-risk criticalCompare two or more saved comparable snapshots from collect --save. Human output keeps the compact verdict, first→last headline, and top metric/key deltas in the terminal; --json emits the full SnapshotJourneyDiff, and --save writes that full matrix to a file. Local MCP compare_to_baseline / query_snapshot(view="diff") calls use a journey://diff/{handle} Resource link when the matrix is large. Proxied pod calls return full results inline because dynamic pod Resources are not forwarded.
For ThreadPool snapshots, only provenance-backed runtime Starvation and
CooperativeBlocking adjustments are regression-driving metrics. Worker counts,
generic hill-climbing, and the window-local enqueue/dequeue difference are neutral
context; missing or summary-trimmed measurements are omitted rather than serialized
as observed zero. Legacy snapshots without provenance remain readable but cannot by
themselves establish a causal regression or healthy control. Saved ThreadPool
comparables also carry bounded per-capture Quality limitations. If either capture
has detected loss, hill-climbing eviction, a processing failure, an inadequate
no-event window, incomplete reason provenance, or legacy/missing quality metadata,
the comparison reports inconclusive and keeps raw deltas as non-directional context
instead of claiming improvement or regression.
| Option | Meaning |
|---|---|
--json |
Emit the full journey diff JSON. |
--save <file> |
Write the full journey diff JSON to disk. |
--mode trend|dispersion |
Interpret captures as an ordered trend (default) or unordered replicas for dispersion/outlier detection. |
dotnet-diagnostics-cli compare ./before.json ./after.json
dotnet-diagnostics-cli compare ./pod-a.json ./pod-b.json ./pod-c.json --mode dispersion
dotnet-diagnostics-cli compare ./before.json ./mid.json ./after.json --save ./matrix.jsonFor how to read the verdict / trend and when to reach for a journey, see investigation-playbooks.md §1d.
Plan a triage investigation. The planner classifies the likely failure mode from what you observed and
returns an ordered, branching plan (next step + rationale + all candidate steps + early-stop conditions)
that you execute yourself with the other CLI commands — the CLI stays stateless, it never runs the
plan for you. Requires --symptom <text> (or --hypothesis <text>) so the plan is anchored to a real
observation; --max-tool-calls <n> (default 8) caps the plan length. Steps are rendered in CLI
vocabulary (e.g. collect), never MCP tool names.
| Option | Meaning |
|---|---|
--symptom <text> |
What you observed (e.g. "p99 latency spiked"). Required unless --hypothesis is given. |
--hypothesis <text> |
A suspected root cause to test (switches the plan into hypothesis mode). |
--max-tool-calls <n> |
Upper bound on plan length (default 8). |
dotnet-diagnostics-cli investigate --pid 1234 --symptom "high CPU after deploy"
dotnet-diagnostics-cli investigate --pid 1234 --hypothesis "lock contention on the cache" --jsonProject a CPU-sample handle into a portable investigation summary (JSON): the top hotspots plus
metadata, suitable for attaching to a ticket or feeding another tool. Requires --handle <id> from a
collect --kind cpu (gated --capture cpu-sample) inside a session. With no --out, the portable
JSON document is written to stdout verbatim (identical to what --out persists), so it pipes
cleanly; --out <file> writes it atomically instead.
| Option | Meaning |
|---|---|
--handle <id> |
CPU-sample handle to summarize (required). |
--top-hotspots <n> |
Number of hotspots to include (default 10). |
--out <file> |
Write the summary to a file (atomic) instead of stdout. |
# inside a session, after a cpu-sample handle exists:
export-summary --handle h-abc123 --top-hotspots 15
export-summary --handle h-abc123 --out ./cpu-summary.jsoncollect --kind counters, exceptions, gc, sweep, cpu, allocation, and thread-snapshot
embed a top-level signals[] array in
the JSON envelope (--json or --depth detail/raw) whenever something is salient — the same
diagnosis-agnostic "vector" the MCP server documents in
tool-reference.md: a stable signal id (e.g.
exceptions.by-type, gc.gen2-share, counters.trend, cpu.self-time.method,
allocations.by-type, threads.by-wait-target, correlation.thread-overlap), a one-line
summary, a salience in [0,1], and buckets[] referencing a handle. It groups and correlates;
it never names a root cause or a fix. Omitted from the wire when nothing stands out.
dotnet-diagnostics-cli collect --kind cpu --pid 1234 --json
# ⇒ top-level "signals": [{ "signal": "cpu.self-time.method", "salience": 0.93, ... }] when one
# method dominates exclusive self-timeoff_cpu and native-alloc/native-lock-contention do not currently emit dedicated signals[] groupings; their value is
the ranked stack output, nativeContentionEvidence, and the shared session query drilldowns.
Re-render a previously-collected handle under a different view without re-collecting.
This is only meaningful inside a session — drill-down handles live for the lifetime of the host, and
the one-shot CLI builds a fresh host per command and exits. Run one-shot, query returns a NotSupported
envelope (exit 1) that redirects you to dotnet-diagnostics session, where a collect (or inspect-heap /
dump) issues a handle you can drill into in the same session; for a one-shot answer instead, re-run the
originating command with --depth detail / --json to get the full result inline.
Inside session, query --handle <id> --view <view> works against the live handle store (see below).
An alias query --latest-of-kind <kind> --view <view> resolves to the most recently registered
non-expired handle of that kind instead of requiring you to copy a handle id (see below).
Start the stateful REPL — covered in the next section. Accepts --launch -- <app> [args] at startup
to spawn the target as a child and bind it for the whole session (zero-privilege live attach under
ptrace_scope=1; see the Linux note). The child is killed when the session ends.
session builds the diagnostic host once and reads commands from stdin until exit / quit / EOF. Every
handle published by collect or inspect-heap stays alive (until it expires or the
target exits), so you can drill in repeatedly with query and never pay the collection cost twice.
When stdin/stdout are a genuine interactive terminal (not redirected/piped — as in CI or when scripting), the REPL line editor adds (issue #657):
- Command history — Up/Down-arrow (or Ctrl-P/Ctrl-N) recall previous commands from this session. History is in-memory only and not persisted to disk (session commands routinely carry pids and file paths).
- Tab-completion — Tab (or Ctrl+Space) offers command names, flags, and known enum values (e.g.
--kind,--source,--view) for the current word, narrowing as you type. A boundtargetpid is offered for--pid/-p.
Redirected/piped input (e.g. echo 'processes' | dotnet-diagnostics-cli session, or the test harness)
transparently falls back to plain line reading — no history or completion, same as before.
$ dotnet-diagnostics-cli session
dotnet-diagnostics session — stateful diagnostics REPL. ...
diag> target 1234
Target bound to pid 1234. capabilities/collect/inspect-heap/dump/get-bytes now use it unless you pass --pid.
diag(pid 1234)> collect --kind gc --duration 10
· using bound target pid 1234
... GC summary ...
→ handle 1TA2BA7KT9PYT60WTWE0 (expires 23:10:18Z) — query --handle 1TA2BA7KT9PYT60WTWE0 --view <pauseHistogram|...>
diag(pid 1234)> query --handle 1TA2BA7KT9PYT60WTWE0 --view pauseHistogram
... re-rendered view, no re-collection ...
diag(pid 1234)> collect --kind datas --duration 15 --save before.json
diag(pid 1234)> collect --kind datas --duration 15 --save after.json
diag(pid 1234)> compare before.json after.json
diag(pid 1234)> exit
Starting with session --launch --acknowledge-risk high -- dotnet App.dll spawns the target as a child, binds its pid for the
whole session (so live attach works under ptrace_scope=1 with no privilege), and kills it on exit.
--launch is a startup-only flag — it cannot be repeated per-command inside the REPL. The bound pid is
also fixed for the session's lifetime (issue #659): since the zero-privilege attach and the child's
lifecycle only hold for the pid the session itself spawned, target <other-pid>/target clear and any
per-command --pid <other-pid> are rejected with an explanatory error; exit the session to investigate
a different process. A no-argument target (status query) and a redundant --pid <the-launched-pid>
still work.
Bind a target pid once instead of repeating --pid on every command. The binding accepts either a
literal pid or a visible .NET process name/prefix using the same matching rules as --pid <name>:
| Input | Effect |
|---|---|
target <pid> / target --pid <pid> |
Bind a default pid. The prompt becomes diag(pid <id>)>. |
target <name-prefix> / target --pid <name-prefix> |
Resolve exactly one visible .NET process by entrypoint/name prefix and bind its pid. Ambiguous matches list pid + name. |
target |
Show the current binding. |
target clear (or none / off / unset) |
Unbind. |
Live-target commands — capabilities, collect, dump, inspect-heap --source live,
get-bytes --kind module — inherit the bound pid when --pid is omitted, and print a
· using bound target pid N note. Offline commands (inspect-heap --source dump, get-bytes --kind dump)
and pid-less commands (processes, query) never inherit it. An explicit per-command --pid always
overrides the binding — except in a session --launch-started session, where the pid is locked for
the session's lifetime and any different --pid/target is rejected (see above). Follow-up hints
omit --pid N when that same pid is already supplied by the session binding.
A background check independently watches the bound target every ~5 s and — separately from any handle you may have collected — prints a one-time notice the first time it detects the bound pid has exited:
diag(pid 1234)> [session] target pid 1234 has exited; self-contained handles (e.g. process dumps) remain queryable, but new captures against this pid will fail and any live-sourced handles for this pid are dropped by the periodic sweep.
A no-argument target also actively re-checks liveness and reflects it in the report (Target bound to pid 1234 (exited).), and on a real terminal, tab-completing collect --kind <TAB> after the notice
annotates every candidate (cpu (target exited)) since a new capture against a dead pid will fail
immediately — self-contained follow-ups (query --handle <id> --view <view>) are unaffected, per #662,
and so is collect --pid <other-pid> --kind <TAB> (an explicit --pid/-p overrides the exited
session binding for that one invocation, so it isn't annotated).
Binding a fresh live pid (target <pid>) clears the exited state.
A collect or inspect-heap command prints a handle plus the views you can re-render:
→ handle <id> — query --handle <id> --view <view1|view2|...>
query --handle <id> --view <view> re-renders that artifact under the chosen view with no new collection.
For an Activities handle, query --handle <id> --view trace --trace-id <32-hex>
uses the same completed-only, capture-window-limited trace projection as the
MCP query_snapshot tool; it never claims that the retained window is a
complete trace.
Handles are evicted when they expire (a TTL) or when the target process exits — a 5 s in-process sweep drops
dead-target handles so you never drill into a stale trace.
--latest-of-kind <kind> (issue #812) is an alias for --handle: instead of copying a handle id from a
prior collect/inspect-heap, query --latest-of-kind cpu-sample --view <view> resolves to the most
recently registered non-expired handle of that kind — useful for iterative collect→query tuning loops.
Exactly one of --handle/--latest-of-kind may be supplied; combining both, or supplying neither, is
rejected with InvalidArgument. If no non-expired handle of that kind is currently registered, the command
returns NotFound with a hint to collect one first. In a session with handles for more than one process,
narrow the match with --pid <id> (the same flag used to bind/override a target).
Handles are process-local. A handle printed by a one-shot invocation disappears when that CLI
process exits and cannot be queried by a later invocation; one-shot human and JSON output include
this notice. Use --depth detail / --json for inline evidence, or run the originating command and
query inside the same session REPL.
The same strictly bounded store as the MCP server is used. Configure its
capacity before starting the CLI with
Diagnostics__HandleStore__MaxEntries (default 32, valid range 1..1024).
When full, it first removes expired entries, then evicts the handle with the
earliest expiry deadline (oldest registration breaks ties). A bounded
four-per-entry tombstone set lets query distinguish HandleExpired,
HandleCapacityEvicted, and HandleNotFound; capacity errors tell you to
re-run the originating command and name the capacity setting. No evicted
artifact is retained for this diagnosis.
For CPU/allocation sample handles (cpu-sample, allocation-sample, native-alloc-sample, native-lock-contention-sample), the session
exposes drilldown views computed from the merged call tree without re-sampling:
| View | What it shows | Relevant flags |
|---|---|---|
call-tree (default) |
the merged inclusive/exclusive call tree; CPU handles can also carry selfSamples on the view and per node |
--max-depth (tree depth, default 8), --max-nodes (default 64; larger requests are clamped to the 64-node wire cap), --min-count, --root-method-filter, --rank-by |
top-methods |
methods ranked by stack-observation frequency; CPU handles include evidence plus per-method selfSamples.runningSamples (OS-backed on-CPU), waitingSamples (heuristic wait), and unknownSamples |
--top (default 20), --rank-by exclusive|inclusive|running, --fold-async (rename async MoveNext leaves to their declaring method) |
by-module |
samples grouped by owning module | --top, --rank-by |
by-namespace |
samples grouped by namespace | --top, --rank-by |
hot-path |
the dominant stack from the root down; CPU handles include per-frame selfSamples |
--threshold (percent, default 50) |
caller-callee |
a focus method with its direct callers + callees; CPU handles include the focus method's selfSamples |
--root-method-filter <substring> (required), --top |
triage |
one round-trip bundle: measured on-CPU leaders for OS-backed captures, otherwise conservative stack-frequency candidates; heuristic wait categories; dominant hot-path leaf; and an evidence-aware verdict (on-cpu-observed or unclassified) |
--top (default 5, smaller than the usual 20), --threshold (hot-path leaf, default 50) |
For session ranked views, --top is preferred. The older --top-types remains a compatibility
alias; when both are present, --top wins.
--rank-by inclusive ranks by inclusive observations; exclusive ranks leaf observations.
--rank-by running uses measured on-CPU self samples only for perf/ETW evidence. On CoreCLR
EventPipe it remains a discoverability ranking over exclusive stack frequency and is explicitly
labeled as not establishing scheduler state. --fold-async (issue #811) is opt-in and only affects top-methods: it renames a
compiler-generated async state-machine MoveNext leaf (e.g. Owner+<Method>d__22.MoveNext()) to its
declaring async method name (Owner.Method() [async]), so the frame reads as recognizable user code;
this does not turn EventPipe frequency into measured CPU evidence. Rows include an asyncFolded
flag reporting whether a match occurred. caller-callee requires --root-method-filter to resolve exactly one method: zero matches
return a NotFound envelope, more than one returns InvalidArgument with the candidate list.
Rather than a new multi-kind collect verb, the session REPL surfaces the same cross-collector
"investigation digest" the MCP server's collect_batch tool computes (issue #825) automatically:
once both collect --kind cpu and collect --kind allocation have run against the same bound pid in
one session (in either order), the next cpu/allocation collect prints a compact correlated summary
right after its handle line — CPU evidence-aware method candidates, heuristic wait categories, the
dominant hot-path leaf, and the top allocation types/call sites, in one place instead of two separate
query --view triage / query --view call-tree round trips:
diag(pid 1234)> collect --kind cpu --duration 10
... cpu summary ...
→ handle 1TA2... (expires 23:12:04Z) — query --handle 1TA2... --view <call-tree|top-methods|...>
diag(pid 1234)> collect --kind allocation --duration 10
... allocation summary ...
→ handle 1TB3... (expires 23:12:18Z) — query --handle 1TB3... --view <call-tree|top-methods|...>
→ investigation digest (cpu + allocation correlated):
top cpu self-time: MyApp.Worker.Crunch (61.4%), MyApp.Worker.Parse (12.8%)
top wait categories: ThreadPool worker idle wait (9.1%)
hot-path leaf: MyApp.Worker.Crunch (depth 3, 61.4% inclusive)
top allocation types (bytes): MyApp.Widget (612,000 bytes), MyApp.Gadget (88,400 bytes)
top allocation call sites: MyApp.Worker.Allocate (612,000 bytes)
Collecting only one of the two kinds prints no digest — it is only worth surfacing once it correlates
both collectors. This reuses the exact same DotnetDiagnostics.Core.CpuSampling.InvestigationDigestBuilder
logic the MCP tool and the BenchmarkDotNet diagnoser's exported report call (see that package's README),
so the ranking/gating is not duplicated per surface.
For collect --kind cpu, interpret selfSamples as a self/exclusive-time split:
runningSamples are leaf frames that do not match a known wait primitive; waitingSamples
are leaf frames such as Monitor.Wait, WaitHandle.Wait*, LowLevelLifoSemaphore.*,
SemaphoreSlim.Wait*, Task.Wait, or ThreadPool idle waits. On CoreCLR this helps you spot
when EventPipe SampleProfiler is surfacing blocked managed threads rather than genuine on-core
work. For direct blocking analysis, pivot to collect --kind off_cpu or collect --kind thread-snapshot.
GC handles (collect --kind gc) expose pause-analysis views over the events already collected:
| View | What it shows | Relevant flags |
|---|---|---|
summary (default) |
v2 suspension status/totals and collection counts/elapsed | — |
events |
completed collection elapsed rows | --top (--top-types compatibility alias) |
pauseHistogram |
validated fully-suspended-phase duration buckets | — |
timeline |
per-collection rows (index, gen, reason, type, elapsed, start gap), chronological | --top (--top-types compatibility alias; earliest N) |
longestPauses |
N longest validated suspension intervals, not collections | --top (--top-types compatibility alias) |
byGeneration |
count + total/mean/max collection elapsed per gen0/gen1/gen2/background bucket | — |
byGeneration keeps background GCs in their own bucket, so gen2 counts non-background gen2 collections only.
GC measurement v2 uses GCSuspendEEStop → GCRestartEEStart, reasons 1 and 6:
the fully-suspended GC-related phase, excluding acquisition/restart tails. Background collection
elapsed is not pause. Legacy pause-named collection fields retain their old elapsed meaning for
source compatibility; use nullable suspension.totalSuspensionTime and its explicit status.
GC query payloads now return the versioned GcMeasurementView contract; unavailable/legacy
evidence does not become zero or a healthy hint. Portable corrected pause metrics have .v2
identities and are never compared to legacy elapsed values under an unchanged metric name.
In the session REPL, acquire overlapping captures with
collect --kind gc-activities, then correlate the returned handles:
collect --kind gc-activities --source MyApp.* --duration 10 --json
query --handle <activities-handle> --view gc-overlay --gc-handle <gc-handle> --json
Both handles must already exist in that session with overlapping observation windows (sequential non-overlapping one-shot captures cannot establish overlap). The CLI and MCP use identical Core validation and UTC union attribution. Read candidate-retention, pause-retention, lifetime/window and measurement status independently; missing provenance remains unknown. A retained span can have exact attribution even when other activities were dropped. Rankings are not lower bounds. See the measurement contract for compatibility and quality details.
Catalog handles (collect --kind catalog) expose a metadata-only event inventory. The collector captures
provider name, event name, level and timestamps only — no payload field values. By default it enables a
broad curated provider set (Microsoft-Windows-DotNETRuntime, System.Runtime,
Microsoft-Diagnostics-DiagnosticSource, Microsoft-Extensions-Logging,
System.Threading.Tasks.TplEventSource) at Informational level; pass --provider one or more times to
replace that set for custom EventSources, because EventPipe cannot wildcard providers.
| View | What it shows | Relevant flags |
|---|---|---|
catalog (default) |
distinct (provider,eventName,level) rows ranked by count |
--top, --provider-filter, --root-method-filter (event-name substring) |
byProvider |
provider rollup with total count + distinct event type count | --top, --provider-filter, --root-method-filter |
events |
bounded chronological metadata occurrence sample, never payloads | --top, --provider-filter, --root-method-filter |
Use the targeted event_source collector if you need payload values; it carries the allowlist/redaction gates.
DATAS handles (collect --kind datas) expose the Server GC's Dynamic Adaptation To
Application Sizes tuning loop (default-on in .NET 9+; Workstation GC emits nothing, returning a
graceful NoDatasEvents result). The collector decodes the three DATAS GCDynamicEvent payloads from
Microsoft-Windows-DotNETRuntime (GCKeyword, Informational). The default window is 15 s — DATAS
decisions accrue over time, so a sustained window is best.
| View | What it shows | Relevant flags |
|---|---|---|
overview (default) |
heap-count range + change count, TCP statistics, mean gen0 budget / SOH stable size | — |
tuning |
per-decision heap-count timeline | --top, --changes-only (only transitions + baseline) |
samples |
per-GC measurements behind the decisions | --top |
gen2 |
gen2 "backstop" tuning events | --top |
Heap-snapshot handles (inspect-heap) expose the projection views rendered from the walked snapshot
(top-types, retention-paths, roots-by-kind, finalizer-queue, fragmentation, static-fields,
delegate-targets, gchandles, async, timers, alc) plus two address-addressed drilldowns:
| View | What it shows | Relevant flags |
|---|---|---|
top-types (default) |
top types by bytes/instances | --top (--top-types compatibility alias), --rank-by bytes|instances |
retention-paths |
short GC retention chains | --type-filter <substring>, --top (--top-types compatibility alias) |
gcroot |
shortest GC-root chain for one object (SOS !gcroot) |
--address <decimal|0x-hex> (dump-origin handles only) |
object |
one managed object's shape (SOS !do) |
--address <decimal|0x-hex> (dump-origin handles only) |
gcroot and object re-open the snapshot's origin with ClrMD to answer the address-scoped question.
The Core-only session serves them for dump-origin handles (inspect-heap --source dump) by re-reading
the recorded .dmp — no live attach — so an offline dump can still answer "what roots this object". The
object view never prints raw string/field values in-session (the standalone CLI holds no sensitive-value
gate); previews are replaced with <redacted:metadata-only>. Live-origin gcroot/object and the
objsize / duplicate-strings views stay server-only — use the MCP server's query_snapshot tool.
Off-CPU handles (collect --kind off_cpu or off-cpu) expose the off-CPU drilldowns already
captured in the artifact:
| View | What it shows | Relevant flags |
|---|---|---|
topStacks (default) |
blocking stacks ranked by off-CPU time | --top (--top-types compatibility alias) |
byThread |
per-thread off-CPU rollup | --top (--top-types compatibility alias) |
stack |
one specific blocking stack | --stack-rank <n> |
Thread-snapshot handles (collect --kind thread-snapshot, or a gated --capture thread-snapshot
inside a session) expose the call-stack / blocking views (threads-summary, stack,
lock-graph, deadlocks, top-blocked, unique-stacks, async-stalls, wait-chains,
threadpool) plus frame-vars:
| View | What it shows | Relevant flags |
|---|---|---|
threads-summary |
decisive thread summaries, eight per bounded page with up to eight frames each | --top (page size, capped at 8), --cursor <opaque> from nextThreadCursor |
top-blocked (default) |
blocked threads and real lock waiters, eight per bounded page with up to eight frames each | --top (page size, capped at 8), --cursor <opaque> from nextThreadCursor |
lock-graph |
contended locks, twelve per bounded page; or one lock's waiter IDs | --top (page size, capped at 12/8), --cursor <opaque> from nextLockCursor; add --address <decimal|0x-hex> and use nextWaiterCursor to page one lock's waiters |
wait-chains |
ranked CoreCLR monitor waiter→owner and async-continuation chains, including ThreadPool-starvation sinks and inferred cycle candidates; links expose edgeSource / confidence |
— |
async-stalls |
stalled async state machines and their await points |
— |
unique-stacks |
threads folded into shared stack signatures, ranked by group size | --frames-to-hash (top frames in the signature hash, default 20), --min-count (drop groups smaller than N, default 1) |
frame-vars |
one thread's local variables and parameters for a chosen stack frame (re-opens the origin via ClrMD) | --thread-id <id> (required) |
frame-vars requires --thread-id to pick the thread whose frame variables to resolve; the thread must
be present in the captured snapshot.
MCP and CLI session queries share the same versioned opaque cursors. Pass a returned cursor unchanged
to the next query command; it is bound to the snapshot handle, view, deterministic final sort key,
and (for waiter pages) lock address. Malformed and cross-handle cursors fail with InvalidArgument.
--offset remains a compatibility option for positions 0..256; deeper random offsets are rejected
with guidance to restart at zero and follow cursors, avoiding quadratic rescans.
For every ranked/list query view, --top N is the common row/group cap. --top-types N remains a
backward-compatible query alias; when both are supplied, --top wins. Views whose shape is controlled
by a different bound (call-tree uses --max-nodes, stack selects one rank/thread) are unchanged.
- While a command runs: the first Ctrl-C cancels only that command (cleaning up any temp
.nettrace/ perf files) and keeps the session alive; a second Ctrl-C force-quits the process. - At an idle prompt: Ctrl-C leaves the session (exit code 130).
exit/quit/ EOF leave cleanly (exit code 0).
inspect-heap --source live attaches via ptrace(2). On
Debian/Ubuntu/WSL the default kernel.yama.ptrace_scope=1 blocks same-UID peer attach, surfacing as a
PermissionDenied envelope. Prefer a path that preserves the host policy:
-
Zero privilege (dev):
--launch -- <app> [args]makes the CLI the target's parent. Underptrace_scope=1a tracer may attach to its own descendants, so live attach works with no sysctl change and no capability:dotnet-diagnostics-cli inspect-heap --launch --acknowledge-risk high -- dotnet App.dll dotnet-diagnostics-cli session --launch --acknowledge-risk high -- dotnet App.dll # binds the child for the whole sessionLaunch the app directly (
dotnet App.dllor a published apphost), not viadotnet run(which spawns a separate runtime child whose PID won't match). The child is killed when the command / session exits. This only helps underptrace_scope=1;scope=2still needsCAP_SYS_PTRACEandscope=3forbids attach entirely — use the dump-based workflow there. Whencapabilitiesdetects this exact environment it advertises the--launchtip. -
No live attach: use
inspect-heap --source dump --dump-file <path>for offline analysis, or use EventPipe-backedcollectkinds when they answer the diagnostic question. Thedumpcommand writes through diagnostic IPC and does not require LinuxCAP_SYS_PTRACE. -
Container or Kubernetes: if live attach is required, add
CAP_SYS_PTRACEonly to the diagnostics sidecar (Docker--cap-add SYS_PTRACE; KubernetessecurityContext.capabilities.add). -
Bare host, isolated personal-development only:
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scopeenables peer attach, but relaxes a host-wide security boundary for every same-UID process. Never use this on a shared or production host. See the canonical consumer-install safety note.
The MCP sidecar must also run as the same UID as the target so it can open
/tmp/dotnet-diagnostic-<pid>. EventPipe-based commands (collect, counters, GC, exceptions)
do not need CAP_SYS_PTRACE, but still need access to that diagnostic socket.
consumer-install.md— install walkthrough (MCP server distributions)client-setup.md— connecting an MCP client to the servertool-reference.md— the MCP tool surface (the server's analogue of these commands)