1#!/usr/bin/env python3
2"""Run on the VM to measure the dashboard through its trusted loopback listener."""
3import argparse
4import concurrent.futures
5import json
6from pathlib import Path
7import time
8import urllib.request
9
10
11parser = argparse.ArgumentParser(description=__doc__)
12parser.add_argument("--clients", type=int, default=24)
13parser.add_argument("--rounds", type=int, default=20)
14parser.add_argument("--port", type=int, default=7072)
15parser.add_argument("--unit", default="studio-dashboard.service")
16parser.add_argument("--include-files", action="store_true")
17args = parser.parse_args()
18if args.clients < 1 or args.rounds < 1:
19 parser.error("clients and rounds must be positive")
20
21paths = ["/", "/api/me", "/api/host", "/api/live", "/api/services", "/api/launcher", "/api/status", "/api/storage"]
22paths += [f"/api/metrics/{metric}?range=3600" for metric in ["host.cpu", "host.memory", "service.cpu", "service.memory"]]
23if args.include_files:
24 paths.append("/api/media/list")
25
26
27def request(path):
28 started = time.monotonic()
29 try:
30 request = urllib.request.Request(f"http://127.0.0.1:{args.port}" + path,
31 headers={"User-Name": "benchmark", "User-Groups": "infra-admin"})
32 with urllib.request.urlopen(request, timeout=15) as response:
33 if path == "/api/live":
34 while response.readline().strip():
35 pass
36 else:
37 response.read()
38 error = None
39 except Exception as failure:
40 error = str(failure)
41 return path, time.monotonic() - started, error
42
43
44def cpu():
45 values = dict(line.split() for line in Path(f"/sys/fs/cgroup/system.slice/{args.unit}/cpu.stat").read_text().splitlines())
46 return int(values["usage_usec"]) / 1_000_000
47
48
49for path in paths:
50 request(path)
51started = time.monotonic()
52before = cpu()
53with concurrent.futures.ThreadPoolExecutor(max_workers=args.clients) as pool:
54 results = list(pool.map(request, paths * args.rounds))
55elapsed = time.monotonic() - started
56report = {}
57for path in paths:
58 samples = sorted(duration for route, duration, _ in results if route == path)
59 errors = [error for route, _, error in results if route == path and error]
60 report[path] = {
61 "requests": len(samples),
62 "p50_ms": round(samples[len(samples) // 2] * 1000, 1),
63 "p95_ms": round(samples[min(len(samples) - 1, int(len(samples) * .95))] * 1000, 1),
64 "errors": errors,
65 }
66print(json.dumps({"clients": args.clients, "requests": len(results), "seconds": round(elapsed, 2),
67 "dashboard_cpu_cores": round((cpu() - before) / elapsed, 3), "routes": report}, indent=2))