#!/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-control PERF_SKIP_RESET=1 PERF_STEPS=control4x,aggregate \ # PERF_CONTROL_TARGETS='FORM_2/AHR/null@y,svod/svod1_MAIN,svod/svod2_GO,modal:sheet_select_vsp@big' perf/run-all.sh # контроль CPU 4x # ВНИМАНИЕ: control4x с тегом основного прогона (PERF_RUN_TAG=20261026) допишет в results/20261026-s1 и пересоберёт # report-data/20261026-s1 (summary, копии манифеста/gen-stats/grow) - для контроля нужен отдельный тег (design.md). # # Переменные: # PERF_SCALES масштабы по порядку (по умолчанию "1 2") # PERF_STEPS шаги через запятую/пробел (по умолчанию все, кроме control4x); выполняются в каноническом порядке # PERF_SKIP_RESET=1 не пересоздавать данные: шаги reset и gen пропускаются (SKIP), даже если перечислены # PERF_RUN_TAG префикс run_id (по умолчанию дата YYYYMMDD); run_id = -s, каталог perf/results// # PERF_RUN_ID_S1 / PERF_RUN_ID_S2 явный run_id масштаба (вместо -s) # 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:` - модалка, `coauthor:` - скролл при # соавторе, остальное - 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-all.log (START/END шага со временем, кодом возврата и длительностью, вывод команд). # Статус: perf/results//run-all.status.json (по шагу: OK | FAIL | SKIP, rc, время). Падение шага не # останавливает прогон; падение reset, gen или stand прекращает текущий масштаб (без данных/стенда дальше нет смысла). # Код выхода: 0 - все шаги OK/SKIP, 1 - был FAIL. set -uo pipefail PERF="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(dirname "$PERF")" # копия run-all.status.json в report-data - с путями относительно корня репозитория 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() { # "" -> строка команды 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// (коммитится): 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;" \ "do if [ -f \"\$f\" ]; then cp \"\$f\" '$out/' || exit 1; fi; done" \ "&& { [ ! -f 'results/$RID/run-all.status.json' ] || sed 's#$REPO_ROOT/##g' 'results/$RID/run-all.status.json' > '$out/run-all.status.json'; }" \ "&& 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 "$PERF/report-data/$RID/run-all.status.json" 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