.NET Tutorial
Middle7 мин чтенияВопрос 1 из 10

Как профилировать .NET приложение: dotnet-trace, PerfView, BenchmarkDotNet?

Процесс расследования

высокий CPU

растёт память / частые Gen2

зависание, потоки не отвечают

конкретный метод медленный

Симптом: медленно / много памяти /
100% CPU / зависание

Метрики и трассировки (APM, OTel):
какой эндпоинт, когда, зависимости

dotnet-counters:
CPU, GC, потоки, исключения

Что не так?

dotnet-trace (cpu-sampling)
→ flame graph

dotnet-gcdump / dotnet-dump
→ PerfView / VS: кто держит объекты

dotnet-dump + clrstack /
dotnet-stack: где висят потоки

BenchmarkDotNet:
сравнить варианты

Исправление → повторное измерение

высокий CPU

растёт память / частые Gen2

зависание, потоки не отвечают

конкретный метод медленный

Симптом: медленно / много памяти /
100% CPU / зависание

Метрики и трассировки (APM, OTel):
какой эндпоинт, когда, зависимости

dotnet-counters:
CPU, GC, потоки, исключения

Что не так?

dotnet-trace (cpu-sampling)
→ flame graph

dotnet-gcdump / dotnet-dump
→ PerfView / VS: кто держит объекты

dotnet-dump + clrstack /
dotnet-stack: где висят потоки

BenchmarkDotNet:
сравнить варианты

Исправление → повторное измерение

От симптома к причине

Инструменты .NET diagnostics

Shell
dotnet tool install -g dotnet-counters
dotnet tool install -g dotnet-trace
dotnet tool install -g dotnet-dump
dotnet tool install -g dotnet-gcdump
dotnet tool install -g dotnet-stack

dotnet-counters ps                           # список .NET-процессов

dotnet-counters

Shell
dotnet-counters monitor -p 1234 --counters System.Runtime,Microsoft.AspNetCore.Hosting
Что смотреть
[System.Runtime]
    CPU Usage (%)                                  87
    GC Heap Size (MB)                           1 420     ← растёт без остановки? утечка
    Gen 0 GC Count (Count / 1 sec)                 45
    Gen 2 GC Count (Count / 1 sec)                  3     ← частые Gen2 — дорого
    % Time in GC since last GC (%)                 22     ← > 10–20% — много аллокаций
    Allocation Rate (B / 1 sec)               850 MB
    ThreadPool Thread Count                       180     ← растёт — возможно, starvation
    ThreadPool Queue Length                       950     ← работа ждёт потоков
    Monitor Lock Contention Count (/ 1 sec)       300     ← конкуренция за lock
    Exception Count (Count / 1 sec)               120     ← исключения как поток управления?
[Microsoft.AspNetCore.Hosting]
    Requests / sec                              2 300
    Current Requests                              410

dotnet-trace

Shell
# CPU-профиль на 30 секунд
dotnet-trace collect -p 1234 --profile cpu-sampling --duration 00:00:30 -o cpu.nettrace

# Конвертация для speedscope.app (flame graph в браузере)
dotnet-trace convert cpu.nettrace --format Speedscope

# Аллокации и GC
dotnet-trace collect -p 1234 --profile gc-verbose

Flame graph: ширина «пламени» — доля времени CPU в функции (включая вызываемые). Ищите самые широкие «плато» вверху стека — там тратится время.

Main / Kestrel — 100%

OrdersController.GetList — 70%

Прочее — 30%

JsonSerializer.Serialize — 15%

OrderMapper.Map — 50%

Regex конструируется
на каждый вызов — 45%

Main / Kestrel — 100%

OrdersController.GetList — 70%

Прочее — 30%

JsonSerializer.Serialize — 15%

OrderMapper.Map — 50%

Regex конструируется
на каждый вызов — 45%

Как читать flame graph

dotnet-dump и dotnet-gcdump

Shell
dotnet-gcdump collect -p 1234 -o heap.gcdump        # только управляемая куча, быстро, файл небольшой
dotnet-dump collect -p 1234 -o full.dmp             # полный дамп процесса

dotnet-dump analyze full.dmp
> dumpheap -stat                                     # какие типы занимают память
> dumpheap -type Shop.Orders.OrderDto -min 1000     # экземпляры
> gcroot 00007f1a2b3c4d50                            # кто удерживает объект (путь до корня)
> clrthreads                                         # управляемые потоки
> clrstack -all                                      # стеки всех потоков — где зависли
> syncblk                                            # кто владеет блокировками

