Skip to content

Reduce snapshot CI sizing overhead - #219

Merged
kyleve merged 5 commits into
mainfrom
codex/reduce-ci-snapshot-time
Aug 9, 2026
Merged

Reduce snapshot CI sizing overhead#219
kyleve merged 5 commits into
mainfrom
codex/reduce-ci-snapshot-time

Conversation

@kyleve

@kyleve kyleve commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Posted by an AI agent on kve's behalf.

Summary

  • separate intrinsic measurement readiness from final snapshot capture settling
  • skip redundant sizing settles for deterministic WhereUI fixtures while preserving all 381 references and full PR/main coverage
  • retain settled sizing for DataSettings after immediate measurement reproduced an iPad accessibility mismatch
  • extend snapshot timing metadata and add CI-shaped profiling with separate cold DerivedData

Performance

  • intrinsic measurement median: 120.4s before, 70.0s after (41.9% faster)
  • total capture-phase median: 336.0s before, 291.7s after (13.2% faster)
  • local snapshot execution wall median: 363s before, 322s after (11.3% faster)
  • GitHub snapshot test execution: 18m51s baseline, 16m25s on this PR (12.9% faster)
  • GitHub snapshot job: 24m52s on this PR versus a recent 26.5m median
  • GitHub iOS unit job: 13m24s on this PR versus a recent 14.4m median
  • CI-shaped local build walls: 44s unit, 26s snapshot

The safe result is below the 15% stretch target because DataSettings immediate measurement was reverted after a reproducible accessibility snapshot failure.

Validation

  • ./swiftformat --lint
  • ./test SnapshotKitTests SnapshotKitTestingTests (73 tests passed)
  • focused DataSettings snapshot regression (10 images byte-identical)
  • three successful post-change ./profile runs, including --ci-shape
  • ./test --everything (1,833 unit tests and 42 snapshot tests / 381 images passed)
  • all GitHub Actions checks passed

@kyleve

kyleve commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Posted by an AI agent on kve's behalf.

The practical options fall into four tiers. For this repository, I’d trial Depot or GitHub’s M2 runner first; a self-hosted Mac mini is potentially fastest but carries much more operational and security baggage.

Option macOS hardware Price Best fit
Current GitHub standard 3-core M1, 7 GB Free for public repos; otherwise $0.062/min after allowance Zero administration
GitHub XLarge 5-core M2, 14 GB $0.102/min Lowest-risk paid experiment
Depot 8-core M2/M4, 24 GB, 400 GB disk $0.08/min Strongest pay-as-you-go candidate
Cirrus Runners 4 M4 Pro vCPUs, 16 GB $150/month per concurrent runner Frequent, steady CI usage
Self-hosted Mac Whatever you purchase Hardware + maintenance Maximum performance/control
MacStadium Orka Managed Apple Silicon fleet Quote/node-based Multi-team enterprise CI

1. GitHub-hosted XLarge M2

GitHub’s current standard macOS runner is an M1 VM with 3 CPU cores and only 7 GB RAM. The XLarge runner moves to an M2 configuration with 5 CPU cores, 14 GB RAM, and GPU acceleration. The relevant label would be macos-26-xlarge. GitHub runner specifications

Current pricing is $0.102/minute. Larger-runner minutes are always billed: included Actions minutes do not apply, and public repositories do not make them free. GitHub also currently limits larger-runner management to organizations on Team or Enterprise Cloud, so a personally owned repository may need organizational/account changes. GitHub runner pricing

At the recent 26.5-minute snapshot-job median:

  • Unchanged runtime: about $2.70 per snapshot job.
  • 30% faster: about 18.6 minutes and $1.90.
  • 40% faster: about 15.9 minutes and $1.62.

Advantages:

  • Same GitHub-maintained images and Actions integration.
  • Ephemeral environment.
  • Minimal workflow change.
  • Straightforward short benchmark.

Caveats:

  • Only 14 GB SSD, the same small storage allocation as standard runners.
  • First jobs can encounter larger-runner pool startup delay; GitHub keeps some machines warm after use.
  • The suite is intentionally serial, so five cores do not imply a fivefold test improvement.
  • No nested virtualization, static arm64 UDID, private networking, or macOS static IP support.

My expectation would be a meaningful build improvement and a smaller—but still useful—capture improvement from the newer CPU/GPU. It is the cleanest controlled benchmark, not necessarily the fastest choice.

