---
course: ПиТПМ
lecture: 12
title: "Лекция №12. Профилирование программного обеспечения, диагностика утечек памяти и оптимизация выполнения алгоритмов."
---

# Лекция №12. Профилирование программного обеспечения, диагностика утечек памяти и оптимизация выполнения алгоритмов.

## §12.1. Почему тестировщик должен думать о производительности кода

Представьте, что вы написали **программу**. Она отлично справляется со своей задачей: считает налоги, отправляет отчеты, сохраняет данные. Всё работает.

Но работает медленно. Очень медленно. Пользователь нажимает кнопку — и ждет. И ждет. И ждет.

Заказчик говорит: *«Программа работает»*. Но пользователи говорят: *«Программа неудобная»*. Как так вышло? Просто функционально программа работает. А вот пользовательский опыт — страдает.

Обычно все начинают с **функционального тестирования**. Программа открывается, кнопки нажимаются, данные сохраняются. Всё по инструкции.

Пример **функционального теста**:

csharp

```csharp
[Fact]
public void CalculateDiscount_ShouldReturnCorrectValue()
{
    // Проверяем, что метод возвращает правильную скидку
    var result = discountService.CalculateDiscount(100, 10);
    Assert.Equal(90, result); // Тест проходит
}
```

Этот тест проверяет **корректность возвращаемого результата**, однако он никак не оценивает **производительность метода** — выполняется ли он достаточно быстро.

Далее идет **тестирование производительности**. Здесь мы уже замеряем скорость. Например: *«При 100 пользователях заказ сохраняется за 2 секунды»*. Это уже лучше. Мы знаем, что есть проблема. Пример **теста производительности** (*NBomber*):

csharp

```csharp
var scenario = Scenario.Create("test", async context =>
{
    var request = Http.CreateRequest("GET", "/api/products");
    var response = await Http.Send(httpClient, request);
    return response;
})
.WithLoadSimulations(
    Simulation.Inject(100, TimeSpan.FromSeconds(1), 
                      TimeSpan.FromSeconds(30))
);
```

**Тестирование производительности** позволяет ответить на вопрос, *выдерживает ли система нагрузку в 100 RPS*, однако он не детализирует, какой именно компонент или метод является узким местом. Из-за этого остаётся открытым вопрос о том, что именно **вызывает замедление**: база данных, сетевой запрос или неоптимальный алгоритм.

**Тесты производительности** показывают, что плохо, но не показывают почему. Это как услышать стук в двигателе и сказать: *«Да, стучит»*. А что стучит — непонятно.

Чтобы понять, где именно кроется баг или узкое место, разработчики используют **специальный инструмент**, который позволяет заглянуть *«под капот»* работающей программы, который называется **профилирование**.

**Профилирование**

— это как рентген для программы. Оно не просто говорит: *«У вас температура»*. Оно показывает: *«У вас воспаление легких»*. Конкретно — правое легкое, нижняя доля.

Пример отчета **профилировщика**:

wireframe

```
=================================================================

CPU Profiling Report

=================================================================

Top 5 slowest methods:

1. OrderService.SaveOrder()          → 2,345 ms   (45% of total)

2. PaymentService.ProcessPayment()   → 1,234 ms   (23% of total)

3. UserRepository.GetUser()          → 890 ms     (17% of total)

4. EmailService.SendNotification()   → 456 ms     (8% of total)

5. Logger.Log()                      → 234 ms     (4% of total)

Memory Profiling Report

=================================================================

Top 5 memory allocations:

1. OrderService.Orders list          → 256 MB

2. PaymentService.Transactions list  → 128 MB

3. UserRepository.Cache              → 64 MB

4. Logger.Buffer                     → 32 MB

5. EmailService.EmailQueue           → 16 MB
```

Что мы видим:

`1.` **SaveOrder()** — самый медленный метод (2.3 секунды). На него уходит 45% времени.

`2.` **Orders list** — потребляет 256 МБ памяти.

Теперь понятно, что исправлять. Вместо *«что-то тормозит»* мы видим: *«Метод SaveOrder выполняется 2 секунды, потому что он делает 1000 запросов к базе данных в цикле»*.

Сравнительная таблица **функциональных тестов**, **тестов производительности** и **профилирования** представлена ниже:

| Характеристика | Функциональные тесты | Тесты производительности | Профилирование |
| --- | --- | --- | --- |
| **Что проверяет** | «Работает или нет?» | «Как быстро?» | «Почему медленно?» |
| **Когда** | При каждом push | Регулярно (CI/CD) | По требованию (при проблемах) |
| **Инструменты** | xUnit, Jest | NBomber, k6 | Visual Studio Diagnostics, dotMemory, dotTrace |
| **Результат** | ✅ / ❌ | RPS, Latency, Error Rate | Отчет с методами и памятью |
| **Кто использует** | Разработчики, Тестировщики | Тестировщики | Разработчики, Тестировщики |

Давайте приведем простую аналогию в виде врача и пациента.

| Что делает | Аналогия в жизни |
| --- | --- |
| **Функциональное тестирование** | Врач спрашивает: «Что у вас болит?» |
| **Тестирование производительности** | Врач измеряет давление и пульс |
| **Профилирование** | Врач делает УЗИ и видит, что именно внутри |

**УЗИ**

— это, своего рода, профилирование. Только оно показывает реальную причину.

