#!/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()