repomatic.github.job_timings module¶
Measure how long each runner image actually takes, from finished runs.
Runner selection rests on measurement, and the measurement that matters is the
whole job. A benchmark that times only the tool pass misses checkout and
install, which is where most of the difference between two images sits. The
jobs API reports startedAt and completedAt, so a duration read from it is
whole-job by construction, and that mistake cannot be made here.
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[ColumnSpec, ...] = (ColumnSpec(id='runner', label='Runner', description='', max_width=None, optional=False, sortable=True), ColumnSpec(id='jobs', label='Jobs', description='', max_width=None, optional=False, sortable=True), ColumnSpec(id='median', label='Median', description='', max_width=None, optional=False, sortable=True), ColumnSpec(id='slowest-job', label='Slowest job', description='', max_width=None, optional=False, sortable=True), ColumnSpec(id='slowest', label='Slowest', description='', max_width=None, optional=False, sortable=True))¶
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. The runs come from
workflow_runs(), whosecreatedfilter keeps a stale snapshot of old runs out of the sample.- 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.
- runner: str¶
The runner image, or
UNATTRIBUTED.
- 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.
