diff --git a/openspec/changes/perf-profiling/.openspec.yaml b/openspec/changes/perf-profiling/.openspec.yaml new file mode 100644 index 0000000..0a82258 --- /dev/null +++ b/openspec/changes/perf-profiling/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +created: 2026-10-25 +skip_specs: true diff --git a/openspec/changes/perf-profiling/design.md b/openspec/changes/perf-profiling/design.md new file mode 100644 index 0000000..f577fd6 --- /dev/null +++ b/openspec/changes/perf-profiling/design.md @@ -0,0 +1,105 @@ +# Design: профилирование фронта и бэка (#202) + +## Зачем + +Заказчик сообщил (пересказом, без деталей), что: +1. тормозит скролл — предположительно любой большой формы, там много скроллить; +2. модальное окно по нажатию кнопки открывается не сразу. + +Бэк ранее профилировался базово, поэтому основное подозрение — фронт, включая утечки памяти. Задача — подсветить проблемные места с доказательствами. Исправлять ничего не нужно: исправления пойдут отдельными задачами по результатам отчёта. + +## Цели + +- Воспроизводимый локальный стенд, поднимаемый одной-двумя командами, чтобы позже снять замеры «после» на тех же данных и сценариях. +- Замеры по двум симптомам заказчика и по памяти, для фронта и для бэка, которые обслуживают эти сценарии. +- Отчёт: список проблемных мест, у каждого — симптом, доказательство (цифры и ссылка на артефакт: trace, профиль, heap snapshot, EXPLAIN), место в коде (`file:line`), оценка влияния, приоритет. + +## Не цели + +- Исправления кода приложения, даже очевидные. +- Нагрузочное тестирование многими пользователями. +- Интеграция замеров в CI. +- Воспроизведение прод-ресурсов на стенде (см. «Ограничения»). + +## Анти-цели (провал, даже если формально сделано) + +- Выводы без замера. Находки статического анализа допустимы только с явной пометкой «не подтверждено замером» и отдельно от подтверждённых. +- Замеры только на dev-сборке фронта (`rsbuild dev`): она искажает картину React. +- Правки в `api/src`, `api/alembic`, `web/src`, `web/rsbuild.config.ts` или в зависимостях приложения ради замеров. Всё, что нужно стенду, живёт в `perf/`. +- Цифры, которые нельзя повторить: каждая цифра отчёта получается командой из `perf/`. + +## Ограничения и известные факты + +- Машина заказчика сильно медленнее машины разработчика. Фронт мерить с замедлением CPU через CDP (`Emulation.setCPUThrottlingRate`) в двух режимах — 4× и 6×, а также без замедления для сравнения. +- Прод-сервер — около 1 vCPU / 4 GB (`raisa_config.yml` устарел). Стенд по указанию владельца ресурсами **не ограничивается**. Цифры бэка в отчёте помечаются как полученные на быстром железе, без лимитов. +- БД — только Postgres, схема `v3`. SQLite не годится: миграции на PL/pgSQL. Postgres 16 в docker (образ доступен). +- Зависимости: npm берётся из публичного registry, для Python — `api/requirements-dev.txt`, без корпоративного Nexus. `raisa-fastapi-protected-api` не нужен (закомментирован в коде). +- При `DEBUG=true` миграции на старте не выполняются. Стенд сам делает `cd api && alembic upgrade head`. Авторизация — локальный JWT (`/api/v1/auth/login`). +- cwd бэка — `api/`. Собранный фронт API отдаёт из `api/web` — либо так, либо через отдельный статический сервер; на выбор исполнителя, лишь бы это была prod-сборка. +- Таблица листа (`web/src/components/RealtimeTable/RealtimeTable.jsx`) уже использует виртуализацию строк и колонок `material-react-table`. Гипотезу «нет виртуализации» не проверять как основную. +- Состояние WebSocket-блокировок в памяти процесса — бэк в один процесс. +- Браузер — Chromium (Playwright), методика на CDP. У заказчика Яндекс.Браузер (Chromium) — цифры переносимы. В методике отчёта зафиксировать версию Chromium стенда и оговорку: расширения и собственные фоновые процессы Яндекс.Браузера стенд не воспроизводит. + +## Что решено + +### Стенд (`perf/`) +- Postgres 16 в docker, миграции alembic до head, затем генерация данных. +- Бэк — `uvicorn` из `api/` в одном процессе, без лимитов. Фронт — `npm run build`, отдача prod-сборки. +- Для React Profiler — отдельная профилирующая сборка (alias `react-dom` → `react-dom/profiling`; `scheduler/tracing` в React 18 нет) через собственный конфиг rsbuild в `perf/`, который расширяет `web/rsbuild.config.ts` и не меняет его. Основные цифры (время, FPS, long tasks) снимаются на обычной prod-сборке; профилирующая сборка нужна только для ответа «что перерисовывается». +- `perf/README.md`: как поднять стенд, сгенерировать данные, прогнать сценарии и где лежат результаты. + +### Данные +- Объём заказчик не сообщил. Генератор параметризуется масштабом. Базовый масштаб — «большой, но правдоподобный»: формы FORM_1, FORM_2, FORM_4 за один год по оргединицам, в листах — строки по всему дереву статей расходов из сида (`0003_initial_data.sql`), строки ВСП и проекты для FORM_3/FORM_4. Выбранные числа и их обоснование исполнитель записывает в отчёт. +- Второй прогон на удвоенном масштабе показывает, как время растёт с объёмом. +- Данные создаются через те же SQL-функции `v3.add_*`, что использует приложение, где это практично. Прямые INSERT — только там, где функции нет; в этом случае генератор не должен нарушать инварианты, которые обеспечивают функции. + +### Роль и этапы +- Основная роль замеров — EXECUTOR_RF с открытым окном этапа на правку (жалуется, скорее всего, исполнитель): таблица рендерит колонки и права на строки по роли и этапу (`RealtimeTable.jsx:111`, `:263–273`), модалки выбора ВСП и статьи доступны только при праве править строки. Контрольный прогон — ADMIN, только для скролла на базовом масштабе. +- Генератор создаёт пользователей, оргединицы и этапы (`form_phase`) с окнами, открытыми на дату замера. + +### Сценарии фронта (Playwright + CDP) +1. **Скролл.** Для каждого листа каждой формы (FORM_1: AHR/CAP/OPER/SMETA с direction; FORM_2: основные листы и сателлиты; FORM_4: листы и STRUCTURE) и отчёта проекта (FORM_3): вертикальный скролл от начала до конца и обратно, горизонтальный по всем колонкам. Метрики: кадры и потерянные кадры, long tasks (>50 мс) и их суммарное время, время скриптов, стиля и layout из trace. Каждый прогон — в трёх режимах CPU. +1a. **Скролл при живом соавторе.** Тот же скролл на базовом масштабе в режиме 6×, пока второй пользователь (скрипт через WebSocket, без браузера) правит ячейки этого листа с заданной частотой (1 и 5 правок/с). Входящие `cell_updated`/`cell_edit_start`/`row_added` обрабатываются в `RealtimeContext.js:105–160` и могут перерисовывать таблицу во время скролла. Это один фоновый соавтор, а не нагрузочный тест. +1b. **Своды** (вторым приоритетом после листов форм): свод 1 (`pages/SvodPage/SummaryTable.jsx`), свод 2 (`pages/Svod2Page/Svod2Page.jsx`), `ProjectSummariesPage` — скролл и смена фильтров; данные генератора должны давать правдоподобно заполненные своды по всем оргединицам. SQL сводных функций — в замеры бэка. +2. **Модалки.** Перечень строится по коду: все модалки и диалоги, доступные со страниц форм, задачи и проекта — этапы (`SettingModal`), добавление года, экспорт, редактирование проекта, создание проекта и т.д. Модалки админки — вторым приоритетом. Метрика: от клика до первого кадра с видимой модалкой (`requestAnimationFrame` после появления в DOM), long tasks в этом окне, какие компоненты перерисовались (по профилирующей сборке). Отдельно — открытие модалки поверх открытого большого листа, если такое есть. +3. **Память.** Длительный сценарий — циклы «открыть лист → скролл → правка ячеек через WebSocket → уйти на список задач → открыть другой лист», около 20 циклов. Метрики: `JSHeapUsedSize` по циклам после принудительного GC, heap snapshots в начале, середине и конце, detached DOM nodes, число слушателей событий, число открытых WebSocket. Рост между снимками разбирается до retainers. + +### Замеры бэка +- Время ответа каждого запроса, который фронт делает в сценариях выше (из сетевого лога Playwright), плюс отдельный прогон этих же запросов без браузера — медиана и p95 по N повторам. +- `pg_stat_statements` за время сценариев: топ запросов и функций по суммарному и среднему времени. Для топа — `EXPLAIN (ANALYZE, BUFFERS)`. +- Размер ответов: тяжёлый JSON листа влияет и на фронт. +- Профиль Python (py-spy) — только если время ответа заметно больше времени SQL. + +### Статический разбор +Параллельно замерам — разбор кода фронта по местам, на которые указывают профили: нестабильные значения контекстов, пересоздание колонок и конфигов на рендер, слушатели и подписки без очистки, таймеры, WebSocket без закрытия. Каждая находка ссылается на `file:line` и помечена: подтверждена замером или нет. + +### Отчёт `perf/REPORT.md` +По-русски, для заказчика и команды: +1. Короткое резюме: что тормозит, почему и насколько, топ-5 мест. +2. Методика и окружение: железо, версии, масштаб данных, режимы CPU, оговорка про прод-ресурсы. +3. Таблица проблемных мест: № | место (`file:line`) | симптом | доказательство | влияние | приоритет (P1–P3) | подтверждено замером (да/нет). +4. Подробности по каждому месту. +5. Что не удалось проверить и почему. + +Сырые артефакты (traces, heap snapshots) складываются в `perf/results/<дата>/` и в git не коммитятся (`.gitignore`). Коммитятся сводные JSON с цифрами и отчёт. + +## На усмотрение исполнителя + +- Язык и инструменты генератора данных (Python + asyncpg или SQL), структура `perf/`, docker compose или скрипты. +- Точные числа масштаба (с обоснованием в отчёте), число повторов, пороги. +- Как обходить UI в сценариях (селекторы, прямые URL страниц). + +## Decisions made without discussion + +| Дата | Решение | Обоснование | Цена ошибки | +|---|---|---|---| +| 2026-10-25 | Стенд коммитится в `perf/` в репозиторий | Нужен повтор замеров «после» на тех же данных | Лишний каталог в репо; удаляется одним коммитом | +| 2026-10-25 | «Стенд не ограничивай» понято как «без лимитов CPU и памяти у бэка и БД» | Ближайшая трактовка реплики владельца сразу после обсуждения лимитов | Цифры бэка оптимистичнее прода; оговорено в отчёте | +| 2026-10-25 | Замедление CPU фронта 4× и 6× | Стандартные пресеты DevTools для медленной машины; реальная машина заказчика неизвестна | Неверный масштаб абсолютных цифр; относительный рейтинг мест не меняется | +| 2026-10-25 | Отчёт — markdown в `perf/REPORT.md` | Формат для заказчика владелец не выбрал; markdown легко превратить в документ | Переоформление отчёта | +| 2026-10-25 | Масштаб данных задаёт исполнитель, плюс прогон на удвоенном масштабе | Реальные объёмы неизвестны | Неверная абсолютная величина; удвоение показывает чувствительность | +| 2026-10-25 | `skip_specs: true` | Change не меняет поведение системы | — | +| 2026-10-25 | Основная роль — EXECUTOR_RF с открытым окном, ADMIN — контрольный прогон скролла | Жалобы, скорее всего, от исполнителей; рендер зависит от роли и этапа | Замеры на конфигурации, которой нет у заказчика; ADMIN-прогон показывает разницу | +| 2026-10-25 | Добавлен сценарий «скролл при одном фоновом соавторе» (1 и 5 правок/с), но не нагрузочный тест | Совместная работа — основной режим системы, входящие WS-события перерисовывают таблицу | +1 сценарий; если не влияет — так и будет написано | +| 2026-10-25 | Своды 1, 2 и сводные проектов — в скоупе вторым приоритетом | Пользователь может называть свод «формой»; это тяжёлые таблицы | Больше работы по данным и сценариям | +| 2026-10-25 | Postgres 16 | Версия прод-БД неизвестна; 16 — актуальная стабильная | Мелкие расхождения планов запросов с продом | diff --git a/openspec/changes/perf-profiling/proposal.md b/openspec/changes/perf-profiling/proposal.md new file mode 100644 index 0000000..b056dfb --- /dev/null +++ b/openspec/changes/perf-profiling/proposal.md @@ -0,0 +1,26 @@ +# Proposal + +## Why + +Заказчик жалуется на производительность: медленный скролл форм и медленное открытие модальных окон по кнопке. Подробностей нет, только пересказ. Бэк раньше профилировался базово; фронт подозревается, в том числе в утечках памяти. Нужно найти и доказательно показать проблемные места до того, как что-либо исправлять (issue #202). + +## What Changes + +- Новый каталог `perf/`: воспроизводимый локальный стенд — Postgres в docker, бэк, prod-сборка фронта, генератор данных реалистичного объёма — и сценарии замеров на Playwright. +- Замеры «до»: скролл листов форм, открытие модалок, рост памяти при длительной работе, время ответов API и тяжёлые SQL под этими сценариями. +- Отчёт `perf/REPORT.md`: проблемные места с доказательствами, оценкой влияния и приоритетом. +- Код приложения (`api/src`, `api/alembic`, `web/src`, `web/rsbuild.config.ts`) не меняется. Исправлений в этом change нет. + +## Capabilities + +### New Capabilities +Нет. Change добавляет инструменты и отчёт, поведение системы не меняется (`skip_specs: true`). + +### Modified Capabilities +Нет. + +## Impact + +- Новые файлы только в `perf/`, плюс при необходимости строки в `.gitignore` для сырых артефактов замеров. +- Dev-зависимости стенда (Playwright и др.) живут в `perf/` и не попадают в `web/package.json` и `api/requirements*.txt`. +- Для #199 (выгрузка на пром без следов обвязки): `perf/` к обвязке не относится. Включать ли его в выгрузку, решается там.