Files
MapLibreParticleLab/tools/particle_android_validation/compare_step119_cpu.py
tei 32ac795fc4
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
test: benchmark particle-only C FastPath CPU across R layouts
2026-10-11 18:02:33 +09:00

125 lines
5.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""STEP-119 RCRR CPU效益对比;仅整合已采集真机JSON,不会重新启动设备/构建。"""
import argparse
import json
import statistics
from datetime import datetime
from pathlib import Path
def read(path):
data = json.loads(Path(path).read_text(encoding="utf-8"))
if data.get("schema") != "step119-rcrr-cpu-benchmark-v1":
raise ValueError(f"invalid schema: {path}")
return data
def check_common(reports):
ref = reports[0]
for r in reports:
for key in ("sourceCommit", "apkSha256", "device"):
if r.get(key) != ref.get(key):
raise ValueError(f"cross-run {key} mismatch: {r['scenario']}")
for key in ("fingerprint", "actualLayers"):
if r["nativeStart"].get(key) != ref["nativeStart"].get(key):
raise ValueError(f"style/actual layers mismatch {key}: {r['scenario']}")
if r["nativeStart"].get("actualLayers") != 64:
raise ValueError("not 64-layer isolated Test App")
if r["nativeEnd"].get("lifecycle") != r["nativeStart"].get("lifecycle"):
raise ValueError("scene lifecycle changed")
if r.get("nativeCpuBoundaryOffsetMs") is None or max(
r["nativeCpuBoundaryOffsetMs"]) > 9000:
raise ValueError("CPU/Native summary not well-aligned")
if not r.get("parityValid") or not r.get("controlMatched"):
raise ValueError("parity/control mismatch")
if r["nativeEnd"].get("decisionMismatch") != 0:
raise ValueError("FastGate decisionMismatch")
return {"commit": ref["sourceCommit"], "apkSha256": ref["apkSha256"],
"device": ref["device"], "styleFingerprint": ref["nativeStart"]["fingerprint"]}
def summarize(full_before, fast, full_after, others):
inputs = [full_before, fast, full_after, *others]
identity = check_common(inputs)
if full_before["status"] != "FULL_CONTROL_OBSERVED" or full_after[
"status"] != "FULL_CONTROL_OBSERVED":
raise ValueError("Full control run missing/invalid")
if full_before["scenario"] != "RCRR_FULL_CONTROL_NO_TIDE" or full_after[
"scenario"] != "RCRR_FULL_CONTROL_NO_TIDE":
raise ValueError("Full AB control is not the same RCRR scene")
if fast["scenario"] != "RCRR_C_NARROW_NO_TIDE" or fast["status"] != "FAST_DOMINANT":
raise ValueError("fast run has no accepted particle-only-C FastPath")
def value(r, key):
x = r["metrics"].get(key)
if not isinstance(x, (int, float)) or x <= 0:
raise ValueError(f"invalid {key} in {r['scenario']}")
return x
baseline_ms = statistics.median([
value(full_before, "estimatedCpuMsPerNativeFrame"),
value(full_after, "estimatedCpuMsPerNativeFrame"),
])
drift = abs(value(full_before, "cpuCoreEquivalent") -
value(full_after, "cpuCoreEquivalent")) / max(
value(full_before, "cpuCoreEquivalent"), 1e-9)
comparisons = []
for r in [fast, *others]:
if r["status"] != "FAST_DOMINANT":
comparisons.append({"scenario": r["scenario"], "status": "FAST_NOT_STABLE",
"sourceStatus": r["status"]})
continue
ms = value(r, "estimatedCpuMsPerNativeFrame")
comparisons.append({
"scenario": r["scenario"],
"cpuMsPerNativeFrameEstimated": ms,
"cpuMsPerFrameSavingPercentVsFull": 100 * (1 - ms / baseline_ms),
"cpuCoreEquivalent": value(r, "cpuCoreEquivalent"),
"nativeFps": value(r, "nativeObservedFps"),
"fastFraction": r["metrics"]["fastFraction"],
"buildReuse": [
{"segment": s["segment"], "build": s["build"], "reuse": s["reuse"],
"invalidation": s["invalidation"]}
for s in r["nativeEnd"].get("retained", [])
],
})
return {
"schema": "step119-rcrr-cpu-comparison-v1", "identity": identity,
"fullControlCpuMsPerFrameEstimatedMedian": baseline_ms,
"fullControlCpuCoreEquivalent": [value(full_before, "cpuCoreEquivalent"),
value(full_after, "cpuCoreEquivalent")],
"fullControlFrameRate": [value(full_before, "nativeObservedFps"),
value(full_after, "nativeObservedFps")],
"fullBeforeAfterCpuCoreRelativeDrift": drift,
"comparisons": comparisons,
"conclusion": "CPU_DRIFT_REQUIRES_RETEST" if drift > .15 else
"NUMERICAL_COMPARISON_ONLY_NEEDS_VISUAL_AND_THERMAL_VALIDATION",
"caveats": ("同源/同层序A-B-A的进程CPU时间近似对比;Full/Fast FPS不同时必须同时解读CPU/秒、CPU/帧和FPS。"
"CPU指标不等于GPU耗时或电量,未通过人工视觉和失效矩阵不能据此认定正式性能PASS。"),
}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--full-before", required=True)
ap.add_argument("--fast", required=True)
ap.add_argument("--full-after", required=True)
ap.add_argument("--rcr")
ap.add_argument("--rcrrr")
ap.add_argument("--output")
args = ap.parse_args()
others = [read(p) for p in (args.rcr, args.rcrrr) if p]
result = summarize(read(args.full_before), read(args.fast),
read(args.full_after), others)
target = Path(args.output) if args.output else (
Path(__file__).resolve().parents[2] /
"artifacts/maplibre-particle/android-validation/step119-rcrr-cpu" /
f"compare-{datetime.now().strftime('%Y%m%d_%H%M%S')}.json")
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8")
print(json.dumps(result, ensure_ascii=False, indent=2))
print(f"EVIDENCE={target}")
if __name__ == "__main__":
main()