capture_reader_c3_first_frame.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. #!/usr/bin/env python3
  2. """Capture cold route-to-first-content draw timings for both reader renderers."""
  3. from __future__ import annotations
  4. import argparse
  5. import json
  6. import re
  7. import sys
  8. import time
  9. from datetime import datetime
  10. from pathlib import Path
  11. sys.dont_write_bytecode = True
  12. from capture_reader_c3_baseline import ( # noqa: E402
  13. Adb,
  14. BaselineError,
  15. MAIN_ACTIVITY_CLASS,
  16. PACKAGE_DEFAULT,
  17. READ_ROUTE,
  18. connected_serial,
  19. require_unlocked,
  20. restore_lab_state,
  21. set_compose_renderer,
  22. )
  23. FIRST_FRAME_EXTRA = "readerFirstFrameStartedAtNanos"
  24. FRAME_PATTERN = re.compile(r"renderer=(legacy|compose) durationMs=([\d.]+)")
  25. def elapsed_realtime_nanos(adb: Adb) -> int:
  26. seconds = float(adb.run("shell", "cat", "/proc/uptime").split()[0])
  27. return int(seconds * 1_000_000_000)
  28. def percentile(values: list[float], fraction: float) -> float:
  29. ordered = sorted(values)
  30. index = min(len(ordered) - 1, int((len(ordered) - 1) * fraction + 0.5))
  31. return ordered[index]
  32. def capture_mode(adb: Adb, package: str, mode: str, runs: int) -> dict[str, object]:
  33. set_compose_renderer(adb, package, mode == "compose")
  34. durations: list[float] = []
  35. launches: list[str] = []
  36. for _ in range(runs):
  37. adb.run("logcat", "-c")
  38. started_at = elapsed_realtime_nanos(adb)
  39. launches.append(
  40. adb.run(
  41. "shell", "am", "start", "-S", "-W",
  42. "-n", f"{package}/{MAIN_ACTIVITY_CLASS}",
  43. "--es", "startRoute", READ_ROUTE,
  44. "--el", FIRST_FRAME_EXTRA, str(started_at),
  45. )
  46. )
  47. deadline = time.monotonic() + 10.0
  48. duration: float | None = None
  49. while time.monotonic() < deadline:
  50. logs = adb.run("logcat", "-d", "-s", "ReaderFirstFrame:I", "*:S")
  51. matches = FRAME_PATTERN.findall(logs)
  52. matching = [float(value) for renderer, value in matches if renderer == mode]
  53. if matching:
  54. duration = matching[-1]
  55. break
  56. time.sleep(0.2)
  57. if duration is None:
  58. raise BaselineError(f"{mode} 冷启动未观察到首个非空正文帧")
  59. durations.append(duration)
  60. return {
  61. "durationsMs": durations,
  62. "p50Ms": percentile(durations, 0.50),
  63. "p90Ms": percentile(durations, 0.90),
  64. "launches": launches,
  65. }
  66. def main() -> int:
  67. parser = argparse.ArgumentParser(description=__doc__)
  68. parser.add_argument("--serial")
  69. parser.add_argument("--package", default=PACKAGE_DEFAULT)
  70. parser.add_argument("--runs", type=int, default=5)
  71. parser.add_argument("--out", type=Path)
  72. args = parser.parse_args()
  73. if args.runs < 1:
  74. parser.error("--runs 必须大于 0")
  75. serial = connected_serial(args.serial)
  76. adb = Adb(serial)
  77. require_unlocked(adb)
  78. output = args.out or Path("build/reader-c3-first-frame") / datetime.now().strftime("%Y%m%d-%H%M%S")
  79. output.mkdir(parents=True, exist_ok=False)
  80. original: tuple[bool, bool] | None = None
  81. try:
  82. original = set_compose_renderer(adb, args.package, False)
  83. modes = {
  84. mode: capture_mode(adb, args.package, mode, args.runs)
  85. for mode in ("legacy", "compose")
  86. }
  87. result = {"serial": serial, "runs": args.runs, "modes": modes}
  88. (output / "result.json").write_text(
  89. json.dumps(result, ensure_ascii=False, indent=2),
  90. encoding="utf-8",
  91. )
  92. print(json.dumps({"output": str(output), **result}, ensure_ascii=False, indent=2))
  93. return 0
  94. finally:
  95. restore_lab_state(adb, args.package, original)
  96. if __name__ == "__main__":
  97. raise SystemExit(main())