Перейти к основному содержимому

Сборка и культура производительности

Разработчику Архитектору Инженеру

Сборка и культура производительности

Ошибки компиляции и предупреждения

Play ITЗагрузка интерактивного демо…

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

Упрощённый конвейер сборки:

АЛГОРИТМ СобратьПроект(исходники)
для каждого файл в исходники
дерево := разобрать_синтаксис(файл)
если дерево содержит ошибки то
вернуть ошибка_компиляции(список_ошибок)
конец
проверить_типы(дерево)
если типы не сходятся то
вернуть ошибка_компиляции(список_ошибок)
конец
конец
объектные_файлы := скомпилировать_в_машинный_код(исходники)
исполняемый := связать(объектные_файлы, библиотеки)
вернуть успех(исполняемый)
КОНЕЦ
ЭтапЧто ломается чаще всего
разобрать_синтаксисПропущена скобка, опечатка в ключевом слове
проверить_типыПрисвоение строки числу, несуществующее имя
связатьНет реализации метода, не подключена библиотека

Типы ошибок компиляции:

КатегорияПримерРешение
СинтаксическиеОтсутствующая точка с запятой, непарные скобкиИсправление синтаксиса
СемантическиеИспользование необъявленной переменнойОбъявление переменной
ТиповыеНесоответствие типов при присваиванииПриведение типов или изменение типа
СвязыванияОтсутствие реализации методаРеализация метода или подключение библиотеки

Пример ошибок компиляции в C#:

Код ITЗагрузка примера кода…

Предупреждение компилятора — сообщение о потенциальной проблеме, не препятствующее сборке.

Примеры предупреждений:

Код ITЗагрузка примера кода…


Предупреждения можно игнорировать

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

Причины игнорирования предупреждений:

  1. Ложные срабатывания Некоторые предупреждения могут быть некорректными в контексте конкретной бизнес-логики.

  2. Временные решения В процессе разработки допустимо временное игнорирование предупреждений с обязательным планом их устранения.

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

Однако систематическое игнорирование предупреждений приводит к:

  • накоплению технического долга
  • маскировке реальных проблем под "фоном" предупреждений
  • снижению доверия команды к системе сборки

Рекомендуемая практика — обработка всех предупреждений:

  • исправление кода
  • явное подавление предупреждения с комментарием причины
  • настройка правил анализа под проект

Пример явного подавления предупреждения в C#:

#pragma warning disable CS0168 // Переменная объявлена, но не используется
int unused = CalculateValue();
#pragma warning restore CS0168

Что делать если сборка поломалась

Поломка сборки — состояние, при котором проект не может быть скомпилирован или протестирован.

План действий при поломке сборки:

  1. Идентификация проблемы

    • просмотр логов сборки
    • определение первого сломанного коммита (бисекция)
    • воспроизведение проблемы локально
  2. Локализация причины

    • анализ изменений в сломанном коммите
    • проверка зависимостей и версий
    • проверка конфигурации окружения
  3. Восстановление работоспособности

    • откат проблемного коммита (если критично)
    • исправление ошибок в коде
    • обновление зависимостей
  4. Предотвращение повторения

    • добавление тестов для выявления подобных проблем
    • улучшение процесса код-ревью
    • внедрение предварительных проверок перед мержем

Пример диагностики поломки сборки через бисекцию в Git:

Код ITЗагрузка примера кода…


Как читать предупреждения и ошибки в консоли IDE

Структура сообщения об ошибке компилятора:

[Путь к файлу]([Строка],[Колонка]): [Код ошибки] [Тип]: [Описание]

Пример сообщения в C#:

C:\Project\OrderService.cs(42,15): error CS0103: The name 'custmer' does not exist in the current context

Разбор сообщения:

  • C:\Project\OrderService.cs — путь к файлу
  • (42,15) — строка 42, колонка 15
  • error — тип сообщения (ошибка, предупреждение, информация)
  • CS0103 — код ошибки для поиска документации
  • The name 'custmer' does not exist... — описание проблемы (опечатка в custmer вместо customer)

Пример сообщения в Java:

OrderService.java:42: error: cannot find symbol
custmer.process();
^
symbol: variable custmer
location: class OrderService

Пример сообщения в Python:

File "order_service.py", line 42, in process_order
custmer.process()
^
NameError: name 'custmer' is not defined
Поиск по коду ошибки

Коды ошибок (например, CS0103, CS0246) являются ключами для поиска официальной документации. Поиск "C# CS0103" ведёт к странице с описанием ошибки и способами её устранения.


Play ITЗагрузка интерактивного демо…


Почему важно сокращать время сборки

Время сборки — период от запуска процесса сборки до получения готового артефакта.

