test: prepare STEP-113 PBF Fast Path APK retest
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
tei
2026-10-11 10:34:08 +09:00
parent 21a1081643
commit e2d202b560
4 changed files with 233 additions and 0 deletions

View File

@@ -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。

View File

@@ -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)