Andrey Epifancev 29867535b0 perf: полный прогон run-all.sh и правки по ревью
- perf/run-all.sh: масштабы 1 и 2, шаги reset -> gen -> verify -> stand ->
  preflight -> scroll -> coauthor -> grow -> modals -> modals_profiling ->
  memory -> memtrend -> [control4x] -> bench -> aggregate; лог и
  run-all.status.json, возобновление (PERF_STEPS, PERF_SKIP_RESET),
  сухой режим с оценкой длительности, PERF_S2_REPEATS/PERF_S2_TARGETS,
  контроль CPU 4x (PERF_CONTROL_TARGETS).
- Основная метрика: main_thread_ms и main_thread_ms_per_step в скролле
  (CDP TaskDuration), frame_excess_ms; в summary.md первыми строками.
- SQL-статистика по шагам: PERF_SQLSTATS_TAG -> sql/<шаг>/ (sqlstats --out).
- netlog: тела /auth/ не пишутся, поля password/token/secret вырезаются.
- scroll.spec: уникальные имена целей (лист ССП с суффиксом /ssp, год в
  отчёте проекта) - иначе Playwright не запускал файл из-за дублей.
- PERF_TRACE=0 для прогонов с одним повтором.
- README: scroll-coauthor, run-all, возобновление, без CPU 4/6 в матрицах.
- design.md: уточнена формулировка R-2, решения Task 10a.

Refs #202

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 03:01:33 +04:00

243 lines
16 KiB
Bash
Executable File
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.