Почему **тестировщику** это важно? **Тестировщик** — это тот, кто смотрит на систему снаружи. Он видит, как пользователь взаимодействует с программой. И если пользователь недоволен скоростью — это **проблема тестировщика**.

Разработчик знает, как работает код. **Тестировщик** знает, что чувствует пользователь. **Профилирование** помогает соединить эти две картины.

## §12.2. Что такое профилирование и зачем оно нужно

**Профилирование**

— это анализ работающей программы, который показывает, что именно происходит внутри неё во время выполнения.

Теперь проблема найдена. Можно исправлять.

Что измеряет **профилировщик**? **Профилировщик** измеряет **три основные вещи**:

**1. Время выполнения методов (CPU Time)**

Показывает, сколько времени тратится на выполнение каждого метода. Отчет **профилировщика** (*CPU*) выглядит примерно следующим образом:

wireframe

```
┌─────────────────────────────────────────────────────────────┐

│ Метод                    │ Время     │ % от общего времени  │

├──────────────────────────┼───────────┼──────────────────────┤

│ OrderService.SaveOrder() │ 2,345 мс  │ 45%                  │

│ PaymentService.Pay()     │ 1,234 мс  │ 23%                  │

│ UserRepository.GetUser() │ 890 мс    │ 17%                  │

│ ... остальные методы     │ 800 мс    │ 15%                  │

└─────────────────────────────────────────────────────────────┘
```

Мы видим, что метод `SaveOrder()` занимает почти половину всего времени. Это и есть **узкое место**.

**2. Использование памяти (Memory Usage)**

Показывает, сколько памяти потребляет программа и какие объекты её занимают. Отчет **профилировщика** (*Memory*) выглядит примерно следующим образом:

wireframe

```
┌─────────────────────────────────────────────────────────────┐

│ Объект / Структура        │ Размер    │ Количество          │

├───────────────────────────┼───────────┼─────────────────────┤

│ List<Order>               │ 256 МБ    │ 1 объект            │

│ List<Transaction>         │ 128 МБ    │ 1 объект            │

│ User[]                    │ 64 МБ     │ 1000 объектов       │

│ ... остальное             │ 32 МБ     │ ...                 │

└─────────────────────────────────────────────────────────────┘
```

Мы видим, что `List<Order>` потребляет **256 МБ**. Это много. Возможно, там хранятся данные, которые уже не нужны.

**3. Количество вызовов методов**

Показывает, сколько раз вызывается каждый метод. Отчет **профилировщика** (*Вызовы*) выглядит примерно следующим образом:

wireframe

```
┌─────────────────────────────────────────────────────────────┐

│ Метод                    │ Количество вызовов               │

├──────────────────────────┼──────────────────────────────────┤

│ UserRepository.GetUser() │ 1,234                            │

│ OrderService.Validate()  │ 987                              │

│ Logger.Log()             │ 5,678                            │

└─────────────────────────────────────────────────────────────┘
```

Мы видим, что метод `GetUser()` вызывается **1234 раза**. Может быть, его можно вызывать реже?

Использование **профилирования тестировщиками** похоже на то, как врач ставит диагноз. Представьте, что вы пришли к врачу.

| Что происходит | Аналогия с программой |
| --- | --- |
| **«У меня болит голова»** | «Программа работает медленно» |
| **Врач измеряет давление** | Тестировщик запускает тест производительности |
| **Врач делает УЗИ** | Тестировщик запускает профилировщик |
| **Врач видит: «У вас воспаление»** | Профилировщик показывает: «Метод X тормозит» |
| **Врач назначает лечение** | Разработчик оптимизирует метод X |
| **Голова перестает болеть** | Программа ускоряется |

Только УЗИ (*профилирование*) показывает, что именно внутри.

Многие путают **профилирование** и **отладку**. Это разные вещи.

| Характеристика | Отладка (Debugging) | Профилирование |
| --- | --- | --- |
| **Что делает** | Пошагово выполняет код | Анализирует работу в целом |
| **Когда используется** | Когда код падает с ошибкой | Когда код работает медленно или потребляет много памяти |
| **Что показывает** | Значения переменных в конкретный момент | Статистику: время, память, вызовы |
| **Результат** | Исправление конкретной ошибки | Понимание, что оптимизировать |

Простая аналогия:

- **Отладка** — это как смотреть на пациента под микроскопом, чтобы найти бактерию.
- **Профилирование** — это как сделать общий анализ крови и увидеть, что воспаление есть, но не знать, где именно.

После **профилирования** тестировщик получает:

`1.` Отчет с цифрами. Не «мне кажется, медленно», а «вот конкретные миллисекунды».

`2.` Доказательства. «Я знаю, какой метод тормозит, вот данные».

`3.` Приоритеты. «Метод А тормозит сильнее всего, его оптимизируем в первую очередь».

`4.` Инструмент для разговора с разработчиками. Не просто «у нас проблемы», а «вот конкретная проблема, вот где она, давайте исправлять».

Запомните

Тестировщик — это диагност. Он не лечит (не пишет код), но он находит проблему и говорит врачу (разработчику), где болит.

## §12.3. Виды профилирования

**Профилирование**

— это не один инструмент, а целый набор подходов. В зависимости от того, что мы ищем, мы используем разные виды профилирования.

Не будем отходить далеко от предыдущих аналогий. Врач использует разные инструменты для диагностики:

