ArthurZ's picture
ArthurZ HF Staff
wire /bisect endpoint + auto-clone transformers on first run
b021f63 verified
Raw
History Blame Contribute Delete
15.8 kB
"""Render the triage report to a single static HTML page.
We hand-roll markdown → HTML rather than depending on a `markdown` library
to keep the Docker image lean and the output deterministic. CSS is inlined.
"""
from __future__ import annotations
import datetime
import html
from collections import Counter, defaultdict
HEADER = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>transformers integration-test failure triage</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica,
Arial, sans-serif; max-width: 1200px; margin: 24px auto; color: #1f2328;
padding: 0 16px; }
h1 { border-bottom: 1px solid #d0d7de; padding-bottom: 8px; }
h2 { margin-top: 32px; border-bottom: 1px solid #eaeef2; padding-bottom: 4px; }
h3 { margin-top: 24px; }
table { border-collapse: collapse; width: 100%; font-size: 13px; margin: 8px 0 20px; }
th, td { border-bottom: 1px solid #eaeef2; padding: 6px 10px; text-align: left;
vertical-align: top; }
th { background: #f6f8fa; font-weight: 600; }
tr:hover td { background: #f6f8fa; }
code, pre { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
background: #f6f8fa; padding: 1px 4px; border-radius: 4px; font-size: 12px; }
pre { padding: 8px 12px; overflow-x: auto; }
.pill { display: inline-block; padding: 1px 8px; border-radius: 10px;
font-size: 12px; font-weight: 500; }
.pill-OOM { background: #fff8c5; color: #6e5500; }
.pill-output_mismatch { background: #ddf4ff; color: #054270; }
.pill-cuda_runtime { background: #ffdce0; color: #67050a; }
.pill-load_error { background: #fbecd0; color: #6e3a00; }
.pill-import_or_config { background: #f3e5f5; color: #4a148c; }
.pill-other { background: #eaeef2; color: #57606a; }
.meta { color: #57606a; font-size: 13px; }
details { margin: 4px 0; }
summary { cursor: pointer; }
.num { text-align: right; font-variant-numeric: tabular-nums; }
</style>
</head>
<body>
"""
FOOTER = """
</body>
</html>
"""
def _pill(tag: str) -> str:
return f'<span class="pill pill-{tag}">{html.escape(tag)}</span>'
def _trace_excerpt(trace: str, max_chars: int = 220) -> str:
"""Last non-empty line of trace, trimmed."""
if not trace:
return ""
for line in reversed(trace.splitlines()):
line = line.strip()
if line:
return (line[: max_chars - 1] + "…") if len(line) > max_chars else line
return ""
def _table(headers: list[str], rows: list[list[str]]) -> str:
if not rows:
return "<p class='meta'><em>(none)</em></p>"
head = "".join(f"<th>{html.escape(h)}</th>" for h in headers)
body = "".join(
"<tr>" + "".join(f"<td>{cell}</td>" for cell in r) + "</tr>" for r in rows
)
return f"<table><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table>"
def _model_breakdown(failures: list[dict]) -> list[list[str]]:
"""Group failures by model, return rows sorted by total desc."""
by_model: dict[str, list[dict]] = defaultdict(list)
for f in failures:
by_model[f["model"]].append(f)
rows = []
for model in sorted(by_model, key=lambda m: -len(by_model[m])):
items = by_model[model]
mode_counts = Counter(f["failure_mode"] for f in items)
mode_str = " ".join(f"{_pill(m)} {n}" for m, n in mode_counts.most_common())
gpu_str = "/".join(sorted({f["gpu"] for f in items}))
rows.append([
html.escape(model),
f'<span class="num">{len(items)}</span>',
html.escape(gpu_str),
mode_str,
])
return rows
def _cluster_block(cluster: dict, idx: int) -> str:
bc = cluster["bad_commit"]
pr = cluster.get("pr_number")
author = cluster.get("author") or "?"
merged_by = cluster.get("merged_by") or "?"
rows = []
for f in sorted(cluster["failures"], key=lambda f: (f["model"], f["gpu"], f["test"])):
rows.append([
html.escape(f["model"]),
html.escape(f["gpu"]),
f'<code>{html.escape(f["test"].split("::")[-1])}</code>',
_pill(f["failure_mode"]),
f'<span class="num">{f["days_seen"]}/7</span>',
f'<code>{html.escape(_trace_excerpt(f.get("latest_trace") or f.get("trace") or ""))}</code>',
])
pr_link = (
f'<a href="https://github.com/huggingface/transformers/pull/{pr}">PR #{pr}</a>'
if pr else "<em>no PR</em>"
)
commit_link = (
f'<a href="https://github.com/huggingface/transformers/commit/{bc}">'
f'<code>{html.escape(bc[:12])}</code></a>'
)
excerpt = cluster.get("failure_excerpt") or ""
excerpt_html = (
f"<details><summary>CI trace from bad commit</summary><pre>{html.escape(excerpt[:2000])}</pre></details>"
if excerpt else ""
)
return (
f"<h3>Cluster {idx} · {commit_link} · {pr_link} · "
f"{html.escape(author)} / merged by {html.escape(merged_by)} · "
f"{len(cluster['failures'])} failures</h3>"
+ _table(
["model", "gpu", "test", "mode", "days", "trace excerpt"],
rows,
)
+ excerpt_html
)
def _examples_for_unpinned(unpinned: list[dict], k_per_mode: int = 5) -> str:
by_mode: dict[str, list[dict]] = defaultdict(list)
for f in unpinned:
by_mode[f["failure_mode"]].append(f)
sections = []
for mode in ("output_mismatch", "OOM", "load_error", "cuda_runtime", "import_or_config", "other"):
items = by_mode.get(mode, [])
if not items:
continue
# Sort: most-recent first by latest_seen, then model.
items = sorted(items, key=lambda f: (f.get("latest_seen") or "", f["model"]), reverse=True)
sample = items[:k_per_mode]
rows = []
for f in sample:
rows.append([
html.escape(f["model"]),
html.escape(f["gpu"]),
f'<code>{html.escape(f["test"].split("::")[-1])}</code>',
f'<span class="num">{f["days_seen"]}/7</span>',
f'<code>{html.escape(_trace_excerpt(f.get("latest_trace") or f.get("trace") or ""))}</code>',
])
sections.append(
f"<h3>{_pill(mode)} {len(items)} unpinned failures — sample of {len(sample)}</h3>"
+ _table(["model", "gpu", "test", "days", "trace excerpt"], rows)
)
return "".join(sections)
def _regression_day_block(report: dict) -> str:
"""The killer table: how many tests regressed on each historical day,
sorted by size descending. Big buckets = candidate fleet-regressions."""
buckets = report.get("regression_day_buckets") or {}
if not buckets:
return ""
out = ["<h2>Regression-day clustering (historical first-failure)</h2>"]
out.append(
'<p class="meta">For every persistent failure we walked the daily CI '
"dataset backwards to find the first day it appeared as failing. The "
"table below groups failures by that day — large buckets are likely "
"<strong>fleet regressions</strong> from a single landed PR. Click a "
"date to see the commits merged in the 24h window before it.</p>"
)
rows = []
total = sum(buckets.values())
for day, n in list(buckets.items())[:25]:
if day == "unknown":
link = day
else:
since = day # before that day's CI run
link = (
f'<a href="https://github.com/huggingface/transformers/commits/main?'
f'until={day}T23:59:59Z">{day}</a>'
)
pct = f"{(n / total) * 100:0.1f}%" if total else "—"
rows.append([link, f'<span class="num">{n}</span>', f'<span class="num">{pct}</span>'])
out.append(_table(["first-failure day", "failures", "share"], rows))
return "".join(out)
def _regression_day_detail_blocks(report: dict, top_k: int = 5) -> str:
"""For each of the top-k regression days, show which tests broke (table).
This gives the reader the "what got hit when" inline."""
buckets = report.get("regression_day_buckets") or {}
if not buckets:
return ""
# All failures, with first_failure_day attached
all_failures = (
[f for c in report["clusters"].values() for f in c["failures"]]
+ report["flaky"]
+ report["unpinned"]
)
by_day: dict[str, list[dict]] = {}
for f in all_failures:
by_day.setdefault(f.get("first_failure_day") or "unknown", []).append(f)
out = ["<h2>Top regression days — failure breakdown</h2>"]
for day, _ in list(buckets.items())[:top_k]:
items = by_day.get(day, [])
if not items:
continue
mode_counts = Counter(f["failure_mode"] for f in items)
models = Counter(f["model"] for f in items)
out.append(f"<h3>{html.escape(day)}{len(items)} failures</h3>")
out.append('<p class="meta">Failure-mode mix: '
+ " ".join(f"{_pill(m)} {n}" for m, n in mode_counts.most_common())
+ f' · {len(models)} distinct models touched. '
f'<a href="https://github.com/huggingface/transformers/commits/main?'
f'until={day}T23:59:59Z">commit log around {day}</a></p>')
# Top 12 affected models
rows = []
for model, n in models.most_common(12):
sample = next(f for f in items if f["model"] == model)
rows.append([
html.escape(model),
f'<span class="num">{n}</span>',
_pill(sample["failure_mode"]),
f'<code>{html.escape(_trace_excerpt(sample.get("latest_trace") or sample.get("trace") or "", 180))}</code>',
])
if len(models) > 12:
rows.append([
f"<em>… and {len(models) - 12} more models</em>", "", "", ""
])
out.append(_table(["model", "failures", "sample mode", "sample trace excerpt"], rows))
# Full failure list (collapsed). Sort by (failure_mode, model, gpu, test)
# so visually similar failures cluster.
all_rows = []
for f in sorted(
items,
key=lambda f: (
f.get("failure_mode") or "",
f.get("model") or "",
f.get("gpu") or "",
f.get("test") or "",
),
):
all_rows.append([
html.escape(f["model"]),
html.escape(f["gpu"]),
f'<code>{html.escape(f["test"].split("::")[-1])}</code>',
_pill(f["failure_mode"]),
f'<span class="num">{f["days_seen"]}/7</span>',
f'<code>{html.escape(_trace_excerpt(f.get("latest_trace") or f.get("trace") or ""))}</code>',
])
out.append(
f"<details><summary>Show all {len(items)} failures in this bucket</summary>"
+ _table(
["model", "gpu", "test", "failure_mode", "days_seen", "trace excerpt"],
all_rows,
)
+ "</details>"
)
return "".join(out)
def render(report: dict, *, generated_at: datetime.datetime, dates_window: list[str]) -> str:
t = report["totals"]
out = [HEADER]
out.append(f"<h1>transformers · integration-test failure triage</h1>")
out.append(
f'<p class="meta">Generated <code>{generated_at.isoformat(timespec="seconds")}Z</code> · '
f"window <code>{dates_window[0]}</code> → <code>{dates_window[-1]}</code> "
f"({len(dates_window)} daily runs, ≥5/7 intersection)</p>"
)
# TL;DR
out.append("<h2>TL;DR</h2><ul>")
out.append(f"<li><strong>{t['total']}</strong> persistent integration-test failures (≥5/7 days)</li>")
out.append(
f"<li><strong>{t['in_clusters']}</strong> attributed to <strong>{t['clusters']}</strong> "
f"distinct bad commits (CI bisect)</li>"
)
out.append(f"<li><strong>{t['flaky']}</strong> tagged flaky by CI</li>")
out.append(
f"<li><strong>{t['unpinned']}</strong> unpinned — CI bisect didn't converge "
"(usually because the regression predates CI's 7-day bisect window)</li>"
)
buckets = report.get("regression_day_buckets") or {}
if buckets:
top_day, top_n = next(iter(buckets.items()))
out.append(
f"<li>Historical sweep finds <strong>{top_n}</strong> failures "
f"({(top_n / t['total']) * 100:0.0f}% of the total) first appeared on "
f"<code>{html.escape(top_day)}</code> — likely a single fleet-regression PR.</li>"
)
out.append("</ul>")
out.append(_regression_day_block(report))
out.append(_regression_day_detail_blocks(report, top_k=5))
# Failure-mode breakdown for unpinned
if report["unpinned"]:
modes = Counter(f["failure_mode"] for f in report["unpinned"])
out.append("<h2>Unpinned failure modes</h2>")
out.append(_table(
["mode", "count"],
[[_pill(m), f'<span class="num">{n}</span>'] for m, n in modes.most_common()],
))
# Per-model
out.append("<h2>Per-model breakdown (all failures)</h2>")
all_failures = (
[f for c in report["clusters"].values() for f in c["failures"]]
+ report["flaky"]
+ report["unpinned"]
)
out.append(_table(
["model", "failures", "gpu", "mode mix"],
_model_breakdown(all_failures),
))
# Clusters
out.append("<h2>Pinned clusters (CI bisect)</h2>")
if report["clusters"]:
for i, (_bc, c) in enumerate(report["clusters"].items(), start=1):
out.append(_cluster_block(c, i))
else:
out.append("<p class='meta'><em>(none)</em></p>")
# Flaky
out.append("<h2>Flaky (CI flagged)</h2>")
if report["flaky"]:
rows = []
for f in report["flaky"]:
rows.append([
html.escape(f["model"]),
html.escape(f["gpu"]),
f'<code>{html.escape(f["test"].split("::")[-1])}</code>',
_pill(f["failure_mode"]),
f'<span class="num">{f["days_seen"]}/7</span>',
])
out.append(_table(["model", "gpu", "test", "mode", "days"], rows))
else:
out.append("<p class='meta'><em>(none)</em></p>")
# Unpinned examples
if report["unpinned"]:
out.append("<h2>Unpinned — samples per mode</h2>")
out.append(
"<p class='meta'>These failures persisted across the window but CI couldn't"
" attribute a bad commit. They likely regressed before the 7-day bisect window."
" Showing the most-recently-seen samples per failure mode.</p>"
)
out.append(_examples_for_unpinned(report["unpinned"]))
out.append(FOOTER)
return "".join(out)
if __name__ == "__main__":
import sys
from classify import classify
from cluster import cluster_failures
from fetch import fetch_last_n
from filter import per_day_integration_failures, intersect_across_days
cache = sys.argv[1] if len(sys.argv) > 1 else None
out_path = sys.argv[2] if len(sys.argv) > 2 else "/tmp/integration_failures.html"
daily = fetch_last_n(7, cache_dir=cache)
per_day = per_day_integration_failures(daily)
kept = intersect_across_days(per_day, min_days=5)
latest = max(daily.keys())
nf = daily[latest].get("new_failures")
report = cluster_failures(kept, nf, classify)
html_out = render(
report,
generated_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None),
dates_window=sorted(daily.keys()),
)
with open(out_path, "w") as f:
f.write(html_out)
print(f"wrote {out_path} ({len(html_out)} bytes)")