repomatic.github.job_timings module¶
Measure how long each runner image actually takes, from finished runs.
Runner selection is supposed to rest on measurement rather than on architecture
folklore, and this repository has already paid for the alternative: the lean
ubuntu-slim image held every mechanical job for years on a benchmark that
timed only the tool pass, where it looked near-parity. Timed end to end it was
20-56% slower, because most of the difference sat in checkout and install,
which that measurement could not see.
The counter-measure was a prose warning plus a two-command gh recipe and a
median taken by hand. This module is that recipe, which makes the rule
mechanical rather than advisory: the jobs API reports startedAt and
completedAt, so a duration read from it is whole-job by construction and
the mistake above is not expressible.
Note
Why the job name, and not a second API call
Attributing a duration to an image needs no extra request, because the matrix
job names already carry it (⁉️ ubuntu-26.04 / py3.15-dev). Matching them
against KNOWN_RUNNERS is the same technique
literal_runners() uses on runs-on: values, and it
degrades honestly: a job whose name carries no known image is reported under
UNATTRIBUTED rather than guessed at.
Caution
These numbers are a sample of a shared, noisy fleet, not a benchmark. A cold image, a queue stall or a flaky network inflates a single cell, which is why the report is a median across several runs rather than a mean of one. Read a gap of a few percent as noise and act only on the systematic ones.
- repomatic.github.job_timings.UNATTRIBUTED = '(no image in job name)'¶
Bucket for a job whose name matches no known runner image.
Non-matrix jobs land here by design: they carry no image in their name because they never had a choice of one. Keeping them visible rather than dropping them is what stops the report reading as though it covered the whole workflow.
- repomatic.github.job_timings.JOB_TIMINGS_HEADER_DEFS: tuple[tuple[str, str], ...] = (('Runner', 'runner'), ('Jobs', 'jobs'), ('Median', 'median'), ('Slowest job', 'slowest-job'), ('Slowest', 'slowest'))¶
Column definitions for the
job-timingstable.
- class repomatic.github.job_timings.JobTiming(name, runner, seconds)[source]¶
Bases:
objectOne finished job, and how long it occupied a runner.
- runner: str¶
Runner image matched out of
name, orUNATTRIBUTED.
- repomatic.github.job_timings.match_runner(job_name)[source]¶
Attribute a job to the runner image named in it.
- Parameters:
job_name (
str) – Job name as GitHub reports it.- Return type:
- Returns:
The matched image, or
UNATTRIBUTED.
- repomatic.github.job_timings.fetch_job_timings(workflow, branch='main', limit=5)[source]¶
Read finished job durations from the most recent successful runs.
Only successful runs are sampled. A failed run’s jobs stop early, so their durations measure where the failure landed rather than what the image costs, and a cancelled matrix reports whatever fraction ran before the cancellation swept it.
- Parameters:
- Return type:
- Returns:
One
JobTimingper finished job across the sampled runs.
- class repomatic.github.job_timings.RunnerReport(runner, job_count, median_seconds, slowest_job, slowest_seconds)[source]¶
Bases:
objectAggregated timings for one runner image.
- repomatic.github.job_timings.summarize(timings)[source]¶
Aggregate per-job timings into one row per runner image.
Sorted slowest-median first: the question this answers is which image is holding the matrix up, and that one belongs at the top rather than alphabetically buried.
- Parameters:
timings (
Iterable[JobTiming]) – Job timings, typically fromfetch_job_timings().- Return type:
- Returns:
One
RunnerReportper image seen.
- repomatic.github.job_timings.format_duration(seconds)[source]¶
Render a duration zero-padded to a fixed width.
The padding is not cosmetic.
--sort-byorders the rendered table lexicographically, so an unpadded21ssorts between1m37sand2m03sand the default view reads as though a 21-second job were slower than a 97-second one. Fixed-width00m21smakes the string order the chronological one, for every column and both directions, without the table layer needing to know these cells are durations.- Return type:
- repomatic.github.job_timings.render_markdown(reports, workflow, runs)[source]¶
Render a report as a Markdown table, for pasting into documentation.
Emitted on request rather than written by a sync job. Timings move on every run, so a job regenerating a checked-in table would open a pull request forever and never converge: this is a measurement to take when a decision needs one, not a file to keep in sync.
- Parameters:
reports (
Sequence[RunnerReport]) – Aggregated rows fromsummarize().workflow (
str) – Workflow the sample came from.runs (
int) – How many runs were sampled.
- Return type:
- Returns:
A Markdown table, newline-terminated.