- Термометр — для температуры.
- Тонометр — для давления.
- УЗИ — для внутренних органов.
- Анализ крови — для состава крови.

Так и в **профилировании**: каждый вид показывает свою картину.

**Вид №1. CPU-профилирование**

**CPU - профилирование**

- это анализ того, сколько времени процессор тратит на выполнение каждого метода.

Данный метод нам демонстрирует:

- Какие методы выполняются дольше всего.
- Какие методы вызываются чаще всего.
- Где процессор «буксует» (выполняет тяжелые вычисления).

Его нужно использовать тогда, когда программа работает медленно, *«тормозит»*, но не падает. Пример отчета выглядит примерно следующим образом:

wireframe

```
CPU Profiling Report

=================================================================

Top 5 slowest methods:

1. OrderService.SaveOrder()          → 2,345 ms   (45% of total)

2. PaymentService.ProcessPayment()   → 1,234 ms   (23% of total)

3. ReportGenerator.Generate()        → 890 ms     (17% of total)

4. EmailService.SendNotification()   → 456 ms     (8% of total)

5. Logger.Log()                      → 234 ms     (4% of total)
```

В данном случае, мы видим, что `SaveOrder()` занимает почти половину всего времени. Он и есть **узкое место**.

При использовании данного метода **профилирования**, мы ищем **методы**, которые выполняются **дольше всего**. Это кандидаты на оптимизацию, чтобы программа работала быстрее.

**Вид №2. Memory-профилирование**

**Memory-профилирование**

- это анализ того, сколько памяти потребляет программа и какие объекты её занимают.

Данный метод нам демонстрирует:

- Какие объекты занимают больше всего памяти.
- Какие объекты не освобождаются (утечки).
- Как меняется потребление памяти со временем.

Его нужно использовать тогда, когда программа потребляет слишком много памяти или падает с **`OutOfMemoryException`**. Пример отчета выглядит примерно следующим образом:

wireframe

```
Memory Profiling Report

=================================================================

Top 5 memory consumers:

1. List<Order> (OrderService.Orders)      → 256 MB   (40% of total)

2. Dictionary<int, User> (Cache)          → 128 MB   (20% of total)

3. Transaction[] (PaymentService)         → 64 MB    (10% of total)

4. Logger.Buffer                          → 32 MB    (5% of total)

5. EmailQueue (EmailService)              → 16 MB    (2.5% of total)
```

В данном случае, мы видим, что `List<Order>` потребляет **256 МБ**. Это много. Возможно, там хранятся старые заказы, которые уже не нужны.

Что в данном случае мы ищем:

- Объекты, которые занимают слишком много памяти.
- Объекты, которые не освобождаются (утечки).
- Объекты, которые растут бесконтрольно.

**Вид №3. Allocation-профилирование**

**Allocation-профилирование**

- это анализ того, где и когда создаются новые объекты.

Данный метод нам демонстрирует:

- Какие методы создают больше всего объектов.
- Где происходит самая активная аллокация (выделение памяти).
- Какие объекты создаются часто, но живут недолго.

Его нужно использовать тогда, когда программа работает медленно из-за частого создания объектов (нагрузка на *GC — сборщик мусора*). Пример отчета выглядит примерно следующим образом:

wireframe

```
Allocation Profiling Report

=================================================================

Top 5 methods by allocations:

1. OrderService.CreateOrder()        → 12,345 objects   (45% of total)

2. ReportGenerator.BuildReport()     → 8,234 objects    (30% of total)

3. UserService.GetUser()             → 3,456 objects    (12% of total)

4. Logger.Log()                      → 2,134 objects    (8% of total)

5. PaymentService.Pay()              → 1,234 objects    (5% of total)
```

В данном случае, мы видим, что метод `CreateOrder()` создает **12 345 объектов**. Это огромное количество.

Что в данном случае мы ищем:

- Методы, которые создают слишком много объектов.
- Объекты, которые создаются в циклах.
- Объекты, которые можно переиспользовать.

Для наглядности, таблица видов профилирования представлена ниже:

| Вид | Что измеряет | Когда использовать | Что ищем |
| --- | --- | --- | --- |
| **CPU-профилирование** | Время выполнения методов | Программа медленная | Медленные методы |
| **Memory-профилирование** | Потребление памяти | OutOfMemoryException | Утечки памяти |
| **Allocation-профилирование** | Создание объектов | Нагрузка на GC | Лишние аллокации |

При работе с профилированием мы три типа проблем:

Так же при работе с **профилированием** может возникнуть вопрос:

На практике поиск узких мест всегда идет по одному и тому же сценарию — от общей жалобы пользователя до конкретной строчки кода, которую нужно исправить. Давайте разберем этот процесс на простом примере.

**Шаг №1. Слышим жалобу**

Пользователь: *«Программа стала тормозить после последнего обновления!»*.

**Шаг №2. Запускаем CPU-профилирование**

Отчет: `OrderService.SaveOrder()` — 45% времени. Поняли: это метод сохранения заказа.

**Шаг №3. Смотрим внутрь метода**

Внутри `SaveOrder()`:

- Проверка: 1% времени
- Сохранение в БД: 44% времени ← проблема здесь!

**Шаг №4. Проверяем, что происходит в БД**

Оказывается, метод делает **1000 запросов к БД в цикле**. Вместо одного запроса с массовой вставкой.

**Шаг №5. Исправляем**

Оптимизируем: **один запрос вместо 1000**. Время выполнения падает с `2.3 секунд` до `50 мс`.

