emit_partial.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. #!/usr/bin/env python3
  2. import json
  3. import os
  4. import pathlib
  5. import re
  6. import subprocess
  7. import sys
  8. from typing import Tuple, Optional, List
  9. def run(cmd: List[str]) -> str:
  10. """Run a command and return stdout as text; return empty string on any error."""
  11. try:
  12. return subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT)
  13. except Exception:
  14. return ""
  15. def detect(platform: str) -> Tuple[str, List[str], str]:
  16. """Detect cache tool, command to print stats, and a family label from platform."""
  17. if platform.startswith("msvc"):
  18. return ("sccache", ["sccache", "--show-stats"], "windows-msvc")
  19. if platform.startswith("mingw_"):
  20. return ("ccache", ["ccache", "-s"], "windows-mingw")
  21. if platform.startswith("mac"):
  22. return ("ccache", ["ccache", "-s"], "macos")
  23. if platform == "ios":
  24. return ("ccache", ["ccache", "-s"], "ios")
  25. if platform.startswith("android"):
  26. return ("ccache", ["ccache", "-s"], "android")
  27. if platform.startswith("linux"):
  28. return ("ccache", ["ccache", "-s"], "linux")
  29. return ("ccache", ["ccache", "-s"], "other")
  30. def parse_ccache(text: str) -> Tuple[int, int]:
  31. """
  32. Parse ccache stats. Supports:
  33. - Legacy lines: "Hits: 123" / "Misses: 45"
  34. - Modern lines: "cache hit (direct) 10"
  35. "cache hit (preprocessed) 5"
  36. "cache hit (remote) 2" (optional)
  37. "cache miss 12"
  38. Returns (hits, misses).
  39. """
  40. # Legacy format
  41. m_hits = re.search(r"^\s*Hits:\s*(\d+)\b", text, re.M)
  42. m_miss = re.search(r"^\s*Misses:\s*(\d+)\b", text, re.M)
  43. if m_hits and m_miss:
  44. return int(m_hits.group(1)), int(m_miss.group(1))
  45. # Modern format: sum all hit buckets
  46. def pick(pattern: str) -> int:
  47. m = re.search(pattern, text, re.M | re.I)
  48. return int(m.group(1)) if m else 0
  49. hits_direct = pick(r"^cache hit\s*\(direct\)\s+(\d+)\b")
  50. hits_pre = pick(r"^cache hit\s*\(preprocessed\)\s+(\d+)\b")
  51. hits_remote = pick(r"^cache hit\s*\(remote\)\s+(\d+)\b") # may be absent
  52. misses = pick(r"^cache miss\s+(\d+)\b")
  53. hits_total = hits_direct + hits_pre + hits_remote
  54. return hits_total, misses
  55. def parse_sccache(text: str) -> Tuple[int, int]:
  56. """
  57. Parse sccache --show-stats lines:
  58. "Cache hits 123"
  59. "Cache misses 45"
  60. Returns (hits, misses).
  61. """
  62. def pick(label: str) -> int:
  63. m = re.search(rf"^{re.escape(label)}\s+(\d+)\b", text, re.M | re.I)
  64. return int(m.group(1)) if m else 0
  65. hits = pick("Cache hits")
  66. misses = pick("Cache misses")
  67. return hits, misses
  68. def arch_label(platform: str) -> str:
  69. """Produce a nice arch label."""
  70. mapping = {
  71. "mac-intel": "Intel",
  72. "mac-arm": "Apple Silicon",
  73. "ios": "ARM64",
  74. "msvc-x64": "x64",
  75. "msvc-x86": "x86",
  76. "msvc-arm64": "ARM64",
  77. "mingw_x86": "x86",
  78. "mingw_x86_64": "x64",
  79. "android-32": "ARMv7",
  80. "android-64": "ARM64",
  81. "android-64-intel": "x86_64",
  82. "linux-appimage-x64": "x86_64",
  83. "linux-appimage-arm64": "ARM64",
  84. }
  85. return mapping.get(platform, platform)
  86. def main() -> int:
  87. # Prefer our explicit PLATFORM env; fall back to VS's "Platform" on Windows if needed.
  88. platform = os.getenv("PLATFORM") or os.getenv("Platform") or "unknown"
  89. arch = arch_label(platform)
  90. tool, cmd, family = detect(platform)
  91. stats_raw = run(cmd)
  92. if tool == "sccache":
  93. hits, misses = parse_sccache(stats_raw)
  94. else:
  95. hits, misses = parse_ccache(stats_raw)
  96. total = hits + misses
  97. rate = f"{(100.0 * hits / total):.2f}%" if total else "n/a"
  98. payload = {
  99. "platform": platform,
  100. "family": family,
  101. "arch": arch,
  102. "tool": tool,
  103. "hits": hits,
  104. "misses": misses,
  105. "total": total,
  106. "rate": rate,
  107. "artifact_url": os.getenv("ARTIFACT_URL", ""),
  108. "debug_symbols_url": os.getenv("DEBUG_SYMBOLS_URL", ""),
  109. "aab_url": os.getenv("AAB_URL", ""),
  110. "stats_cmd": " ".join(cmd),
  111. "stats_raw": stats_raw,
  112. }
  113. outdir = pathlib.Path(".summary")
  114. outdir.mkdir(parents=True, exist_ok=True)
  115. outpath = outdir / f"{platform}.json"
  116. outpath.write_text(json.dumps(payload, ensure_ascii=False, indent=2))
  117. print(f"Wrote {outpath}")
  118. return 0
  119. if __name__ == "__main__":
  120. sys.exit(main())