capture_reader_c3_first_frame.py 3.8 KB

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