211 lines
6.3 KiB
Python
211 lines
6.3 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""basic-benchmark: background power/thermal sampler.
|
||
|
|
|
||
|
|
RAPL energy counters are root-only, so run with sudo:
|
||
|
|
|
||
|
|
sudo python3 powerlog.py --tag before --interval 1
|
||
|
|
|
||
|
|
Writes results/power-<tag>.csv at --interval seconds until SIGINT/SIGTERM.
|
||
|
|
Columns: epoch,iso,pkg_w,core_w,cpu_temp_c,cpu_mhz,gpu_w,gpu_temp_c,gpu_busy,fan1,fan2
|
||
|
|
"""
|
||
|
|
import argparse
|
||
|
|
import csv
|
||
|
|
import glob
|
||
|
|
import os
|
||
|
|
import signal
|
||
|
|
import sys
|
||
|
|
import time
|
||
|
|
from datetime import datetime
|
||
|
|
|
||
|
|
|
||
|
|
def rd(path):
|
||
|
|
try:
|
||
|
|
with open(path) as fh:
|
||
|
|
return fh.read().strip()
|
||
|
|
except OSError:
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def rdi(path):
|
||
|
|
try:
|
||
|
|
return int(rd(path))
|
||
|
|
except (TypeError, ValueError):
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def rapl(name):
|
||
|
|
for d in sorted(glob.glob("/sys/class/powercap/intel-rapl:*/")):
|
||
|
|
if rd(os.path.join(d, "name")) == name:
|
||
|
|
energy = os.path.join(d, "energy_uj")
|
||
|
|
if os.path.exists(energy):
|
||
|
|
return energy, rdi(os.path.join(d, "max_energy_range_uj"))
|
||
|
|
return None, None
|
||
|
|
|
||
|
|
|
||
|
|
def hwmon(name):
|
||
|
|
for h in sorted(glob.glob("/sys/class/hwmon/hwmon*")):
|
||
|
|
if rd(os.path.join(h, "name")) == name:
|
||
|
|
return h
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def first(paths):
|
||
|
|
for p in paths:
|
||
|
|
if p and os.path.exists(p):
|
||
|
|
return p
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def amdgpu_hwmons():
|
||
|
|
out = []
|
||
|
|
for h in sorted(glob.glob("/sys/class/hwmon/hwmon*")):
|
||
|
|
if rd(os.path.join(h, "name")) == "amdgpu":
|
||
|
|
out.append(h)
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def pick_gpu_hwmon():
|
||
|
|
"""Prefer the discrete GPU.
|
||
|
|
|
||
|
|
On hybrid AMD systems both the iGPU and the dGPU expose a hwmon named
|
||
|
|
"amdgpu"; the dGPU is the one that reports power1_average. Fall back to the
|
||
|
|
first amdgpu hwmon (and to GPU_DEVPATH, if set) so this stays stable.
|
||
|
|
"""
|
||
|
|
hs = amdgpu_hwmons()
|
||
|
|
want = os.environ.get("GPU_DEVPATH")
|
||
|
|
if want:
|
||
|
|
for h in hs:
|
||
|
|
if os.path.realpath(os.path.join(h, "device")) == os.path.realpath(want):
|
||
|
|
return h
|
||
|
|
for h in hs:
|
||
|
|
if os.path.exists(os.path.join(h, "power1_average")):
|
||
|
|
return h
|
||
|
|
return hs[0] if hs else None
|
||
|
|
|
||
|
|
|
||
|
|
def card_for_device(devpath):
|
||
|
|
"""Match a hwmon device to its /sys/class/drm/cardN node."""
|
||
|
|
if not devpath:
|
||
|
|
return None
|
||
|
|
real = os.path.realpath(devpath)
|
||
|
|
for c in sorted(glob.glob("/sys/class/drm/card[0-9]*")):
|
||
|
|
name = os.path.basename(c)
|
||
|
|
if not (name.startswith("card") and name[4:].isdigit()):
|
||
|
|
continue
|
||
|
|
dev = os.path.join(c, "device")
|
||
|
|
if os.path.exists(dev) and os.path.realpath(dev) == real:
|
||
|
|
return c
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def cpu_temp():
|
||
|
|
for name in ("k10temp", "coretemp", "zenpower"):
|
||
|
|
h = hwmon(name)
|
||
|
|
if not h:
|
||
|
|
continue
|
||
|
|
hit = first(sorted(glob.glob(os.path.join(h, "temp*_input"))))
|
||
|
|
if hit:
|
||
|
|
return hit
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def avg_mhz():
|
||
|
|
freq = [rdi(f) for f in glob.glob("/sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq")]
|
||
|
|
freq = [f for f in freq if f]
|
||
|
|
return int(sum(freq) / len(freq) / 1000) if freq else None
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
ap = argparse.ArgumentParser()
|
||
|
|
ap.add_argument("--tag", required=True)
|
||
|
|
ap.add_argument("--interval", type=float, default=1.0)
|
||
|
|
ap.add_argument("--out", default=None)
|
||
|
|
ap.add_argument("--count", type=int, default=0, help="0 = run until signalled")
|
||
|
|
args = ap.parse_args()
|
||
|
|
|
||
|
|
here = os.path.dirname(os.path.abspath(__file__))
|
||
|
|
out = args.out or os.path.join(here, "results", f"power-{args.tag}.csv")
|
||
|
|
os.makedirs(os.path.dirname(out), exist_ok=True)
|
||
|
|
|
||
|
|
pkg_e, pkg_max = rapl("package-0")
|
||
|
|
core_e, core_max = rapl("core")
|
||
|
|
cpu_t = cpu_temp()
|
||
|
|
gpu_h = pick_gpu_hwmon()
|
||
|
|
gpu_p = os.path.join(gpu_h, "power1_average") if gpu_h else None
|
||
|
|
gpu_t = os.path.join(gpu_h, "temp1_input") if gpu_h else None
|
||
|
|
fan_h = hwmon("nct6798") or hwmon("nct6775")
|
||
|
|
fan1 = os.path.join(fan_h, "fan1_input") if fan_h else None
|
||
|
|
fan2 = os.path.join(fan_h, "fan2_input") if fan_h else None
|
||
|
|
gpu_busy = None
|
||
|
|
gpu_card = card_for_device(os.path.join(gpu_h, "device")) if gpu_h else None
|
||
|
|
if gpu_card:
|
||
|
|
gpu_busy = first([os.path.join(gpu_card, "device", "gpu_busy_percent")])
|
||
|
|
|
||
|
|
if not pkg_e and not core_e:
|
||
|
|
sys.stderr.write("powerlog: no RAPL counters found; run as root\n")
|
||
|
|
|
||
|
|
state = {"stop": False}
|
||
|
|
|
||
|
|
def on_signal(*_):
|
||
|
|
state["stop"] = True
|
||
|
|
|
||
|
|
signal.signal(signal.SIGINT, on_signal)
|
||
|
|
signal.signal(signal.SIGTERM, on_signal)
|
||
|
|
|
||
|
|
prev = {"pkg": None, "core": None}
|
||
|
|
last = time.time()
|
||
|
|
rows = 0
|
||
|
|
|
||
|
|
with open(out, "w", newline="") as fh:
|
||
|
|
w = csv.writer(fh)
|
||
|
|
w.writerow(["epoch", "iso", "pkg_w", "core_w", "cpu_temp_c", "cpu_mhz",
|
||
|
|
"gpu_w", "gpu_temp_c", "gpu_busy", "fan1", "fan2"])
|
||
|
|
fh.flush()
|
||
|
|
|
||
|
|
def watts(key, path, mx):
|
||
|
|
cur = rdi(path) if path else None
|
||
|
|
if cur is None:
|
||
|
|
return None
|
||
|
|
old = prev[key]
|
||
|
|
prev[key] = cur
|
||
|
|
if old is None:
|
||
|
|
return None
|
||
|
|
delta = cur - old
|
||
|
|
if delta < 0 and mx:
|
||
|
|
delta += mx
|
||
|
|
return round(delta / 1e6 / max(now - last, 1e-3), 2)
|
||
|
|
|
||
|
|
while not state["stop"]:
|
||
|
|
now = time.time()
|
||
|
|
pkg_w = watts("pkg", pkg_e, pkg_max)
|
||
|
|
core_w = watts("core", core_e, core_max)
|
||
|
|
gpu_w = None
|
||
|
|
gw = rdi(gpu_p) if gpu_p else None
|
||
|
|
if gw is not None:
|
||
|
|
gpu_w = round(gw / 1e6, 2)
|
||
|
|
temp = rdi(cpu_t) if cpu_t else None
|
||
|
|
gt = rdi(gpu_t) if gpu_t else None
|
||
|
|
w.writerow([
|
||
|
|
int(now),
|
||
|
|
datetime.fromtimestamp(now).isoformat(timespec="seconds"),
|
||
|
|
pkg_w, core_w,
|
||
|
|
round(temp / 1000, 1) if temp is not None else None,
|
||
|
|
avg_mhz(),
|
||
|
|
gpu_w,
|
||
|
|
round(gt / 1000, 1) if gt is not None else None,
|
||
|
|
rdi(gpu_busy) if gpu_busy else None,
|
||
|
|
rdi(fan1) if fan1 else None,
|
||
|
|
rdi(fan2) if fan2 else None,
|
||
|
|
])
|
||
|
|
last = now
|
||
|
|
rows += 1
|
||
|
|
fh.flush()
|
||
|
|
if args.count and rows >= args.count:
|
||
|
|
break
|
||
|
|
time.sleep(max(0.05, args.interval - (time.time() - now)))
|
||
|
|
|
||
|
|
sys.stderr.write(f"powerlog: wrote {rows} samples to {out}\n")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|