**Шаг №6. Профилируем снова**

Отчет: `SaveOrder()` теперь — `2% времени`. Программа ускорилась.

## §12.4. Утечки памяти: как их найти и исправить

**Утечка памяти**

— это ситуация, когда программа выделяет память, но не освобождает её, даже когда она больше не нужна.

Представьте, что вы наливаете воду в ведро. Вода поступает (выделяется память). Но на дне ведра есть дыра, через которую вода не уходит (память не освобождается). Ведро переполняется, вода заливает пол (программа падает).

В **C# ситуация сложне**е, чем в **языках без сборщика мусора** (*C, C++*). В C# **сборщик мусора** (*GC*) автоматически **освобождает память**, когда объекты перестают использоваться.

Но **сборщик мусора** может ошибаться: *он не освобождает объекты, если считает, что они всё еще нужны*. Это и есть утечка памяти в управляемой среде.

Утечки памяти опасны, потому что они происходят постепенно. Программа может работать часы или дни, прежде чем память закончится. Схема роста памяти:

![](/images/lectures/pitpm/12/image-01.webp)

Что происходит на схеме:

| Время | Состояние |
| --- | --- |
| **0-1 час** | Память стабильна (50 МБ) |
| **1-2 часа** | Память растет (100 МБ) |
| **2-3 часа** | Память быстро растет (500 МБ) |
| **3-4 часа** | Память заканчивается → OutOfMemoryException |

Пользователь не понимает, почему программа упала. Она работала, работала, а потом вдруг упала. Никаких ошибок не было.

Причин утечек в C# может быть несколько.

**Причина №1. Незакрытые ресурсы (Dispose)**

Когда вы используете **ресурсы** (*файлы, соединения с БД, сетевые потоки*), они требуют **явного освобождения**. Если вы забыли вызвать `Dispose()`, ресурсы остаются в памяти. Пример:

csharp

```csharp
// ❌ УТЕЧКА: файл не закрывается
public void ReadFile(string path)
{
    var stream = new FileStream(path, FileMode.Open);
    // ... работа с файлом ...
    // Нет вызова stream.Dispose() или using!
    // Файл остается открытым, память не освобождается
}

// ✅ ИСПРАВЛЕНО: файл автоматически закрывается
public void ReadFile(string path)
{
    using var stream = new FileStream(path, FileMode.Open);
    // ... работа с файлом ...
    // При выходе из using вызывается Dispose()
}
```

Чтобы найти эту ошибку, взгляните на код: **профилировщик памяти** показывает, что объект **FileStream** не освобождается, хотя должен быть закрыт.

**Причина №2. Подписка на события (Event Handlers)**

Когда вы **подписываетесь на событие**, **объект-подписчик** держит **ссылку** на **источник события**.

Если вы **не отписались**, **объект** не может быть **собран** сборщиком мусора. Пример:

csharp

```csharp
// ❌ УТЕЧКА: не отписались от события
public class Button
{
    public event EventHandler Click;
}

public class Window
{
    private readonly Button _button;

    public Window(Button button)
    {
        _button = button;
        _button.Click += OnButtonClick; // Подписались
    }

    private void OnButtonClick(object sender, EventArgs e)
    {
        // ... обработка ...
    }
    // Нет отписки от события!
    // Window не может быть собран GC, пока жив Button
}

// ✅ ИСПРАВЛЕНО: отписываемся при закрытии окна
public class Window : IDisposable
{
    private readonly Button _button;

    public Window(Button button)
    {
        _button = button;
        _button.Click += OnButtonClick;
    }

    public void Dispose()
    {
        _button.Click -= OnButtonClick; // Отписались!
    }
}
```

Если **профилировщик** показывает, что объект **Window** продолжает **жить** в памяти, хотя должен быть **уничтожен**, причиной чаще всего является неудаленная подписка на событие.

**Причина №3. Статические ссылки (Static References)**

**Статические поля** живут **всё время работы программы**. Если вы положили **объект** в **статическое поле**, он никогда **не будет удален**. Пример:

csharp

```csharp
// ❌ УТЕЧКА: объекты в статическом списке растут бесконечно
public static class Cache
{
    public static List<Order> Orders = new List<Order>();
}

public class OrderService
{
    public void SaveOrder(Order order)
    {
        // Каждый заказ сохраняется в статический список
        Cache.Orders.Add(order);
        // Список растет бесконечно, заказы никогда не удаляются
    }
}

// ✅ ИСПРАВЛЕНО: использовать кеш с ограничением размера
public class Cache
{
    private readonly List<Order> _orders = new List<Order>();

    private readonly int _maxSize = 1000;

    public void Add(Order order)
    {
        _orders.Add(order);

        if (_orders.Count > _maxSize)
        {
            _orders.RemoveAt(0); // Удаляем старые
        }
    }
}
```

Если **профилировщик** показывает, что список `Cache.Orders` постоянно **растет** и занимает слишком много памяти, ищите место, куда данные **бесконечно добавляются** без **очистки** или **ограничения** по размеру.

Так же, как и **причин**, **симптомов утечек** может быть **несколько**.

**Симптом №1. OutOfMemoryException**

wireframe

```
System.OutOfMemoryException: Exception of type 'System.OutOfMemoryException' was thrown.

   at System.Collections.Generic.List`1.Add(...)

   at OrderService.SaveOrder(...)
