Start Debugging
2026-01-21 Updated 2026-01-23 dotnetdotnet-10dotnet-9performance

Perfetto + dotnet-trace: a practical profiling loop for .NET 9/.NET 10

A practical profiling loop for .NET 9 and .NET 10: capture traces with dotnet-trace, visualize them in Perfetto, and iterate on CPU, GC, and thread pool issues.

The fastest way to get unstuck on “it’s slow” in .NET is to stop guessing and start looking at a timeline. A neat write-up making the rounds this week shows a clean workflow: capture traces with dotnet-trace, then inspect them in Perfetto (the same trace viewer ecosystem many people know from Android and Chromium land): Using dotnet-trace with Perfetto.

Why Perfetto is worth adding to your toolbox

If you already use dotnet-counters or a profiler, Perfetto is not a replacement. It is a complement:

For .NET 9 and .NET 10 apps, this is especially useful when you are trying to validate that a “small” change did not accidentally introduce extra allocations, extra threads, or a new sync bottleneck.

The capture loop (repro first, trace second)

The trick is to treat tracing as a loop, not a one-off:

Here’s the minimal capture sequence using the global tool:

dotnet tool install --global dotnet-trace

# Find the PID of the target process (pick one)
dotnet-trace ps

# Capture an EventPipe trace (default providers are usually a good starting point)
dotnet-trace collect --process-id 12345 --duration 00:00:15 --output app.nettrace

You will end up with app.nettrace. From there, follow the conversion/open steps in the source post above (the exact “open in Perfetto” path depends on which Perfetto UI you use and what conversion step you choose).

What to look for when you open the trace

Start with questions you can answer in minutes:

Once you find a suspicious window, jump back to code and add a surgical change (for example: reduce allocations, avoid sync-over-async, remove a lock from the request hot path, or batch expensive calls).

One pragmatic pattern: trace in Release, without losing symbols

If you can, run the slow path in Release (closer to production), but still keep enough info to reason about frames. In SDK-style projects, PDBs are produced by default; for a profiling session you usually want predictable output paths:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Configuration>Release</Configuration>
    <DebugType>portable</DebugType>
  </PropertyGroup>
</Project>

Keep it boring: stable input, stable configuration, short traces, repeat.

If you want the detailed Perfetto steps and screenshots, the original post is the best reference to keep open while you run the loop: Using dotnet-trace with Perfetto.

< Back