2. Depot macOS runners

Depot currently offers:

  • M2 runners for macOS 14/15.
  • M4 runners for macOS 26.
  • 8 CPUs, 24 GB RAM.
  • 400 GB disk, with a disk accelerator.
  • 12,000 provisioned IOPS and 1 GB/s throughput.
  • $0.08/minute. Depot runner specifications

This is unusually attractive for Stuff because it combines newer Apple Silicon with substantially more RAM and storage while costing less per minute than GitHub XLarge.

At the current snapshot-job duration:

  • Unchanged runtime: about $2.12/job.
  • 40% faster: about $1.27/job.
  • 50% faster: about $1.06/job.

The workflow change is essentially replacing runs-on: macos-26 with Depot’s runner label after installing/configuring its GitHub integration.

Caveats:

  • macOS capacity is not fully elastic; Depot explicitly warns that jobs may queue during high demand.
  • Images aim to track GitHub’s but can lag slightly.
  • This introduces another vendor and permission boundary.
  • We would need to confirm that its macOS 26 image has the exact Xcode 27/runtime combination and that snapshot pixels match. A different OS/Xcode build can invalidate every reference even when the UI is correct.

Of the paid options, this is the one I’d be most interested in benchmarking for Stuff.

3. Cirrus Runners

Cirrus sells managed, ephemeral GitHub Actions runners backed by:

  • 4 M4 Pro virtual CPU cores.
  • 16 GB RAM.
  • Paravirtualized GPU.
  • Single-use VMs destroyed after each job.
  • Fixed $150/month per concurrent runner. Cirrus pricing

Cirrus claims roughly 2× performance over GitHub’s M1 macOS runners, and its published customer examples report 30-minute iOS builds dropping to roughly 12 minutes. Those are vendor/customer claims, so I would treat them as reasons to benchmark rather than forecasts. Cirrus Runners

The economics:

  • Break-even against GitHub XLarge is roughly 1,470 occupied runner-minutes/month.
  • Break-even against Depot is roughly 1,875 minutes/month.
  • Against the current free public runner, there is no monetary break-even—you’re paying entirely for lower latency and predictable capacity.

One important topology detail: one Cirrus concurrency slot would serialize the iOS unit and snapshot jobs if both used it. Options would be:

  • Route only snapshots to Cirrus for $150/month.
  • Buy two slots for $300/month and preserve parallel execution.
  • Mark less urgent work low-priority, which Cirrus supports.

This is most attractive if the repository runs several long CI jobs every day.

4. A self-hosted Mac mini

An M4 or M4 Pro Mac mini running GitHub’s self-hosted runner could plausibly produce the largest improvement. The local snapshot suite already runs dramatically faster than GitHub’s M1 VM, and persistent package/build caches could reduce build setup further.

Benefits:

  • Modern bare-metal Apple Silicon.
  • Persistent SwiftPM, Tuist, DerivedData, simulator-runtime, and tool caches.
  • No cloud runner queue.
  • Complete control over installed Xcode and simulator runtimes.
  • Fixed capital cost rather than per-minute billing.

But it is not merely “install the runner and forget it”:

  • macOS and Xcode updates.
  • Simulator cleanup and disk-pressure management.
  • Runner upgrades and launchd reliability.
  • Power/network availability.
  • Protecting signing material and credentials.
  • Preventing one job’s state from contaminating another.
  • Building an ephemeral VM or cleanup strategy.

Most importantly, GitHub recommends self-hosted runners only for private repositories because an untrusted fork PR can execute arbitrary code on the machine. GitHub self-hosted runner security guidance

For this public repository, I would not attach a persistent personal Mac directly to unrestricted pull_request jobs. Safer designs include:

  • Run the self-hosted job only after maintainer approval.
  • Restrict it to branches in the main repository.
  • Keep untrusted PR validation on GitHub-hosted runners.
  • Use ephemeral Tart/Orka VMs restored from a clean image for every job.
  • Expose no developer credentials or home-directory data to the runner account.

5. MacStadium / Orka

MacStadium offers hosted bare-metal Macs and Orka, its Kubernetes-oriented macOS VM orchestration platform. Orka supports ephemeral Apple Silicon VMs and direct GitHub Actions integration. It starts conceptually around a multi-node fleet and scales to hundreds of nodes. Orka overview

