Repository navigation
Add collect-linux mode to dotnet-trace integration - #887
LoopedBard3 merged 10 commits into
Conversation
Layers a CLI-driven dotnet-trace collection path on top of PR A's
in-process EventPipe path, with collect-linux as a Linux-only opt-in
that captures kernel + native frames via perf_event_open.
What:
- New `Job.DotNetTraceCollectMode` (`default` | `collect` |
`collect-linux`). Default preserves today's in-process behavior.
- New `Job.DotNetTraceStopTimeoutSec` (0 = per-mode default: 60s for
`collect`, 180s for `collect-linux`).
- Agent lazily `dotnet tool install`s pinned `dotnet-trace 9.0.661903`
on first CLI-mode use, asserts the reported version, caches the path.
- `StartDotNetTrace` dispatches on the mode. `collect` / `collect-linux`
shell out via a new `CollectViaDotNetTraceCliAsync` that:
* normalizes `DotNetTraceProviders` through crank's keyword table
(rewrites `cpu-sampling`\u2192`dotnet-sampled-thread-time` for
`collect` mode -- required, since modern `cpu-sampling` is
collect-linux-exclusive; cosmetic alias rewrites for `fusion`,
`gcheapcollect`, the `...allcation...` typos);
* groups all rewrites into one `[INF]` line per job;
* spawns the CLI directly (not via `ProcessUtil.RunAsync`) so we keep
lifecycle control, signals SIGINT on stop, falls back to
`Kill(entireProcessTree:true)` on grace expiry;
* logs `[WRN]` on grace expiry but keeps the partial trace.
- `collect-linux` preflight is a HARD FAIL (no silent fallback):
Linux + `geteuid()==0` + kernel >= 6.4. Uses the verbatim error
text from the plan so it's greppable.
Why:
- LTTng-based `Collect=true` is structurally fragile on modern Ubuntu
(empty managed event sequences on 24.04). `collect-linux` is the
perf_event-based replacement that doesn't depend on LTTng.
- Existing `DotNetTrace=true` users keep their exact current behavior;
`collect-linux` is purely opt-in.
Notes:
- Mixed-version compat: all new Job properties default to today's
behavior. New controller + old agent silently uses `default` mode.
New agent + old controller is byte-for-byte identical to today.
- `collect-linux` rejects `--buffersize` (perf_event ring buffers, not
EventPipe); the builder strips it in that mode regardless of the knob.
- Stop timeout is per-mode by design: `collect-linux`'s rundown +
native symbolication is genuinely heavier than `collect`'s.
Tested:
- `dotnet build Microsoft.Crank.sln -c Release` clean.
- `dotnet test test/Microsoft.Crank.UnitTests` 100/100 passing
(33 new across BuildDotnetTraceCliArgs / NormalizeClrEventExpression
/ ParseKernelVersion / mixed-version-compat).
- Manual collect-linux validation on cobalt aarch64 Ubuntu 24.04 pending
reviewer sign-off.
Stacked on loopedbard3/modernize-dotnet-trace (PR A).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…, hard-fail on version mismatch Three Copilot review comments on PR #3 surfaced real backcompat / soundness issues in BuildDotnetTraceCliArgs and EnsureDotnetTraceCliInstalledAsync. All three are fixed here with regression tests. 1. Tokenizer split on ',' only; legacy Collect() splits on ',' and ' '. Users with `gc-collect gc+jit`-style configs would tokenize differently under the CLI modes. Fixed by adding ' ' to the split delimiters. 2. Token classifier routed any non-':', non-profile token to --clrevents. A bare provider name like Microsoft-DotNETCore-SampleProfiler (the example shown in the README) is neither, so it would hit --clrevents and be rejected by the CLI. Fixed by adding TraceExtensions.IsRecognizedClrKeywordExpression: classify as --clrevents only when every '+'-part is a known CLR keyword; otherwise fall through to --providers, which accepts bare provider names. 3. dotnet-trace --version mismatch with the pinned DotnetTraceVersion was warn-and-proceed. Defeats the assert's purpose: a previous run's wrong-version binary in toolPath silently shadows the pin. Promoted to hard fail (with _dotnetTracePath reset so a manual cleanup recovers), matching the 'no silent fallback' theme established for the collect-linux preflight. Tests: +3 regression tests (BuildArgs_BareProviderName_GoesToProvidersFlag, BuildArgs_PlusJoinedWithUnknownPart_RoutesToProviders, BuildArgs_SpaceSeparatedTokens_AreTokenizedSeparately). Suite: 103 / 103 passing (was 100 / 100). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Position crash When dotnet-trace collect-linux is spawned by crank as a child process with stdout/stderr redirected to .NET pipes, its progress LineRewriter calls Console.SetCursorPosition(0, -1) and crashes with ArgumentOutOfRangeException. Console.CursorTop returns -1 in a non-TTY context; the 9.0.661903 build we previously pinned doesn't guard against that. Two upstream fixes addressing this are present in 10.0.721401 (tag v10.0.721401 on dotnet/diagnostics release/stable, commit 3f576a7): - dotnet/diagnostics#5771 'Fix no-tty crash with console capability aware ProgressWriter' (commit 767b7c0e) - dotnet/diagnostics#5745 'Handle redirected input/output in collect-linux' (commit 1f02e6f6) 10.x is not on nuget.org -- nuget.org currently tops out at 9.0.661903. Resolve the tool from the dnceng public dotnet-tools feed by writing a hermetic NuGet.config alongside the tool path and passing --configfile to dotnet tool install. A bare --add-source conflicts with package-source-mapping when the host has it configured, so --configfile is the portable form. The 10.x package layout is tools/net8.0/any/ (managed-IL only), so linux-arm64 on the cobalt-hosted aarch64 box is satisfied by the 'any' RID -- no native bits needed. Also redirect stdin in the child ProcessStartInfo as defense in depth: with IsInputRedirected true, the rewriter skips its ANSI-capability probe entirely (the path that produces the -1 from Console.CursorTop). Stop is signaled with SIGINT on Linux so we never write to stdin. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ect-linux dotnet-trace collect-linux reads tracefs to translate perf_event_open ring-buffer records into symbolicated frames. In a container the /sys/kernel/tracing directory exists but is empty unless the host's tracefs is bind-mounted, surfacing as 'Tracefs is not accessible: It appears tracefs is not mounted.' at runtime. Add -v /sys/kernel/tracing:/sys/kernel/tracing to the agent's docker run line. EventPipe-based collection (collect mode, or Collect=true perfcollect) does not need this mount; only collect-linux does. Host must have tracefs mounted -- Ubuntu 24.04 + systemd does this automatically at boot. Validated on the cobalt-hosted aarch64 Ubuntu 24.04 (kernel 6.8) host. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Removes the 3-row matrix that PR A's 2-row version was expanded into when collect-linux mode was added. Same reasoning as the matching removal on PR A's branch: keep the controller help text a flat flag reference and rehome the decision guidance to a dedicated docs page later, once the trace collection surface (collect-linux preview status, perfcollect behavior on modern distros) stabilizes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
401fcbd to
0b06a0b
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new DotNetTraceCollectMode=collect-linux option to crank’s dotnet-trace integration, enabling perf_event-based whole-machine tracing on modern Linux (kernel + native frames) via the dotnet-trace CLI, without relying on LTTng/perfcollect, while keeping the legacy in-process EventPipe behavior as the default.
Changes:
- Introduces
DotNetTraceCollectModeandDotNetTraceStopTimeoutSecJobknobs (defaults preserve existing behavior). - Adds agent-side dotnet-trace CLI installation + lifecycle management, plus
collect-linuxprerequisite validation and provider-token normalization for CLI modes. - Expands documentation and unit tests to cover mixed-version compatibility, CLI arg normalization, and Linux preflight parsing/contract.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Microsoft.Crank.Agent/Startup.cs | Implements CLI-based collect / collect-linux modes, lazy tool install, preflight checks, argv builder, and stop/grace/kill behavior. |
| src/Microsoft.Crank.Agent/TraceExtensions.cs | Adds CLR keyword-expression recognition helper used for CLI token classification. |
| src/Microsoft.Crank.Models/Job.cs | Adds DotNetTraceCollectMode + DotNetTraceStopTimeoutSec with legacy-preserving defaults. |
| src/Microsoft.Crank.Controller/Documentation.cs | Documents the new dotnet-trace mode and stop-timeout CLI flags. |
| src/Microsoft.Crank.Controller/README.md | Updates controller help text for the new dotnet-trace flags. |
| docker/agent/run.sh | Adds /sys/kernel/tracing bind mount required for dotnet-trace collect-linux tracefs access. |
| test/Microsoft.Crank.UnitTests/DotNetTraceCliNormalizationTests.cs | Adds unit tests for CLI argv building, token classification, and alias normalization behavior. |
| test/Microsoft.Crank.UnitTests/DotNetTraceCollectLinuxPreflightTests.cs | Adds unit tests for kernel-version parsing and the non-Linux error-message contract. |
| test/Microsoft.Crank.UnitTests/JobMixedVersionCompatTests.cs | Extends mixed-version deserialization/roundtrip tests to include PR-B knobs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The pinned dotnet-trace version (10.0.721401) only lives on the dnceng public dotnet-tools feed; nuget.org tops out at 9.0.661903 and cannot supply it. Listing nuget.org here is dead weight at best and a footgun at worst (an org-scoped 9.x mirror could shadow the intended 10.x). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Startup.cs: align EnsureDotnetTraceCliInstalledAsync comment with code (throws, not warns, on version mismatch). - DotNetTraceCollectLinuxPreflightTests: drop dead Build==-1 branch; ParseKernelVersion always returns a 3-part Version. - DotNetTraceCliNormalizationTests: rename LeavesUnknownKeywordsAlone -> LeavesNonAliasedKeywordExpressionAlone for clarity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
Addresses PR dotnet#887 review feedback: move DotNetTraceCollectMode validation (and the collect-linux host prerequisite check) out of StartDotNetTrace -- which runs only after the app has launched -- and into the JobState.New acceptance path, so a bad mode or unsupported host fails before any asset restore, clone/build, or app launch. - New ValidateDotNetTraceOptions(job) centralizes the mode allow-list and delegates collect-linux to ValidateCollectLinuxPrerequisites. - StartDotNetTrace's collect-linux arm no longer re-runs the preflight; the default arm is now a defensive-only guard. - 12 new unit tests cover the gate, mode allow-list, unknown-mode rejection, profile-type gating, and collect-linux delegation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses PR dotnet#887 review: the SemaphoreSlim only serialized concurrent installs within a single agent process and could not coordinate across separate crank agent processes. The SDK install path (EnsureDotnetInstallExistsAsync) uses no such lock, so this was inconsistent. Rely on the File.Exists idempotency short-circuit instead, matching the established pattern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses PR dotnet#887 review: replace the IndexOfAny delimiter list in ParseKernelVersion with an anchored regex (^(\d+)\.(\d+)(?:\.(\d+))?). The regex captures the leading major.minor[.patch] and naturally stops at any suffix delimiter, so enumerating '-', '+', ' ', '~' is no longer needed. Added test cases for ~ and space suffixes plus bare major.minor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DrewScoggins
left a comment
There was a problem hiding this comment.
Still not convinced on the necessity of the euid validation method, and still think we should get rid of the Go-style tuple return, but those aren't pr stoppers.
What
Adds a
collect-linuxmode to crank'sdotnet-traceintegration so Linux agents on modern kernels can capture whole-machine, perf_event-based traces with kernel + native frames, without LTTng.New
Jobproperties (defaults reproduce today's exact behavior):DotNetTraceCollectMode(default|collect|collect-linux) —defaultkeeps today's in-processDiagnosticsClient.StartEventPipeSessionpath;collectandcollect-linuxshell out to thedotnet-traceCLI.DotNetTraceStopTimeoutSec— grace period after SIGINT before the CLI is killed.0selects the per-mode default (60s forcollect, 180s forcollect-linux).Agent additions in
Startup.cs:dotnet-trace 10.0.721401(latest on nuget.org withcollect-linux; ships alinux-arm64build).dotnet tool install --tool-pathon first CLI-mode use, with a post-install--versionassert and a single-flight semaphore.CollectViaDotNetTraceCliAsyncspawns the CLI directly (not viaProcessUtil.RunAsync) so we keep lifecycle control: wait for the benchmark to signal stop → send SIGINT → wait per-mode grace → fall back toKill(entireProcessTree:true)on expiry.[WRN] dotnet-trace did not finalize within {N}s; trace may be incompleteand keeps the partial trace — does not fail the job.Provider-string compatibility:
BuildDotnetTraceCliArgsclassifies each token into profile / provider-spec / CLR-keyword-expression using crank's existingTraceExtensionstable.cpu-sampling→dotnet-sampled-thread-timeincollectmode only (the moderncpu-samplingprofile hasVerbExclusivity="collect-linux"and is rejected by thecollectverb).fusion→assemblyloader,gcheapcollect→managedheapcollect, plus the twogcsampledobjectallcation{high,low}typos.[INF] dotnet-trace providers normalized: …line per job.cpu-samplingforcollect-linuxanddotnet-sampled-thread-time,dotnet-commonforcollect. Empty-providers default fordefaultmode is unchanged (stillcpu-samplingvia PR A's preserved behavior).collect-linuxpreflight (hard fail — no silent fallback tocollect, per design):/proc/sys/kernel/osrelease), effective UID == 0 (viaMono.Unix.Native.Syscall.geteuid()).DotNetTraceCollectMode=collect-linux requires Linux, root (effective UID 0), and kernel >= 6.4. Detected: OS={os}, EUID={euid}, kernel={ver}. Set DotNetTraceCollectMode=collect to use EventPipe collection instead.Docs:
Documentation.cs+README.md.DotNetTrace=true(any OS),DotNetTrace=true+DotNetTraceCollectMode=collect-linux(modern Linux + root),Collect=true(legacy PerfView pipelines).Why
LTTng-based
Collect=trueis structurally fragile on modern Ubuntu — cobalt-hosted Ubuntu 24.04 aarch64 returns empty managed-event sequences even withlttng-tools+liblttng-ust-devinstalled correctly (likely runtime↔liblttng-ust ABI mismatch).dotnet-trace collect-linuxis the perf_event-based replacement: same kernel-sampling capability as perfcollect provided, but without the LTTng dependency. Cobalt hosts (kernel 6.8, agent container runs as root) meet every prerequisite.Existing
--application.dotNetTrace trueusers keep their exact current behavior.collect-linuxis purely opt-in via the new mode flag.Notes
collect-linuxrejects--buffersize(TreatUnmatchedTokensAsErrors=truein upstream'sCollectLinuxCommand; perf_event uses kernel ring buffers, not EventPipe circular buffers). The argv builder strips it in that mode regardless ofDotNetTraceBufferSizeMB.collect-linuxrequires the traced process to be .NET 10+. If a user points it at a .NET 8/9 process, the CLI's stderr ("EventPipe IPC command not understood") is captured verbatim intojob.Error.Mono.Unix.UnixUserInfo.RealUserIdwould return the pre-sudo UID under elevation;geteuid()is the signal that matters forperf_event_opencapability checks. (Rubber-duck blind-spot fix from the planning session.)collectmode is best-effort.GenerateConsoleCtrlEventdoesn't reliably deliver Ctrl+C to non-console children spawned withUseShellExecute=false; the helper falls back toCloseMainWindow()+ the grace-period kill. Windows users almost always wantdefaultmode anyway.Mixed-version compatibility
DotNetTraceCollectMode/DotNetTraceStopTimeoutSec. If the controller asks forcollect-linux, the old agent silently runsdefault. Documented expectation.defaultmode,0-as-per-mode-default timeout). Byte-for-byte identical to today.Validated by
JobMixedVersionCompatTestscovering legacy and PR-A-era payloads.Tested
dotnet build Microsoft.Crank.sln -c Release→ clean (0 errors, 0 warnings).dotnet test test/Microsoft.Crank.UnitTests→ 103 / 103 passing, including 33 new tests across:DotNetTraceCliNormalizationTests(BuildDotnetTraceCliArgs / NormalizeClrEventExpression — defaults per mode, cpu-sampling rewrite, classification, alias rewrites, buffer-size gating)DotNetTraceCollectLinuxPreflightTests(ParseKernelVersion across the 6.4 boundary, error-message contract on non-Linux)JobMixedVersionCompatTests(PR B knob defaults + round-trip)collect-linuxvalidation on cobalt aarch64 Ubuntu 24.04 host — pending reviewer. Manual-validation checklist before un-drafting:dotnet tool install dotnet-trace --version 10.0.721401 --tool-path /tmp/xresolves an arm64 tool on the cobalt host (no x64-only fallback).--application.dotNetTrace true --application.dotNetTraceCollectMode collect-linuxproduces a non-empty.nettracewith both managed and native frames.collect).Operator notes
dotnet-trace collect-linux reads tracefs to translate
perf_event_openrecords. In a container the/sys/kernel/tracingdirectory exists but is empty unless the host's tracefs is bind-mounted, surfacing asError: Tracefs is not accessible: It appears tracefs is not mounted.at runtime. This PR adds-v /sys/kernel/tracing:/sys/kernel/tracingtodocker/agent/run.shso the wrapper script handles this for new agent deployments. EventPipe-based collection (DotNetTraceCollectMode=collectorCollect=trueperfcollect) does not need this mount; onlycollect-linuxdoes. The host must also have tracefs mounted (Ubuntu 24.04 + systemd does this at boot; check withmount | grep tracefs). Operators upgrading an existing agent need to./docker/agent/stop.sh && ./docker/agent/run.shto pick up the new mount -- volume mounts cannot be added to a running container. Validated on the cobalt-hosted aarch64 Ubuntu 24.04 (kernel 6.8) host.The pinned
dotnet-traceCLI version is 10.0.721401 (tagv10.0.721401ondotnet/diagnostics:release/stable, commit3f576a7). This is the latest stable release that contains both upstream fixes for the no-ttySetCursorPosition(0, -1)crash that occurs whencollect-linuxis spawned with redirected stdout:dotnet/diagnostics#5771("Fix no-tty crash with console capability aware ProgressWriter") anddotnet/diagnostics#5745("Handle redirected input/output in collect-linux"). 10.x is not on nuget.org; the agent writes a hermeticNuGet.confignext to the tool path and resolves from the dnceng publicdotnet-toolsfeed via--configfile.Stacking
PR A (#886) has merged into
dotnet/crank:mainas squash commitd392425. This branch has been rebased onto currentmainand now contains only the collect-linux commits.