125 lines
5.8 KiB
Python
125 lines
5.8 KiB
Python
#!/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()
|