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

70 KiB
Raw Blame History

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-питоном
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 КБ/строка) с оговоркой, что сеть не эмулировалась Решение человека, передано координатором