- 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>
253 lines
17 KiB
Bash
Executable File
253 lines
17 KiB
Bash
Executable File
#!/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
|