basic-benchmark: portable Fedora benchmark kit
Headless + headed suites with power/thermal logging and efficiency reporting. Includes PassMark, llama.cpp, app workloads (LibreOffice/GEGL/Inkscape/GIMP), a GOVERNOR selector, compare-report.py, and captured results.
This commit is contained in:
commit
0cd63d0233
533 changed files with 43679 additions and 0 deletions
238
power-report.py
Executable file
238
power-report.py
Executable file
|
|
@ -0,0 +1,238 @@
|
|||
#!/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
|
||||
pts-core,420
|
||||
"""
|
||||
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()
|
||||
Loading…
Reference in a new issue