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
321 lines
11 KiB
Python
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()
|