Skip to main content

hummingbird throughput

Measured from the GitHub Actions logs of five real Build Hummingbird desktops runs, 2026-07-25 to 2026-08-08. Per-package numbers come from mock's own INFO: Done(...) Config(hummingbird-ci) N minutes M seconds / ERROR: Exception(...) lines; wall clock comes from the log timestamps of ==> Build chain starting to ==> ===== Summary.

Method note: build-chain.sh runs each package in a background subshell whose stdout is a pipe, so a worker's whole block of output carries the timestamp of when the worker exited. Per-package durations therefore have to be read off mock's own timers, not off the surrounding log timestamps.

The runs​

runtier(s)packages builtΞ£ mockbuild-step wallΞ£ mock / wall
31137480986gnome-00106282.0 m288.4 m97.8%
31158689244gnome-00106240.5 m246.2 m97.7%
31179614825gnome-012975.7 m77.5 m97.6%
31215339645kde-004787.6 m90.3 m97.0%
31242725235niri-001215.7 m16.4 m95.6%

194 distinct packages, 6.80 hours of mock time. min 42 s, p10 52 s, median 77.5 s, mean 126 s, p90 186 s, max 2686 s.

Finding 1 β€” the job runs at concurrency 1.0, not 2​

Ξ£ mock is 95.6%–97.8% of the build step's entire wall clock, in every run, with --jobs 2. Two workers cannot both be inside mock 97% of the time; one can. The flock /local-repo/repo.lock around mock --rebuild in build_package_podman is exclusive, so --jobs selects how many workers wait. This is exactly what #266 diagnosed from the source; these are the numbers that confirm it from the outside.

Nothing else in the build step is worth optimising until that lock is a reader/writer split: at concurrency 1, every scheduling improvement upstream of it is invisible.

Finding 2 β€” 34% of mock time is rebuilding the same buildroot, 194 times​

Start: creating root cache appears once per package across all five runs. unpacking root cache appears zero times. /var/cache/mock is not mounted (MOCK_CACHE_DIR was unset for this workflow), so it lives inside the per-package podman run --rm container and is discarded with it. Every package therefore pays installing minimal buildroot with dnf5 in full, and then pays again to tar a root cache nothing will ever read.

The shortest mock invocation observed anywhere in the corpus is 42 s (python-aiohappyeyeballs, which did chroot init and then failed at %pyproject_buildrequires); the shortest successful one is 46 s (vpnc-script, whose %install copies a single shell script). The eight niri-00 packages that failed at %pyproject_buildrequires β€” i.e. that did chroot init, then stopped β€” ran 42–52 s.

Taking 43 s as the floor: it is paid 194 times, 2.32 h of the 6.80 h, 34.1%. It is a floor, not an average, so this is the conservative end.

Mock is built to avoid this and --uniqueext does not defeat it. From mockbuild/buildroot.py:

self.shared_root_name = config['root']
if 'unique-ext' in config:
config['root'] = "%s-%s" % (config['root'], config['unique-ext'])
...
self.cachedir = os.path.join(self.cache_topdir, self.shared_root_name)

The cache is keyed on the name from before uniqueext is appended, so per-package chroots share one cache by design, with an fcntl lock (shared to unpack, exclusive to rebuild) in plugins/root_cache.py. On a hit, _init() recomputes chroot_was_initialized after the preinit hooks, finds the chroot populated, and skips _init_pkg_management() β€” and _rebuild_root_cache() then declines to re-tar it.

It is safe against the local repo changing mid-run, which it does after every tier: the root cache holds only the minimal buildroot, BuildRequires are resolved after the unpack against the live repos, and mock's templates/fedora-rawhide.tpl β€” which mock/hummingbird-ci.cfg includes β€” sets metadata_expire=0 in [main], so cached metadata is revalidated on every transaction.

Finding 3 β€” there is a long pole, but it is 11%, not 93%​

The largest single package in the corpus is highway at 44.8 m, then abseil-cpp at 30.0 m. highway is 16% of gnome-00's mock time in the run it appeared in, 11% of the tier's wall clock.

That matters for how much parallelism is worth buying, not for whether to buy any. For gnome-00 (Ξ£ 16921 s, max 2707 s) the wall clock with W workers is max(Ξ£/W, max_pkg):