```

В данном случае, **программа** попыталась **выделить память**, но её **не осталось**. Всё, что можно было выделить — **уже занято**.

**Симптом №2. Замедление работы**

wireframe

```
Программа работала быстро → через час стала медленной → и так далее
```

Когда **памяти мало**, система начинает **использовать диск** (*swap*). Это в сотни раз **медленнее**, чем работа с **оперативной памятью**.

**Симптом №3. Рост потребления памяти**

wireframe

```
Запуск: 50 МБ

Через 1 час: 100 МБ

Через 2 часа: 250 МБ

Через 3 часа: 500 МБ

→ Падение
```

В данном случае, необходимо **следить за графиком потребления памяти**. Если он постоянно **растет** — **есть утечка**.

Для того, чтобы найти **утечку памяти**, необходимо следовать **следующим шагам**.

**Шаг №1. Запустить профилировщик памяти**

Использовать `Visual Studio Diagnostic Tools`, `dotMemory` или `PerfView`.

**Шаг №2. Сделать снимок (Snapshot) памяти**

wireframe

```
Снимок 1: при запуске (50 МБ)

Снимок 2: через 1 час (100 МБ)

Снимок 3: через 2 часа (250 МБ)
```

**Шаг №3. Сравнить снимки**

Профилировщик показывает, какие объекты появились между снимками:

wireframe

```
Diff between Snapshot 1 and Snapshot 3:

┌─────────────────────────────────────────────────────────────┐

│ Тип объекта              │ Новые объекты  │ Занято памяти   │

├──────────────────────────┼────────────────┼─────────────────┤

│ Order                    │ 10 000         │ 50 МБ           │

│ FileStream               │ 500            │ 10 МБ           │

│ EventHandler             │ 100            │ 5 МБ            │

└─────────────────────────────────────────────────────────────┘
```

В данном случае, мы видим, что `Order` создал **10 000 новых объектов**. Это явно утечка.

**Шаг №4. Найти, кто создает эти объекты**

**Профилировщик** показывает, какой метод создал объекты:

wireframe

```
Order objects created by:

┌─────────────────────────────────────────────────────────────┐

│ Метод                       │ Количество созданных объектов │

├─────────────────────────────┼───────────────────────────────┤

│ OrderService.SaveOrder()    │ 10 000                        │