#!/usr/bin/env bash
# Полный прогон замеров на масштабах 1 и 2 (Task 10). Долго: часы. Описание и порядок шагов - perf/README.md («Полный прогон»).
#
# perf/run-all.sh # всё: масштаб 1, затем 2
# PERF_DRY_RUN=1 perf/run-all.sh # план: шаги, команды, оценка длительности; ничего не запускает
# PERF_SCALES=2 PERF_RUN_TAG=20261026 PERF_SKIP_RESET=1 PERF_STEPS=modals,bench,aggregate perf/run-all.sh # возобновить
# PERF_SCALES=1 PERF_RUN_TAG=20261026 PERF_STEPS=control4x,aggregate \
# PERF_CONTROL_TARGETS='FORM_2/AHR/null@y,modal:sheet_select_vsp' perf/run-all.sh # контроль CPU 4x
#
# Переменные:
# PERF_SCALES масштабы по порядку (по умолчанию "1 2")
# PERF_STEPS шаги через запятую/пробел (по умолчанию все, кроме control4x); выполняются в каноническом порядке
# PERF_SKIP_RESET=1 не пересоздавать данные: шаги reset и gen пропускаются (SKIP), даже если перечислены
# PERF_RUN_TAG префикс run_id (по умолчанию дата YYYYMMDD); run_id = <tag>-s<scale>, каталог perf/results/<run_id>/
# PERF_RUN_ID_S1 / PERF_RUN_ID_S2 явный run_id масштаба (вместо <tag>-s<scale>)
# PERF_S2_REPEATS повторы скролла на масштабе 2 (по умолчанию 3, как на масштабе 1; при 1 - без trace)
# PERF_S2_TARGETS glob(ы) PERF_TARGETS для скролла на масштабе 2 (по умолчанию все)
# PERF_BENCH_REPEATS повторы bench (20)
# PERF_CONTROL_TARGETS шаг control4x: список через запятую; `modal:<id>` - модалка, `coauthor:<glob>` - скролл при
# соавторе, остальное - glob PERF_TARGETS скролла. CPU 4x, 1 повтор, без trace.
# PERF_CONTROL_ROLES роли скролла в control4x (по умолчанию rf,dfip: rf - листы и отчёты, dfip - своды)
# PERF_DRY_RUN=1 только план
# Сквозные переменные сценариев (PERF_LOAD_TIMEOUT_MS, PERF_TEST_TIMEOUT_MS, PERF_TEST_TIMEOUT_SVOD2_MS, PERF_MAX_STEPS)
# передаются как есть; матричные (PERF_TARGETS, PERF_REPEATS, PERF_CPU_RATES, PERF_ROLES, PERF_MODALS, PERF_PRIORITY...)
# из окружения вызывающего игнорируются - их задаёт шаг.
#
# Лог: perf/results/<run_id>/run-all.log (START/END шага со временем, кодом возврата и длительностью, вывод команд).
# Статус: perf/results/<run_id>/run-all.status.json (по шагу: OK | FAIL | SKIP, rc, время). Падение шага не
# останавливает прогон; падение reset, gen или stand прекращает текущий масштаб (без данных/стенда дальше нет смысла).
# Код выхода: 0 - все шаги OK/SKIP, 1 - был FAIL.
set -uo pipefail
PERF="$(cd "$(dirname "$0")" && pwd)"
E2E="$PERF/e2e"
PY="$PERF/.venv/bin/python"
WEB_URL="http://localhost:8080"
PROF_URL="http://localhost:8081"
API_URL="${PERF_API:-http://localhost:8000}"
SCALES="${PERF_SCALES:-1 2}"
RUN_TAG="${PERF_RUN_TAG:-$(date +%Y%m%d)}"
S2_REPEATS="${PERF_S2_REPEATS:-3}"
S2_TARGETS="${PERF_S2_TARGETS:-}"
BENCH_REPEATS="${PERF_BENCH_REPEATS:-20}"
CONTROL_TARGETS="${PERF_CONTROL_TARGETS:-}"
CONTROL_ROLES="${PERF_CONTROL_ROLES:-rf,dfip}"
DRY="${PERF_DRY_RUN:-0}"
SKIP_RESET="${PERF_SKIP_RESET:-0}"
# канонический порядок: grow меняет размеры листов FORM_2 AHR_UTILITY/AHR_SECURITY и FORM_4 CAP,
# поэтому скролл и соавтор - до grow, модалки (малые и @big) - после (design.md, R-2)
ALL_STEPS="reset gen verify stand preflight scroll coauthor grow modals modals_profiling memory memtrend control4x bench aggregate"
DEFAULT_STEPS="reset gen verify stand preflight scroll coauthor grow modals modals_profiling memory memtrend bench aggregate"
FATAL_STEPS="reset gen stand"
STEPS="$(echo "${PERF_STEPS:-$DEFAULT_STEPS}" | tr ',' ' ')"
for s in $STEPS; do
case " $ALL_STEPS " in *" $s "*) ;; *) echo "неизвестный шаг: $s (есть: $ALL_STEPS)" >&2; exit 2 ;; esac
done
# переменные матрицы сценариев, которые шаг задаёт сам (из окружения вызывающего не наследуются)
CLEAN_ENV="env -u PERF_TARGETS -u PERF_REPEATS -u PERF_CPU_RATES -u PERF_ROLES -u PERF_AXES -u PERF_MODALS -u PERF_PRIORITY -u PERF_PROFILING -u PERF_SQLSTATS_TAG -u PERF_SQLSTATS -u PERF_COAUTHOR_RATES -u PERF_TRACE -u PERF_RESULTS_DIR -u PERF_MEM_CYCLES -u PERF_MEM_SNAPSHOTS"
# Оценка длительности, мин (по замерам Task 3, 5-9 на масштабе 1, без троттлинга; масштаб 2 - экстраполяция:
# листы x2 строк, отчёты проектов x2 штук, свод 2 ~x4 строк). Подробно - task-10a-report.md / README.
estimate() { # step scale -> минуты (целое)
local st="$1" sc="$2"
if [ "$sc" = 1 ]; then
case "$st" in
reset) echo 2 ;; gen) echo 12 ;; verify) echo 1 ;; stand) echo 1 ;; preflight) echo 1 ;;
scroll) echo 250 ;; coauthor) echo 13 ;; grow) echo 4 ;; modals) echo 10 ;; modals_profiling) echo 3 ;;
memory) echo 60 ;; memtrend) echo 0 ;; bench) echo 30 ;; aggregate) echo 1 ;;
control4x) control_estimate ;;
esac
else
case "$st" in
reset) echo 2 ;; gen) echo 60 ;; verify) echo 2 ;; stand) echo 1 ;; preflight) echo 1 ;;
scroll) echo $((200 * S2_REPEATS + 50)) ;; grow) echo 4 ;; modals) echo 12 ;;
bench) echo 45 ;; aggregate) echo 1 ;; control4x) control_estimate ;;
*) echo 0 ;;
esac
fi
}
control_estimate() { # ~5 мин на цель скролла/модалки на CPU 4 (свод 2 - до 40 мин)
local n=0; IFS=',' read -ra arr <<<"$CONTROL_TARGETS"; for t in "${arr[@]}"; do [ -n "$t" ] && n=$((n + 1)); done
echo $((n * 5))
}
only_scale1() { case "$1" in coauthor|modals_profiling|memory|memtrend) return 0 ;; esac; return 1; }
hours() { echo "$(( $1 / 60 )).$(( ($1 % 60) * 10 / 60 ))"; }
ts() { date '+%Y-%m-%dT%H:%M:%S%z'; }
pw() { # "<env assignments>" <spec> -> строка команды Playwright
echo "cd '$E2E' && $CLEAN_ENV PERF_SCALE=$SCALE PERF_RUN_ID=$RID PERF_WEB=${3:-$WEB_URL} $1 npx playwright test $2"
}
# Команда шага (строка для eval). Пустая строка - шаг не применим (SKIP).
step_cmd() {
local st="$1"
case "$st" in
reset) echo "'$PERF/stand.sh' reset" ;;
gen) echo "cd '$PERF' && '$PY' -m perfkit.gen.run --scale $SCALE" ;;
verify) echo "cd '$PERF' && '$PY' -m perfkit.gen.verify --scale $SCALE" ;;
stand)
# api и prod-сборка фронта на :8080 (index.html совпадает с perf/.build/prod), smoke (логин, proxy /api, WS)
echo "(curl -fsS -o /dev/null '$API_URL/healthz' || '$PERF/stand.sh' api)" \
"&& ([ -f '$PERF/.build/prod/index.html' ] || '$PERF/stand.sh' web-build)" \
"&& (curl -fsS -o /dev/null '$WEB_URL/' || '$PERF/stand.sh' web)" \
"&& curl -fsS '$WEB_URL/' | cmp -s - '$PERF/.build/prod/index.html'" \
"&& echo 'web :8080 = prod-сборка' && '$PERF/stand.sh' smoke" ;;
preflight)
# список тестов каждого сценария на манифесте масштаба: ловит ошибки манифеста и дубли названий до часов прогона
local specs="scenarios/scroll.spec.ts scenarios/modals.spec.ts"
[ "$SCALE" = 1 ] && specs="$specs scenarios/scroll-coauthor.spec.ts scenarios/memory.spec.ts"
echo "cd '$E2E' && for f in $specs; do $CLEAN_ENV PERF_SCALE=$SCALE PERF_PRIORITY=1,2 npx playwright test --list \$f | tail -1 || exit 1; done" ;;
scroll)
if [ "$SCALE" = 1 ]; then
pw "PERF_SQLSTATS_TAG=scroll" scenarios/scroll.spec.ts
else
local extra="PERF_REPEATS=$S2_REPEATS"
[ "$S2_REPEATS" = 1 ] && extra="$extra PERF_TRACE=0"
[ -n "$S2_TARGETS" ] && extra="$extra PERF_TARGETS='$S2_TARGETS'"
pw "PERF_SQLSTATS_TAG=scroll $extra" scenarios/scroll.spec.ts
fi ;;
coauthor) pw "PERF_SQLSTATS_TAG=coauthor PERF_COAUTHOR_RATES=1,5" scenarios/scroll-coauthor.spec.ts ;;
grow) echo "cd '$PERF' && '$PY' -m perfkit.gen.grow --scale $SCALE" ;;
modals) pw "PERF_SQLSTATS_TAG=modals PERF_PRIORITY=1,2" scenarios/modals.spec.ts ;;
modals_profiling)
echo "'$PERF/stand.sh' web-profiling && $(pw "PERF_SQLSTATS_TAG=modals_profiling PERF_PROFILING=1 PERF_PRIORITY=1" scenarios/modals.spec.ts "$PROF_URL")" ;;
memory) pw "PERF_SQLSTATS_TAG=memory" scenarios/memory.spec.ts ;;
memtrend) echo "cd '$PERF' && '$PY' -m perfkit.memtrend 'results/$RID' | tee 'results/$RID/memtrend.txt'" ;;
control4x) control_cmd ;;
bench) echo "cd '$PERF' && '$PY' -m perfkit.bench 'results/$RID' --repeats $BENCH_REPEATS --scale $SCALE" ;;
aggregate)
# в report-data/<run_id>/ (коммитится): summary.json/md, modals-inventory.md, sql/<шаг>/{sql_top.json,explain/},
# memtrend.txt, manifest/gen-stats/grow масштаба, run-all.status.json. Сырое (traces, snapshots, jsonl) - в results/.
local out="$PERF/report-data/$RID"
echo "cd '$PERF' && '$PY' -m perfkit.aggregate 'results/$RID'" \
"&& { [ ! -f 'results/$RID/modal.jsonl' ] || '$PY' -m perfkit.modals_inventory 'results/$RID'; }" \
"&& mkdir -p '$out' && for d in results/$RID/sql/*/; do [ -d \"\$d\" ] || continue; t=\$(basename \"\$d\");" \
"mkdir -p '$out/sql/'\$t && cp \"\$d\"sql_top.json '$out/sql/'\$t/ 2>/dev/null;" \
"[ -d \"\$d\"explain ] && cp -R \"\$d\"explain '$out/sql/'\$t/; done;" \
"for f in results/$RID/memtrend.txt results/manifest-s$SCALE.json results/gen-stats-s$SCALE.json results/grow-s$SCALE.json results/$RID/run-all.status.json;" \
"do [ -f \"\$f\" ] && cp \"\$f\" '$out/'; done; ls -R '$out'" ;;
esac
}
# control4x: цели из PERF_CONTROL_TARGETS по видам, по одному вызову Playwright на вид; CPU 4, 1 повтор, без trace
control_cmd() {
[ -n "$CONTROL_TARGETS" ] || { echo "echo 'PERF_CONTROL_TARGETS пуст: нечего гонять' >&2; exit 1"; return; }
local scroll="" modal="" coauthor="" t parts=()
IFS=',' read -ra arr <<<"$CONTROL_TARGETS"
for t in "${arr[@]}"; do
t="$(echo "$t" | xargs)"; [ -n "$t" ] || continue
case "$t" in
modal:*) modal="${modal:+$modal,}${t#modal:}" ;;
coauthor:*) coauthor="${coauthor:+$coauthor,}${t#coauthor:}" ;;
*) scroll="${scroll:+$scroll,}$t" ;;
esac
done
local common="PERF_CPU_RATES=4 PERF_REPEATS=1 PERF_TRACE=0"
[ -n "$scroll" ] && parts+=("$(pw "PERF_SQLSTATS_TAG=control4x_scroll $common PERF_ROLES=$CONTROL_ROLES PERF_TARGETS='$scroll'" scenarios/scroll.spec.ts)")
[ -n "$coauthor" ] && parts+=("$(pw "PERF_SQLSTATS_TAG=control4x_coauthor $common PERF_TARGETS='$coauthor'" scenarios/scroll-coauthor.spec.ts)")
[ -n "$modal" ] && parts+=("$(pw "PERF_SQLSTATS_TAG=control4x_modals $common PERF_PRIORITY=1,2 PERF_MODALS='$modal'" scenarios/modals.spec.ts)")
# все части выполняются, код возврата - последней упавшей
local joined="rc=0" p
for p in "${parts[@]}"; do joined="$joined; ( $p ) || rc=\$?"; done
echo "$joined; exit \$rc"
}
status_update() { # step status rc start end cmd
"$PY" - "$STATUS" "$RID" "$SCALE" "$@" <<'EOF'
import json, sys, pathlib
f, rid, scale, step, status, rc, start, end, dur, cmd = sys.argv[1:]
p = pathlib.Path(f)
d = json.loads(p.read_text(encoding="utf-8")) if p.exists() else {"run_id": rid, "scale": int(scale), "steps": {}}
d["steps"][step] = {"status": status, "rc": int(rc), "start": start, "end": end, "duration_s": int(dur), "cmd": cmd}
p.write_text(json.dumps(d, ensure_ascii=False, indent=2), encoding="utf-8")
EOF
}
log() { echo "[$(ts)] $*" | tee -a "$LOG"; }
total_all=0
anyfail=0
s2total=0
for SCALE in $SCALES; do
rid_var="PERF_RUN_ID_S$SCALE"; RID="${!rid_var:-$RUN_TAG-s$SCALE}"
RDIR="$PERF/results/$RID"
LOG="$RDIR/run-all.log"
STATUS="$RDIR/run-all.status.json"
if [ "$DRY" = 1 ]; then
echo "=== масштаб $SCALE, run_id $RID (perf/results/$RID) ==="
total=0
for st in $ALL_STEPS; do
case " $STEPS " in *" $st "*) ;; *) continue ;; esac
if only_scale1 "$st" && [ "$SCALE" != 1 ]; then echo " SKIP $st (только масштаб 1)"; continue; fi
if [ "$SKIP_RESET" = 1 ] && { [ "$st" = reset ] || [ "$st" = gen ]; }; then echo " SKIP $st (PERF_SKIP_RESET=1)"; continue; fi
m="$(estimate "$st" "$SCALE")"; total=$((total + m))
printf ' %-17s ~%4s мин %s\n' "$st" "$m" "$(step_cmd "$st")"
done
printf ' ИТОГО масштаб %s: ~%s мин (~%s ч)\n\n' "$SCALE" "$total" "$(hours "$total")"
total_all=$((total_all + total))
[ "$SCALE" = 2 ] && s2total=$total
continue
fi
mkdir -p "$RDIR"
log "=== run-all: масштаб $SCALE, run_id $RID, шаги: $STEPS"
abort=""
for st in $ALL_STEPS; do
case " $STEPS " in *" $st "*) ;; *) continue ;; esac
now="$(ts)"
if [ -n "$abort" ]; then anyfail=1; log "SKIP $st (прервано: упал $abort)"; status_update "$st" SKIP -1 "$now" "$now" 0 "aborted after $abort"; continue; fi
if only_scale1 "$st" && [ "$SCALE" != 1 ]; then log "SKIP $st (только масштаб 1)"; status_update "$st" SKIP 0 "$now" "$now" 0 "scale 1 only"; continue; fi
if [ "$SKIP_RESET" = 1 ] && { [ "$st" = reset ] || [ "$st" = gen ]; }; then log "SKIP $st (PERF_SKIP_RESET=1)"; status_update "$st" SKIP 0 "$now" "$now" 0 "PERF_SKIP_RESET=1"; continue; fi
cmd="$(step_cmd "$st")"
log "START $st (оценка ~$(estimate "$st" "$SCALE") мин): $cmd"
t0=$(date +%s)
(eval "$cmd") >>"$LOG" 2>&1 </dev/null
rc=$?
dur=$(( $(date +%s) - t0 ))
if [ $rc -eq 0 ]; then res=OK; else res=FAIL; anyfail=1; fi
log "END $st $res rc=$rc ${dur}s"
status_update "$st" "$res" "$rc" "$now" "$(ts)" "$dur" "$cmd"
if [ $rc -ne 0 ]; then
case " $FATAL_STEPS " in *" $st "*) abort="$st"; log "FATAL: $st упал, остальные шаги масштаба $SCALE пропускаются" ;; esac
fi
done
# итоговый статус - и в report-data (если aggregate уже создал каталог)
[ -d "$PERF/report-data/$RID" ] && cp "$STATUS" "$PERF/report-data/$RID/"
log "=== масштаб $SCALE готов: $(jq -r '[.steps|to_entries[]|"\(.key)=\(.value.status)"]|join(" ")' "$STATUS" 2>/dev/null)"
done
if [ "$DRY" = 1 ]; then
printf 'ИТОГО: ~%s мин (~%s ч)\n' "$total_all" "$(hours "$total_all")"
[ "$S2_REPEATS" = 3 ] && case " $SCALES " in *" 2 "*)
s2rest=$(( s2total - (200 * S2_REPEATS + 50) ))
echo "Рекомендация: масштаб 2 при 3 повторах ~$(hours "$s2total") ч (> 6 ч). PERF_S2_REPEATS=2 -> ~$(hours $((s2rest + 450))) ч (n=1 без trace на группу)," \
"PERF_S2_REPEATS=1 -> ~$(hours $((s2rest + 250))) ч (n=1, trace не снимается). Крупнейшая часть - свод 2 (~4x строк). Рулинг - за контроллером." ;;
esac
fi
exit $anyfail