Law/scripts/ops/perf_baseline.sh
2026-03-16 21:01:13 +03:00

216 lines
7.2 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT_DIR"
BASE_URL="${1:-http://localhost:8081}"
REPORT_DIR="${REPORT_DIR:-reports/perf}"
ITERATIONS="${PERF_ITERATIONS:-5}"
ADMIN_EMAIL="${PERF_ADMIN_EMAIL:-admin@example.com}"
ADMIN_PASSWORD="${PERF_ADMIN_PASSWORD:-admin123}"
KANBAN_LIMIT="${PERF_KANBAN_LIMIT:-400}"
TS_HUMAN="$(date -u +"%Y-%m-%d %H:%M:%S UTC")"
TS_FILE="$(date -u +"%Y%m%d-%H%M%S")"
REPORT_FILE="${REPORT_DIR}/perf-baseline-${TS_FILE}.md"
mkdir -p "$REPORT_DIR"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "missing command: $1" >&2
exit 1
}
}
require_cmd curl
require_cmd python3
json_escape() {
python3 - "$1" <<'PY'
import json
import sys
print(json.dumps(sys.argv[1]))
PY
}
LOGIN_BODY="$(printf '{"email":%s,"password":%s}' "$(json_escape "$ADMIN_EMAIL")" "$(json_escape "$ADMIN_PASSWORD")")"
LOGIN_RESPONSE_FILE="$TMP_DIR/login.json"
curl -fsS \
-H "Content-Type: application/json" \
-X POST \
-d "$LOGIN_BODY" \
"$BASE_URL/api/admin/auth/login" >"$LOGIN_RESPONSE_FILE"
AUTH_TOKEN="$(python3 - "$LOGIN_RESPONSE_FILE" <<'PY'
import json
import sys
with open(sys.argv[1], "r", encoding="utf-8") as fh:
data = json.load(fh)
token = str(data.get("access_token") or "").strip()
if not token:
raise SystemExit("login did not return access_token")
print(token)
PY
)"
KANBAN_BODY_FILE="$TMP_DIR/kanban.json"
curl -fsS \
-H "Authorization: Bearer $AUTH_TOKEN" \
"$BASE_URL/api/admin/requests/kanban?limit=${KANBAN_LIMIT}&sort_mode=created_newest" >"$KANBAN_BODY_FILE"
REQUEST_ID="$(python3 - "$KANBAN_BODY_FILE" <<'PY'
import json
import sys
with open(sys.argv[1], "r", encoding="utf-8") as fh:
data = json.load(fh)
rows = data.get("rows") or []
if not rows:
raise SystemExit("kanban returned no rows; seed manual data first")
request_id = str((rows[0] or {}).get("id") or "").strip()
if not request_id:
raise SystemExit("kanban first row has no id")
print(request_id)
PY
)"
ATTACHMENTS_BODY="$(printf '{"filters":[{"field":"request_id","op":"=","value":%s}],"sort":[{"field":"created_at","dir":"asc"}],"page":{"limit":500,"offset":0}}' "$(json_escape "$REQUEST_ID")")"
INVOICES_BODY="$(printf '{"filters":[{"field":"request_id","op":"=","value":%s}],"sort":[{"field":"issued_at","dir":"desc"}],"page":{"limit":500,"offset":0}}' "$(json_escape "$REQUEST_ID")")"
measure_endpoint() {
local name="$1"
local method="$2"
local path="$3"
local body="${4:-}"
local headers_file body_file curl_meta status_code total_ms perf_label perf_duration
for run in $(seq 1 "$ITERATIONS"); do
headers_file="$TMP_DIR/${name}-${run}.headers"
body_file="$TMP_DIR/${name}-${run}.body"
if [[ "$method" == "POST" ]]; then
curl_meta="$(curl -sS \
-D "$headers_file" \
-o "$body_file" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-X POST \
-d "$body" \
-w '%{http_code} %{time_total}' \
"$BASE_URL$path")"
else
curl_meta="$(curl -sS \
-D "$headers_file" \
-o "$body_file" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-w '%{http_code} %{time_total}' \
"$BASE_URL$path")"
fi
status_code="$(echo "$curl_meta" | awk '{print $1}')"
total_ms="$(echo "$curl_meta" | awk '{printf "%.2f", $2 * 1000}')"
if [[ "$status_code" != "200" ]]; then
echo "endpoint ${name} failed: HTTP ${status_code}" >&2
cat "$body_file" >&2 || true
exit 1
fi
python3 - "$headers_file" "$name" "$run" "$total_ms" >>"$TMP_DIR/raw.tsv" <<'PY'
import sys
headers_path, name, run, total_ms = sys.argv[1:5]
headers = {}
with open(headers_path, "r", encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if not line or ":" not in line:
continue
key, value = line.split(":", 1)
headers[key.strip().lower()] = value.strip()
print("\t".join([
name,
run,
total_ms,
headers.get("x-perf-label", ""),
headers.get("x-perf-duration-ms", ""),
]))
PY
done
}
: >"$TMP_DIR/raw.tsv"
measure_endpoint "kanban" "GET" "/api/admin/requests/kanban?limit=${KANBAN_LIMIT}&sort_mode=created_newest"
measure_endpoint "request_detail" "GET" "/api/admin/crud/requests/${REQUEST_ID}"
measure_endpoint "chat_messages" "GET" "/api/admin/chat/requests/${REQUEST_ID}/messages"
measure_endpoint "chat_live" "GET" "/api/admin/chat/requests/${REQUEST_ID}/live"
measure_endpoint "status_route" "GET" "/api/admin/requests/${REQUEST_ID}/status-route"
measure_endpoint "attachments_query" "POST" "/api/admin/crud/attachments/query" "$ATTACHMENTS_BODY"
measure_endpoint "invoices_query" "POST" "/api/admin/invoices/query" "$INVOICES_BODY"
python3 - "$TMP_DIR/raw.tsv" "$REPORT_FILE" "$TS_HUMAN" "$BASE_URL" "$REQUEST_ID" "$ITERATIONS" <<'PY'
import csv
import statistics
import sys
from collections import defaultdict
raw_path, report_path, ts_human, base_url, request_id, iterations = sys.argv[1:7]
rows = defaultdict(list)
with open(raw_path, "r", encoding="utf-8") as fh:
reader = csv.reader(fh, delimiter="\t")
for name, run, total_ms, perf_label, perf_duration in reader:
rows[name].append(
{
"run": int(run),
"total_ms": float(total_ms or 0),
"perf_label": perf_label or "-",
"perf_duration_ms": float(perf_duration or 0),
}
)
def percentile(sorted_values, ratio):
if not sorted_values:
return 0.0
if len(sorted_values) == 1:
return sorted_values[0]
index = round((len(sorted_values) - 1) * ratio)
return sorted_values[index]
with open(report_path, "w", encoding="utf-8") as out:
out.write("# Perf Baseline Report\n\n")
out.write(f"- Timestamp: `{ts_human}`\n")
out.write(f"- Base URL: `{base_url}`\n")
out.write(f"- Request ID sample: `{request_id}`\n")
out.write(f"- Iterations per endpoint: `{iterations}`\n\n")
out.write("| Endpoint | Perf Label | Avg Total ms | P95 Total ms | Avg Server ms |\n")
out.write("|---|---|---:|---:|---:|\n")
for name in [
"kanban",
"request_detail",
"chat_messages",
"chat_live",
"status_route",
"attachments_query",
"invoices_query",
]:
items = rows.get(name, [])
totals = sorted(item["total_ms"] for item in items)
servers = [item["perf_duration_ms"] for item in items if item["perf_duration_ms"] > 0]
avg_total = statistics.mean(totals) if totals else 0.0
p95_total = percentile(totals, 0.95)
avg_server = statistics.mean(servers) if servers else 0.0
label = items[0]["perf_label"] if items else "-"
out.write(f"| {name} | `{label}` | {avg_total:.2f} | {p95_total:.2f} | {avg_server:.2f} |\n")
out.write("\n## Raw Runs\n\n")
out.write("| Endpoint | Run | Total ms | Server ms |\n")
out.write("|---|---:|---:|---:|\n")
for name, items in rows.items():
for item in sorted(items, key=lambda value: value["run"]):
out.write(f"| {name} | {item['run']} | {item['total_ms']:.2f} | {item['perf_duration_ms']:.2f} |\n")
PY
echo "report: $REPORT_FILE"