W1246816
gnome-00 wall282 m141 m70 m47 m45 m45 m

The long pole binds from Wβ‰ˆ7. Below that the packing is essentially perfect β€” at W=4 the tier barrier wastes no measurable time at all (4 Γ— 4230 s = 16920 s against Ξ£ 16921 s).

What that means for issue #267's three items​

  1. Tier barriers. Real, but worth nothing yet. At the 4 vCPU the job runs on, a tier's idle tail is ~0; a DAG wavefront only starts paying above ~6 concurrent builds per tier, which requires both #266 and more cores.
  2. One runner per dispatch. This is the live ceiling. 680 packages at the measured 126 s mean is ~23.8 h of serialised mock, and it is being asked of one runner out of the org's 60. desktop: all cannot ever complete: gnome-00 alone was 288 m against a 360 m job cap.
  3. Runner size. Not actionable. Blacksmith runners were removed org-wide on 2026-08-08 and every runs-on: in this repository is now ubuntu-latest or ubuntu-24.04-arm; no larger GitHub-hosted label is in use anywhere here and there is nothing to verify an entitlement against. Note also that four concurrent mock builds on 4 vCPU share those 4 cores with %{_smp_mflags}, so for the compile-bound tail (highway, abseil-cpp) in-job concurrency repacks the cores rather than multiplying them β€” only more machines do that.

Runner budget​

One job per desktop is 5 concurrent runners for the desktop: all path, plus one short-lived plan job. That is 8% of the org's 60, and it displaces nothing that runs on a cron: this workflow is workflow_dispatch only.

The bootstrap tiers (10 packages, ~10 m) are rebuilt by each desktop job rather than being built once and handed over. That is 40 runner-minutes of duplicate work, entirely off the critical path, against the alternative of a cross-job dependency plus an artifact round-trip. Sharding within a tier was rejected for the same reason at a larger scale: each additional job pays the fixed per-job cost measured at 184 s (setup + checkout + apt + podman pull + R2 seed, run 31242725235, 05:54:29β†’05:57:33), so one job per package for all 680 would spend ~35 runner-hours on setup to parallelise ~24 runner-hours of work.

Canary A/B, and the flag that made the first attempt worthless​

Tier niri-00, 24 packages, force: true, publish: false, same runner class, both after #266 landed (so --jobs $(nproc) = 4):

runbranchbuild-chain wallΞ£ mockcreating root cacheunpacking root cache
31265993115main39.02 m74.2 m240
31268488082+ MOCK_CACHE_DIR39.49 mβ€”240

No improvement. The mount was correct β€” the log shows mock writing /var/cache/mock/hummingbird-ci/root_cache/cache.tar.gz with return code 0, on the shared path with no uniqueext in it, exactly as buildroot.py's shared_root_name predicts. What it also shows, 18 times:

INFO: /tmp/mock-configdir/hummingbird-ci.cfg newer than root cache; cache will be rebuilt

_unpack_root_cache unlinks the tarball when any file in config_paths is newer than it. build-chain.sh assembles the configdir inside every package's container with

cp -a /etc/mock/. /tmp/mock-configdir/
cp /repo-mock/*.cfg /tmp/mock-configdir/

and the second cp has no -p, so hummingbird-ci.cfg is stamped with the current time microseconds before mock starts β€” always newer than a cache any earlier package wrote. Every package deleted the cache, rebuilt the buildroot, re-tarred it, and threw it away.

With -p the profile keeps its checkout mtime, which precedes every cache the run writes. That is the whole fix, and nothing in the run reports its absence: an invalidated root cache is merely slow.

Concurrency, for the record, is now 74.2 m of mock over 39.02 m of wall = 1.90, not the 4 that --jobs $(nproc) asks for, and per-package mock time roughly doubled (median 77.5 s at jobs=2-but-serialised, 138.5 s at jobs=4) because four builds share four cores with %{_smp_mflags}. That is the ceiling more machines address and in-job concurrency does not.

Summary Status (#267)​

  • One runner per dispatch: Matrixed plan/build jobs implemented in PR #277.
  • Mock chroot root cache: Shared cache directory with -p flag implemented in PR #277.
  • Runner size: GitHub-hosted 4-vCPU standard runners in use across org.
  • DAG wavefront: Documented; evaluated for higher worker concurrency tiers.