Влияние времени сборки на разработку:

  1. Цикл обратной связи Короткий цикл (сборка + тесты < 10 секунд) позволяет быстро проверять изменения и поддерживать концентрацию.

  2. Производительность команды При времени сборки 5 минут разработчик может выполнить 96 сборок в день. При времени сборки 30 секунд — 960 сборок в день. Разница в 10 раз.

  3. Качество кода Длинные сборки снижают мотивацию к запуску тестов и проверке изменений перед коммитом.

  4. Интеграция изменений Быстрые сборки в CI/CD позволяют быстрее обнаруживать конфликты и проблемы интеграции.

Пример влияния на разработчика:

Сценарий: внесение небольшого исправления

Время сборки 2 минуты:
- внесение изменений: 2 минуты
- ожидание сборки: 2 минуты
- проверка результата: 1 минута
Итого: 5 минут на итерацию

Время сборки 10 секунд:
- внесение изменений: 2 минуты
- ожидание сборки: 10 секунд
- проверка результата: 1 минута
Итого: 3 минуты 10 секунд на итерацию

Экономия: 1 минута 50 секунд на итерацию
При 20 итерациях в день: экономия 36 минут

Факторы — зависимость, кэширование, параллелизация

Факторы, влияющие на время сборки:

  1. Зависимости между модулями Циклические зависимости и тесная связность вынуждают пересобирать большие части системы при малейших изменениях.

  2. Кэширование Повторное использование результатов предыдущих сборок для неизменённых модулей.

  3. Параллелизация Выполнение независимых задач сборки одновременно на нескольких ядрах процессора.

  4. Инкрементальность Пересборка только изменённых файлов и их зависимостей.

  5. Размер кодовой базы Количество исходных файлов и объём кода напрямую влияют на время анализа и компиляции.

Пример оптимизации через управление зависимостями:

Код ITЗагрузка примера кода…


Инкрементальная сборка, hot reload

Инкрементальная сборка — процесс пересборки только изменённых файлов и их зависимостей.

Принцип работы:

  1. Система сборки отслеживает временные метки файлов
  2. При изменении файла помечаются как "грязные" все зависящие от него модули
  3. Пересобираются только грязные модули
  4. Чистые модули используются из предыдущей сборки

Пример в .NET:

Код ITЗагрузка примера кода…

Hot reload — технология применения изменений в работающем приложении без полной перезагрузки.

Сценарии применения:

  • веб-разработка: обновление CSS/JS без перезагрузки страницы
  • мобильная разработка: применение изменений интерфейса без перезапуска приложения
  • десктопные приложения: обновление логики без перезапуска процесса

Пример веб-разработки с hot reload:

Код ITЗагрузка примера кода…

При изменении CSS файл перезагружается мгновенно без потери состояния приложения.


Влияние на разработчика — feedback loop

Цикл обратной связи — время от внесения изменения в код до получения результата (успех/ошибка).

Этапы цикла обратной связи:

1. Внесение изменения в код → 30 секунд
2. Запуск сборки → 5 секунд ожидания
3. Выполнение тестов → 45 секунд
4. Запуск приложения → 10 секунд
5. Проверка результата → 20 секунд
-----------------------------------------------
Итого: 2 минуты на итерацию

Оптимизированный цикл:

1. Внесение изменения в код → 30 секунд
2. Инкрементальная сборка → 2 секунды
3. Юнит-тесты для изменённого модуля → 5 секунд
4. Hot reload в работающем приложении → 1 секунда
5. Проверка результата → 20 секунд
-----------------------------------------------
Итого: 58 секунд на итерацию

Психологические эффекты короткого цикла обратной связи:

  • поддержание состояния потока (flow)
  • снижение когнитивной нагрузки
  • повышение мотивации и удовлетворённости работой
  • уменьшение количества ошибок из-за потери контекста

Культура производительности

Культура производительности — совокупность ценностей, практик и инструментов, направленных на обеспечение высокой производительности системы и процессов разработки.

Элементы культуры производительности:

  1. Производительность как нефункциональное требование Чёткие метрики и бюджеты производительности в технических заданиях.

  2. Раннее выявление проблем Профилирование и нагрузочное тестирование на ранних этапах разработки.

  3. Ответственность всей команды Не только бэкенд-разработчики, но и фронтенд, тестировщики, аналитики учитывают влияние своих решений на производительность.

  4. Инструменты и автоматизация Встроенные в процесс разработки инструменты мониторинга и анализа производительности.

  5. Обучение и обмен опытом Регулярный разбор инцидентов, связанных с производительностью, и распространение лучших практик.


Производительность как часть качества кода

Производительность — неотъемлемый аспект качества программного обеспечения наряду с читаемостью, надёжностью и поддерживаемостью.