└─────────────────────────────────────────────────────────────┘
```

**Шаг №5. Исправить**

Теперь понятно, что `SaveOrder()` создает объекты `Order`, которые никогда не удаляются. Нужно добавить **механизм очистки**.

Чтобы успешно бороться с **утечками памяти**, важно не только находить их с помощью **профилировщика**, но и знать типовые **способы устранения**. Основные причины проблем и методы их решения собраны в таблице ниже:

| Причина | Как исправить |
| --- | --- |
| **Незакрытые ресурсы** | Использовать using или try-finally с Dispose() |
| **Подписка на события** | Отписываться при уничтожении объекта (-=) |
| **Статические ссылки** | Ограничить размер коллекций, удалять старые объекты |
| **Кеш без ограничений** | Добавить максимальный размер, удалять по времени (TTL) |

Запомните

Если объект создан — он должен быть удален. Если вы создаете объект, вы отвечаете за его уничтожение.

## §12.5. Инструменты профилирования для .NET

В мире `.NET` есть несколько инструментов для **профилирования**. Они делятся на две категории:

| Категория | Что делают | Примеры |
| --- | --- | --- |
| **Профилировщики** | Анализируют работающую программу | `Visual Studio Diagnostics`, `dotMemory`, `dotTrace`, `PerfView` |
| **Бенчмарки** | Измеряют производительность кода в изоляции | BenchmarkDotNet |

Простая аналогия:

- **Профилировщик** — это как врач, который смотрит на пациента целиком и ищет проблему.
- **Бенчмарк** — это как лабораторный анализ, который проверяет конкретный показатель.

**Инструмент №1. Visual Studio Diagnostic Tools**

**Visual Studio Diagnostic Tools**

- это строенный инструмент прямо в `Visual Studio`. Доступен в меню `Debug → Windows → Show Diagnostic Tools`.

Он умеет:

| Возможность | Что показывает |
| --- | --- |
| **CPU Usage** | Какие методы загружают процессор |
| **Memory Usage** | Какие объекты занимают память |
| **Allocations** | Где и когда создаются объекты |

Его нужно использовать тогда, когда нужно **быстро проверить производительность**, *не устанавливая дополнительных инструментов*. Для того, чтобы его запустить, необходимо следовать следующим шагам:

`1.` Открыть проект в `Visual Studio`.

`2.` Запустить отладку (`F5`).

`3.` В окне `Diagnostic Tools` выбрать нужный инструмент.

`4.` Профилировать программу.

`5.` Остановить и посмотреть отчет.

Плюсы:

- Бесплатно (входит в Visual Studio).
- Просто использовать.
- Достаточно для базового анализа.

Минусы:

- Ограниченные возможности.
- Не подходит для сложных случаев.

**Инструмент №2. dotMemory (JetBrains)**

**dotMemory (JetBrains)**

- это инструмент для анализа памяти от компании **JetBrains** (создатели `ReSharper`, `IntelliJ IDEA`).

Он умеет:

| Возможность | Что показывает |
| --- | --- |
| **Снимки памяти (Snapshots)** | Состояние памяти в разные моменты времени |
| **Сравнение снимков** | Какие объекты появились между снимками |
| **Поиск утечек** | Объекты, которые не освобождаются |
| **Анализ ссылок** | Почему объект не удаляется |

Его нужно использовать тогда, когда `Visual Studio Diagnostics` не хватает, и нужно глубокое исследование **утечек памяти**. Его работа выглядит следующим образом (примерные шаги):

`1.` Запустить `dotMemory`.

`2.` Прикрепиться к запущенному приложению.

`3.` Сделать снимок (`Snapshot`).

`4.` Выполнить действия в программе.

`5.` Сделать второй снимок.

`6.` Сравнить снимки.

`7.` Увидеть, какие объекты появились и почему не удалились.

Плюсы:

- Мощный анализ памяти.
- Находит сложные утечки.
- Удобный интерфейс.

Минусы:

- Платный (но есть триал-версия).
- Требует времени на освоение.

**Инструмент №3. dotTrace (JetBrains)**

**dotTrace (JetBrains)**

- это инструмент для анализа **CPU** от той же компании **JetBrains**. Он умеет:

| Возможность | Что показывает |
| --- | --- |
| **Время выполнения методов** | Какой метод выполняется дольше всего |
| **Количество вызовов** | Какой метод вызывается чаще всего |
| **Линейный профиль** | Где время тратится в каждом методе |
| **Горячие пути** | Самые медленные участки кода |

Его необходимо использовать тогда, когда программа работает **медленно**, и нужно найти, какой **метод тормозит**. Его работа выглядит следующим образом (примерные шаги):

`1.` Запустить `dotTrace`.

`2.` Прикрепиться к запущенному приложению.

`3.` Начать профилирование.

`4.` Выполнить действия в программе.

`5.` Остановить профилирование.

`6.` Увидеть, какой метод занял больше всего времени.

Плюсы:

- Мощный анализ `CPU`.
- Находит узкие места в производительности.
- Интегрируется с `Visual Studio`.

Минусы:

- Платный (но есть триал-версия).
- Требует времени на освоение.

**Инструмент №4. PerfView (Microsoft)**

**PerfView (Microsoft)**

- это бесплатный инструмент от **Microsoft** для глубокого анализа производительности.

Он умеет:

| Возможность | Что показывает |
| --- | --- |
| **CPU анализ** | Какие методы загружают процессор |
| **Memory анализ** | Утечки памяти, GC, аллокации |
| **Событийный анализ** | Что происходит в системе в моменты времени |
| **Профилирование продакшена** | Можно запускать на работающем сервере |

Его необходимо использовать тогда, когда **нужно профилировать продакшен-окружение** или **глубоко копать в сложных случаях**. Его работа выглядит следующим образом (примерные шаги):

`1.` Скачать `PerfView` с сайта `Microsoft`.

`2.` Запустить от имени администратора.

`3.` Выбрать `Collect → Start Collection`.

`4.` Выполнить действия в программе.

`5.` Остановить сбор.

`6.` Открыть отчет.

Плюсы:

- Бесплатный.
- Мощный (может профилировать продакшен).
- От `Microsoft`.

Минусы:

- Сложный интерфейс.
- Сложно освоить новичкам.

**Инструмент №5. BenchmarkDotNet**

**BenchmarkDotNet**

- это библиотека для написания бенчмарков (измерения производительности кода в изоляции).

Отличие от **профилировщиков** заключается в следующем:

| Характеристика | Профилировщик | BenchmarkDotNet |
| --- | --- | --- |
| **Что делает** | Анализирует работающую программу | Измеряет конкретный метод |
| **Когда использовать** | Когда программа медленная | Когда нужно сравнить варианты кода |
| **Результат** | Отчет о всех методах | Время выполнения конкретного метода |
| **Точность** | Приблизительная | Высокая (среднее по 1000 запусков) |

Простая аналогия:

- **Профилировщик** — это как посмотреть на трассу и увидеть, где пробка.
- **BenchmarkDotNet** — это как замерить скорость конкретной машины на гоночном треке.

Пример:

csharp

```csharp
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser] // Покажет аллокации
public class MyBenchmarks
{
    private readonly Calculator _calculator = new Calculator();

    [Benchmark]
    public int SumByLoop()
    {
        var result = 0;
        for (int i = 0; i < 1000; i++)
            result += i;
        return result;
    }

    [Benchmark]
    public int SumByLinq()
    {
        return Enumerable.Range(0, 1000).Sum();
    }
}

// Запуск
class Program
{
    static void Main() => BenchmarkRunner.Run<MyBenchmarks>();
}
```

Результат:

wireframe

```
┌─────────────────┬───────────┬──────────────┬─────────────────┐

│ Method          │ Mean      │ Error        │ Allocated       │

├─────────────────┼───────────┼──────────────┼─────────────────┤

│ SumByLoop       │ 1.50 µs   │ 0.03 µs      │ 0 B             │

│ SumByLinq       │ 8.20 µs   │ 0.15 µs      │ 152 B           │