This gives you:

  • Ephemeral macOS images.
  • Autoscaling runner pools.
  • Centralized image control.
  • Better isolation than a long-lived Mac mini.
  • Support for multiple CI systems and teams.

It also brings:

  • Node-based/quoted pricing.
  • Kubernetes/image/fleet administration.
  • Much more platform than one repository presently needs.

I would consider it only if this grows into CI infrastructure shared by several Apple-platform projects or developers.

What faster hardware will and won’t fix

The snapshot suite cannot safely be parallelized: captures serialize around UIKit/global simulator state, and prior sharding experiments made it both slower and flaky. More cores therefore mainly accelerate:

  • Swift compilation.
  • Linking.
  • PNG encoding and comparison.
  • Some rendering work.
  • Setup/tool operations.

The serial settle floors remain real wall time. Newer Apple Silicon can render and compare faster, but it cannot eliminate a deliberate 250 ms or 1-second settle window.

That makes the likely ranking:

  1. Depot M4 — best apparent performance/price for a short evaluation.
  2. GitHub M2 XLarge — simplest and least disruptive, if account eligibility permits.
  3. Cirrus M4 Pro — best when usage is frequent enough to justify $150/month.
  4. Self-hosted M4 Mac mini — potentially fastest, but only with a secure ephemeral/trusted-job design.
  5. Orka — strong enterprise solution, excessive for the current scale.

A sensible benchmark would route only the snapshot job to each candidate for 5–10 runs, retaining the standard runner as a control. Compare queue time, build time, 381-image execution time, pixel stability, and cost per successful run.

…hot-time

# Conflicts:
#	Shared/SnapshotKitTesting/AGENTS.md
@kyleve
kyleve enabled auto-merge (squash) August 9, 2026 22:31
@kyleve
kyleve disabled auto-merge August 9, 2026 22:57
@kyleve
kyleve merged commit c158e65 into main Aug 9, 2026
5 checks passed
kyleve added a commit that referenced this pull request Aug 10, 2026
## Summary

- split the snapshot suite into two duration-balanced, serial jobs on
isolated Xcode runners
- expose `Snapshot Tests (iOS, 1/2)` and `Snapshot Tests (iOS, 2/2)` as
the checks branch protection requires directly
- add checked-in shard membership plus `./snapshot-shards` commands for
validation and balancing from local, profile, or successful-CI timing
reports
- make `./test` and `./profile` emit the same versioned per-suite
duration format

## Why

PR #219 reduced work inside each capture, but snapshot execution still
owns most of the CI critical path. The seeded longest-processing-time
assignment measures 146.33s versus 146.24s of suite self-time, allowing
separate runners to halve test execution without reintroducing unsafe
same-Mac render-server contention.

## Design decisions

- shard membership is explicit and checked so each run is reproducible
and a new suite cannot silently disappear
- rebalancing is dry-run by default; `--write` is required to change the
configuration
- CI history uses medians from complete successful `main` runs with both
timing artifacts
- each shard stays serial; concurrent shards remain prohibited on one
developer Mac
- branch protection requires both shard checks directly; there is no
legacy aggregate compatibility job

## Review focus

- branch protection must require both exact shard check names before
merge
- duplicated checkout/build work increases total runner use by an
estimated 30% in exchange for roughly eight minutes off the critical
path

## Testing

- `python3 -m unittest discover -s Tools/Tests -p
test_snapshot_shards.py` (10 tests passed)
- `./snapshot-shards check` (42 suites exactly once: 20 + 22)
- existing `.xcresult` report extraction and `./snapshot-shards balance
--report` (146.332s / 146.243s)
- `./test --snapshots --timing-report
/private/tmp/snapshot-full-report.json` (42 tests passed in 5:19)
- `./test --snapshots --snapshot-shard 1/2 --no-generate --no-build
--timing-report /private/tmp/snapshot-shard-1-report.json` (20 tests
passed in 2:35)
- `./test --snapshots --snapshot-shard 2/2 --no-generate --no-build
--timing-report /private/tmp/snapshot-shard-2-report.json` (22 tests
passed in 2:50)
- shard report union: 42 suites, zero overlap; observed self-time
147.426s / 146.248s
- GitHub Actions: shard 1 passed in 18:29; shard 2 passed in 18:02
- `./swiftformat --lint`
- workflow YAML parse, shell syntax checks, and `git diff --check`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant