Andrey Epifancev bc52212811 perf: REPORT, README, design.md — правки по финальному ревью
- REPORT: правки FORM_4 — вывод по коду, не замер (I-1); цена фильтра свода
  по записям основного прогона, hotspots — верхняя граница (I-2); М6 поднята
  до P1, пояснение состава топ-5 (I-4); формулировки кадров, малых листов,
  admin-контроля, М8, время прогона (M-1, M-2, M-9, M-11); раздел 6 дополнен,
  новый раздел 6а с находками при подготовке стенда (M-10, M-12, T7.3)
- README: контроль 4x с отдельным тегом и предупреждением (I-3), команды
  modal-renders и netlog-sizes (M-4), гипотезы в «Как читать» (T8.7)
- design.md: R-7, правило приоритетов и топ-5, решения волны исправлений
- план: заметка о замене CPU [1,4,6] решением человека (M-12)

Refs #202

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 13:34:58 +04:00

218 lines
70 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 — решение человека 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)
1. **Скролл.** Для каждого листа каждой формы (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 сводных функций — в замеры бэка.
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 1× без троттлинга, оговорка про прод-ресурсы.
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× (отменено решением человека 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: форм-модалки (выбор ВСП/статьи, создание программы/проекта) мерятся и на малом листе (FORM_2 AHR_RENT, FORM_4 AHR), и на «выращенном» (@big: FORM_2 AHR_UTILITY/AHR_SECURITY ~500 строк, FORM_4 CAP ~300; исходная формулировка рулинга называла выращиваемыми RENT и FORM_4 AHR - уточнено при исполнении, см. следующую строку) через 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 между ними запускает и контролирует контроллер, а не исполнитель | Прогон идёт часами, исполнитель-сабагент не должен висеть в ожидании и терять контекст | Лишний диспатч; при сбое прогона перезапуск по частям |
| 2026-10-26 | Основная метрика «время main thread на действие» для скролла - `main_thread_ms` = прирост CDP `Performance.TaskDuration` за скролл (как у модалок, `lib/probe.ts` `startProbe/stop`), и `main_thread_ms_per_step` = `main_thread_ms / steps` (шаг колеса 200 px); для смены фильтра свода - `main_thread_ms` на действие. Дополнительно probe даёт `frame_excess_ms` (сумма превышений кадра над 16,7 мс) для сверки. В `summary.md` эти две метрики идут первыми строками группы | Одинаковый источник для скролла и модалок; учитывает задачи короче 50 мс (то, что станет long task на слабой машине); не требует разбора trace и есть в каждом повторе, а не только в трассированном | TaskDuration включает накладные расходы харнесса (page.evaluate на шаг, < 1 мс) и фоновые задачи страницы; при сравнении «до/после» они одинаковы |
| 2026-10-26 | SQL-статистика по шагам: `globalTeardown` пишет `sql_top.json`/`explain/` в `<run_dir>/sql/<PERF_SQLSTATS_TAG>/` (`sqlstats dump --out`); run-all задаёт тег на каждый вызов Playwright (scroll, coauthor, modals, modals_profiling, memory, control4x_*). Параметры EXPLAIN берутся из общего `<run_dir>/network.jsonl` | Каждый вызов Playwright делает `pg_stat_statements_reset()` в globalSetup и dump в teardown: в одном run_id они перезатирали бы друг друга; общий dump на весь масштаб смешал бы сценарии и захватил бы SQL grow (add_line FORM_4 O(n)) | Для сравнения «весь масштаб» T10b складывает несколько sql_top; параметры EXPLAIN шага могут прийти из запроса другого сценария (берётся самый долгий подходящий) |
| 2026-10-26 | `netlog.ts`: тело не-GET для путей с `/auth/` не пишется (`post_data: null`), из остальных JSON/form-urlencoded тел вырезаются поля `password`/`passwd`/`pass`/`*token*`/`*secret*`; неразбираемое тело с таким словом - null | Ревью T9 (I-a): логин и пароль попадали в `network.jsonl`; bench повторяет только GET и идемпотентный PATCH ячеек, тела логина ему не нужны | Поле с «token» в имени у ячейки потерялось бы в логе (сейчас таких нет) |
| 2026-10-26 | Имена целей скролла: лист не-rf владельца с тем же именем, что у rf-листа, получает суффикс владельца (`FORM_2/AHR/null/ssp`); отчёт проекта - `project/<report_type>/<project_id>/<year>` (было без года) | Найдено при проверке `--list`: FORM_2 есть у ССП-01 и РФ-01, отчёты проекта за 2026 и 2027 одного типа - Playwright отказывался запускать `scroll.spec.ts` целиком из-за одинаковых названий тестов, а записи смешались бы в одну группу | Другие имена target, чем в прогонах t5-*; glob `project/LIMIT/1` больше не совпадает (нужен `project/LIMIT/1/*`) |
| 2026-10-26 | Trace в scroll и scroll-coauthor снимается только на повторе 0 и только если повторов > 1 (`lib/trace.ts` `shouldTrace`); `PERF_TRACE=0` отключает trace совсем (run-all дополнительно ставит его при 1 повторе). Modals и memory trace не снимают | aggregate исключает `traced=1` (O-9): при единственном повторе (admin-контроль - 61 тест на масштабе 1, control4x, `PERF_S2_REPEATS=1`) группа осталась бы без метрик (ревью T10a, C1) | Нет trace-файлов admin-контроля и одноповторных прогонов; для разбора трасс есть rf/своды с 3 повторами |
| 2026-10-26 | `perf/run-all.sh`: канонический порядок reset -> gen -> verify -> stand -> preflight -> scroll -> coauthor -> grow -> modals -> modals_profiling -> memory -> memtrend -> [control4x] -> bench -> aggregate; run_id `<PERF_RUN_TAG>-s<scale>` (тег по умолчанию - дата); фатальны только reset, gen и stand (остаток масштаба SKIP, следующий масштаб идёт); verify не фатален | Порядок - R-2/T7 (grow после скролла и соавтора); verify может споткнуться о допуски масштаба 1 на масштабе 2 (T3.1), а данные уже есть | Прогон на «плохих» данных при ложно-нефатальном verify - видно по FAIL в status.json |
| 2026-10-26 | Шаг stand проверяет, что `:8080` отдаёт именно prod-сборку (`index.html` совпадает с `perf/.build/prod/index.html`), поднимает api/web при необходимости и гоняет smoke; шаг preflight делает `playwright test --list` всех сценариев масштаба | Профилирующая сборка на :8080 исказила бы основные цифры; ошибки манифеста и дубли названий ловятся за секунды, а не через часы | Ложный FAIL при пересборке prod без перезапуска preview |
| 2026-10-26 | Профилирующий прогон модалок (только масштаб 1) - `PERF_PRIORITY=1` (все цели приоритета 1, включая @big), 1 повтор, `:8081` (`stand.sh web-profiling`); модалки на обоих масштабах - `PERF_PRIORITY=1,2` одним вызовом (p1 x5, p2 x3) | Счётчики рендеров нужны для форм-модалок и страниц задач/проектов (p1); p2 - админка, вторым приоритетом | Нет render counts для модалок админки |
| 2026-10-26 | control4x: `PERF_CONTROL_TARGETS` - список через запятую, `modal:<id>` - модалка, `coauthor:<glob>` - скролл при соавторе, остальное - glob скролла; CPU 4, 1 повтор, без trace, роли скролла `PERF_CONTROL_ROLES` (rf,dfip); пишет в тот же run_id масштаба (группы различаются `cpu_rate`) | Решение человека: контроль 4x только для топ-3-4 находок, 1 повтор; один summary.md показывает 1x и 4x рядом | Контроль после grow: скролл UTILITY/SECURITY/FORM_4 CAP несопоставим с замером до grow |
| 2026-10-26 | `PERF_S2_REPEATS` (по умолчанию 3) и `PERF_S2_TARGETS` регулируют только скролл масштаба 2; модалки масштаба 2 - с обычными повторами | Решение человека: повторы не сокращать, регулятор - только для масштаба 2 при оценке > 6 ч; модалки дешёвые (~12 мин) | Нет |
| 2026-10-26 | В `perf/report-data/<run_id>/` (коммитится) шаг aggregate кладёт summary.json/md, modals-inventory.md, `sql/<шаг>/{sql_top.json,explain/}`, memtrend.txt, manifest/gen-stats/grow масштаба и run-all.status.json; traces, heap snapshots и сырые jsonl остаются в `perf/results/` | Каждая цифра REPORT.md должна ссылаться на файл в report-data (дизайн); sql_top ~110 КБ на шаг | Рост репозитория на ~1-2 МБ |
| 2026-10-26 | `sqlstats dump` берёт пользователей из манифеста `PERF_SCALE` (было всегда s1) | Масштаб 2 - свой манифест | Нет (логины одинаковые) |
| 2026-10-26 | R-4: на масштабе 2 повторы сокращены до 1 (`PERF_S2_REPEATS=1`), масштаб 1 — 3 повтора | Оценка прогона масштаба 2 с 3 повторами ≈ 13 ч, с 1 повтором ≈ 6,3 ч; правило человека: при > ~6 ч сокращать повторы только на масштабе 2 | Масштаб 2 без разброса и p95: годится для оценки роста с объёмом, не для абсолютных цифр |
| 2026-10-26 | Масштаб 2: run-all передаёт сценариям `PERF_LOAD_TIMEOUT_MS=2400000` (40 мин) и `PERF_TEST_TIMEOUT_MS=5400000` (90 мин), если пользователь не задал свои; свод 2 по-прежнему `PERF_TEST_TIMEOUT_SVOD2_MS` 3 ч | Ответ свода 2 на масштабе 2 ~150-190 МБ (x4 от 31-47 МБ): 15 мин ожидания дали бы `incomplete`; 45 мин на тест меньше 40 мин загрузки + скролл (ревью T10a, I1) | Зависший замер обнаружится позже (до 90 мин) |
| 2026-10-26 | Шаг aggregate строгий: цепочка `&&`/`|| exit 1`, перед запуском удаляется прежний `report-data/<run_id>/summary.json`, в конце `test -f summary.json`; `PERF_PY` подменяет python perfkit (только для тестов run-all, `perf/tests/test_run_all.py`), статус пишется всегда venv-питоном | Иначе rc шага определял `ls -R`, и падение `perfkit.aggregate` при возобновлении (каталог report-data уже есть) маскировалось (ревью T10a, I2) | FAIL шага при сбое копирования необязательного файла |
| 2026-10-26 | После шага modals_profiling run-all останавливает preview профилирующей сборки на :8081 (по `perf/.run/web-profiling.pid`), :8080 и :8000 не трогает; код возврата - Playwright | Профилирующий preview не нужен остальным шагам и держит память/CPU во время memory и bench (ревью T10a, M2) | Ручной запуск профилирования после run-all требует снова `stand.sh web-profiling` |
| 2026-10-26 | R-5: бот-соавтор на FORM_4/STRUCTURE не дорабатывается; 6 замеров соавтора на этом листе — в «не проверено» с причиной (нет редактируемых ячеек у роли, программы и проекты создаются через WS, а не cell_updated) | STRUCTURE — лист структуры, не таблица ячеек; перегон потребует reset+gen масштаба 1 ради листа без правок ячеек | Нет замера соавтора на FORM_4; косвенно закрыто O-4/O-10 и замерами FORM_2/AHR |
| 2026-10-26 | R-6: перед EXPLAIN вложенных запросов и контрольным прогоном 4× данные масштаба 1 восстанавливаются (reset + gen s1 + grow s1, ~20 мин), т.к. после run-all в БД масштаб 2 | Цифры контроля и планы должны быть сопоставимы с масштабом 1 основного отчёта | ~20 минут; состав данных эквивалентен замеренному, но не побитово (id, значения) |
| 2026-10-26 | Основная метрика в REPORT.md — `main_thread_ms` (прирост CDP `Performance.TaskDuration`) и `main_thread_ms_per_step` из probe; разбивка trace первого повтора по категориям (`perf/analysis/timeline_breakdown.py`) — только сверка и объяснение «куда уходит время» | probe есть в каждом повторе, trace — только в одном; в 66 из 68 trace сумма задач главного потока совпала с probe в пределах 2 % (у двух trace свода 2 RF по 600 МБ — на 20 % меньше, по-видимому, trace обрезан) | Задачи вне главного потока (растр, компоновщик) в основную цифру не входят |
| 2026-10-26 | Пороги приоритетов: P1 — основной сценарий (скролл листа/свода, правка, совместная работа) и ≥ 50 мс главного потока на элементарное действие или ≥ 0,5 с сервера, либо рост с объёмом; P2 — ≥ 100 мс на действие на отдельных страницах/формах/масштабах или большие ответы; P3 — меньше 100 мс, редкие случаи, гигиена | Порог 50 мс — граница long task и ~3 кадра; действие — то, что пользователь делает многократно. Правило применяется без исключений (М6, скролл свода 1, — P1 после финального ревью); топ-5 резюме выбирается из P1/P2 по влиянию на жалобы заказчика и охвату пользователей, поэтому М6 (только ДФиП) в топ-5 не вошла, а М5 (вторая жалоба, P2) вошла | Другая расстановка P1/P2 у пограничных мест (М5, М7); другой состав топ-5 |
| 2026-10-26 | Места отчёта нумеруются М1–М13 (маркеры для заказчика и задач на исправление), а не O-n/Tn.k | O-n/Tn.k — внутренние маркеры процесса; в отчёт не копируются, найденное по приложению учтено в М-местах | Нет |
| 2026-10-26 | Контроль CPU 4× — в отдельный run_id `20261026-control-s1` (`PERF_RUN_TAG=20261026-control`), а не в `20261026-s1` | Шаг aggregate для `20261026-s1` перезаписал бы копии `manifest-s1.json`, `gen-stats`, `grow` в report-data данными восстановленной генерации и смешал бы в одной сводке данные двух генераций | Сравнение 1× и 4× — по двум summary.md, а не по одному |
| 2026-10-26 | Цели контроля 4×: скролл FORM_2/AHR (М1), свод 1 MAIN (М6), свод 2 GO (М2), модалка `sheet_select_vsp@big` (М5, жалоба заказчика); бэк-находки М3/М4/М7 в контроль не входят (CPU браузера на них не влияет), соавтор и отчёты проектов — тоже | Решение человека: топ-3–4 находки, 1 повтор; эти четыре закрывают обе жалобы и самые тяжёлые страницы | Нет контрольных цифр для М8 и отчётов проектов |
| 2026-10-26 | Для привязки CPU-профиля к `file:line` — третья сборка: prod с source maps (`PERF_SOURCEMAP=1` в `perf/web/rsbuild.perf.config.ts`, `perf/.build/prod-sourcemap`, `perf/stand.sh web-sourcemap` на :8082) | В prod-сборке нет .map, а профилирующая — другая сборка React; JS проверен: совпадает с prod побайтно с точностью до хешей имён чанков | Минифицированные имена в профиле без карты — только через source map |
| 2026-10-26 | Новый сценарий `perf/e2e/scenarios/hotspots.spec.ts` (в run-all не входит), run_id `20261026-hot`: CPU-профиль CDP с сэмплами 200 мкс или счётчики рендеров React на 40 шагов вперёд и 40 назад; фильтр — 30 с наблюдения мутаций DOM; cssgrowth — циклы без перезагрузки со счётчиком CSS-правил и `matchMedia` | Профили нужны для «что именно тормозит», а не для времени; под профайлером абсолютное время выше — в отчёте сравниваются доли и счётчики | Профайлер искажает соотношения коротких функций |
| 2026-10-26 | py-spy не запускался: на macOS требует root (sudo запрещён); критерий «API ≥ 2× SQL» проверен заголовком `X-DB-Time-Ms`, который ставит само приложение (`perf/analysis/api_split.py`) | Заголовок уже есть в обработчиках листов, сводов и отчётов; даёт разделение «БД / Python» без профайлера | Нет разбивки Python-части по функциям |
| 2026-10-26 | Ручной EXPLAIN вложенных запросов: текст из последних версий функций по диапазону строк с якорями, параметры литералами (`p_sections = NULL` → все флаги секций TRUE), эксперименты `jit=off` и `work_mem=64MB` через SET LOCAL в транзакции с ROLLBACK; затем `pg_stat_statements_reset()` и 8 вызовов в новом соединении | Вложенные запросы plpgsql ссылаются на переменные и отдельно не выполняются; JIT обнаружен на первом же EXPLAIN | Литералы дают custom-план; в приложении после 5 вызовов — generic-план (показан отдельно) |
| 2026-10-26 | Сравнение масштабов — по каждому URL bench с приведением form_id к `<form_type>/<sheet>` через манифест масштаба; незавершённые записи s2 (лимит шагов) берутся из сырого jsonl | form_id различаются между масштабами; группы bench сливают разные формы с одним листом | Нет |
| 2026-10-26 | «filter did not stabilize» на своде 2 GO (s1) классифицирован как артефакт критерия стабилизации, сценарий не меняли | 30 с наблюдения: 0 мутаций DOM контейнера после ввода фильтра при 6 коммитах React | Если видимые строки должны были измениться — это была бы ошибка фильтра (не проверялась по данным) |
| 2026-10-26 | Иллюстрация «на слабой машине хуже» (CPU 6×) — агрегат раннего прогона `perf/results/t5-types` в `perf/report-data/t5-types/` | Требование: каждая цифра отчёта — из report-data; ранний прогон с другими именами target помечен как иллюстрация | Старые имена target (без года в отчётах проектов) |
| 2026-10-26 | R-7: отдельное ревью задачи T10b на модели sonnet не проводилось — его покрывает финальное opus-ревью всей ветки (одна волна исправлений и один scoped re-review) | T10b — анализ и отчёт без кода приложения; финальное ревью читает те же файлы | Находки T10b, видимые только задачному ревью, могли быть пропущены |
| 2026-10-26 | Копия `run-all.status.json` в report-data пишется с путями относительно корня репозитория (`sed` в шаге aggregate и в конце масштаба); три закоммиченные копии очищены тем же способом | Финальное ревью (M-8): абсолютные пути машины в коммитах | Команды в копии статуса нельзя выполнить «как есть» не из корня репозитория |
| 2026-10-26 | Long tasks в окне открытия модалки считаются по пересечению интервалов (`tasksInWindow` в `perf/e2e/lib/modal.ts`), а не «началась после pointerdown» | Финальное ревью (M-7): задача клика начинается раньше, чем её обработчик ставит t0, и терялась; данные 20261026 не пересчитывались (в выводах не используются) | `long_task_*` модалок новых прогонов несравнимы с 20261026 |
## 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 КБ/строка) с оговоркой, что сеть не эмулировалась | Решение человека, передано координатором |