docs java javascript python c / c++ c# reading reports

How to profile C# and .NET

The .NET runtime has a sampling profiler built into it — EventPipe — and a cross-platform CLI that drives it. No IDE required, no recompile, and it attaches to a process that's already running in production. That's dotnet-trace, and it's the path flamelens takes.

Install and collect

dotnet tool install --global dotnet-trace

# list candidate processes
dotnet-trace ps

# attach to a running app for 60 seconds
dotnet-trace collect --process-id <pid> --duration 00:00:01:00

# ...or launch the app under the profiler
dotnet-trace collect -- dotnet run -c Release

That writes trace.nettrace. It's a binary EventPipe file, so convert it to the interchange format before uploading:

dotnet-trace convert trace.nettrace --format speedscope
# → trace.speedscope.json

Upload the .speedscope.json. flamelens reads the file's exporter field, recognizes it came from dotnet-trace rather than py-spy, and analyzes it as .NET — allocation pressure, async state machines, ThreadPool starvation, boxing, and the rest of the .NET performance vocabulary.

Profile a Release build. A Debug build disables JIT optimizations and inlining, so a Debug profile measures a program you don't ship: it will show time in methods that vanish entirely under -c Release. Also set DOTNET_TieredPMStubs aside and just warm the path up first — tiered compilation means the first few hundred calls run cold Tier-0 code, and a profile of startup is mostly JIT.

Choosing what to collect

The default profile is CPU sampling, which is what you want for "why is this slow". Two other presets pay off often:

# allocation-heavy investigation (GC events + allocation ticks)
dotnet-trace collect --process-id <pid> --profile gc-verbose --duration 00:00:00:30

# minimal overhead, just GC collections and pauses
dotnet-trace collect --process-id <pid> --profile gc-collect --duration 00:00:01:00

--profile cpu-sampling is the default and the right first move. gc-verbose is genuinely expensive on an allocation-heavy service — use it for a short window when you already suspect allocation.

In JetBrains Rider

  1. Rider bundles dotTrace: Run → Profile '<config>', or the profile button next to Run. Choose Timeline or Sampling — Sampling is the closer analogue to what dotnet-trace collects.
  2. Exercise the slow path, then stop. Rider opens its own flame view.
  3. dotTrace saves proprietary .dtp/.dtt snapshots, which aren't a format flamelens can read. For a file to upload, run the dotnet-trace pair above from Rider's built-in terminal against the same process — Rider's Run window shows the pid on the first line.

In Visual Studio

  1. Debug → Performance Profiler (Alt+F2), tick CPU Usage, and start. This is the fastest way to look at a hotspot interactively.
  2. Visual Studio writes .diagsession, which is again a proprietary container. For a portable profile, use the Developer PowerShell:
    dotnet-trace collect --process-id <pid> --duration 00:00:01:00
    dotnet-trace convert trace.nettrace --format speedscope
  3. Everything works identically on Windows, Linux, and macOS — EventPipe is part of the runtime, not the OS tooling.

In VS Code

  1. With the C# Dev Kit installed, run your app as usual (F5 or dotnet run -c Release in the terminal).
  2. Wire the collection as a task in .vscode/tasks.json:
    {
      "version": "2.0.0",
      "tasks": [{
        "label": "profile with dotnet-trace",
        "type": "shell",
        "command": "dotnet-trace collect --name ${input:procName} --duration 00:00:01:00 && dotnet-trace convert trace.nettrace --format speedscope"
      }]
    }
  3. Upload the resulting trace.speedscope.json.

Now analyze it. Take trace.speedscope.json to the flamelens terminalauth <email>verify <code>scan init. F# and VB.NET profile identically; it's the same runtime and the same EventPipe.

Why there's no “scan live” for .NET — yet

Java exposes JMX and Node exposes the inspector, so flamelens can attach to those over the network. .NET's diagnostic channel is a local IPC transport — a Unix domain socket on Linux/macOS, a named pipe on Windows — which dotnet-trace connects to by process id. It doesn't listen on a TCP port, so there's nothing for us to dial from outside the machine.

There is a real path to changing that: dotnet-monitor is a Microsoft-supported sidecar that exposes exactly these diagnostics over HTTP, with authentication. If you're already running it, tell us — a live-scan integration against dotnet-monitor is the natural next step, and it's the one .NET path that wouldn't require weakening anything. Until then, use an agent.

Continuous sampling with an agent ENTERPRISE

The agent runs on the host, connects out over HTTPS, and samples on a cadence — a profile every N minutes, each diffed against the last:

agent new prod-api-1
# → prints a token, ONCE. store it like a password.

schedule new api 1234 dotnet 30 60
# → sample pid 1234 for 60s, every 30 minutes

The target is a pid (or whatever your script resolves to one), since that's what dotnet-trace takes. The recorder step is two lines:

dotnet-trace collect --process-id "$TARGET" --duration 00:00:00:"$secs" \
  --output /tmp/trace.nettrace
dotnet-trace convert /tmp/trace.nettrace --format speedscope --output "$out"

The agent needs to reach the diagnostic socket, which lives in /tmp/dotnet-diagnostic-* on Linux and is owned by the app's user — so run the agent as that user, or share /tmp and match the uid. In Kubernetes a sidecar needs shareProcessNamespace: true plus an emptyDir mounted at /tmp in both containers. The full polling loop is on the JavaScript page; only the recorder line differs per runtime.

What makes a good profile