mirror of
https://github.com/hanzoai/datastore.git
synced 2026-10-30 14:48:37 +00:00
172 lines
5.7 KiB
Python
Executable File
172 lines
5.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Parse ClickHouse tests/performance/scripts/perf.py TSV-ish output."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
def pct(x: float) -> str:
|
|
return f"{x * 100:+.2f}%"
|
|
|
|
|
|
def sec(x: Any) -> str:
|
|
try:
|
|
return f"{float(x):.6g}s"
|
|
except Exception:
|
|
return "—"
|
|
|
|
|
|
def percentile(values: list[float], p: float) -> float:
|
|
if not values:
|
|
return float("nan")
|
|
xs = sorted(values)
|
|
if len(xs) == 1:
|
|
return xs[0]
|
|
pos = (len(xs) - 1) * p
|
|
lo = int(pos)
|
|
hi = min(lo + 1, len(xs) - 1)
|
|
frac = pos - lo
|
|
return xs[lo] * (1 - frac) + xs[hi] * frac
|
|
|
|
|
|
def verdict(diff: float, pvalue: float, threshold: float) -> str:
|
|
if abs(diff) < threshold:
|
|
return "within threshold"
|
|
if pvalue > 0.05:
|
|
return "not statistically significant"
|
|
return "candidate slower" if diff > 0 else "candidate faster"
|
|
|
|
|
|
def parse(path: Path) -> dict[str, Any]:
|
|
threshold = 0.05
|
|
display: dict[int, str] = {}
|
|
query_runs: dict[int, dict[int, list[float]]] = defaultdict(lambda: defaultdict(list))
|
|
diffs = []
|
|
medians = {}
|
|
servers = []
|
|
stages = []
|
|
skipped = []
|
|
partial = []
|
|
profile_total = None
|
|
|
|
for raw in path.read_text(errors="replace").splitlines():
|
|
if not raw.strip():
|
|
continue
|
|
parts = raw.rstrip("\n").split("\t")
|
|
tag = parts[0]
|
|
try:
|
|
if tag == "report-threshold" and len(parts) >= 2:
|
|
threshold = float(parts[1])
|
|
elif tag == "display-name" and len(parts) >= 3:
|
|
display[int(parts[1])] = parts[2].replace("\\n", "\n").replace("\\t", "\t")
|
|
elif tag == "server" and len(parts) >= 6:
|
|
servers.append(parts[1:])
|
|
elif tag == "stage":
|
|
stages.append(parts[1:])
|
|
elif tag == "skipped":
|
|
skipped.append("\t".join(parts[1:]))
|
|
elif tag == "partial" and len(parts) >= 3:
|
|
partial.append((int(parts[1]), parts[2]))
|
|
elif tag == "query" and len(parts) >= 5:
|
|
qidx = int(parts[1])
|
|
conn = int(parts[3])
|
|
elapsed = float(parts[4])
|
|
query_runs[qidx][conn].append(elapsed)
|
|
elif tag == "median" and len(parts) >= 3:
|
|
medians[int(parts[1])] = float(parts[2])
|
|
elif tag == "diff" and len(parts) >= 6:
|
|
qidx = int(parts[1])
|
|
old = float(parts[2])
|
|
new = float(parts[3])
|
|
rel = float(parts[4])
|
|
pvalue = float(parts[5])
|
|
diffs.append({"query": qidx, "old": old, "new": new, "diff": rel, "pvalue": pvalue})
|
|
elif tag == "profile-total" and len(parts) >= 2:
|
|
profile_total = float(parts[1])
|
|
except Exception:
|
|
# Keep parser forgiving; malformed lines are ignored.
|
|
pass
|
|
|
|
return {
|
|
"threshold": threshold,
|
|
"display": display,
|
|
"query_runs": query_runs,
|
|
"diffs": diffs,
|
|
"medians": medians,
|
|
"servers": servers,
|
|
"stages": stages,
|
|
"skipped": skipped,
|
|
"partial": partial,
|
|
"profile_total": profile_total,
|
|
}
|
|
|
|
|
|
def print_report(path: Path, data: dict[str, Any]) -> None:
|
|
print(f"# perf.py result: `{path}`\n")
|
|
print(f"Report threshold: `{pct(data['threshold'])}`\n")
|
|
if data["servers"]:
|
|
print("## Servers\n")
|
|
print("| Index | Host | Port | User | TLS |")
|
|
print("|---:|---|---:|---|---|")
|
|
for s in data["servers"]:
|
|
# perf.py: server idx host port user ssl_status
|
|
print(f"| {s[0]} | `{s[1]}` | {s[2]} | `{s[3]}` | {s[4]} |")
|
|
print()
|
|
|
|
if data["skipped"]:
|
|
print("## Skipped\n")
|
|
for s in data["skipped"]:
|
|
print(f"- {s}")
|
|
print()
|
|
|
|
print("## Diff rows\n")
|
|
if not data["diffs"]:
|
|
print("No `diff` rows. Either only one server was used, too few samples were collected, or no query passed perf.py's diff/p-value gate.\n")
|
|
else:
|
|
print("| Query | Old median | New median | Diff | p-value | Verdict | Query preview |")
|
|
print("|---:|---:|---:|---:|---:|---|---|")
|
|
for d in sorted(data["diffs"], key=lambda x: abs(x["diff"]), reverse=True):
|
|
qidx = d["query"]
|
|
preview = (data["display"].get(qidx) or "").replace("\n", " ")[:160]
|
|
print(
|
|
f"| {qidx} | {sec(d['old'])} | {sec(d['new'])} | {pct(d['diff'])} | "
|
|
f"{d['pvalue']:.4g} | {verdict(d['diff'], d['pvalue'], data['threshold'])} | `{preview}` |"
|
|
)
|
|
print()
|
|
|
|
print("## Per-query sample summary\n")
|
|
print("| Query | Server | Runs | p05 | p50 | p95 | Min | Max |")
|
|
print("|---:|---:|---:|---:|---:|---:|---:|---:|")
|
|
for qidx in sorted(data["query_runs"]):
|
|
for conn in sorted(data["query_runs"][qidx]):
|
|
vals = data["query_runs"][qidx][conn]
|
|
print(
|
|
f"| {qidx} | {conn} | {len(vals)} | {sec(percentile(vals, 0.05))} | "
|
|
f"{sec(percentile(vals, 0.50))} | {sec(percentile(vals, 0.95))} | {sec(min(vals))} | {sec(max(vals))} |"
|
|
)
|
|
print()
|
|
|
|
if data["partial"]:
|
|
print("## Partial queries\n")
|
|
for qidx, conns in data["partial"]:
|
|
print(f"- query {qidx}: ran only on connections `{conns}`")
|
|
print()
|
|
|
|
if data["profile_total"] is not None:
|
|
print(f"Profile total seconds: `{data['profile_total']:.3f}`\n")
|
|
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
ap.add_argument("path", type=Path)
|
|
args = ap.parse_args()
|
|
print_report(args.path, parse(args.path))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|