Дамп также можно снять автоматически при сбое: DOTNET_DbgEnableMiniDump=1, или по порогу через dotnet-monitor.

dotnet-monitor

Сайдкар/агент, предоставляющий HTTP API для сбора трассировок, дампов, метрик и логов из контейнеров по запросу или по триггерам (например, «CPU > 80% в течение минуты → снять трассировку»). Удобен в Kubernetes.

PerfView

Бесплатный инструмент Microsoft (Windows GUI, но анализирует и трассировки с Linux):

  • CPU Stacks — где тратится CPU, группировка, фильтры.
  • GC Stats — паузы, частота, размеры поколений, причины сборок.
  • GC Heap Alloc Stacks — кто аллоцирует.
  • Heap Snapshots + Diff — сравнение двух снимков кучи: что выросло (поиск утечек).
  • Wall Clock / Thread Time — время ожидания (I/O, блокировки), а не только CPU.

BenchmarkDotNet

C#
[MemoryDiagnoser]                        // аллокации и сборки GC
[SimpleJob(RuntimeMoniker.Net90)]
public class ParsingBenchmarks
{
    private const string Input = "2025-09-30";

    [Benchmark(Baseline = true)]
    public int Substring() => int.Parse(Input.Substring(0, 4)) + int.Parse(Input.Substring(5, 2));

    [Benchmark]
    public int Span() => int.Parse(Input.AsSpan(0, 4)) + int.Parse(Input.AsSpan(5, 2));
}

// Program.cs
BenchmarkRunner.Run<ParsingBenchmarks>();
// dotnet run -c Release
Результат
| Method    | Mean      | Ratio | Gen0   | Allocated | Alloc Ratio |
|---------- |----------:|------:|-------:|----------:|------------:|
| Substring | 38.12 ns  |  1.00 | 0.0076 |      64 B |        1.00 |
| Span      | 19.45 ns  |  0.51 |      - |         - |        0.00 |

Почему не Stopwatch в цикле:

  • JIT: Tier-0 / Tier-1, прогрев, OSR — первые вызовы медленнее.
  • Оптимизации: мёртвый код может быть удалён, если результат не используется.
  • Шум: GC, другие процессы, частота CPU — нужна статистика по многим итерациям. BenchmarkDotNet решает всё это: отдельный процесс, прогрев, пилотные прогоны, выбросы, доверительные интервалы.

Другие инструменты

Инструмент Для чего
Visual Studio Diagnostic Tools / Performance Profiler CPU, память, async, БД — в IDE
JetBrains dotTrace / dotMemory Коммерческие профилировщики CPU и памяти с удобным UI
Application Insights Profiler, Datadog, Pyroscope Непрерывное профилирование в продакшене
OpenTelemetry + Jaeger/Tempo Где время тратится между сервисами
MiniProfiler Время SQL и шагов на странице
k6, NBomber, JMeter Нагрузочное тестирование (воспроизвести проблему)

Вопросы на засыпку

Чем сэмплирующий профилировщик отличается от инструментирующего?

Сэмплирующий периодически (≈1 мс) снимает стеки потоков — низкие накладные расходы, подходит для продакшена, но статистический. Инструментирующий вставляет замеры в каждый вызов — точные счётчики вызовов, но сильно замедляет и искажает картину. dotnet-trace cpu-sampling — сэмплирующий.

Как профилировать приложение в Docker/Kubernetes?

Установить инструменты в образ (или использовать отдельный debug-контейнер с общим /tmp и PID namespace — kubectl debug), либо dotnet-monitor как сайдкар. Диагностический IPC работает через Unix-сокет в /tmp. Файлы трассировок копировать kubectl cp и анализировать локально.

Что такое Wall Clock vs CPU time?

CPU time — время, когда поток реально исполнял инструкции. Wall clock — полное время, включая ожидание I/O, блокировок, сна. Если запрос долгий, а CPU низкий — ищите ожидание (БД, HTTP, lock contention), а не «медленный код».

Как найти, кто аллоцирует больше всего?

dotnet-trace --profile gc-verbose (события AllocationTick) → PerfView «GC Heap Alloc Stacks» или VS/dotMemory allocation view. В бенчмарках — [MemoryDiagnoser]. В счётчиках — Allocation Rate.

Итог

Профилирование — последовательность: метрики (dotnet-counters, APM) показывают, что не так, трассировки (dotnet-trace + flame graph/PerfView) — где тратится CPU, дампы (dotnet-gcdump/dump) — кто держит память и где висят потоки, BenchmarkDotNet — какой вариант реализации быстрее. Измеряйте до и после каждого изменения.