└─────────────────┴───────────┴──────────────┴─────────────────┘
```

Мы видим, что **SumByLoop** быстрее и не создает объектов. **SumByLinq** медленнее и создает объекты.

Данный метод необходимо использовать тогда, когда нужно сравнить **два варианта кода** и **выбрать быстрый**.

**Арсенал тестировщика** полон **специализированных утилит**, но каждая из них лучше всего решает свою задачу. Чтобы не запутаться в выборе, воспользуйтесь шпаргалкой по подбору инструментов под конкретную проблему:

| Ситуация | Инструмент |
| --- | --- |
| **Быстро проверить, что медленно** | Visual Studio Diagnostic Tools |
| **Найти утечку памяти** | dotMemory |
| **Найти медленный метод** | dotTrace |
| **Глубокий анализ продакшена** | PerfView |
| **Сравнить два варианта кода** | BenchmarkDotNet |

## §12.6. Профилирование в CI/CD: как не допустить деградацию

В предыдущих параграфах мы говорили о профилировании как о разовом действии: *«Программа стала медленной — запустили профилировщик — нашли причину — исправили»*.

Но есть одна проблема: **завтра кто-то снова сделает код медленным. И вы снова будете искать. И снова. И снова**.

Как это выглядит в разработке:

wireframe

```
Месяц 1: Мы оптимизировали код. Всё быстро.

Месяц 2: Кто-то добавил новую фичу. Код стал чуть медленнее.

Месяц 3: Кто-то еще добавил фичу. Код стал еще медленнее.

Месяц 6: Программа снова тормозит. Мы не знаем, когда это произошло.
```

Проблема заключается в том, что **деградация производительности** происходит постепенно. Никто не замечает, что стало чуть хуже. А через полгода становится очевидно, что всё плохо.

Но существует решение! Необходимо **автоматизировать профилирование в CI/CD**. Каждый раз, когда разработчик отправляет код, **конвейер** не только запускает тесты, но и **проверяет производительность**. Схема **конвейера с профилированием** выглядит примерно следующим образом:

![](/images/lectures/pitpm/12/image-02.webp)

Что происходит при автоматизации профилирования в CI / CD:

`1.` Разработчик отправляет код (`git push`).

`2.` Конвейер собирает проект.

`3.` Конвейер запускает функциональные тесты (проверяет, что ничего не сломалось).

`4.` Конвейер запускает профилирование (замеряет время выполнения ключевых методов).

`5.` Конвейер сравнивает результаты с эталонными значениями.

`6.` Если время выполнения выросло — конвейер падает.

Чтобы **автоматические проверки** приносили **пользу**, а не просто создавали **видимость работы**, важно четко понимать **метрики** и **цели контроля**.

Что именно попадает под прицел автоматики и зачем это нужно проверять, показано в таблице ниже:

| Что замеряем | Как замеряем | Почему это важно |
| --- | --- | --- |
| **Время выполнения API** | Запускаем ключевые эндпоинты | Проверяем, не стали ли они медленнее |
| **Время выполнения метода** | Запускаем бенчмарки (BenchmarkDotNet) | Проверяем, не стал ли метод медленнее |
| **Потребление памяти** | Замеряем память после выполнения | Проверяем, нет ли утечек |
| **Время запуска приложения** | Замеряем время старта | Проверяем, что приложение не запускается дольше |

Пример:

wireframe

```
Эталонное значение (из прошлого релиза):

  - GET /api/products: 50 мс

  - POST /api/orders: 200 мс

  - Запуск приложения: 3 секунды

Текущее значение (после изменений):

  - GET /api/products: 52 мс  (+4%)   ✅ OK

  - POST /api/orders: 280 мс (+40%)   ❌ ПАДЕНИЕ!

  - Запуск приложения: 3.5 сек (+17%) ⚠️ Внимание!
```

**Конвейер** падает на `POST /api/orders` — время выросло на `40%`, что больше порога в `20%`.

Главный вопрос:

| Уровень | Что значит | Действие |
| --- | --- | --- |
| **< 10%** | Изменение незначительное | Предупреждение, релиз разрешен |
| **10–20%** | Изменение заметное | Предупреждение, релиз разрешен, но нужно разобраться |
| **> 20%** | Производительность сильно упала | Конвейер падает, релиз заблокирован |

Пример:

wireframe

```
Оригинальное время: 100 мс

- 20% = 120 мс → пользователь не заметит

- 50% = 150 мс → пользователь заметит

- 100% = 200 мс → пользователь раздражается
```

А разве у нас уже нет **тестов производительности**? Есть. Но они решают другую задачу.

| Что делают тесты производительности | Что делают профилирование и бенчмарки в CI/CD |
| --- | --- |
| **Проверяют систему под большой нагрузкой** | Проверяют отдельные методы и эндпоинты |
| **Запускаются редко (в main)** | Запускаются при каждом push |
| **Находят проблемы на уровне всей системы** | Находят проблемы в конкретном коде |

Они дополняют друг друга:

wireframe

```
Тесты производительности (NBomber, k6)

  → "Система стала медленнее"

  → Но не знаем, где именно

Профилирование / Бенчмарки в CI/CD

  → "Метод X стал медленнее на 30%"

  → Знаем конкретное место
```

Для разработчика это выглядит в виде нескольких сценариев.

Разработчик:

`1.` Видит красный конвейер.

`2.` Читает сообщение: «`POST /api/orders` выросло на `40%`».

`3.` Проверяет изменения в `OrderController`.

`4.` Находит, что добавил новый запрос к БД в цикле.

`5.` Исправляет.

`6.` Отправляет снова.

`7.` Конвейер зеленый.

А что делать, если нет эталонных значений. В самом начале проекта эталонных значений нет. Их нужно создать. Первый шаг – **запустить конвейер** и **сохранить текущие значения** как эталон.

wireframe

```
Первый запуск: сохраняем значения

  → GET /api/products: 52ms

  → POST /api/orders: 205ms

  → Startup: 3.2s

