From e2d202b560ad0165101546597c43f2ef0ccf4542 Mon Sep 17 00:00:00 2001 From: tei Date: Sun, 11 Oct 2026 10:34:08 +0900 Subject: [PATCH] test: prepare STEP-113 PBF Fast Path APK retest --- .../STEP-115_STEP113原PBF专用App复测准备_v1.md | 22 ++ tools/particle_android_validation/README.md | 18 ++ .../verify_step113_fast_path.py | 191 ++++++++++++++++++ 粒子开发记录.md | 2 + 4 files changed, 233 insertions(+) create mode 100644 docs/50_Step记录/STEP-115_STEP113原PBF专用App复测准备_v1.md create mode 100755 tools/particle_android_validation/verify_step113_fast_path.py diff --git a/docs/50_Step记录/STEP-115_STEP113原PBF专用App复测准备_v1.md b/docs/50_Step记录/STEP-115_STEP113原PBF专用App复测准备_v1.md new file mode 100644 index 000000000..693a953f5 --- /dev/null +++ b/docs/50_Step记录/STEP-115_STEP113原PBF专用App复测准备_v1.md @@ -0,0 +1,22 @@ +# STEP-115:STEP-113 原 PBF 专用 APP Fast Path 复测准备 v1 + +日期:2026-10-11(JST)。状态:TEST-PREPARATION COMPLETE / MANUAL ANDROID BUILD REQUIRED / FAST NOT VERIFIED。 + +## 任务意图 + +原 STEP-113 专用 Android Test App 的 PBF STATIC_RC 是 Fast Path 正式验证主体,不以 STEP-114 FAST_SMOKE 或删掉原正式图层的测试代替。STEP-113 的五个场景不修改,不要求 NavSea 113 层完整复刻,但每个结果必须基于真实 Native 的 Fast/Full、缓存 Build/Reuse 以及 Gate 记录。 + +## 本次已核实事实 + +1. Integration main 已 fetch 并确认 80667d6c343411d03a760425438e3d292f452118。Particle 当前 main 为 21a10816435a82de3471bdb55ed83223101f5689。 +2. 现有 APK 的 build-info 显示构建源码 c6d13e1059d464d312ac33acac87967300fa5dc4;APK SHA-256 为 0054248bc3872e8670df4e9a10d11f1d65c0912a0482586cd7a247e880a6eaef,与设备 192.168.200.139:5555 的当前安装包 SHA 一致。这是 STEP-114 修改以前的旧 APK,不能拿来验收新修复。 +3. STEP-114 未解决原 PBF 缺 tide Source、R Symbol Placement 首次 Build 及 C 内建 Annotation Symbol 拒绝,因此不能提前承诺原 STATIC_RC 会进入 Fast。 +4. 新增工具 tools/particle_android_validation/verify_step113_fast_path.py;不修改原 STEP-113 Test App、Native 渲染逻辑、RN 公共 API。工具对版本/安装包 SHA fail-closed,按当前进程 PID 解码现有 STEP113Gate Base64 低频摘要并比较帧计数。 +5. 使用 python3 -B 解码旧设备 Logcat 的 STEP113 SOURCE_CHANGE 摘要成功,历史累计 Fast=0、decisionMismatch=0。旧 APK 执行 --preflight-only 返回 STALE_APK,exit=3,属于**预期的旧包拒绝**,不是“修复后测试失败”。 +6. 没有执行 Android 编译、安装、启动新 APK、完整 120 帧采集、真实 Fast/性能/像素验收。用户需手工完成最新源码的唯一 Validation APK Pipeline,并手工 install -r,之后才能进行新二进制测试。 + +## 通过准则与下一步 + +实际源提交与安装 APK 完全匹配、原 STATIC_RC Native Plan valid、场景 READY、同一进程和场景内 Full/Fast 计数真实变化,FastDelta > 0 方可称 Fast observed。需另外核对 cache Build/Reuse 和 decisionMismatch=0、24 Gate、影像等价及 Source mutation/Annotation fail-closed。若 FastDelta=0,记录具体 Gate/R fallback 后停止将其宣称为 PASS,回到源码定位。原五场景以及 STEP-102 维持保护,非正式发行,不改 Source Lock。 + +步骤见 tools/particle_android_validation/README.md。没有修复完成或新的 APK 时,保持 STEP-114 PARTIAL,本 STEP 仅测试工具准备完成。 diff --git a/tools/particle_android_validation/README.md b/tools/particle_android_validation/README.md index 44ae6c0e1..1e6c7ce90 100644 --- a/tools/particle_android_validation/README.md +++ b/tools/particle_android_validation/README.md @@ -111,3 +111,21 @@ MAPLIBRE_VALIDATION_OUT_DIR=/tmp/my-validation \ - STEP 场景验收。 这样构建产物与上机验证保持分离,避免脚本意外操作设备。 + +## STEP-113 原 PBF 专用 Test App Fast Path 复测 + +本测试不使用 STEP-114 的简化 FAST_SMOKE。保持原 Step113PbfGateScene、PBF URL、63 个正式基础层与原 Native 诊断不变。需要用户手工完成新源码的标准 APK 编译和安装。工具先检查 build-info 中的 Git commit 与当前 HEAD、APK SHA、设备已安装 APK SHA 是否一致;任何不一致即拒绝并说明原因。 + +在 Mac 的 MapLibreParticleLab 根目录依序执行: + + ./tools/particle_android_validation/build_validation_apk.sh + + /Users/tei/Library/Android/sdk/platform-tools/adb -s 192.168.200.139:5555 install -r artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk + + python3 -B tools/particle_android_validation/verify_step113_fast_path.py --preflight-only + + python3 -B tools/particle_android_validation/verify_step113_fast_path.py --scenario STATIC_RC --seconds 75 + +前两项为用户手工 Android 构建和更新安装,不得在未授权情况下代劳。后两项由 RDC 或用户执行。脚本使用明确 ADB serial 启动原有 Activity 的 STEP-113 原场景,按当前 PID 还原 STEP113Gate 的 Base64 Logcat summary,计算实际 Fast/Full 计数差及 R Build/Reuse,保存到忽略 Git 的 artifacts/maplibre-particle/android-validation/step113-retest-时间/场景/ 目录。 + +FAST_OBSERVED 仅表示实际 Fast 数字增长,不代表像素等价、性能或所有安全回退已验收;NO_FAST_OBSERVED 表示仍未进入,记录完整 R fallback 和 24 Gate 聚合。STALE_APK / INSUFFICIENT_SAMPLES 不是 PASS。依次需要时用 --scenario PBF63、PARTICLE_OFF、PARTIAL_RCRR、SOURCE_CHANGE。SOURCE_CHANGE 的手动 Source mutation 仍须通过 App 原菜单做独立验证,脚本不会自行修改 Source。阶段事实详见 docs/50_Step记录/STEP-115_STEP113原PBF专用App复测准备_v1.md。 diff --git a/tools/particle_android_validation/verify_step113_fast_path.py b/tools/particle_android_validation/verify_step113_fast_path.py new file mode 100755 index 000000000..90d951751 --- /dev/null +++ b/tools/particle_android_validation/verify_step113_fast_path.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""STEP-113 on-device PBF Fast Path verifier. Does not build, install or alter APKs.""" +import argparse +import base64 +import datetime +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess +import sys +import time + +ROOT = Path(__file__).resolve().parents[2] +ARTIFACTS = ROOT / "artifacts/maplibre-particle/android-validation" +APK = ARTIFACTS / "MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk" +BUILD_INFO = ARTIFACTS / "build-info.txt" +PACKAGE = "org.maplibre.android.testapp" +ACTIVITY = f"{PACKAGE}/{PACKAGE}.activity.particle.ParticleCoreLayerValidationActivity" +SCENARIOS = ("STATIC_RC", "PBF63", "PARTICLE_OFF", "PARTIAL_RCRR", "SOURCE_CHANGE") +LOG_ENTRY = re.compile(r"^[A-Z]/STEP113Gate\(\s*(\d+)\):\s*(\{.+\})\s*$") + + +def call(args, *, check=True): + result = subprocess.run(args, text=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=False) + if check and result.returncode: + raise RuntimeError(f"command returned {result.returncode}: {' '.join(args)}\n" + f"{result.stderr[-1200:]}\n{result.stdout[-1200:]}") + return result.stdout.strip() + + +def sha256(path): + h = hashlib.sha256() + with open(path, "rb") as f: + for block in iter(lambda: f.read(1048576), b""): + h.update(block) + return h.hexdigest() + + +def inspect(apk, adb, serial): + if not apk.is_file() or not BUILD_INFO.is_file(): + raise RuntimeError("缺少 Validation APK/build-info.txt,必须先由用户手工运行标准构建脚本") + info = dict(line.split("=", 1) for line in BUILD_INFO.read_text(encoding="utf-8").splitlines() + if "=" in line) + source_head = call(["git", "-C", str(ROOT), "rev-parse", "HEAD"]) + if info.get("commit") != source_head: + raise RuntimeError(f"STALE_APK: build-info commit={info.get('commit')}, " + f"current HEAD={source_head}; 禁止用旧APK验收本轮修复") + local_sha = sha256(apk) + if local_sha != info.get("sha256"): + raise RuntimeError(f"APK_SHA_MISMATCH: actual={local_sha}, build-info={info.get('sha256')}") + if info.get("abi") != "x86_64": + raise RuntimeError(f"unexpected ABI: {info.get('abi')}") + device = lambda *a: call([adb, "-s", serial, *a]) + abi = device("shell", "getprop", "ro.product.cpu.abi") + if abi != "x86_64": + raise RuntimeError(f"DEVICE_ABI_MISMATCH: {abi}") + paths = device("shell", "pm", "path", PACKAGE).splitlines() + if not paths or not paths[0].startswith("package:"): + raise RuntimeError(f"TEST_APP_NOT_INSTALLED: {PACKAGE}") + remote = paths[0].split("package:", 1)[1].strip() + installed_sha = device("shell", "sha256sum", remote).split()[0] + if installed_sha != local_sha: + raise RuntimeError(f"STALE_INSTALLED_APK: installed={installed_sha} local={local_sha}; " + "用户须核实后手工 adb install -r,不能用旧版本验收") + return info, local_sha, source_head, device + + +def extract(logcat, pid, expected): + chunks = {} + for line in logcat.splitlines(): + found = LOG_ENTRY.match(line.strip()) + if not found or int(found.group(1)) != pid: + continue + try: + item = json.loads(found.group(2)) + if item.get("encoding") != "base64-utf8" or item.get("kind") != "summary": + continue + ident = (item["exportId"], item["kind"]) + record = chunks.setdefault(ident, {"count": item["chunkCount"], "items": {}}) + if record["count"] == item["chunkCount"]: + record["items"][int(item["chunkIndex"])] = item["payload"] + except (KeyError, ValueError, TypeError): + continue + result = [] + for (ident, _), record in chunks.items(): + if len(record["items"]) != record["count"]: + continue + try: + packed = "".join(record["items"][i] for i in range(record["count"])) + sample = json.loads(base64.b64decode(packed, validate=True).decode("utf-8")) + if sample.get("scenario") == expected and sample.get("sceneReady") and not sample.get("blockedReason"): + result.append((int(ident.split("-", 1)[0]), sample)) + except (KeyError, ValueError, TypeError, UnicodeError): + continue + return [x[1] for x in sorted(result, key=lambda item: item[0])] + + +def run(): + parser = argparse.ArgumentParser(description="Check actual STEP-113 PBF on-device Fast frames") + parser.add_argument("--serial", default="192.168.200.139:5555") + parser.add_argument("--adb", default=str(Path.home() / "Library/Android/sdk/platform-tools/adb")) + parser.add_argument("--apk", default=str(APK)) + parser.add_argument("--scenario", choices=SCENARIOS, default="STATIC_RC") + parser.add_argument("--seconds", type=int, default=75) + parser.add_argument("--preflight-only", action="store_true") + args = parser.parse_args() + if args.seconds < 65 and not args.preflight_only: + parser.error("至少65秒以取得首次及下一次30秒周期摘要;不是FPS测量") + if not Path(args.adb).is_file(): + raise RuntimeError(f"adb not found: {args.adb}") + info, local_sha, head, device = inspect(Path(args.apk), args.adb, args.serial) + print(f"APK_SOURCE_HEAD={head}") + print(f"APK_SHA256={local_sha}") + print(f"DEVICE={args.serial} ABI=x86_64 INSTALLED_SHA_MATCH=True") + if args.preflight_only: + print("PREFLIGHT_PASS(仅证明同版本APK已安装,未测试Fast)") + return 0 + + stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + output = ARTIFACTS / f"step113-retest-{stamp}" / args.scenario.lower() + output.mkdir(parents=True, exist_ok=False) + command = ["shell", "am", "start", "-S", "-n", ACTIVITY, + "--es", "step113_scenario", args.scenario] + print(f"START_SCENARIO={args.scenario}") + output.joinpath("launch.txt").write_text(device(*command) + "\n", encoding="utf-8") + process_id = None + for _ in range(12): + time.sleep(1) + pid_text = call([args.adb, "-s", args.serial, "shell", "pidof", PACKAGE], check=False) + if pid_text.strip().split(): + process_id = int(pid_text.strip().split()[0]) + break + if process_id is None: + raise RuntimeError("APP_PROCESS_NOT_RUNNING after launch") + print(f"APP_PID={process_id}") + time.sleep(max(0, args.seconds - 12)) + raw_log = device("logcat", "-d", "-v", "brief", "-s", "STEP113Gate:I", "*:S") + output.joinpath("logcat.txt").write_text(raw_log + "\n", encoding="utf-8") + samples = extract(raw_log, process_id, args.scenario) + if not samples: + status = "NO_READY_SUMMARY" + fast_delta = full_delta = 0 + else: + start, end = samples[0], samples[-1] + fast_delta = end["fastRendered"] - start["fastRendered"] + full_delta = end["fullRendered"] - start["fullRendered"] + if len(samples) < 2: + status = "INSUFFICIENT_SAMPLES" + elif any(s["decisionMismatch"] != 0 for s in samples): + status = "DECISION_MISMATCH" + elif fast_delta > 0: + status = "FAST_OBSERVED" + else: + status = "NO_FAST_OBSERVED" + report = { + "schema": "step113-fast-path-retest", + "sourceCommit": head, + "apkSha256": local_sha, + "installedShaMatched": True, + "device": args.serial, + "appPid": process_id, + "scenario": args.scenario, + "sourceReport": "STEP-113 original PBF test scene; not STEP-114 smoke", + "durationSecondsRequested": args.seconds, + "completeSummarySamples": len(samples), + "status": status, + "fastDelta": fast_delta, + "fullDelta": full_delta, + "initialSummary": samples[0] if samples else None, + "finalSummary": samples[-1] if samples else None, + } + output.joinpath("result.json").write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8") + print(f"STATUS={status} FastDelta={fast_delta} FullDelta={full_delta} Samples={len(samples)}") + print(f"EVIDENCE={output / 'result.json'}") + # Fail closed: no Fast observed is a meaningful negative result, not PASS. + if status == "FAST_OBSERVED": + print("FAST_OBSERVED仅表示真实Fast计数增长;缓存/像素/安全回退仍须逐帧验收。") + return 0 + return 2 + + +if __name__ == "__main__": + try: + sys.exit(run()) + except (OSError, RuntimeError, ValueError, KeyError) as exc: + print(f"STEP113_RETEST_BLOCKED: {exc}", file=sys.stderr) + sys.exit(3) diff --git a/粒子开发记录.md b/粒子开发记录.md index 1f8caaf6c..0a7430a4d 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -1155,3 +1155,5 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2` - 下一步 RDC 独立审计/执行 Mac Native 定向检查;用户一次手工 Android/设备闭环并保存源码diff/制品SHA/逐帧/截图证据。详见 docs/报告/STEP-114_FastPath安全准入与TestApp闭环_Codex完成报告_v1.md 与 docs/60_测试/STEP-114_独立FastSmoke场景与RDC验收_v1.md。 - 2026-10-11 STEP-114 RDC:独立核对安全Gate改动、3项C++测试源码、Android Test App接线及STEP-113/102冻结SHA,git diff --check PASS。Mac缺libuv,单独C++ header检查因Xcode找不到标准头array环境阻塞;Android/真机/Fast未执行,PARTIAL,后续用户一次手工构建验收。 + +- 2026-10-11 STEP-115:为原STEP-113专用APP增加Fast Path复测工具,严格校验源码HEAD、build-info APK SHA与设备安装SHA;旧包源码为c6d13e1,与现行21a1081不匹配,preflight正确拒绝。已验证旧Logcat可解码STEP113真实摘要,原五场景保持不改。尚无新Android编译/安装/真正Fast,下一步用户手工build/install后以STATIC_RC为先验收,状态PARTIAL。