Drops the optional RUN_PTS group, the pts-core phase, and every Phoronix reference from the scripts and docs. The suite is now just PassMark, the bundled llama.cpp, the app workloads, and the quick tools.
238 lines
8 KiB
Python
Executable file
238 lines
8 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""basic-benchmark: turn powerlog CSVs into graphs + an efficiency table.
|
|
|
|
python3 power-report.py --tags before-headless
|
|
python3 power-report.py --tags before-headless before-headed # comparison
|
|
|
|
Reads results/power-<tag>.csv (+ optional results/phases-<tag>.csv and
|
|
results/scores-<tag>.csv). Writes graphs to results/graphs/ and prints a
|
|
per-phase power/thermal summary.
|
|
|
|
scores-<tag>.csv format (one benchmark per line, name must match a phase):
|
|
PassMark-cpu,24500
|
|
PassMark-mem,2500
|
|
libreoffice,3.9
|
|
"""
|
|
import argparse
|
|
import csv
|
|
import os
|
|
import statistics as st
|
|
|
|
import matplotlib
|
|
matplotlib.use("Agg")
|
|
import matplotlib.pyplot as plt
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
RES = os.path.join(HERE, "results")
|
|
GRAPHS = os.path.join(RES, "graphs")
|
|
|
|
|
|
def fnum(v):
|
|
try:
|
|
return float(v)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def load_power(tag):
|
|
path = os.path.join(RES, f"power-{tag}.csv")
|
|
if not os.path.exists(path):
|
|
raise SystemExit(f"missing {path}")
|
|
with open(path) as fh:
|
|
rows = list(csv.DictReader(fh))
|
|
return [r for r in rows]
|
|
|
|
|
|
def load_series(rows, key):
|
|
return [(int(r["epoch"]), fnum(r.get(key))) for r in rows]
|
|
|
|
|
|
def load_phases(tag):
|
|
path = os.path.join(RES, f"phases-{tag}.csv")
|
|
out = []
|
|
if os.path.exists(path):
|
|
with open(path) as fh:
|
|
for line in fh:
|
|
a, _, b = line.strip().partition(",")
|
|
try:
|
|
out.append((int(float(a)), b))
|
|
except ValueError:
|
|
pass
|
|
return sorted(out)
|
|
|
|
|
|
def load_scores(tag):
|
|
path = os.path.join(RES, f"scores-{tag}.csv")
|
|
out = {}
|
|
if os.path.exists(path):
|
|
with open(path) as fh:
|
|
for line in fh:
|
|
parts = [p.strip() for p in line.strip().split(",")]
|
|
if len(parts) >= 2 and fnum(parts[1]) is not None:
|
|
out[parts[0]] = fnum(parts[1])
|
|
return out
|
|
|
|
|
|
def load_cpu(tag):
|
|
path = os.path.join(RES, f"cpu-{tag}.txt")
|
|
if os.path.exists(path):
|
|
with open(path) as fh:
|
|
name = fh.read().strip()
|
|
if name:
|
|
return name
|
|
return None
|
|
|
|
|
|
def desc(vals):
|
|
vals = [v for v in vals if v is not None]
|
|
if not vals:
|
|
return None
|
|
srt = sorted(vals)
|
|
return {
|
|
"n": len(vals),
|
|
"mean": st.mean(vals),
|
|
"p95": srt[min(len(srt) - 1, int(len(srt) * 0.95))],
|
|
"max": srt[-1],
|
|
}
|
|
|
|
|
|
def window(series, lo, hi):
|
|
return [v for t, v in series if v is not None and lo <= t < hi]
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--tags", nargs="+", required=True)
|
|
args = ap.parse_args()
|
|
os.makedirs(GRAPHS, exist_ok=True)
|
|
|
|
data = {}
|
|
for tag in args.tags:
|
|
rows = load_power(tag)
|
|
data[tag] = {
|
|
"rows": rows,
|
|
"pkg": load_series(rows, "pkg_w"),
|
|
"core": load_series(rows, "core_w"),
|
|
"gpu": load_series(rows, "gpu_w"),
|
|
"temp": load_series(rows, "cpu_temp_c"),
|
|
"phases": load_phases(tag),
|
|
"scores": load_scores(tag),
|
|
"cpu": load_cpu(tag),
|
|
}
|
|
|
|
for tag in args.tags:
|
|
d = data[tag]
|
|
pkg = d["pkg"]
|
|
if not pkg:
|
|
continue
|
|
t0 = pkg[0][0]
|
|
cpu = d["cpu"] or "unknown CPU"
|
|
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(11, 7), sharex=True)
|
|
for label, series, color in (("package W", d["pkg"], "#c0392b"),
|
|
("core W", d["core"], "#e67e22"),
|
|
("GPU W", d["gpu"], "#2980b9")):
|
|
pts = [((t - t0) / 60, v) for t, v in series if v is not None]
|
|
if pts:
|
|
ax1.plot([p[0] for p in pts], [p[1] for p in pts], label=label, color=color, lw=1)
|
|
ax1.set_ylabel("power (W)")
|
|
ax1.legend(loc="upper right", fontsize=8)
|
|
ax1.set_title(f"power & thermal profile — {tag}\nCPU: {cpu}")
|
|
ax1.grid(alpha=0.3)
|
|
|
|
temps = [((t - t0) / 60, v) for t, v in d["temp"] if v is not None]
|
|
if temps:
|
|
ax2.plot([p[0] for p in temps], [p[1] for p in temps], color="#16a085", lw=1)
|
|
ax2.set_ylabel("CPU temp (°C)")
|
|
ax2.set_xlabel("minutes from start")
|
|
ax2.grid(alpha=0.3)
|
|
|
|
for t, name in d["phases"]:
|
|
for ax in (ax1, ax2):
|
|
ax.axvline((t - t0) / 60, color="0.6", ls="--", lw=0.8)
|
|
ax1.text((t - t0) / 60, ax1.get_ylim()[1], name, rotation=90,
|
|
va="top", ha="right", fontsize=7, color="0.35")
|
|
out = os.path.join(GRAPHS, f"power-{tag}.png")
|
|
fig.tight_layout()
|
|
fig.savefig(out, dpi=110)
|
|
plt.close(fig)
|
|
print(f"graph: {out}")
|
|
|
|
print(f"\n== {tag} — per phase (CPU: {cpu}) ==")
|
|
print(f"{'phase':<22}{'dur s':>7}{'avg W':>7}{'p95 W':>7}{'peak W':>7}"
|
|
f"{'avg °C':>7}{'peak °C':>8}{'energy kJ':>11}")
|
|
ph = d["phases"]
|
|
spans = []
|
|
if ph:
|
|
for i, (t, name) in enumerate(ph):
|
|
hi = ph[i + 1][0] if i + 1 < len(ph) else pkg[-1][0] + 1
|
|
spans.append((name, t, hi))
|
|
else:
|
|
spans.append(("whole run", pkg[0][0], pkg[-1][0] + 1))
|
|
for name, lo, hi in spans:
|
|
pw = window(d["pkg"], lo, hi)
|
|
tp = window(d["temp"], lo, hi)
|
|
s = desc(pw)
|
|
ts = desc(tp)
|
|
if not s:
|
|
continue
|
|
dur = hi - lo
|
|
print(f"{name:<22}{dur:>7}{s['mean']:>7.1f}{s['p95']:>7.1f}{s['max']:>7.1f}"
|
|
f"{(ts['mean'] if ts else 0):>7.1f}{(ts['max'] if ts else 0):>8.1f}"
|
|
f"{s['mean'] * dur / 1000:>11.1f}")
|
|
|
|
if d["scores"]:
|
|
def find_span(key):
|
|
low = key.lower()
|
|
for s in spans:
|
|
if s[0] == key:
|
|
return s
|
|
for s in spans:
|
|
if low in s[0].lower() or s[0].lower() in low:
|
|
return s
|
|
return None
|
|
|
|
print(f"\n== {tag} — efficiency (score / avg package W) ==")
|
|
print(f"{'benchmark':<22}{'score':>12}{'avg W':>8}{'score/W':>10}")
|
|
for name, score in d["scores"].items():
|
|
span = find_span(name)
|
|
if not span:
|
|
print(f"{name:<22}{score:>12.1f}{'no phase match':>18}")
|
|
continue
|
|
s = desc(window(d["pkg"], span[1], span[2]))
|
|
if not s:
|
|
continue
|
|
print(f"{name:<22}{score:>12.1f}{s['mean']:>8.1f}{score / s['mean']:>10.2f}")
|
|
|
|
if len(args.tags) > 1:
|
|
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(11, 7), sharex=True)
|
|
palette = ("#c0392b", "#2980b9", "#27ae60", "#8e44ad", "#d35400", "#16a085")
|
|
for i, tag in enumerate(args.tags):
|
|
color = palette[i % len(palette)]
|
|
d = data[tag]
|
|
if not d["pkg"]:
|
|
continue
|
|
t0 = d["pkg"][0][0]
|
|
pts = [((t - t0) / 60, v) for t, v in d["pkg"] if v is not None]
|
|
ax1.plot([p[0] for p in pts], [p[1] for p in pts], label=tag, color=color, lw=1)
|
|
tt = [((t - t0) / 60, v) for t, v in d["temp"] if v is not None]
|
|
if tt:
|
|
ax2.plot([p[0] for p in tt], [p[1] for p in tt], label=tag, color=color, lw=1)
|
|
ax1.set_ylabel("package W")
|
|
ax1.legend(fontsize=8)
|
|
cpus = [data[t]["cpu"] for t in args.tags if data[t]["cpu"]]
|
|
cpu_note = " | ".join(dict.fromkeys(cpus)) if cpus else "unknown CPU"
|
|
ax1.set_title(f"BEFORE vs AFTER — package power & CPU temp\nCPUs: {cpu_note}")
|
|
ax1.grid(alpha=0.3)
|
|
ax2.set_ylabel("CPU temp (°C)")
|
|
ax2.set_xlabel("minutes from start")
|
|
ax2.legend(fontsize=8)
|
|
ax2.grid(alpha=0.3)
|
|
out = os.path.join(GRAPHS, "compare-before-after.png")
|
|
fig.tight_layout()
|
|
fig.savefig(out, dpi=110)
|
|
plt.close(fig)
|
|
print(f"\ngraph: {out}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|