fix(ci): exclude skipped jobs from timing deltas and stats

Skipped jobs (conclusion == 'skipped') have null/zero-duration timestamps
that polluted every downstream computation: they counted as 'unchanged'
(0 vs 0) in faster/slower tallies, showed meaningless '0.0s (0%)' deltas
in the job table, rendered phantom gantt bars, and inflated wall/compute
totals.

Add is_skipped() helper and apply it consistently:
- compute_stats: exclude from wall/compute + faster/slower/unchanged;
  add 'skipped' and 'bl_skipped' counts
- _gantt_bars: filter from current bars, baseline bars, and axis calc
- _job_table: show 'skipped' label instead of durations/deltas
- _step_details: skip entirely (no meaningful step data)
- _regressions: exclude from both current and baseline sides
This commit is contained in:
ethernet 2026-07-13 16:30:10 -04:00
parent 2a25d53ee5
commit 7beca22bc0

View file

@ -157,6 +157,17 @@ def dur_s(started: str | None, completed: str | None) -> float | None:
return (e - s).total_seconds()
def is_skipped(job: dict) -> bool:
"""A job is 'skipped' when GitHub didn't actually run it.
Skipped jobs have conclusion == 'skipped' and typically have null or
equal started_at/completed_at timestamps, yielding duration_s of None
or 0. They should be excluded from delta comparisons, gantt bars,
regression tables, and aggregate stats (wall/compute).
"""
return job.get("conclusion") == "skipped"
# ---------------------------------------------------------------------------
# Timings collection
# ---------------------------------------------------------------------------
@ -309,10 +320,13 @@ def fmt_tick(seconds: int) -> str:
# ---------------------------------------------------------------------------
def compute_stats(timings: dict, baseline: dict | None = None) -> dict:
jobs = timings.get("jobs", [])
bl_jobs = {j["name"]: j for j in (baseline or {}).get("jobs", [])}
jobs_all = timings.get("jobs", [])
jobs = [j for j in jobs_all if not is_skipped(j)]
bl_jobs_all = (baseline or {}).get("jobs", [])
bl_jobs = [j for j in bl_jobs_all if not is_skipped(j)]
bl_map = {j["name"]: j for j in bl_jobs}
# Wall time
# Wall time (skipped jobs have no real timestamps)
starts = [s for s in (parse_ts(j.get("started_at")) for j in jobs) if s is not None]
ends = [e for e in (parse_ts(j.get("completed_at")) for j in jobs) if e is not None]
wall = (max(ends) - min(starts)).total_seconds() if starts and ends else 0
@ -322,19 +336,19 @@ def compute_stats(timings: dict, baseline: dict | None = None) -> dict:
bl_wall = None
bl_compute = None
if baseline:
bl_starts = [s for s in (parse_ts(j.get("started_at")) for j in baseline.get("jobs", [])) if s is not None]
bl_ends = [e for e in (parse_ts(j.get("completed_at")) for j in baseline.get("jobs", [])) if e is not None]
bl_starts = [s for s in (parse_ts(j.get("started_at")) for j in bl_jobs) if s is not None]
bl_ends = [e for e in (parse_ts(j.get("completed_at")) for j in bl_jobs) if e is not None]
if bl_starts and bl_ends:
bl_wall = (max(bl_ends) - min(bl_starts)).total_seconds()
bl_compute = sum(j.get("duration_s") or 0 for j in baseline.get("jobs", []))
bl_compute = sum(j.get("duration_s") or 0 for j in bl_jobs)
# Per-job deltas
# Per-job deltas (skipped excluded)
faster = 0
slower = 0
unchanged = 0
no_baseline = 0
for j in jobs:
bl = bl_jobs.get(j["name"])
bl = bl_map.get(j["name"])
if not bl:
no_baseline += 1
continue
@ -347,6 +361,9 @@ def compute_stats(timings: dict, baseline: dict | None = None) -> dict:
else:
faster += 1
skipped = sum(1 for j in jobs_all if is_skipped(j))
bl_skipped = sum(1 for j in bl_jobs_all if is_skipped(j))
return {
"wall": wall,
"compute": compute,
@ -356,7 +373,9 @@ def compute_stats(timings: dict, baseline: dict | None = None) -> dict:
"slower": slower,
"unchanged": unchanged,
"no_baseline": no_baseline,
"total_jobs": len(jobs),
"skipped": skipped,
"bl_skipped": bl_skipped,
"total_jobs": len(jobs_all),
}
@ -447,7 +466,8 @@ def _gantt_bars(timings: dict, baseline: dict | None) -> str:
(relative to each run's earliest job start). The axis scale spans
0..max_end across both runs so bars are directly comparable.
"""
jobs = [j for j in timings.get("jobs", []) if j.get("started_at") and j.get("completed_at")]
jobs = [j for j in timings.get("jobs", [])
if j.get("started_at") and j.get("completed_at") and not is_skipped(j)]
bl_map = {j["name"]: j for j in (baseline or {}).get("jobs", [])}
# Current run: relative offsets from earliest start
@ -463,6 +483,8 @@ def _gantt_bars(timings: dict, baseline: dict | None) -> str:
bl_max = 0.0
bl_jobs_timed = []
for bl_j in bl_map.values():
if is_skipped(bl_j):
continue
s = parse_ts(bl_j.get("started_at"))
e = parse_ts(bl_j.get("completed_at"))
if s is not None and e is not None:
@ -489,7 +511,7 @@ def _gantt_bars(timings: dict, baseline: dict | None) -> str:
bl = bl_map.get(j["name"])
bl_bar = ""
if bl and bl_t0 is not None:
if bl and not is_skipped(bl) and bl_t0 is not None:
bl_s = parse_ts(bl.get("started_at"))
bl_e = parse_ts(bl.get("completed_at"))
if bl_s is not None and bl_e is not None:
@ -507,7 +529,7 @@ def _gantt_bars(timings: dict, baseline: dict | None) -> str:
name_display = f'{escape(j["workflow_name"])} / {escape(j["name"])}'
delta_info = ""
if bl and bl.get("duration_s") is not None:
if bl and not is_skipped(bl) and bl.get("duration_s") is not None:
d_text, d_cls = fmt_delta(dur, bl.get("duration_s"))
delta_info = f'{d_text}'
@ -575,11 +597,6 @@ def _job_table(timings: dict, baseline: dict | None) -> str:
bl_map = {j["name"]: j for j in (baseline or {}).get("jobs", [])}
rows = []
for j in timings.get("jobs", []):
dur = j.get("duration_s")
bl = bl_map.get(j["name"])
bl_dur = bl.get("duration_s") if bl else None
delta_text, delta_cls = fmt_delta(dur, bl_dur)
name = escape(j["name"])
if j.get("workflow_name"):
name = f'{escape(j["workflow_name"])} / {escape(j["name"])}'
@ -588,6 +605,23 @@ def _job_table(timings: dict, baseline: dict | None) -> str:
concl_icon = {"success": "", "failure": "", "skipped": ""}.get(concl, "?")
concl_cls = {"success": "faster", "failure": "slower", "skipped": "neutral"}.get(concl, "neutral")
if is_skipped(j):
rows.append(
f'<tr>'
f'<td class="job-name">{name}</td>'
f'<td class="num neutral">skipped</td>'
f'<td class="num neutral">—</td>'
f'<td class="num neutral">—</td>'
f'<td class="{concl_cls}" style="text-align:center">{concl_icon}</td>'
f'</tr>'
)
continue
dur = j.get("duration_s")
bl = bl_map.get(j["name"])
bl_dur = bl.get("duration_s") if bl and not is_skipped(bl) else None
delta_text, delta_cls = fmt_delta(dur, bl_dur)
rows.append(
f'<tr>'
f'<td class="job-name">{name}</td>'
@ -612,6 +646,8 @@ def _step_details(timings: dict, baseline: dict | None) -> str:
for j in timings.get("jobs", []):
if not j.get("steps"):
continue
if is_skipped(j):
continue
bl = bl_map.get(j["name"], {})
bl_steps = {s["name"]: s for s in bl.get("steps", [])}
@ -659,8 +695,10 @@ def _regressions(timings: dict, baseline: dict | None) -> str:
deltas = [] # (abs_delta, job_name, step_name, current, baseline, is_slower)
for j in timings.get("jobs", []):
if is_skipped(j):
continue
bl = bl_map.get(j["name"])
if not bl:
if not bl or is_skipped(bl):
continue
bl_steps = {s["name"]: s for s in bl.get("steps", [])}
for s in j.get("steps", []):