Интеграция производительности в процесс разработки:

  1. Определение требований Установка количественных целей производительности на этапе проектирования.

  2. Архитектурные решения Выбор паттернов и технологий с учётом требований к производительности.

  3. Код-ревью с фокусом на производительность Проверка алгоритмической сложности, избыточных операций, утечек ресурсов.

  4. Тестирование производительности Включение нагрузочных и стресс-тестов в регулярный цикл тестирования.

  5. Мониторинг в продакшене Сбор метрик производительности в рабочей среде для выявления регрессий.

Пример чек-листа для код-ревью:

☐ Алгоритмическая сложность операций соответствует требованиям
☐ Отсутствуют избыточные выделения памяти в горячем пути
☐ Нет блокирующих операций в асинхронном коде
☐ Кэширование реализовано корректно (срок жизни, инвалидация)
☐ Пакетная обработка используется вместо множества мелких операций
☐ Индексы базы данных оптимизированы для запросов
☐ Размер сетевых пакетов минимизирован

Code review — как замечать узкие места

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

Признаки узких мест в коде:

  1. Вложенные циклы с большим количеством итераций

Код ITЗагрузка примера кода…

  1. Частые выделения памяти в горячем пути
// Проблема: создание объектов в цикле
for (int i = 0; i < 1000000; i++)
{
var temp = new StringBuilder(); // Выделение на каждой итерации
// ...
}
  1. Синхронные операции в асинхронном контексте
public async Task ProcessAsync()
{
var data = GetData().Result; // Блокирующий вызов в асинхронном методе
// ...
}
  1. Избыточные запросы к внешним системам
// Проблема: N+1 запросов к базе данных
foreach (var order in orders)
{
var customer = await _db.Customers.FindAsync(order.CustomerId);
// ...
}
  1. Отсутствие пагинации при работе с большими наборами данных
// Проблема: загрузка всех записей в память
var allOrders = await _db.Orders.ToListAsync();

Стратегии выявления узких мест при ревью:

  • анализ алгоритмической сложности
  • поиск операций с внешними системами внутри циклов
  • проверка использования примитивов синхронизации
  • оценка объёма данных, передаваемых между компонентами

Профилирование в CI/CD

Интеграция профилирования в конвейер непрерывной интеграции — автоматизированное измерение производительности при каждом изменении кода.

Подходы к интеграции:

  1. Бенчмаркинг критических путей Запуск микро-бенчмарков для ключевых алгоритмов и операций.

  2. Сравнение с базовой веткой Измерение производительности изменений относительно основной ветки.

  3. Обнаружение регрессий Автоматическое выявление ухудшения производительности выше заданного порога.

Пример конфигурации GitHub Actions для бенчмаркинга:

Код ITЗагрузка примера кода…

Пример отчёта о регрессии производительности:

Производительность ухудшилась в 3 тестах:

1. OrderProcessingBenchmark.ProcessLargeOrder
Базовая ветка: 125.3 мс
Текущая ветка: 187.6 мс
Изменение: +49.7% ⚠️

2. DatabaseQueryBenchmark.GetCustomerOrders
Базовая ветка: 42.1 мс
Текущая ветка: 68.9 мс
Изменение: +63.7% ⚠️

3. SerializationBenchmark.SerializeOrder
Базовая ветка: 8.7 мс
Текущая ветка: 9.2 мс
Изменение: +5.7% ✓

Рекомендация: проверить изменения в логике обработки заказов и запросах к БД.

Обучение команды — разбор утечек, анализ дампов

Обучение работе с производительностью — систематический процесс повышения компетенций команды в диагностике и устранении проблем производительности.

Форматы обучения:

  1. Разбор реальных инцидентов Коллективный анализ дампов памяти, трассировок и логов после инцидентов производительности.

  2. Практические воркшопы Работа с профилировщиками на специально подготовленных примерах с проблемами.

  3. Парное профилирование Опытный разработчик работает вместе с новичком над реальной задачей оптимизации.

  4. Библиотека кейсов Документирование типовых проблем производительности и способов их решения.

Пример структуры разбора утечки памяти:

1. Симптомы
- Рост потребления памяти со временем
- Замедление работы приложения после длительной работы
- Ошибки OutOfMemoryException

2. Сбор данных
- Дамп памяти в момент высокого потребления
- Сравнительный дамп после короткого периода работы
- Логи сборки мусора

3. Анализ
- Поиск объектов с наибольшим объёмом памяти
- Определение корневых ссылок (GC roots)
- Выявление паттернов утечки (статические коллекции, события без отписки)

4. Исправление
- Устранение причины утечки
- Добавление тестов для предотвращения регрессии

5. Документирование
- Описание проблемы и решения в базе знаний команды

Инструменты для анализа дампов памяти:

ПлатформаИнструментОсобенности
.NETdotMemory, WinDbg с SOSАнализ управляемой кучи
JavaEclipse MAT, VisualVMАнализ кучи, поиск утечек
Node.jsChrome DevTools, clinic.jsАнализ кучи V8
ОбщиеValgrind (Linux)Обнаружение утечек в нативном коде

