mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
140 lines
6.6 KiB
Python
140 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
|
# MobileGL - tools/device_bench/disagg/freqcheck.py
|
|
# Copyright (c) 2025-2026 MobileGL-Dev
|
|
# Licensed under the GNU Lesser General Public License v3.0:
|
|
# https://www.gnu.org/licenses/gpl-3.0.txt
|
|
# https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
# SPDX-License-Identifier: LGPL-3.0-only
|
|
# End of Source File Header
|
|
|
|
"""freqcheck.py <freq.txt> [pin.txt] - P15 measurement-validity verdict for one run.
|
|
|
|
freq.txt is freq_sampler.sh output; pin.txt is the `pin_clocks.sh show` output taken when the
|
|
session pinned (bench_session.sh writes it). A run is VALID only if every sample shows each CPU
|
|
policy at its pinned frequency, with its scaling_max still at the pin (no cap), and - whenever the
|
|
GPU is busy - the GPU at its pinned frequency, with no kgsl thermal power level or throttling.
|
|
One OPP step of tolerance is allowed: the next available frequency below the pin.
|
|
|
|
Prints one line: VALID|INVALID, per-policy min/max cur freq, GPU min/max (busy samples), peak
|
|
temperatures, and the reasons when invalid. Exit code 0 = valid, 1 = invalid, 2 = no samples.
|
|
|
|
CLOCKS ARE JUDGED OVER THE MEASUREMENT WINDOWS, THERMAL SIGNALS OVER THE WHOLE RUN (P15). When a
|
|
measure_raw.txt sits beside freq.txt (one window per line: <frame0> <frame1> <t_start> <t_end>, in
|
|
the sampler's uptime clock), the clock checks (CPU cur / max, GPU) only read the samples taken inside
|
|
a window, because the rule is "clocks pinned while benchmarking". The sampler starts during warmup,
|
|
and its first sample can catch an idle GPU parked at its lowest OPP. kgsl thermal_pwrlevel and
|
|
throttling are still checked over every sample. The line says which span each check covered.
|
|
"""
|
|
import re
|
|
import sys
|
|
|
|
|
|
def parse(path):
|
|
rows = []
|
|
for line in open(path, encoding="utf-8", errors="replace"):
|
|
kv = dict(re.findall(r"(\w+)=([^\s]+)", line))
|
|
if "t" in kv:
|
|
rows.append(kv)
|
|
return rows
|
|
|
|
|
|
def pins(path):
|
|
"""The INTENDED pin, from pin_clocks.sh's targets (env PIN_CPU_KHZ / PIN_LITTLE_KHZ / PIN_GPU_HZ,
|
|
same defaults) mapped onto each policy's available frequencies - never the caps read back from
|
|
the device, which a frequency daemon may already have lowered."""
|
|
import os
|
|
big = int(os.environ.get("PIN_CPU_KHZ", "2035200"))
|
|
little = int(os.environ.get("PIN_LITTLE_KHZ", "1574400"))
|
|
gpu = int(os.environ.get("PIN_GPU_HZ", "680000000"))
|
|
# The session's profile (pin_clocks.sh writes it on the device; probes save it as profile.txt).
|
|
prof = os.path.join(os.path.dirname(path), "profile.txt") if path else None
|
|
if prof and os.path.exists(prof):
|
|
kv = dict(re.findall(r"(\w+)=(\S+)", open(prof, encoding="utf-8", errors="replace").read()))
|
|
big = int(kv.get("cpu", big)); little = int(kv.get("little", little)); gpu = int(kv.get("gpu", gpu))
|
|
cpu, steps = {}, {}
|
|
if not path:
|
|
return cpu, gpu, steps
|
|
for line in open(path, encoding="utf-8", errors="replace"):
|
|
m = re.search(r"gpu cur=\d+ min=\d+ max=\d+ avail=([\d ]+)", line)
|
|
if m:
|
|
# kgsl power levels index this table from the top (level 0 = fastest).
|
|
steps["gpu"] = sorted((int(x) for x in m.group(1).split()), reverse=True)
|
|
for line in open(path, encoding="utf-8", errors="replace"):
|
|
m = re.search(r"policy(\d+) cur=\d+ min=(\d+) max=(\d+) avail=([\d ]+)", line)
|
|
if m:
|
|
avail = sorted(int(x) for x in m.group(4).split())
|
|
want = little if m.group(1) == "0" else big
|
|
ok = [a for a in avail if a <= want]
|
|
cpu[m.group(1)] = ok[-1] if ok else avail[0]
|
|
steps[m.group(1)] = avail
|
|
return cpu, gpu, steps
|
|
|
|
|
|
def windows(freq_path):
|
|
import os
|
|
path = os.path.join(os.path.dirname(os.path.abspath(freq_path)), "measure_raw.txt")
|
|
spans = []
|
|
if os.path.exists(path):
|
|
for line in open(path, encoding="utf-8", errors="replace"):
|
|
parts = line.split()
|
|
if len(parts) >= 4:
|
|
try:
|
|
spans.append((float(parts[2]), float(parts[3])))
|
|
except ValueError:
|
|
pass
|
|
return spans
|
|
|
|
|
|
def main():
|
|
all_rows = parse(sys.argv[1])
|
|
cpu_pin, gpu_pin, steps = pins(sys.argv[2] if len(sys.argv) > 2 else None)
|
|
if not all_rows:
|
|
print("NO-SAMPLES")
|
|
sys.exit(2)
|
|
spans = windows(sys.argv[1])
|
|
in_window = [r for r in all_rows if any(a <= float(r["t"]) <= b for a, b in spans)]
|
|
rows = in_window if in_window else all_rows
|
|
reasons = []
|
|
out = [f"clocks over {'window' if in_window else 'all'} samples n={len(rows)}/{len(all_rows)}"]
|
|
pols = sorted({k[1:] for k in rows[0] if re.fullmatch(r"c\d+", k)}, key=int)
|
|
for p in pols:
|
|
cur = [int(r["c" + p].split("/")[0]) for r in rows]
|
|
mx = [int(r["c" + p].split("/")[1]) for r in rows]
|
|
out.append(f"c{p}={min(cur) // 1000}-{max(cur) // 1000}MHz")
|
|
pin = cpu_pin.get(p)
|
|
if pin:
|
|
lower = [s for s in steps.get(p, []) if s < pin]
|
|
floor = lower[-1] if lower else pin
|
|
if min(cur) < floor:
|
|
reasons.append(f"c{p} cur {min(cur) // 1000} < pin {pin // 1000}")
|
|
if min(mx) < pin:
|
|
reasons.append(f"c{p} max capped to {min(mx) // 1000}")
|
|
busy = [r for r in rows if int(r.get("busy", "0") or 0) > 0]
|
|
gpus = [int(r["gpu"]) for r in (busy or rows)]
|
|
out.append(f"gpu={min(gpus) // 1000000}-{max(gpus) // 1000000}MHz(busy samples {len(busy)}/{len(rows)})")
|
|
if gpu_pin and busy and min(gpus) < gpu_pin * 0.85:
|
|
reasons.append(f"gpu {min(gpus) // 1000000} < pin {gpu_pin // 1000000}")
|
|
# Thermal signals over EVERY sample, windows or not.
|
|
tpl = max(int(r.get("tpl", 0)) for r in all_rows)
|
|
thr = max(int(r.get("thr", 0)) for r in all_rows)
|
|
if thr:
|
|
reasons.append(f"kgsl throttling {thr}")
|
|
# A devfreq max_freq write (the pin itself) shows up as kgsl's thermal_pwrlevel; only a level
|
|
# whose frequency is below the pin is a cap.
|
|
table = steps.get("gpu", [])
|
|
if tpl and table and tpl < len(table):
|
|
if gpu_pin and table[tpl] < gpu_pin:
|
|
reasons.append(f"gpu thermal_pwrlevel {tpl} = {table[tpl] // 1000000} MHz < pin")
|
|
elif tpl and not table:
|
|
reasons.append(f"thermal_pwrlevel {tpl} (no OPP table to map it)")
|
|
out.append(f"tpl={tpl} thr={thr}")
|
|
peak = lambda k: max(int(r.get(k, 0)) for r in all_rows) / 1000.0
|
|
out.append(f"peak cpu {peak('cpuT'):.1f}C gpu {peak('gpuT'):.1f}C skin {peak('skinT'):.1f}C n={len(all_rows)}")
|
|
verdict = "VALID" if not reasons else "INVALID"
|
|
print(verdict, " ".join(out), ("| " + "; ".join(reasons)) if reasons else "")
|
|
sys.exit(0 if not reasons else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|