0ef5fcb1c5
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Waiting to run
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Blocked by required conditions
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Waiting to run
Docker / promote-latest (push) Blocked by required conditions
Init Native E2E / init-native (macos-latest, claude) (push) Waiting to run
Init Native E2E / init-native (macos-latest, codex) (push) Waiting to run
Init Native E2E / init-native (macos-latest, copilot) (push) Waiting to run
Install Native E2E / install-native (macos-latest) (push) Waiting to run
Wrap Native E2E / wrap-native (macos-latest) (push) Waiting to run
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
446 lines
16 KiB
Python
Executable File
446 lines
16 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Tier-3 replay: reproduce Codex /v1/responses compression load.
|
|
|
|
Parses a production proxy log to extract per-session frame-size scenarios,
|
|
generates synthetic payloads matching those sizes/shapes, and concurrently
|
|
drives the proxy's _compress_openai_responses_payload entry point. Reports
|
|
per-frame latency percentiles, timeout count, and total wall time so a
|
|
before/after comparison proves the P2 scheduler fix.
|
|
|
|
Why this lives in scripts/ (not tests/):
|
|
- It is a measurement tool, not a correctness test.
|
|
- It needs to run against multiple branches (main baseline vs fix
|
|
branch) and report comparable numbers.
|
|
- It exercises the *real* compression dispatch by booting a proxy
|
|
instance via create_app() and calling the handler method directly —
|
|
no HTTP/WS layer, because the bug is in the dispatch, not the wire.
|
|
|
|
Usage:
|
|
.venv/bin/python scripts/replay_codex_ws_load.py \\
|
|
--log "/Users/tchopra/Downloads/proxy (1).log" \\
|
|
--concurrency 10 \\
|
|
--frames-per-session 20
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import concurrent.futures
|
|
import json
|
|
import os
|
|
import statistics
|
|
import sys
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
|
|
# Telemetry off so we don't pollute the user's metrics during replay.
|
|
os.environ.setdefault("HEADROOM_DISABLE_TELEMETRY", "true")
|
|
os.environ.setdefault("HEADROOM_REQUIRE_RUST_CORE", "false")
|
|
|
|
|
|
@dataclass
|
|
class Frame:
|
|
bytes_estimate: int
|
|
text_shape: str # plain_text_like | code_fence | traceback | jsonl_like
|
|
|
|
|
|
@dataclass
|
|
class Scenario:
|
|
request_id: str
|
|
frames: list[Frame] = field(default_factory=list)
|
|
|
|
|
|
# ── Log parser ─────────────────────────────────────────────────────────
|
|
|
|
|
|
# Marker columns. We are not using regex here per the design constraints —
|
|
# the log shape is a single deterministic format set by code we own. If
|
|
# the format changes the parser fails loud, not silently.
|
|
_FRAME_TOKEN = " WS /v1/responses "
|
|
_REQID_OPEN = "["
|
|
_REQID_CLOSE = "]"
|
|
|
|
|
|
def _parse_kv(text: str) -> dict[str, str]:
|
|
"""Parse ``key=value`` pairs out of a slow-unit log tail. Stops at the
|
|
first unquoted space after a value. Quoted values not supported because
|
|
the log never emits them; if it ever does, this raises.
|
|
"""
|
|
out: dict[str, str] = {}
|
|
for token in text.split():
|
|
if "=" not in token:
|
|
continue
|
|
k, _, v = token.partition("=")
|
|
out[k] = v
|
|
return out
|
|
|
|
|
|
def parse_log(log_path: Path) -> dict[str, Scenario]:
|
|
"""Group ``WS /v1/responses slow compression unit`` entries by request_id.
|
|
|
|
Each ``slow compression unit`` line carries the per-unit byte count and
|
|
text_shape — exactly what we need to reconstruct a payload of similar
|
|
compression cost. We deliberately ignore the ``compressed`` / ``frame
|
|
compressed`` lines because they report POST-compression bytes, not the
|
|
pre-compression input the dispatcher sees.
|
|
|
|
Format:
|
|
... [hr_..._...] WS /v1/responses slow compression unit elapsed_ms=N
|
|
strategy=X category=Y modified=Z content_type=T text_shape=S
|
|
bytes=B min_bytes=N tokens_before=T tokens_after=T tokens_saved=S
|
|
strategy_chain=[...]
|
|
"""
|
|
scenarios: dict[str, Scenario] = {}
|
|
with log_path.open("r", encoding="utf-8", errors="replace") as fh:
|
|
for line in fh:
|
|
if "slow compression unit" not in line:
|
|
continue
|
|
if _FRAME_TOKEN not in line:
|
|
continue
|
|
req_open = line.find(_REQID_OPEN)
|
|
req_close = line.find(_REQID_CLOSE, req_open + 1)
|
|
if req_open < 0 or req_close < 0:
|
|
continue
|
|
request_id = line[req_open + 1 : req_close]
|
|
tail = line[req_close + 1 :]
|
|
kv = _parse_kv(tail)
|
|
try:
|
|
size = int(kv["bytes"])
|
|
except (KeyError, ValueError):
|
|
continue
|
|
shape = kv.get("text_shape", "plain_text_like")
|
|
scen = scenarios.setdefault(request_id, Scenario(request_id=request_id))
|
|
scen.frames.append(Frame(bytes_estimate=size, text_shape=shape))
|
|
return scenarios
|
|
|
|
|
|
# ── Payload synthesizer ────────────────────────────────────────────────
|
|
|
|
|
|
_LOREM = (
|
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
|
|
"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. "
|
|
)
|
|
_CODE_LINE = "def compute_metric_{i}(value: int) -> int:\n return value * {i}\n\n"
|
|
_TRACEBACK_LINE = (
|
|
' File "/app/handler.py", line {i}, in process_request\n raise RuntimeError(f"oops {i}")\n'
|
|
)
|
|
|
|
|
|
def _text_for_shape(shape: str, target_bytes: int) -> str:
|
|
"""Generate a string roughly ``target_bytes`` long, shaped like the
|
|
production observation. No randomness — same input produces same output
|
|
so the replay is reproducible.
|
|
"""
|
|
if target_bytes < 64:
|
|
# Below size_floor — generator just returns a short token.
|
|
return "ok"
|
|
if shape == "code_fence":
|
|
body_target = max(target_bytes - 12, 0) # "```python\n" + closing
|
|
repeats = max(body_target // 50, 1)
|
|
body = "".join(_CODE_LINE.format(i=i) for i in range(repeats))
|
|
return "```python\n" + body[:body_target] + "\n```"
|
|
if shape == "traceback":
|
|
header = "Traceback (most recent call last):\n"
|
|
body_target = max(target_bytes - len(header), 0)
|
|
repeats = max(body_target // 65, 1)
|
|
body = "".join(_TRACEBACK_LINE.format(i=i) for i in range(repeats))
|
|
return header + body[:body_target]
|
|
# plain_text_like / unknown / jsonl_like → lorem ipsum is fine as a
|
|
# neutral payload; we are measuring scheduler contention, not compressor
|
|
# quality, so the content shape just needs to traverse the same router.
|
|
repeats = max(target_bytes // len(_LOREM), 1)
|
|
raw = _LOREM * repeats
|
|
return raw[:target_bytes]
|
|
|
|
|
|
def synthesize_payload(frame: Frame, turn_no: int) -> dict:
|
|
"""Build the *inner* Responses payload (no `response.create` envelope)
|
|
with one function_call_output of the target byte size.
|
|
|
|
``_compress_openai_responses_payload`` is envelope-agnostic but routes
|
|
by inspecting ``input``/``messages`` at the top level. The WS handler
|
|
extracts ``payload["response"]`` and passes that downstream — we pass
|
|
the same shape directly so the router actually sees compressible
|
|
candidates instead of a single opaque ``response`` key.
|
|
"""
|
|
output_text = _text_for_shape(frame.text_shape, frame.bytes_estimate)
|
|
return {
|
|
"model": "gpt-4o-mini",
|
|
"input": [
|
|
{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": [
|
|
{
|
|
"type": "input_text",
|
|
"text": f"Turn {turn_no} — please summarize.",
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"type": "function_call",
|
|
"call_id": f"call_replay_{turn_no}",
|
|
"name": "shell",
|
|
"arguments": '{"command": "build"}',
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": f"call_replay_{turn_no}",
|
|
"output": output_text,
|
|
},
|
|
],
|
|
"instructions": "Be brief.",
|
|
"max_output_tokens": 30,
|
|
}
|
|
|
|
|
|
# ── Proxy bring-up ─────────────────────────────────────────────────────
|
|
|
|
|
|
def boot_proxy():
|
|
"""Build a HeadroomProxy instance with optimize=True so the compression
|
|
dispatch is actually exercised.
|
|
|
|
This deliberately does NOT start the FastAPI server. We only need the
|
|
in-process handler methods. Lifecycle hooks (background tasks, model
|
|
pre-loading) that fire on startup are not required for the dispatch
|
|
method we exercise — Kompress will lazy-load on first use, which we
|
|
explicitly warm up below.
|
|
"""
|
|
from headroom.proxy.server import ProxyConfig, create_app
|
|
|
|
config = ProxyConfig(
|
|
optimize=True,
|
|
cache_enabled=False,
|
|
rate_limit_enabled=False,
|
|
)
|
|
app = create_app(config)
|
|
return app.state.proxy
|
|
|
|
|
|
def warmup(proxy, model: str = "gpt-4o-mini") -> float:
|
|
"""Issue one small compression call so model weights are loaded.
|
|
|
|
Returns the warmup wall time so the caller can sanity-check the
|
|
measurements (warmup time is NOT counted toward replay metrics).
|
|
"""
|
|
payload = synthesize_payload(
|
|
Frame(bytes_estimate=4096, text_shape="plain_text_like"), turn_no=0
|
|
)
|
|
started = time.perf_counter()
|
|
proxy._compress_openai_responses_payload(payload, model=model, request_id="replay-warmup")
|
|
return (time.perf_counter() - started) * 1000.0
|
|
|
|
|
|
# ── Replay driver ──────────────────────────────────────────────────────
|
|
|
|
|
|
@dataclass
|
|
class FrameResult:
|
|
request_id: str
|
|
frame_index: int
|
|
bytes_in: int
|
|
elapsed_ms: float
|
|
error: str | None = None
|
|
|
|
|
|
def replay_session(proxy, scenario: Scenario, model: str) -> list[FrameResult]:
|
|
out: list[FrameResult] = []
|
|
for idx, frame in enumerate(scenario.frames):
|
|
payload = synthesize_payload(frame, turn_no=idx + 1)
|
|
started = time.perf_counter()
|
|
err: str | None = None
|
|
try:
|
|
proxy._compress_openai_responses_payload(
|
|
payload, model=model, request_id=scenario.request_id
|
|
)
|
|
except Exception as e: # noqa: BLE001 — surface ALL failure modes
|
|
err = f"{type(e).__name__}: {e}"
|
|
elapsed_ms = (time.perf_counter() - started) * 1000.0
|
|
out.append(
|
|
FrameResult(
|
|
request_id=scenario.request_id,
|
|
frame_index=idx,
|
|
bytes_in=frame.bytes_estimate,
|
|
elapsed_ms=elapsed_ms,
|
|
error=err,
|
|
)
|
|
)
|
|
return out
|
|
|
|
|
|
def _percentile(values: list[float], pct: float) -> float:
|
|
if not values:
|
|
return 0.0
|
|
s = sorted(values)
|
|
k = max(0, min(len(s) - 1, int(round(pct / 100.0 * (len(s) - 1)))))
|
|
return s[k]
|
|
|
|
|
|
# ── Reporting ──────────────────────────────────────────────────────────
|
|
|
|
|
|
def print_report(
|
|
results: list[FrameResult],
|
|
wall_time_s: float,
|
|
concurrency: int,
|
|
warmup_ms: float,
|
|
out_json: Path | None,
|
|
) -> None:
|
|
elapsed = [r.elapsed_ms for r in results]
|
|
errors = [r for r in results if r.error]
|
|
total_bytes = sum(r.bytes_in for r in results)
|
|
by_session: dict[str, list[float]] = {}
|
|
for r in results:
|
|
by_session.setdefault(r.request_id, []).append(r.elapsed_ms)
|
|
session_totals = [sum(v) for v in by_session.values()]
|
|
|
|
summary = {
|
|
"concurrency": concurrency,
|
|
"warmup_ms": round(warmup_ms, 1),
|
|
"frames_total": len(results),
|
|
"sessions": len(by_session),
|
|
"wall_time_s": round(wall_time_s, 2),
|
|
"errors": len(errors),
|
|
"error_classes": sorted({type(e.error).__name__: 1 for e in errors if e.error}.keys()),
|
|
"input_bytes_total": total_bytes,
|
|
"per_frame_elapsed_ms": {
|
|
"p50": round(_percentile(elapsed, 50), 1),
|
|
"p90": round(_percentile(elapsed, 90), 1),
|
|
"p99": round(_percentile(elapsed, 99), 1),
|
|
"max": round(max(elapsed) if elapsed else 0.0, 1),
|
|
"mean": round(statistics.mean(elapsed) if elapsed else 0.0, 1),
|
|
},
|
|
"per_session_total_ms": {
|
|
"p50": round(_percentile(session_totals, 50), 1),
|
|
"p90": round(_percentile(session_totals, 90), 1),
|
|
"max": round(max(session_totals) if session_totals else 0.0, 1),
|
|
},
|
|
}
|
|
|
|
print("─── Codex compression replay summary ───")
|
|
print(f"Concurrency: {summary['concurrency']}")
|
|
print(f"Sessions replayed: {summary['sessions']}")
|
|
print(f"Frames replayed: {summary['frames_total']}")
|
|
print(f"Wall time: {summary['wall_time_s']}s")
|
|
print(f"Warmup wall time: {summary['warmup_ms']}ms (NOT counted in metrics)")
|
|
print(f"Failures: {summary['errors']}")
|
|
print(f"Input bytes total: {summary['input_bytes_total']:,}")
|
|
print("Per-frame elapsed_ms:")
|
|
for k, v in summary["per_frame_elapsed_ms"].items():
|
|
print(f" {k:5} {v}")
|
|
print("Per-session total_ms:")
|
|
for k, v in summary["per_session_total_ms"].items():
|
|
print(f" {k:5} {v}")
|
|
if errors:
|
|
print("\nFirst 5 errors:")
|
|
for e in errors[:5]:
|
|
print(f" [{e.request_id}] frame {e.frame_index}: {e.error}")
|
|
|
|
if out_json:
|
|
out_json.write_text(json.dumps(summary, indent=2))
|
|
print(f"\nWrote machine-readable summary to {out_json}")
|
|
|
|
|
|
# ── Main ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
parser.add_argument(
|
|
"--log",
|
|
type=Path,
|
|
required=True,
|
|
help="Path to production proxy log; per-session frame sizes are extracted from "
|
|
"`slow compression unit` lines.",
|
|
)
|
|
parser.add_argument(
|
|
"--concurrency",
|
|
type=int,
|
|
default=10,
|
|
help="Number of concurrent sessions to replay (default: 10).",
|
|
)
|
|
parser.add_argument(
|
|
"--frames-per-session",
|
|
type=int,
|
|
default=20,
|
|
help="Cap frames per session for bounded run-time (default: 20). "
|
|
"Sessions with more frames are truncated; with fewer are padded.",
|
|
)
|
|
parser.add_argument(
|
|
"--model",
|
|
default="gpt-4o-mini",
|
|
help="Model name passed through the dispatcher (default: gpt-4o-mini).",
|
|
)
|
|
parser.add_argument(
|
|
"--out-json",
|
|
type=Path,
|
|
help="Write machine-readable summary JSON here for before/after comparison.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
if not args.log.exists():
|
|
print(f"error: log file not found: {args.log}", file=sys.stderr)
|
|
return 2
|
|
|
|
print(f"[replay] parsing {args.log} ...", flush=True)
|
|
scenarios = parse_log(args.log)
|
|
if not scenarios:
|
|
print(
|
|
"error: no scenarios extracted from log (no `slow compression unit` lines)",
|
|
file=sys.stderr,
|
|
)
|
|
return 2
|
|
|
|
# Pick the top-N sessions by frame count — those exercised the bug
|
|
# hardest in production and give the most representative replay.
|
|
ranked = sorted(scenarios.values(), key=lambda s: -len(s.frames))
|
|
picked = ranked[: args.concurrency]
|
|
# Cap each scenario's frame count for bounded runtime.
|
|
for s in picked:
|
|
s.frames = s.frames[: args.frames_per_session]
|
|
print(
|
|
f"[replay] picked {len(picked)} scenarios "
|
|
f"(total frames: {sum(len(s.frames) for s in picked)})",
|
|
flush=True,
|
|
)
|
|
|
|
print("[replay] booting proxy in-process ...", flush=True)
|
|
proxy = boot_proxy()
|
|
|
|
print("[replay] warming up Kompress + router ...", flush=True)
|
|
warmup_ms = warmup(proxy, model=args.model)
|
|
print(f"[replay] warmup done in {warmup_ms:.1f}ms", flush=True)
|
|
|
|
print(
|
|
f"[replay] starting replay: {len(picked)} concurrent sessions x "
|
|
f"{args.frames_per_session} frames",
|
|
flush=True,
|
|
)
|
|
results: list[FrameResult] = []
|
|
wall_started = time.perf_counter()
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=args.concurrency) as pool:
|
|
futures = [pool.submit(replay_session, proxy, s, args.model) for s in picked]
|
|
for fut in concurrent.futures.as_completed(futures):
|
|
results.extend(fut.result())
|
|
wall_time_s = time.perf_counter() - wall_started
|
|
|
|
print_report(
|
|
results,
|
|
wall_time_s=wall_time_s,
|
|
concurrency=args.concurrency,
|
|
warmup_ms=warmup_ms,
|
|
out_json=args.out_json,
|
|
)
|
|
return 0 if all(r.error is None for r in results) else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|