Пример анализа дампа в .NET с помощью WinDbg:

0:000> !dumpheap -stat
Statistics:
MT Count TotalSize Class Name
...
00007ff8a8b3c8f8 10000 2400000 System.String
00007ff8a8b4d120 5000 4000000 Order
00007ff8a8b5e340 5000 8000000 OrderItem[]

0:000> !gcroot 0000023a12345678
HandleTable:
0000023a87654321 (strong handle)
-> 0000023a12345678 Order

Found 1 unique roots (and 1 objects).

Анализ показывает, что объекты Order удерживаются через сильную ссылку в таблице хэндлов — потенциальная утечка из-за неправильного управления жизненным циклом.


Обучение команды — разбор утечек, анализ дампов

Систематическое обучение диагностике производительности формирует у команды навыки выявления и устранения проблем до их проявления в продакшене.

Эффективные форматы обучения:


1. Разбор реальных утечек памяти

Анализ дампов памяти развивает понимание жизненного цикла объектов и механизмов сборки мусора.

Пример типовой утечки в .NET — события без отписки:

Код ITЗагрузка примера кода…

Анализ дампа с помощью dotMemory:

1. Открыть дамп в dotMemory
2. Перейти в "Dominators" view
3. Найти объекты с наибольшим "Retained Size"
4. Проследить цепочку ссылок до GC root
5. Выявить причину удержания (статическая коллекция + событие)

Пример утечки в Java — кэш без ограничения размера:

Код ITЗагрузка примера кода…


2. Анализ дампов процессора и блокировок

Дампы потоков (thread dumps) помогают диагностировать зависания и взаимоблокировки.

Пример thread dump в Java при взаимоблокировке:

"Thread-1" #12 prio=5 os_prio=0 tid=0x00007f8b4c00a000 nid=0x1a34 waiting for monitor entry
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.Service.methodA(Service.java:25)
- waiting to lock <0x000000076b8a4e20> (a java.lang.Object)
at com.example.Controller.handleRequest(Controller.java:42)

"Thread-2" #13 prio=5 os_prio=0 tid=0x00007f8b4c00b800 nid=0x1a35 waiting for monitor entry
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.Service.methodB(Service.java:38)
- waiting to lock <0x000000076b8a4e10> (a java.lang.Object)
at com.example.Controller.handleRequest(Controller.java:45)

Анализ показывает:

  • Thread-1 удерживает lock A и ожидает lock B
  • Thread-2 удерживает lock B и ожидает lock A
  • Классическая взаимоблокировка

Решение — унификация порядка захвата блокировок:

Код ITЗагрузка примера кода…


3. Практические лабораторные работы

Структура лабораторной работы по диагностике производительности:

Этап 1: Введение в проблему
- Описание симптомов (рост памяти, высокая загрузка CPU)
- Предоставление "сломанного" приложения

Этап 2: Сбор диагностических данных
- Создание дампа памяти/потоков
- Запуск профилировщика
- Сбор метрик системы

Этап 3: Анализ данных
- Поиск доминирующих объектов
- Определение горячих путей выполнения
- Выявление блокирующих операций

Этап 4: Разработка решения
- Рефакторинг проблемного кода
- Добавление ограничений ресурсов
- Внедрение мониторинга

Этап 5: Верификация
- Повторное профилирование
- Сравнение метрик до/после
- Документирование решения

Инструменты для лабораторных работ:

ПлатформаИнструменты анализаТиповые сценарии
.NETdotMemory, dotTrace, PerfViewУтечки событий, фрагментация кучи, блокировки
JavaVisualVM, Eclipse MAT, async-profilerУтечки кэшей, взаимоблокировки, оверхед сборки мусора
Pythonmemory_profiler, py-spy, objgraphУтечки замыканий, избыточные аллокации
Node.jsChrome DevTools, clinic.jsУтечки замыканий, блокирующий синхронный код

4. Создание библиотеки паттернов проблем

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

Структура записи в библиотеке:

Проблема: Накопление объектов в статической коллекции
Симптомы:
- Линейный рост потребления памяти со временем
- Большое количество объектов одного типа в дампе
- Ссылки от статических полей к объектам

Диагностика:
1. Сравнить два дампа с интервалом в 1 час
2. Найти типы с наибольшим приростом количества экземпляров
3. Проследить GC roots до статического поля

Решение:
- Заменить статическую коллекцию на ограниченную по размеру
- Внедрить стратегию инвалидации старых записей
- Добавить метрику размера коллекции для мониторинга

Пример кода:
// Было
private static readonly List<Session> _sessions = new();

// Стало
private static readonly LimitedCollection<Session> _sessions =
new LimitedCollection<Session>(maxSize: 10000);