Andrey Epifancev 4836580c50 perf: замеры без троттлинга CPU (решение человека)
Refs #202

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 23:20:48 +04:00

159 lines
36 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` должен вести себя как прежде | Нет |
## Decisions made with the human
| Дата | Решение | Источник |
|---|---|---|
| 2026-10-25 | Замеры без троттлинга CPU: все сценарии по умолчанию на CPU 1×; код `throttle()` и env `PERF_CPU_RATES=1,4,6` оставлены для ручного включения; число повторов (3) не сокращено | Решение человека, передано координатором |