Files

122 lines
3.7 KiB
Python

#!/usr/bin/env python3
"""
measure.py - launches one framework-benchmarks app, times cold startup, and samples RSS
right after the app answers its first successful request.
No external dependencies: stdlib only, so the measurement script itself is auditable
without installing anything. See docs/output/ for the real transcripts this script produced.
Usage:
measure.py <label> <port> <ready-path> <runs> -- <command...>
Prints one JSON line per run to stdout, plus a final "summary" line. The first run is
discarded (cold filesystem cache) exactly as the file-cache warm-up convention that
independent Java framework benchmarks (e.g. gillius.org's Java 25 framework comparison)
use, and the summary averages the remaining runs.
"""
import json
import subprocess
import sys
import time
import urllib.request
import urllib.error
import signal
import os
def wait_ready(url, timeout_s=90):
deadline = time.monotonic() + timeout_s
while time.monotonic() < deadline:
try:
with urllib.request.urlopen(url, timeout=0.5) as resp:
if resp.status == 200:
return True
except (urllib.error.URLError, ConnectionError, OSError, TimeoutError):
pass
time.sleep(0.01)
return False
def rss_kb(pid):
try:
with open(f"/proc/{pid}/status") as f:
for line in f:
if line.startswith("VmRSS:"):
return int(line.split()[1])
except FileNotFoundError:
return None
return None
def run_once(label, port, ready_path, cmd, cwd):
url = f"http://127.0.0.1:{port}{ready_path}"
start = time.monotonic()
proc = subprocess.Popen(
cmd, cwd=cwd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
start_new_session=True,
)
ok = wait_ready(url)
elapsed = time.monotonic() - start
rss = rss_kb(proc.pid) if ok else None
# one more request after a short settle, to confirm the app is actually serving,
# not just that the socket accepted a connection
served_ok = False
if ok:
try:
with urllib.request.urlopen(url, timeout=2) as resp:
served_ok = resp.status == 200
except Exception:
served_ok = False
# terminate the whole process group (session leader), not just the Popen pid -
# several of these apps fork a child JVM/launcher
try:
os.killpg(proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass
proc.wait(timeout=10)
return {
"label": label,
"ready": ok,
"served_ok": served_ok,
"startup_s": round(elapsed, 3) if ok else None,
"rss_kb": rss,
}
def main():
args = sys.argv[1:]
sep = args.index("--")
label, port, ready_path, runs = args[0], int(args[1]), args[2], int(args[3])
cmd = args[sep + 1:]
cwd = os.getcwd()
results = []
for i in range(runs):
r = run_once(label, port, ready_path, cmd, cwd)
r["run"] = i + 1
r["discarded"] = (i == 0)
print(json.dumps(r))
sys.stdout.flush()
results.append(r)
time.sleep(1.5)
kept = [r for r in results if not r["discarded"] and r["ready"]]
if kept:
avg_startup = sum(r["startup_s"] for r in kept) / len(kept)
avg_rss = sum(r["rss_kb"] for r in kept if r["rss_kb"]) / len(kept)
summary = {
"label": label,
"summary": True,
"runs_kept": len(kept),
"avg_startup_s": round(avg_startup, 3),
"avg_rss_kb": round(avg_rss, 1),
"avg_rss_mb": round(avg_rss / 1024, 1),
}
else:
summary = {"label": label, "summary": True, "error": "no successful runs"}
print(json.dumps(summary))
if __name__ == "__main__":
main()