#!/usr/bin/env bash # lightpanda S3c: up → idle 5min → T4 tiers ×3 → T5 (worker=4) → summarize set -euo pipefail ROOT="$(cd "$(dirname "$0")" && pwd)" # shellcheck disable=SC1091 source "$ROOT/_lib.sh" NAME="bench-s3c-headless-shell" IMG="docker.io/chromedp/headless-shell:151.0.7922.109" CDP_HTTP="${CDP_HTTP:-http://127.0.0.1:19223}" SCHEME="${SCHEME:-chrome-headless-shell}" COMPOSE_PROJ="${COMPOSE_PROJ:-bench-s3c-headless-shell}" T5_PAR="${T5_PAR:-1}" RAW="$ROOT/raw" SAMP="$ROOT/samples" mkdir -p "$RAW" "$SAMP" export CDP_HTTP SCHEME exec > >(tee -a "$RAW/run.log") 2>&1 log "=== $SCHEME S3c start ===" ensure_image "$IMG" "$RAW/pull.how" PULL_HOW="$(cat "$RAW/pull.how" 2>/dev/null || echo unknown)" if [[ "${SKIP_UP:-}" != "1" ]]; then docker rm -f "$NAME" >/dev/null 2>&1 || true log "docker compose up -d project=$COMPOSE_PROJ" (cd "$ROOT" && docker compose -p "$COMPOSE_PROJ" up -d --force-recreate) fi log "wait CDP $CDP_HTTP/json/version" if ! wait_http "$CDP_HTTP/json/version" 40; then log "json/version not ready; try /json/list" wait_http "$CDP_HTTP/json/list" 20 || true docker logs --tail 80 "$NAME" | tee "$RAW/container-start.log" || true fi curl -sS --max-time 5 "$CDP_HTTP/json/version" | tee "$RAW/cdp-version.json" || true echo ARCH="$(docker image inspect --format '{{.Architecture}}' "$IMG" 2>/dev/null || echo '?')" DIGEST="$(docker inspect --format '{{index .RepoDigests 0}}' "$NAME" 2>/dev/null || true)" IID="$(docker inspect --format '{{.Image}}' "$NAME" 2>/dev/null || true)" log "image arch=$ARCH digest=$DIGEST pull=$PULL_HOW" PROXY_CODE="$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 10 --max-time 20 \ -x http://127.0.0.1:17890 -A onesvm-bench/s3c \ https://www.google.com/generate_204 || true)" log "proxy generate_204 http=$PROXY_CODE" HOST_RES="$(docker exec "$NAME" sh -c 'getent hosts host.docker.internal || true' 2>/dev/null || echo unresolved)" log "host.docker.internal => $HOST_RES" if [[ "${SKIP_IDLE:-}" != "1" ]]; then log "idle 300s interval=5" sample_stats "$RAW/idle.tsv" 5 300 "$NAME" log "idle done" else log "SKIP_IDLE=1" fi T4_CTRL="https://quotes.toscrape.com/js/" T4_MEDIUM="https://medium.com/airbnb-engineering/rearchitecting-airbnbs-frontend-b5da7e3e8395" T4_SHOP="https://www.allbirds.com/products/mens-wool-runners" T4_AMZ="https://www.amazon.com/dp/B08N5WRWNW" T4_T3X="https://duckduckgo.com/?q=best+bluetooth+earbuds+2026+reddit" SENT="$RAW/session.done" rm -f "$SENT" sample_until_file "$RAW/session.tsv" 1 "$SENT" 10 "$NAME" & STAT_PID=$! sleep 10 : > "$RAW/t4.jsonl" run_tier() { local tier="$1" url="$2" n="$3" local i for i in $(seq 1 "$n"); do log "$tier trial $i url=$url" set +e line="$(bash "$ROOT/request.sh" "$tier" "$url" 25 2>"$RAW/${tier}-${i}.excerpt")" rc=$? set -e echo "$line" | tee -a "$RAW/t4.jsonl" if [[ "$i" -eq 1 ]]; then cp "$RAW/${tier}-${i}.excerpt" "$SAMP/${tier}.json" 2>/dev/null || true fi log "$tier trial $i rc=$rc" sleep 1 done } run_tier T4-ctrl "$T4_CTRL" 3 run_tier T4-medium "$T4_MEDIUM" 3 run_tier T4-shop "$T4_SHOP" 3 run_tier T4-amz "$T4_AMZ" 3 run_tier T4-t3x "$T4_T3X" 1 touch "$SENT" wait "$STAT_PID" || true log "T5 60x T4-ctrl parallelism=$T5_PAR queue=client_fifo" SENT5="$RAW/t5.done" rm -f "$SENT5" sample_until_file "$RAW/t5-stats.tsv" 1 "$SENT5" 15 "$NAME" & STAT5_PID=$! T5_START_MS=$(python3 -c 'import time;print(int(time.time()*1000))') : > "$RAW/t5.jsonl" : > "$RAW/t5-queue.jsonl" python3 - "$ROOT" "$T4_CTRL" "$T5_PAR" "$T5_START_MS" "$RAW/t5.jsonl" "$RAW/t5-queue.jsonl" <<'PY' import subprocess, time, json, os, sys from concurrent.futures import ThreadPoolExecutor, as_completed root, url, par, t0, outp, qpath = sys.argv[1:7] par = int(par) t0 = int(t0) / 1000.0 n = 60 req = os.path.join(root, "request.sh") def one(i): enq = time.time() p = subprocess.run(["bash", req, "T5", url, "30"], capture_output=True, text=True) end = time.time() line = p.stdout.strip().splitlines()[-1] if p.stdout.strip() else "{}" try: row = json.loads(line) except Exception: row = {"assert_ok": False, "t_total": end - enq, "note": "parse", "http_code": 0} start = end - float(row.get("t_total") or 0) q = { "i": i, "enqueue_s": enq - t0, "start_s": start - t0, "end_s": end - t0, "queue_wait_s": max(0.0, start - enq), "assert_ok": row.get("assert_ok"), "t_total": row.get("t_total"), } return row, q rows = [] qs = [] with ThreadPoolExecutor(max_workers=par) as ex: futs = [ex.submit(one, i) for i in range(1, n + 1)] for f in as_completed(futs): row, q = f.result() rows.append(row) qs.append(q) with open(outp, "w", encoding="utf-8") as f: for r in rows: f.write(json.dumps(r, ensure_ascii=False) + "\n") with open(qpath, "w", encoding="utf-8") as f: for q in qs: f.write(json.dumps(q) + "\n") print(f"t5 done n={len(rows)} ok={sum(1 for r in rows if r.get('assert_ok'))}") PY T5_END=$(date +%s) # wall from tsv start roughly T5_WALL=$(python3 - "$RAW/t5-queue.jsonl" <<'PY' import json,sys from pathlib import Path qs=[json.loads(l) for l in Path(sys.argv[1]).read_text().splitlines() if l.strip()] if not qs: print(0); raise SystemExit print(round(max(q["end_s"] for q in qs), 3)) PY ) log "T5 wall=${T5_WALL}s" touch "$SENT5" wait "$STAT5_PID" || true python3 - "$RAW/t5.jsonl" "$SAMP" <<'PY' import json from pathlib import Path import sys p=Path(sys.argv[1]); d=Path(sys.argv[2]) rows=[json.loads(l) for l in p.read_text().splitlines() if l.strip()] for i,r in enumerate(rows[:2],1): (d/f"t5-{i}.json").write_text(json.dumps(r,ensure_ascii=False,indent=2)+"\n") print("saved t5 samples", len(rows)) PY # html vs text note from first ctrl excerpt python3 - "$SAMP" "$RAW" <<'PY' import json from pathlib import Path samp=Path("$SAMP".replace("$SAMP", __import__("os").environ.get("SAMP",""))) PY OOM="$(docker inspect --format '{{.State.OOMKilled}}' "$NAME" 2>/dev/null || echo false)" STATUS="$(docker inspect --format '{{.State.Status}}' "$NAME" 2>/dev/null || echo unknown)" log "status=$STATUS oom=$OOM" docker logs --tail 50 "$NAME" | tee "$RAW/container-tail.log" || true python3 - <", "") else "$DIGEST", "id": "$IID", "arch": "$ARCH", "pull": "$PULL_HOW", "probed_at": datetime.datetime.now().astimezone().isoformat(timespec="seconds"), "oom": str("$OOM").lower()=="true", "container_status": "$STATUS", "t5_wall_s": float("$T5_WALL" or 0), "t5_parallelism": int("$T5_PAR"), "queue_impl": "client_fifo", "queue": { "first_complete_s": round(min(ends),4) if ends else None, "last_complete_s": round(max(ends),4) if ends else None, "queue_wait_p50_s": pct(waits,50), "queue_wait_p95_s": pct(waits,95), "rejected": 0, }, "proxy": { "mixed": "http://127.0.0.1:17890", "container_via": "http://host.docker.internal:17890", "generate_204_http": "$PROXY_CODE", "host_docker_internal": """$HOST_RES""".strip(), "egress_note": "HTTP(S)_PROXY + extra_hosts host-gateway", }, "commands": [ "docker compose -p $COMPOSE_PROJ up -d", "node cdp_fetch.mjs $CDP_HTTP ", "T5 ThreadPoolExecutor workers=$T5_PAR n=60", ], "notes": ["queue_impl=client_fifo; 生产应是 SQLite WAL + CONCURRENT"], } (raw/"image-meta.json").write_text(json.dumps(meta, ensure_ascii=False, indent=2)+"\n") PY python3 "$ROOT/summarize.py" log "=== $SCHEME S3c done ==="