Files
TheRock/build_tools/hack/ccache/analyze_ccache_logs.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

321 lines
11 KiB
Python

#!/usr/bin/env python
# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""Analyze ccache logs from TheRock CI to diagnose cache hit rate issues.
Downloads and parses ccache logs from CI artifacts (S3) for a given workflow
run, then produces a summary of hit/miss rates broken down by compiler and
source file patterns.
Usage:
# Analyze a specific run + stage + gfx family
python build_tools/hack/ccache/analyze_ccache_logs.py \
--run-id 25465494022 --stage math-libs --gfx gfx1151
# Specify platform (default: windows)
python build_tools/hack/ccache/analyze_ccache_logs.py \
--run-id 25465494022 --stage math-libs --gfx gfx1151 --platform linux
# Just parse an already-downloaded log file
python build_tools/hack/ccache/analyze_ccache_logs.py \
--log-file /path/to/ccache.log
"""
import argparse
import io
import re
import sys
import tarfile
import tempfile
import urllib.request
from collections import Counter, defaultdict
from pathlib import Path
# Hardcoded for convenience in this debugging script. For production code,
# use _therock_utils.s3_buckets and _therock_utils.workflow_outputs instead.
S3_BASE = "https://therock-ci-artifacts.s3.amazonaws.com"
def download_ccache_logs(
run_id: str, stage: str, gfx: str, platform: str, output_dir: Path
) -> Path:
"""Download and extract ccache logs from S3."""
out_dir = output_dir / f"run_{run_id}" / f"{stage}_{gfx}"
log_file = out_dir / "ccache.log"
if log_file.exists():
print(f" Using cached: {log_file}", file=sys.stderr)
return log_file
url = f"{S3_BASE}/{run_id}-{platform}/logs/{stage}/{gfx}/ccache_logs.tar.zst"
print(f" Downloading: {url}", file=sys.stderr)
try:
import zstandard
except ImportError:
print("ERROR: pip install zstandard", file=sys.stderr)
sys.exit(1)
try:
with urllib.request.urlopen(url) as resp:
data = resp.read()
except urllib.error.HTTPError as e:
print(f"ERROR: {e.code} fetching {url}", file=sys.stderr)
sys.exit(1)
out_dir.mkdir(parents=True, exist_ok=True)
dctx = zstandard.ZstdDecompressor()
reader = dctx.stream_reader(io.BytesIO(data))
with tarfile.open(fileobj=reader, mode="r|") as tf:
tf.extractall(out_dir, filter="tar")
if not log_file.exists():
print("ERROR: ccache.log not found in archive", file=sys.stderr)
sys.exit(1)
print(f" Extracted to: {out_dir}", file=sys.stderr)
return log_file
def parse_ccache_log(log_path: Path) -> dict:
"""Parse a ccache log file and return structured data."""
pid_compiler = {}
pid_source = {}
compiler_results = Counter()
result_counts = Counter()
source_miss_patterns = Counter()
remote_errors = []
compiler_hashes = defaultdict(set)
total_entries = 0
cant_read_count = 0
cant_read_guids = set()
re_compiler = re.compile(r"\[.*? (\d+)\s*\] Compiler: (.+)")
re_source = re.compile(r"\[.*? (\d+)\s*\] Source file: (.+)")
re_result = re.compile(
r"\[.*? (\d+)\s*\] Result: "
r"(direct_cache_hit|preprocessed_cache_hit|"
r"cache_miss|direct_cache_miss|preprocessed_cache_miss|"
r"unsupported_compiler_option|preprocessor_error|"
r"unsupported_code_directive)"
)
re_hash = re.compile(r"\[.*? (\d+)\s*\] Hash of compiler (.+?) is (.+)")
re_remote_err = re.compile(r"\[.*? \d+\s*\].*(error|timeout|failed).*remote", re.I)
re_guid = re.compile(
r"[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}"
)
with open(log_path, "r", errors="replace") as f:
for line in f:
if "mentioned in a manifest" in line and "can't be read" in line:
cant_read_count += 1
m = re_guid.search(line)
if m:
cant_read_guids.add(m.group(0))
continue
m = re_compiler.match(line)
if m:
pid_compiler[m.group(1)] = m.group(2).strip()
continue
m = re_source.match(line)
if m:
pid_source[m.group(1)] = m.group(2).strip()
continue
m = re_hash.match(line)
if m:
compiler_hashes[m.group(2).strip()].add(m.group(3).strip())
continue
m = re_result.match(line)
if m:
pid = m.group(1)
result = m.group(2)
total_entries += 1
result_counts[result] += 1
comp_path = pid_compiler.get(pid, "<unknown>")
comp_short = _shorten_compiler(comp_path)
compiler_results[(comp_short, result)] += 1
# Only count cache_miss (the final verdict), not
# direct_cache_miss/preprocessed_cache_miss (intermediate).
if result == "cache_miss" and pid in pid_source:
src = pid_source[pid]
pattern = _source_pattern(src)
source_miss_patterns[pattern] += 1
continue
if re_remote_err.search(line):
remote_errors.append(line.strip())
return {
"compiler_results": compiler_results,
"result_counts": result_counts,
"source_miss_patterns": source_miss_patterns,
"remote_errors": remote_errors,
"compiler_hashes": dict(compiler_hashes),
"total_entries": total_entries,
"cant_read_count": cant_read_count,
"cant_read_guids": cant_read_guids,
}
def _shorten_compiler(path: str) -> str:
"""Shorten compiler path for display."""
if "cl.exe" in path:
return "cl.exe (MSVC)"
if "clr" in path and "clang++" in path:
return "clr/clang++"
if "clr" in path and "clang" in path:
return "clr/clang"
if "amd-llvm" in path and "clang++" in path:
return "amd-llvm/clang++"
if "c++" in path:
return "gcc (c++)"
if "/cc" in path:
return "gcc (cc)"
return path
def _source_pattern(src: str) -> str:
"""Extract project name from source path."""
src = src.replace("\\", "/")
# Normalize to lowercase to merge source-tree (projects/miopen/)
# and build-tree (ml-libs/MIOpen/) paths for the same project.
m = re.search(r"/projects/([^/]+)/", src)
if m:
return m.group(1).lower()
m = re.search(r"(?:math-libs|ml-libs)/(?:BLAS/)?([^/]+)/", src)
if m:
return m.group(1).lower()
m = re.search(r"third-party/([^/]+)/", src)
if m:
return "3p-" + m.group(1).lower()
parts = src.split("/")
return "/".join(parts[-3:-1]).lower() if len(parts) > 2 else src
def print_analysis(data: dict, label: str = ""):
"""Print human-readable analysis."""
if label:
print(f"\n{'=' * 70}")
print(f" {label}")
print(f"{'=' * 70}")
rc = data["result_counts"]
hits = rc.get("direct_cache_hit", 0) + rc.get("preprocessed_cache_hit", 0)
misses = rc.get("cache_miss", 0)
total_cacheable = hits + misses
uncacheable = (
rc.get("unsupported_compiler_option", 0)
+ rc.get("preprocessor_error", 0)
+ rc.get("unsupported_code_directive", 0)
)
print("\n## Overall Summary")
print(f" Total result entries: {data['total_entries']}")
print(f" Cacheable: {total_cacheable}")
if total_cacheable > 0:
print(f" Hits: {hits} ({100 * hits / total_cacheable:.1f}%)")
print(f" Direct: {rc.get('direct_cache_hit', 0)}")
print(f" Preprocessed: {rc.get('preprocessed_cache_hit', 0)}")
print(
f" Misses: {misses} ({100 * misses / total_cacheable:.1f}%)"
)
print(f" Uncacheable: {uncacheable}")
# Can't-be-read diagnostics
if data["cant_read_count"] > 0:
print(f"\n## Path Issues")
print(f" 'Can't be read' entries: {data['cant_read_count']}")
print(f" Unique GUIDs in paths: {len(data['cant_read_guids'])}")
if data["cant_read_guids"]:
print(" (Stale entries from runners with different workspace GUIDs)")
# Per-compiler breakdown
print("\n## Per-Compiler Breakdown")
compilers = sorted(set(c for c, _ in data["compiler_results"]))
for comp in compilers:
comp_hits = sum(
data["compiler_results"].get((comp, r), 0)
for r in ("direct_cache_hit", "preprocessed_cache_hit")
)
comp_misses = data["compiler_results"].get((comp, "cache_miss"), 0)
comp_total = comp_hits + comp_misses
if comp_total > 0:
rate = 100 * comp_hits / comp_total
print(f" {comp:30s} {comp_hits:5d} / {comp_total:5d} hits ({rate:.1f}%)")
# Top miss sources
if data["source_miss_patterns"]:
print("\n## Top Projects by Cache Misses")
for pattern, count in data["source_miss_patterns"].most_common(15):
print(f" {pattern:30s} {count:5d} misses")
# Remote errors
if data["remote_errors"]:
print(f"\n## Remote Storage Errors ({len(data['remote_errors'])} total)")
for err in data["remote_errors"][:10]:
print(f" {err}")
print()
def main():
p = argparse.ArgumentParser(
description="Analyze ccache logs from TheRock CI.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
p.add_argument("--run-id", help="Workflow run ID to analyze")
p.add_argument("--compare-run-id", help="Second run ID for comparison")
p.add_argument(
"--stage", default="math-libs", help="Stage name (default: math-libs)"
)
p.add_argument("--gfx", default="gfx1151", help="GPU family (default: gfx1151)")
p.add_argument(
"--platform", default="windows", help="Platform: linux or windows (default)"
)
p.add_argument(
"--log-file", type=Path, help="Analyze a local log file instead of downloading"
)
p.add_argument(
"--output-dir",
type=Path,
help="Directory for downloaded logs (default: temp dir)",
)
args = p.parse_args()
output_dir = args.output_dir or Path(tempfile.mkdtemp(prefix="ccache_analysis_"))
if args.log_file:
print(f"Analyzing: {args.log_file}", file=sys.stderr)
data = parse_ccache_log(args.log_file)
print_analysis(data, label=str(args.log_file))
elif args.run_id:
log = download_ccache_logs(
args.run_id, args.stage, args.gfx, args.platform, output_dir
)
data = parse_ccache_log(log)
print_analysis(data, label=f"Run {args.run_id} / {args.stage} / {args.gfx}")
if args.compare_run_id:
log2 = download_ccache_logs(
args.compare_run_id, args.stage, args.gfx, args.platform, output_dir
)
data2 = parse_ccache_log(log2)
print_analysis(
data2,
label=f"Run {args.compare_run_id} / {args.stage} / {args.gfx}",
)
else:
p.error("Provide either --run-id or --log-file")
if __name__ == "__main__":
main()