Wikipedia iOS · 367 unit tests · Xcode 16.4 · 2026-08-27

2m 15sfrom push to green

A real app, built and tested on the doublespeed Mac fleet (one Mac mini M4) from an ordinary Linux runner — median of 4 repeat builds. GitHub's macOS runners take 9m 06s (median of 6 runs on a 3-core virtual M1). Namespace publishes 3m 18s for the same repository on bigger hardware.

4.0×
faster than GitHub-hosted macOS
6×
cheaper per run ($0.12 vs $0.72)
1m 49s
of that is xcodebuild itself — vs 8m 24s on GitHub

Push → green, same commit

xcodebuild test for scheme Wikipedia on an iPhone 16 simulator, pinned to 84f87f1. Bars are wall-clock from the push to the green check; the right column is the list price of that run.

Namespace's bar is their own published figure on M4/M5 Max hardware with cache volumes and Xcode 26, not something we ran — the same shape, not identical inputs, so we make no multiplier claim against it. GitHub and doublespeed figures are medians of our runs (6 and 4), timestamps from GitHub's API. The GitHub column uses no actions/cache: caching would save its Swift package clone (~30 s), not the VM boot, simulator boot or the compile on 3 cores. GitHub's M2 Pro XL runner ($0.10/min, 5 cores) was not measured. Our fleet is one machine today; concurrent jobs queue, and the wait is reported on every job as queued_ms.

Why the repeat build is fast

The worker keeps this org's unchanged source and Swift package clones in a per-tenant cache. The first build of a 4,700-file repo pays ~150 s to fetch it; afterwards only changed files move, and preparation drops to 1.5 s. The remaining 1m 49s (median; one of 4 runs took 155 s) is xcodebuild itself — which took 8m 24s on GitHub's 3-core M1.

0m1m2m3m4m5m6m4m 52srun 1first build4m 48srun 24m 44srun 34m 49srun 43m 25srun 5cache enabled2m 15srun 6repeat builds3m 01srun 72m 14srun 82m 11srun 9

9 runs of the same commit through the fleet on 2026-08-27, in order. Nothing changed between runs except the worker's cache.

Hello-world app: overhead only

One SwiftUI view, two tests — compilation takes seconds, so this measures what a job costs before any real work: a fresh macOS VM and simulator boot on GitHub versus a warm Mac here. Median of 12 runs on 2026-08-27.

Pricing

doublespeed
$0.05/min
per second of Mac time actually held · nothing while queued · the rest of the job on an $0.008/min Linux runner
GitHub macOS
$0.08/min
every second of the workflow, rounded up to the minute per job
Namespace
≈ $0.93
their published cost for one Wikipedia PR cycle (three workflows)

Reproduce it

Both workflows are in the public benchmark repo and run on dispatch; the data files behind this page are served next to it.

gh repo clone doublespeed-main/mac-ci-benchmark && cd mac-ci-benchmark
gh secret set DS_API_KEY                 # your key from mac.doublespeed.ai/dashboard
gh workflow run wikipedia-doublespeed.yml && gh workflow run wikipedia-github-mac.yml
gh workflow run wikipedia-doublespeed.yml   # second run = repeat build
Full write-up, per-run tables and everything the benchmark found

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-15 runner runs xcodebuild natively.
  • doublespeed-mac — an ubuntu-latest runner runs ds 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 → green278 s (4 m 38 s)42 s
Range (successful runs)219 – 332 s37 – 57 s
Median wait for a runner4 s4 s
Median job time on the runner273 s37 s
Median build + test step263 s26 s
Runs6, 1 failed6, 0 failed

Per round (push → job complete):

Roundgithub-macdoublespeed-mac
1 (push)332 s57 s
2278 s41 s
3306 s40 s
4219 s37 s
5failed 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
6246 s50 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-15doublespeed fleet, first build of the repodoublespeed fleet, repeat buildNamespace (their published numbers)
Push → green8m 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 time461 – 646 s xcodebuild266 – 274 s held (≈150 s source materialisation + 110–121 s xcodebuild)112 – 160 s held (1–2 s prepare + 108–155 s xcodebuild)
Tests367 passed367 passed367 passed3-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)
HardwareApple M1 virtual, 3 cores, 7 GBMac mini M4, 10 cores, 16 GBsame6 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-macdoublespeed-mac
Billed273 s of macOS runner → rounded up to 5 min at $0.08/min = $0.4037 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-macdoublespeed-mac
MachineGitHub macos-15 image, "Apple M1 (Virtual)", 3 coresMac mini, Apple M4, 10 cores, 16 GB
Xcode16.416.4
SimulatoriPhone 16 Pro, iOS 18iPhone 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.mjs recomputes 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.