Benchmark: GitHub-hosted macOS runner vs the doublespeed fleet
Two benchmarks: a hello-world app (measures overhead) and, further down, Wikipedia iOS (a real app with 367 tests, compared with Namespace's published numbers).
The same iOS app, the same commit, the same xcodebuild test on an iPhone 16 Pro simulator with Xcode 16.4 — run two ways on every push to doublespeed-main/mac-ci-benchmark:
- github-mac — a GitHub-hosted
macos-15runner runsxcodebuildnatively. - doublespeed-mac — an
ubuntu-latestrunner runsds xcode test .; the Mac work happens on our fleet and the results come back as JSON.
Measured on 2026-08-27, six rounds, both workflows dispatched together each round. Timings come from the GitHub API (run.created_at → job completed_at), so they include waiting for a runner and runner boot — the time between pushing and seeing the check go green. Raw data: benchmark.json.
Results
github-mac (macos-15) | doublespeed-mac (ubuntu-latest → fleet) | |
|---|---|---|
| Median push → green | 278 s (4 m 38 s) | 42 s |
| Range (successful runs) | 219 – 332 s | 37 – 57 s |
| Median wait for a runner | 4 s | 4 s |
| Median job time on the runner | 273 s | 37 s |
| Median build + test step | 263 s | 26 s |
| Runs | 6, 1 failed | 6, 0 failed |
Per round (push → job complete):
| Round | github-mac | doublespeed-mac |
|---|---|---|
| 1 (push) | 332 s | 57 s |
| 2 | 278 s | 41 s |
| 3 | 306 s | 40 s |
| 4 | 219 s | 37 s |
| 5 | failed after 82 s — xcodebuild: error: Unable to find a device matching the provided destination specifier (runner image had no iPhone 16 Pro simulator) | 42 s |
| 6 | 246 s | 50 s |
On the fleet side, what the 37–57 s is made of (from the job's timings): source sync and API round-trips from the Linux runner ~10–15 s, queue wait 0.7–2.1 s, xcodebuild test 19–34 s, Mac slot held 22–37 s in total. The first round was the slowest on both sides (cold caches on ours; nothing to warm on GitHub's, each run is a fresh VM).
Real app: Wikipedia iOS
The hello-world run above measures overhead. This one measures a real, mid-size app: wikimedia/wikipedia-ios — the same project Namespace benchmarks — scheme Wikipedia, 367 unit tests, run on an iPhone 16 simulator. Everything is pinned to commit 84f87f1 (the last one that builds with Xcode 16.4) so both of our columns use the identical toolchain. Workflows: wikipedia-github-mac.yml and wikipedia-doublespeed.yml; the xcodebuild invocation mirrors Wikipedia's own CI.
GitHub macos-15 | doublespeed fleet, first build of the repo | doublespeed fleet, repeat build | Namespace (their published numbers) | |
|---|---|---|---|---|
| Push → green | 8m 35s – 11m 07s, median 9m 06s (6 runs) | 4m 44s – 4m 52s, median 4m 48s (4 runs) | 2m 11s – 3m 01s, median 2m 15s (4 runs) | 3m 45s uncached, 3m 18s with cache volumes |
| Mac time | 461 – 646 s xcodebuild | 266 – 274 s held (≈150 s source materialisation + 110–121 s xcodebuild) | 112 – 160 s held (1–2 s prepare + 108–155 s xcodebuild) | — |
| Tests | 367 passed | 367 passed | 367 passed | 3-scheme matrix |
| Cost per run (list) | ≈ $0.72 (9 min macOS, rounded up) | ≈ $0.26 | ≈ $0.12 (3 min Linux + 112 s Mac) | ≈ $0.93 per PR cycle (their figure, 3 workflows) |
| Hardware | Apple M1 virtual, 3 cores, 7 GB | Mac mini M4, 10 cores, 16 GB | same | 6 vCPU / 14 GB on M4 / M5 Max, Xcode 26 |
"Repeat build" means the worker already holds this org's unchanged source blobs and Swift package clones in its per-tenant cache (added during this benchmark, 2026-08-27) — the normal case for CI, where each push changes a handful of files. The first-ever build of a 4,700-file repo pays ~150 s to materialise the source; after that, only changed files are fetched.
What this shows
- On a real app, 4.0× faster than GitHub-hosted macOS runners push-to-green (2m 15s vs 9m 06s, medians of 4 and 6 runs), and ~6× cheaper per run. On a cold first build, 1.9× faster.
- Namespace's published cached number for the same repository is 3m 18s; ours is 2m 15s on smaller hardware. We don't claim a multiplier against it. Caveats, honestly: their runs are on Xcode 26 at a newer commit and include their full workflow; ours are Xcode 16.4 at an older commit measured from GitHub's own timestamps. Same shape, not identical inputs. The raw xcodebuild step is where hardware matters (their M4/M5 Max would out-compile our base M4); the difference is that our job starts on a warm machine with the source already there.
- Compile time itself is ~108 s on the Mini (one run of four took 155 s) vs 461–646 s on GitHub's 3-core M1 — the rest of GitHub's time is VM boot, simulator boot and cold caches. The GitHub workflow uses no
actions/cache; caching there saves the Swift package clone, not the boot or the compile. GitHub's M2 Pro XL runner ($0.10/min) was not measured.
What the benchmark found and fixed
Running a 4,700-file repository through the pipeline surfaced five bugs, all fixed the same day: symlinked headers were dropped from snapshots; a forced -configuration broke the scheme's test host; one-in-4,600 transient storage errors failed uploads and downloads outright; the CLI stopped following a job that had been requeued; and (methodology) CODE_SIGNING_ALLOWED=NO, harmless for archives, disables app-group entitlements and made Wikipedia's tests hang for an hour on GitHub. The per-tenant blob and Swift package caches came out of the same work.
Reproduce
gh repo clone doublespeed-main/mac-ci-benchmark && cd mac-ci-benchmark
gh secret set DS_API_KEY
gh workflow run wikipedia-doublespeed.yml && gh workflow run wikipedia-github-mac.yml
Run wikipedia-doublespeed.yml twice: the first run fills the cache, the second is the repeat-build number.
Cost per run (list prices)
| github-mac | doublespeed-mac | |
|---|---|---|
| Billed | 273 s of macOS runner → rounded up to 5 min at $0.08/min = $0.40 | 37 s of Linux runner → 1 min at $0.008/min = $0.008, plus 22 s of Mac time held (median timings.total_ms) at $0.05/min = $0.018 → $0.03 |
GitHub bills macOS at 10× the Linux rate and rounds each job up to the next minute. The fleet side is $0.05 per Mac-minute, metered per second of Mac time actually held (GET /v1/xcode/usage), with no charge while queued — about 15× cheaper per run here.
Hardware
| github-mac | doublespeed-mac | |
|---|---|---|
| Machine | GitHub macos-15 image, "Apple M1 (Virtual)", 3 cores | Mac mini, Apple M4, 10 cores, 16 GB |
| Xcode | 16.4 | 16.4 |
| Simulator | iPhone 16 Pro, iOS 18 | iPhone 16 Pro, iOS 18 |
What this does and doesn't show
- The app is tiny (one SwiftUI view, two XCTest cases). Compilation is a few seconds on either machine, so the gap is almost entirely overhead: a GitHub macOS run boots a fresh VM, then a fresh simulator, then warms Xcode from nothing, every time. Our workers keep Xcode and the simulator runtime resident between jobs, and only the changed files are uploaded on each push.
- A bigger app narrows the ratio (compile time is the same order on both, the M4 is faster per core) but not the absolute ~4 minutes of overhead on the GitHub side, which you pay on every push.
- Our fleet is small today; when it is saturated, jobs queue in FIFO order and the wait shows up in
timings.queued_ms, which is reported on every job so you can see it. - Six rounds is a small sample. The repo's
scripts/benchmark.mjsrecomputes this table from the last N runs, and the workflows run on every push, so the numbers keep accumulating.
Reproduce it
gh repo clone doublespeed-main/mac-ci-benchmark && cd mac-ci-benchmark
gh secret set DS_API_KEY # your key
gh workflow run github-mac.yml && gh workflow run doublespeed-mac.yml
node scripts/benchmark.mjs --runs 5 --json results.json
The doublespeed-mac workflow is ten lines; it is the same one shown in the docs.