Последующие запуски: сравниваем с эталоном

  → Если выросло более чем на 20% — конвейер падает
```

Если **изменения** в **производительности неизбежны** (например, *добавили новую тяжелую фичу*), обновляем **эталон вручную**.

## §12.7. Антипаттерны в профилировании

**Профилирование**

— это мощный инструмент. Но только если вы используете его правильно. Есть несколько типичных ошибок, которые сводят на нет все усилия по оптимизации.

Главная мысль этого параграфа заключается в том, что **профилирование – это про регулярный анализ и исправление того, что замедляет систему**.

**Антипаттерн №1. «Профилируем в продакшене»**

Почему это опасно:

| Что может произойти | Последствия |
| --- | --- |
| **Профилировщик замедляет работу сервера** | Пользователи жалуются на тормоза |
| **Профилировщик потребляет память** | Сервер падает с OutOfMemoryException |
| **Профилировщик записывает чувствительные данные** | Утечка данных пользователей |
| **Профилировщик меняет поведение программы** | Пользователи видят странное поведение |

Почему **профилировщик замедляет работу**? **Профилировщик** не просто *«смотрит» на программу*. Он встраивается в неё:

- Записывает каждый вызов метода.
- Замеряет время выполнения каждой операции.
- Отслеживает каждое выделение памяти.

Это создает **дополнительную нагрузку**. На **тестовой среде** это **нормально**. На **продакшене** — **катастрофа**. Как должно быть правильно:

| Где профилировать | Почему |
| --- | --- |
| **Staging (тестовая среда)** | Копия продакшена, но без пользователей |
| **Локально (dev)** | Для быстрой проверки |
| **Продакшен** | Только в крайнем случае, с осторожностью, в нерабочее время |

**Антипаттерн №2. «Запустили один раз и хватит»**

Почему это опасно:

| Что происходит со временем | Последствия |
| --- | --- |
| **Появляются новые фичи** | Новый код может быть медленным |
| **Меняются библиотеки** | Обновления могут замедлить работу |
| **Растет количество данных** | Запросы становятся медленнее |
| **Меняется бизнес-логика** | Медленные методы перестают быть медленными, медленными становятся другие |

Пример:

wireframe

```
Месяц 1: Оптимизировали. Всё быстро.

Месяц 3: Добавили новую фичу. Код стал чуть медленнее.

Месяц 6: Добавили еще фичу. Код стал еще медленнее.

Месяц 9: Снова тормозит. Профилировщик не запускали полгода.
```

Как должно быть правильно:

| Что делать | Как часто |
| --- | --- |
| **Запускать профилировщик** | При каждом релизе |
| **Проверять производительность ключевых методов** | Еженедельно |
| **Анализировать утечки памяти** | Ежемесячно |

**Антипаттерн №3. «Чиним код без профилировщика»**

Почему это опасно:

| Что происходит | Последствия |
| --- | --- |
| **Оптимизируют не тот метод** | Тратят время на бесполезную работу |
| **Оптимизируют не ту часть** | Тратят время на бесполезную работу |
| **Оптимизация ломает код** | Вводят новые баги |

Разработчик потратил день на оптимизацию `GetUser()` и ничего не добился. Потому что проблема была в `SaveOrder()`.

Как должно быть правильно:

| Что делать | Почему |
| --- | --- |
| **Запустить профилировщик** | Знать точно, что тормозит |
| **Посмотреть отчет** | Увидеть, какой метод занимает больше всего времени |
| **Оптимизировать самый медленный метод** | Получить максимальный эффект |

**Антипаттерн №4. «Игнорируем предупреждения»**

Почему это опасно:

| Что происходит со временем | Последствия |
| --- | --- |
| **Метод был медленным** | Стал еще медленнее |
| **Его вызывают чаще** | Проблема становится больше |
| **Данных стало больше** | Проблема становится больше |
| **Пользователи жалуются** | Но разработчик не знает, почему |

Пример:

wireframe

```
Профилировщик: ⚠️ SaveOrder() — 2 секунды.

Разработчик: "Ну, 2 секунды — не критично."

Через месяц:

Профилировщик: ⚠️ SaveOrder() — 4 секунды.

Разработчик: "Ну, пока работает."

Через полгода:

Профилировщик: ❌ SaveOrder() — 10 секунд. Конвейер упал.

Медленный код становится падающим кодом.
```

Как должно быть правильно:

| Что делать | Почему |
| --- | --- |
| **Исправлять медленные методы** | Пока они не стали падающими |
| **Устанавливать пороговые значения** | Если метод > 1 секунды — исправлять |
| **Следить за тенденцией** | Если время растет — разбираться сразу |

Избежать **типичных ошибок** при работе с **производительностью** помогут правила **хорошего тона**. Вот чего стоит **придерживаться на практике**, чтобы оптимизация была **эффективной и безопасной**:

| Что нужно делать | Как |
| --- | --- |
| **Профилировать в staging** | Не в продакшене |
| **Профилировать регулярно** | При каждом релизе, еженедельно |
| **Сначала профилировать, потом оптимизировать** | Не чинить вслепую |
| **Исправлять медленные методы** | Не игнорировать предупреждения |
