49 KiB
Design: профилирование фронта и бэка (#202)
Зачем
Заказчик сообщил (пересказом, без деталей), что:
- тормозит скролл — предположительно любой большой формы, там много скроллить;
- модальное окно по нажатию кнопки открывается не сразу.
Бэк ранее профилировался базово, поэтому основное подозрение — фронт, включая утечки памяти. Задача — подсветить проблемные места с доказательствами. Исправлять ничего не нужно: исправления пойдут отдельными задачами по результатам отчёта.
Цели
- Воспроизводимый локальный стенд, поднимаемый одной-двумя командами, чтобы позже снять замеры «после» на тех же данных и сценариях.
- Замеры по двум симптомам заказчика и по памяти, для фронта и для бэка, которые обслуживают эти сценарии.
- Отчёт: список проблемных мест, у каждого — симптом, доказательство (цифры и ссылка на артефакт: trace, профиль, heap snapshot, EXPLAIN), место в коде (
file:line), оценка влияния, приоритет.
Не цели
- Исправления кода приложения, даже очевидные.
- Нагрузочное тестирование многими пользователями.
- Интеграция замеров в CI.
- Воспроизведение прод-ресурсов на стенде (см. «Ограничения»).
Анти-цели (провал, даже если формально сделано)
- Выводы без замера. Находки статического анализа допустимы только с явной пометкой «не подтверждено замером» и отдельно от подтверждённых.
- Замеры только на dev-сборке фронта (
rsbuild dev): она искажает картину React. - Правки в
api/src,api/alembic,web/src,web/rsbuild.config.tsили в зависимостях приложения ради замеров. Всё, что нужно стенду, живёт вperf/. - Цифры, которые нельзя повторить: каждая цифра отчёта получается командой из
perf/.
Ограничения и известные факты
- Замеры без троттлинга CPU — решение человека 2026-10-25 (слабые места видны и на 1×); код throttle оставлен в perf/e2e/lib/cdp.ts для ручного включения.
- Прод-сервер — около 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)
- Скролл. Для каждого листа каждой формы (FORM_1: AHR/CAP/OPER/SMETA с direction; FORM_2: основные листы и сателлиты; FORM_4: листы и STRUCTURE) и отчёта проекта (FORM_3): вертикальный скролл от начала до конца и обратно, горизонтальный по всем колонкам. Метрики: кадры и потерянные кадры, long tasks (>50 мс) и их суммарное время, время скриптов, стиля и layout из trace. Каждый прогон — на CPU 1×.
1a. Скролл при живом соавторе. Тот же скролл на базовом масштабе на CPU 1×, пока второй пользователь (скрипт через 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 сводных функций — в замеры бэка. - Модалки. Перечень строится по коду: все модалки и диалоги, доступные со страниц форм, задачи и проекта — этапы (
SettingModal), добавление года, экспорт, редактирование проекта, создание проекта и т.д. Модалки админки — вторым приоритетом. Метрика: от клика до первого кадра с видимой модалкой (requestAnimationFrameпосле появления в DOM), long tasks в этом окне, какие компоненты перерисовались (по профилирующей сборке). Отдельно — открытие модалки поверх открытого большого листа, если такое есть. - Память. Длительный сценарий — циклы «открыть лист → скролл → правка ячеек через 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
По-русски, для заказчика и команды:
- Короткое резюме: что тормозит, почему и насколько, топ-5 мест.
- Методика и окружение: железо, версии, масштаб данных, CPU 1× без троттлинга, оговорка про прод-ресурсы.
- Таблица проблемных мест: № | место (
file:line) | симптом | доказательство | влияние | приоритет (P1–P3) | подтверждено замером (да/нет). - Подробности по каждому месту.
- Что не удалось проверить и почему.
Сырые артефакты (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× (отменено решением человека 2026-10-25, см. раздел Ограничения) | Стандартные пресеты 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 — актуальная стабильная | Мелкие расхождения планов запросов с продом |
| 2026-10-25 | R-1 (design #11): Task 3 заполняет svods/modal_pages манифеста по маршрутам из Routes.jsx |
Иначе сценарии T5/T7 не получат список страниц | Доработка T3 при T5/T7 |
| 2026-10-25 | Email админа стенда admin@perf-stand.com вместо admin@perf.local |
/api/v1/users/me падает 500: email-validator отклоняет .local в response_model |
Другой email у админа стенда; на замеры не влияет |
| 2026-10-25 | В perf/requirements.txt добавлен greenlet |
api/requirements-dev.txt его не содержит, alembic на asyncpg падает без него |
Лишняя зависимость в venv стенда |
| 2026-10-25 | perf/web/rsbuild.perf.config.ts импортирует @rsbuild/core по относительному пути ../../web/node_modules/@rsbuild/core |
Конфиг в perf/ не видит web/node_modules через резолв пакетов |
Путь ломается при смене раскладки каталогов |
| 2026-10-25 | В .gitignore шаблон для трейсов perf/**/*trace*.json |
Формулировка «*.json trace» неоднозначна | Лишний или пропущенный файл в игноре |
| 2026-10-25 | В .gitignore добавлено исключение !perf/docker-compose.yml |
Корневой .gitignore игнорирует docker-compose.yml |
Файл стенда не попадёт в коммит |
| 2026-10-25 | В манифесте сегмент «нет direction» в URL листа — литерал null |
TableList.jsx строит путь через ${table?.direction}; при null из API получается null (при undefined было бы undefined) |
Переходы на листы без direction дадут 404/пустую страницу |
| 2026-10-25 | Агрегатор считает median чётного n как среднее двух центральных, p95 — nearest-rank; нечисловые и bool-метрики игнорируются | Бриф задаёт nearest-rank только для p95; median по стандартному определению | Небольшое расхождение чисел с ручным расчётом |
| 2026-10-25 | results.append валидирует наличие всех обязательных ключей записи |
Ранний отказ на кривом формате от Node-стороны | Лишний ValueError при расширении формата |
| 2026-10-25 | План масштаба 1: 3 ССП, 6 РФ, 30 ВСП/РФ, 500 доп. строк на лист (FORM_4 — 100), 4 проекта/РФ, fill_ratio 0.6; масштаб 2 удваивает все поля, включая org_units_ssp | Самый большой лист rf (FORM_2/AHR) >= 1000 строк; генерация масштаба 1 в пределах получаса; обоснование в perf/perfkit/gen/plan.py |
Другой объём данных; масштаб 2 займёт ~4-5x масштаба 1 |
| 2026-10-25 | Строк на листах FORM_4 в 5 раз меньше, чем на листах FORM_1/2 (FORM4_LINES_DIVISOR) |
add_line и правка ячеек FORM_4 стоят O(число строк листа) в БД (0.2 с -> 1.6 с на 300 строках, батч 200 ячеек 9-11 с); 500 строк на лист сделали бы генерацию многочасовой | Листы FORM_4 в замерах меньше, чем у заказчика; ищется там же отдельно |
| 2026-10-25 | Формы: FORM_1 по ССП, FORM_2 по ССП и РФ, FORM_4 по РФ | Свод 2 «РФ» читает FORM_2 РФ (миграция 0041), FORM_4 — форма РФ, FORM_1 — ГО | Своды заполнены иначе, чем в проде |
| 2026-10-25 | Всё заполнение (строки, ячейки) идёт под admin, этапы EXECUTOR_RF/DFIP с окном now-1d..now+30d создаются после заполнения (фазы PERF_RF/PERF_DFIP); колонки этапа — те, что реально записываются на листе | У admin нет ограничений по этапам, быстрее и надёжнее; в конце самопроверка правки под rf, rf_coauthor, dfip | Окно этапа открыто только на колонки заполнения (plan, actual, reserve, ...), не на все |
| 2026-10-25 | Логины пользователей perf_rf, perf_rf_coauthor, perf_dfip (ключи манифеста rf, rf_coauthor, dfip) |
username в API не короче 3 символов, rf отвергается |
Сценарии должны брать логин из манифеста, а не из ключа |
| 2026-10-25 | Программы и проекты FORM_4 создаются через WebSocket (program_added, project_added) |
HTTP-ручки нет; WS — тот же путь, что у фронта, и те же SQL-функции add_form4_* | Зависимость генератора от протокола WS |
| 2026-10-25 | В Sheet манифеста добавлено поле owner ("rf"/"ssp", по умолчанию "rf"); в манифест попадают листы только ССП-01 и РФ-01 |
Манифест без признака владельца не позволяет отличить листы, недоступные rf; полный список форм раздул бы сценарии |
Сценарии T5 должны фильтровать по owner |
| 2026-10-25 | Лист свода кодируется в Page.url после # (/svod#FORM_1, /svod/2#RF) |
Вкладка свода — состояние страницы, не маршрут; хеш не влияет на роутинг, имена svod1_*, svod2_* уникальны |
Сценарий обязан выбирать вкладку по хешу/имени |
| 2026-10-25 | В perf/stand.sh добавлена reset; start_bg отвязывает подоболочку от stdout |
Требование задачи; без отвязки stand.sh api | tail висел |
Правка поведения стенда Task 1 |
| 2026-10-25 | Версии @playwright/test, @types/node, typescript в perf/e2e/package.json зафиксированы точно (без ^) |
Воспроизводимость замеров между прогонами «до/после» | Обновление версий вручную |
| 2026-10-25 | Тест results format пишет в каталог через PERF_RESULTS_DIR и после проверки удаляет perf/report-data/<имя> |
aggregate пишет в perf/report-data/<run_id>; не должно оставаться мусора |
Мусор в report-data при падении теста |
| 2026-10-25 | probe.stop() ждёт 100 мс перед сбором (PerformanceObserver доставляет longtask асинхронно) |
Иначе последняя длинная задача перед stop теряется |
+100 мс к duration_ms |
| 2026-10-25 | heapUsed делает GC перед Runtime.getHeapUsage; startTrace/stopTrace через browser.startTracing Playwright |
Стабильные значения кучи; штатный API трассы | Трасса содержит события всего браузера |
| 2026-10-25 | В .gitignore добавлены !perf/e2e/lib/ и perf/e2e/test-results/ |
Корневой lib/ игнорирует каталог библиотек каркаса; test-results — артефакты Playwright |
Библиотеки не попадут в коммит / мусор в коммите |
| 2026-10-25 | Ось и фильтр кодируются в target суффиксом (.../AHR/null@y, svod/<name>/filter@y); axis дополнительно пишется в метрики (0 = y, 1 = x) |
Агрегатор группирует по target, а не по оси: без суффикса оси y и x смешались бы в одну группу |
Другие имена target, чем в брифе; Task 6+ должны учитывать суффикс |
| 2026-10-25 | Target отчёта проекта — project/<report_type>/<project_id> (в плане project/{report_type}); target листа — <form_type>/<sheet>/<direction|null> |
В манифесте 12 отчётов с 6 одинаковыми report_type: без project_id замеры разных проектов смешались бы |
Другие имена target, чем в брифе |
| 2026-10-25 | Admin-контроль идёт по всем листам манифеста (включая owner=ssp), rf — только по owner=rf; svod1 — вкладки MAIN, FORM_1..4, svod2 — GO и RF, svods_navigator не измеряется |
Админ видит все листы; навигатор — не таблица | Объём матрицы |
| 2026-10-25 | Вкладка свода выбирается кликом role=tab по индексу, #hash из манифеста игнорируется |
Приложение не читает hash (useState('MAIN') в SvodPage/Svod2Page): #FORM_1 открывал MAIN |
Если порядок вкладок изменится, замер уйдёт на другую вкладку |
| 2026-10-25 | CPU-троттлинг включается после загрузки данных (после ≥ 20 строк), перед скроллом | Измеряется скролл, а не загрузка 31–47 МБ свода под замедлением | Загрузка не троттлится |
| 2026-10-25 | Не набрались 20 строк за PERF_LOAD_TIMEOUT_MS (15 мин по умолчанию) — запись incomplete:true с skip_reason, тест не падает |
Матрица на часы не должна обрываться на одной странице; пропуск виден в данных | Пропуск надо заметить в итоговой таблице |
| 2026-10-25 | Контейнер скролла — .MuiTableContainer-root (первый на странице); testDir Playwright расширен до {tests,scenarios}/**/*.spec.ts |
Подтверждено на стенде: единственный скроллируемый элемент; сценарии лежат в scenarios/ |
Модалка с таблицей перехватила бы селектор |
| 2026-10-25 | json_bytes_sheet — размер распакованного тела ответа загрузки данных (максимум по совпавшим URL) |
Брифом задан «размер ответа»; распакованный — то, что парсит JS | Не равен размеру на проводе (gzip) |
| 2026-10-25 | Смена фильтра свода = ввод 1 в поле поиска, стабильность = ≥ 1 мутация DOM контейнера + 2 подряд кадра без long task и мутаций (таймаут 120 с) |
Фильтр «ССП/РФ»/год требуют данных, которых может не быть; поиск стабилен и клиентский | Поиск может быть дешевле смены организации/года |
| 2026-10-25 | Свод 2 не грузит данные сам: сценарий выбирает «Все ССП/РФ» и жмёт «Загрузить» (по умолчанию загрузка = все организации, 31–47 МБ) | Без этого таблица пуста (Svod2Page.jsx: hasCurrentData ложно до нажатия) |
Для свода 2 измеряется самый тяжёлый вариант (все организации) |
| 2026-10-25 | json_bytes_sheet считается по CDP Network.dataReceived (сумма распакованных чанков), не по response.body() |
Для ответов 30–47 МБ body() падает: «evicted from inspector cache» |
Отдельная CDP-сессия на странице (небольшие накладные расходы на загрузке, не на скролле) |
| 2026-10-25 | Если строки появились, но их число не растёт 20 с и их < 20 — замер incomplete:true + skip_reason сразу, без ожидания PERF_LOAD_TIMEOUT_MS |
Листы манифеста с 1 строкой (OTCH9F) иначе ждали бы 15 мин на каждый замер | Медленная отрисовка при простое > 20 с была бы принята за «мало строк» |
| 2026-10-25 | Таймаут теста: 45 мин (PERF_TEST_TIMEOUT_MS), для свода 2 — 3 ч (PERF_TEST_TIMEOUT_SVOD2_MS) |
Свод 2: загрузка до 15 мин + скролл на CPU 6 ≈ 40 мин; общий 45 мин обрывал бы его | Слишком большой таймаут задержит обнаружение зависания |
| 2026-10-25 | Отсутствие переполнения по оси даёт incomplete:true, skip_reason:'no_overflow' (а не «зелёную» запись с 0 шагов); фильтр без изменения содержимого — filter_no_effect; повтор с трейсом помечен metrics.traced=1 |
Пустой замер не должен попасть в статистику; трейс искажает замер, Task 10 исключает такие записи | Нужно не забыть исключить traced=1 при агрегации |
| 2026-10-25 | Цели «скролла при соавторе» — самый большой rf-лист каждой доступной формы: FORM_2 (форма 7, AHR, 1154 строки) и FORM_4 (форма 13, STRUCTURE, 775); FORM_1 не берётся | FORM_1 принадлежит ССП: у perf_rf/perf_rf_coauthor его нет (GET листа форм 1 даёт 404) |
FORM_1 без замера при соавторе |
| 2026-10-25 | Тест бота test_coauthor_first_edit_echoed идёт на первом rf-листе манифеста, а не на «первом листе FORM_1» |
Тот же довод: соавтор не видит FORM_1 | Тест не проверяет FORM_1 |
| 2026-10-25 | Бот правит только числовые колонки (без comment/justification/date/note в имени) у строк INPUT, значение — случайное целое 1000–999999; колонки берёт из META.editable GET листа |
Типы текстовых/датовых колонок не проверять; значения не имеют смысла, стенд тестовый | Данные листа меняются в ходе прогонов (суммы пересчитываются) |
| 2026-10-25 | «Эхо» = cell_updated с is_self:true без поля error; отказ сервера считается в coauthor_errors, не в coauthor_echoed; бот до первого эха повторяет правку (другие ячейки) раз в 0,7 с, но не дольше 5 с с запуска, иначе код 2 |
Сервер при отказе возвращает то же событие с error; одиночный отказ по одной ячейке не должен ронять сценарий |
При постоянных отказах сценарий падает через 5 с |
| 2026-10-25 | --token бота подменяет токен только для WS; чтение листа идёт по настоящему логину |
Нужно проверить неверный токен (Review Focus), не сломав получение ячеек | Нет |
| 2026-10-25 | Запись scroll_coauthor не пишется (тест падает), если coauthor_echoed==0 или страница автора не получила ни одного чужого cell_updated (page_ws_updates==0) |
Иначе сценарий измерил бы «без соавтора» | Ложные падения при сбое WS-подключения страницы |
| 2026-10-25 | Target записи — <form_type>/<sheet>/<direction|null>/coauthor<rate>@y; бот запускается после загрузки листа и ждёт READY до старта замера; бот работает до SIGTERM после скролла; в метрики также coauthor_errors, page_ws_updates; трейс только на повторе 0 (traced=1) |
Rate 1 и 5 не должны смешиваться в одну группу агрегатора | Task 10 исключает traced=1 |
| 2026-10-25 | Только вертикальный скролл (ось y), роль rf |
Бриф: «скролл при соавторе»; горизонтальная ось не зависит от потока входящих правок | Нет замера оси x при соавторе |
| 2026-10-25 | Вспомогательный waitRows продублирован в scroll-coauthor.spec.ts, а не вынесен в lib/ |
Не трогать scroll.spec.ts Task 5; функция короткая |
Дубли при правке |
| 2026-10-25 | Фильтр свода по умолчанию мерится на max(PERF_CPU_RATES) (при умолчании 1), имя теста filter cpu<N> |
Троттлинг отключён по умолчанию, но PERF_CPU_RATES=1,4,6 должен вести себя как прежде |
Нет |
| 2026-10-25 | main_thread_ms модалки = прирост CDP Performance.TaskDuration (Performance.getMetrics) между кликом и кадром с отрисованной модалкой (без 100 мс ожидания longtask) |
TaskDuration включает скрипт, стили, layout, paint-подготовку; не зависит от порога 50 мс long task | Метрика чуть шире «реакции React»: в окно попадают фоновые задачи страницы |
| 2026-10-25 | Признак появления модалки в measureOpen — opts.appear {css, text} (по умолчанию [role=dialog]); для common/Modal и emotion-модалок (ModalBackdrop) без role используется h2 + заголовок |
В приложении role=dialog есть только у MUI Dialog; иначе замер падал бы по таймауту | Ложное срабатывание на одноимённом заголовке вне модалки |
| 2026-10-25 | Клик в замере — page.mouse.click по координатам после hover и scrollIntoViewIfNeeded; каждый замер в новом browser context |
Иначе locator.click() включает в окно проверки actionability |
Нет |
| 2026-10-25 | Профилирующая сборка собирается с minify:false и портом из PERF_WEB_PORT (8081); stand.sh web-profiling |
Без неминификации displayName компонентов превращаются в однобуквенные имена |
Другой размер бандла, чем у prod: сравнимы только счётчики рендеров |
| 2026-10-25 | Рендер компонента в коммите: flags & PerformedWork для тегов 0/1/11/15 + обход как в DevTools; self_ms = actualDuration - сумма детей |
Единственный способ получить renders/self_ms без React DevTools extension | Приближение: не учитывает memo-bailout'ы вне обхода |
| 2026-10-25 | modals.json: Menu/Popover-контекстные меню (RowActionMenu, StagesTable Menu, ValidationErrorsAlert) не включены; Popover выбора колонок (sheet_columns_tree) включён как единственный оверлей, доступный на большом листе |
Меню не модалки; select_vsp/select_expense/create_* на самом большом листе rf (FORM_2/AHR) не открываются (проверено на стенде) | Неполное покрытие «модалка поверх большого листа» |
| 2026-10-25 | Для открытия под admin без права на лист — роль admin только для модалок страниц /tasks, задач и админки; остальные под rf | Права строк есть у rf | Нет |
| 2026-10-26 | R-2: форм-модалки (выбор ВСП/статьи, создание программы/проекта) мерятся и на малом листе, и на «выращенном» (@big: FORM_2 AHR_RENT/UTILITY/SECURITY ~500 строк, FORM_4 AHR ~300) через perfkit/gen/grow.py, без пересоздания БД; grow запускается после скролл-сценариев | Ревью T7: на листах, где эти модалки существуют, 31–110 строк, а жалоба заказчика — на модалку большой формы; FORM_4 add_line O(n) не позволяет выращивать до 1000+ | БД после grow отличается от базовой на этих листах; цели одинаковы на масштабах 1 и 2 |
| 2026-10-26 | R-2, детали: grow растит FORM_2 AHR_UTILITY и AHR_SECURITY (rf) до 500 строк и FORM_4 CAP (rf, форма 13) до 300; AHR_RENT и FORM_4 AHR остаются малыми и служат «малым» листом пары (поэтому порядок запуска не влияет на малые замеры; на скролл-сценарии по UTILITY/SECURITY/CAP grow влияет, их запускать до grow). Цели те же на масштабах 1 и 2 | Один и тот же лист нельзя замерить и малым, и большим без жёсткого порядка; на FORM_4 add_line O(n) | После grow БД отличается от базовой; скролл-замеры этих трёх листов после grow не сравнимы с до |
| 2026-10-26 | Строки grow пустые (значения не заполняются), FORM_2: дубли ВСП допустимы (API принимает) | Для открытия модалки важно число строк в DOM/таблице, не данные | Нет |
| 2026-10-26 | Время grow на масштабе 1: FORM_2 +468/+469 строк по ~1 c (параллельно), FORM_4 CAP +190 строк 213 c (последовательно, ~1.1 c на строку, O(n)) | Замер | Для масштаба 2 цели те же - время не растёт |
| 2026-10-26 | Порядок run-all (предложение для T10): reset -> gen -> скролл -> соавтор -> grow -> модалки (малые и @big) -> память | grow меняет размер листов | Решение за T10 |
| 2026-10-26 | Сценарий памяти: вместо «самого большого листа FORM_1» берётся самый большой лист FORM_2 с owner=rf (AHR, 1154 строки; на нём 3 правки за цикл), вместо FORM_2 - самый большой лист FORM_4 с owner=rf (STRUCTURE, 775 строк; только открытие и скролл, правок нет). Переопределение: PERF_MEM_SHEET1/2 |
У роли rf нет листов FORM_1 (они ССП, 404/нет прав) | Нагрузка цикла отличается от формулировки брифа; пара листов меняется одной переменной |
| 2026-10-26 | Правка ячейки в сценарии памяти - двойной клик (material-react-table, editDisplayMode cell), числа 1000+cycle*10+i, Enter; успех = эхо cell_updated is_self без error + закрытие редактора. Скролл scrollThrough идёт вниз и обратно |
Так редактор открывается в приложении; эхо подтверждает прохождение через фронтовый WS | Значения ячеек rf на листах меняются (стенд тестовый) |
| 2026-10-26 | Навигация между страницами - history.pushState + popstate, перед выборкой проверяется маркер window.__sessionMark; перед каждым циклом access-токен перевыпускается через API, если до истечения (60 мин, ACCESS_TOKEN_EXPIRE_MINUTES) меньше 10 мин |
Одна SPA-сессия; полный сценарий дольше жизни токена (минор T4.1: init-script не перезаписывает токен без перезагрузок) | Без перелогина правки после 60-й минуты падали бы по токену |
| 2026-10-26 | Сетевой лог подключается не расширением test и не правкой results.ts, а функцией attachNetlog(ctx, meta) в perf/e2e/lib/netlog.ts (одна строка + flush() перед ctx.close() в четырёх сценариях) |
Контексты создаются в measure() каждого сценария, общей фикстуры context нет; фикстуры потребовали бы переписать сценарии |
Забытый вызов в новом сценарии - нет лога для bench; ловится по отсутствию network.jsonl |
| 2026-10-26 | В лог попадают запросы и к PERF_WEB/api/ (proxy), и к PERF_API/api/; body_size - сжатый (request.sizes()), bench.bytes - после распаковки |
Фронт на стенде ходит в API через proxy своего origin; body() недоступен для 30-47 МБ |
Размеры в логе и в bench различаются в ~19 раз (gzip) - оговорено в README |
| 2026-10-26 | PATCH ячеек в bench повторяется только при idempotent: true в строке лога; сейчас никто это поле не ставит, PATCH не повторяются |
Правки в сценариях идут по WebSocket, а по HTTP-телу нельзя доказать, что значение совпадает с текущим | Бэк-метрики правок через HTTP не снимаются; WS-правки покрыты сценарием соавтора |
| 2026-10-26 | Запросы, длившиеся в логе > 60 с (--max-logged-s), bench пропускает |
Свод 2 (31-47 МБ, минуты) x 20 повторов заняли бы часы; единичный замер уже есть в логе | Нет 20-кратной статистики для свода 2 |
| 2026-10-26 | Подстановка параметров EXPLAIN: для v3.v_form_view/v3.v_summary_view - из сетевого лога по правилам репозитория (режим analyze), для остальных - generic plan без ANALYZE; EXPLAIN только для top-level SELECT, в транзакции с ROLLBACK |
pg_stat_statements не хранит значения параметров; вложенные запросы функций ссылаются на переменные plpgsql | Для ORM-запросов нет ANALYZE (они дешёвые, 2-5 мс); для новых v3-вызовов нужно дописать builder в sqlstats.BUILDERS |
| 2026-10-26 | perfkit.results.read_all и aggregate игнорируют network.jsonl; aggregate исключает metrics.traced == 1 и скрывает метрику traced |
Сетевой лог лежит рядом с результатами; O-9 (трассировка искажает время) | Трассировочные повторы не видны в таблицах, только счётчик в заголовке группы |
| 2026-10-26 | globalSetup задаёт PERF_RUN_ID (минутный, если не задан), globalTeardown дампит SQL только при наличии network.jsonl |
Один run_id для воркеров и teardown; тесты lib не должны писать sql_top | Запуск без PERF_RUN_ID через границу минуты раньше писал в разные каталоги |
| 2026-10-26 | R-3: Task 10 исполняется в два шага — T10a (run-all.sh, README, правки из carry, коммит) и T10b (анализ и REPORT.md); многочасовой прогон run-all.sh между ними запускает и контролирует контроллер, а не исполнитель | Прогон идёт часами, исполнитель-сабагент не должен висеть в ожидании и терять контекст | Лишний диспатч; при сбое прогона перезапуск по частям |
Decisions made with the human
| Дата | Решение | Источник |
|---|---|---|
| 2026-10-25 | Замеры без троттлинга CPU: все сценарии по умолчанию на CPU 1×; код throttle() и env PERF_CPU_RATES=1,4,6 оставлены для ручного включения; число повторов (3) не сокращено |
Решение человека, передано координатором |
| 2026-10-25 | Основная метрика сравнения в REPORT.md — суммарное время работы main thread (scripting + rendering/layout) на действие (шаг скролла, открытие модалки), а не число long tasks: задачи короче 50 мс на этом железе станут long task на слабой машине. Источник — probe (суммарное время long tasks + время кадров сверх 16,7 мс) либо суммы по категориям из trace первого повтора; выбор за исполнителем Task 10 с записью в Decisions | Решение человека, передано координатором |
| 2026-10-25 | Контрольный прогон на CPU 4× только для топ-3–4 находок REPORT.md, по 1 повтору; в отчёте цифры помечены как контрольные («на слабой машине») | Решение человека, передано координатором |
| 2026-10-25 | Отдельной строкой в REPORT.md — размеры ответов API (свод 2 31–47 МБ, листы ~15 КБ/строка) с оговоркой, что сеть не эмулировалась | Решение человека, передано координатором |