Andrey Epifancev 05d1586006 perf: исправления run-all по ревью (раунд 1)
- C1: trace только на повторе 0 при числе повторов > 1 (lib/trace.ts
  shouldTrace) в scroll и scroll-coauthor: admin-контроль, control4x и
  PERF_S2_REPEATS=1 больше не теряют единственную запись (traced=1).
- I1: масштаб 2 - PERF_LOAD_TIMEOUT_MS=40 мин и PERF_TEST_TIMEOUT_MS=90 мин,
  если не заданы.
- I2: шаг aggregate падает при сбое perfkit.aggregate/копирования и если
  summary.json не создан; тесты run-all (perf/tests/test_run_all.py).
- M2: после modals_profiling останавливается preview :8081.
- README, design.md: решения раунда.

Refs #202

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

253 lines
17 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_PY:-$PERF/.venv/bin/python}" # python для perfkit (подмена - только тесты run-all)
STATUS_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
# масштаб 2: свод 2 ~150-190 МБ - ожидание загрузки 40 мин и таймаут замера 90 мин, если не заданы пользователем
local s2=""
[ "$SCALE" = 2 ] && s2="PERF_LOAD_TIMEOUT_MS=${PERF_LOAD_TIMEOUT_MS:-2400000} PERF_TEST_TIMEOUT_MS=${PERF_TEST_TIMEOUT_MS:-5400000} "
echo "cd '$E2E' && $CLEAN_ENV PERF_SCALE=$SCALE PERF_RUN_ID=$RID PERF_WEB=${3:-$WEB_URL} $s2$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)
# после прогона профилирующий preview на :8081 останавливается (prod :8080 и api :8000 не трогаются)
local pid="$PERF/.run/web-profiling.pid"
echo "rc=0; '$PERF/stand.sh' web-profiling && ( $(pw "PERF_SQLSTATS_TAG=modals_profiling PERF_PROFILING=1 PERF_PRIORITY=1" scenarios/modals.spec.ts "$PROF_URL") ) || rc=\$?;" \
"if [ -f '$pid' ]; then pkill -P \$(cat '$pid') 2>/dev/null; kill \$(cat '$pid') 2>/dev/null; rm -f '$pid'; fi; exit \$rc" ;;
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"
# Любой сбой - FAIL шага: цепочка через && и || exit 1, в конце проверка, что summary.json создан этим шагом
# (каталог report-data при возобновлении может остаться от прошлого прогона, поэтому старый summary.json удаляется).
echo "cd '$PERF' && rm -f '$out/summary.json' && '$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 if [ -d \"\$d\" ]; then t=\$(basename \"\$d\");" \
"mkdir -p '$out/sql/'\$t || exit 1; if [ -f \"\$d\"sql_top.json ]; then cp \"\$d\"sql_top.json '$out/sql/'\$t/ || exit 1; fi;" \
"if [ -d \"\$d\"explain ]; then cp -R \"\$d\"explain '$out/sql/'\$t/ || exit 1; fi; fi; 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 if [ -f \"\$f\" ]; then cp \"\$f\" '$out/' || exit 1; fi; done" \
"&& ls -R '$out' && test -f '$out/summary.json'" ;;
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
"$STATUS_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