122 lines
3.7 KiB
Python
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()
|