Context
The Screenshots GitHub Actions job is reliable but remains slow and highly variable on hosted macOS runners.
Initial evidence came from PR #159 job 99810822044, which completed in approximately 23m50s:
- Simulator readiness: 7m07s
- Screenshot capture and upload: 15m47s
- Everything else: approximately 56s
PR #166 implements the first optimization pass.
Improvements validated in PR #166
- Build the screenshot test bundle once with
build-for-testing.
- Run retries with
test-without-building instead of rebuilding.
- Build only for the resolved arm64 simulator in single-device CI.
- Reuse the same derived-data path for build and test.
- Pass the simulator action's resolved UDID into Fastlane.
- Replace flaky context-menu long presses with exact swipe actions.
- Preserve in-process retries and simulator reboot recovery.
- Validate four screenshots for the single-device CI run.
Three consecutive runs of commit ce9ad40 succeeded without a screenshot test retry or simulator reboot:
Average job duration was 18m13s, approximately 5m37s faster than the original job. The optimized screenshot capture step averaged 10m39s instead of 15m47s.
A later restored-prewarming run also succeeded in 17m39s.
Remaining bottlenecks
Simulator readiness and Xcode prewarming
The complete simulator readiness step varied from 5m20s to 7m24s.
Runtime resolution and simulator boot typically consumed 1m37s to 2m22s. The remaining 3m43s to 5m17s was xcodebuild -showBuildSettings prewarming. It commonly required three or four attempts and sometimes exhausted its 300-second timeout.
The restored-prewarming run spent:
- 1m53s resolving and booting the simulator
- 5m02s prewarming Xcode
- 3m32s building the screenshot bundle
- 5m25s running XCTest and collecting screenshots
Cold builds
Each GitHub-hosted macOS job and rerun receives a new ephemeral VM. /tmp/screenshot_derived_data and Xcode DerivedData therefore start empty. The workflow currently restores Ruby dependencies but does not restore compiled Xcode products.
The single-device architecture-specific build still takes roughly 2m28s to 4m15s depending on runner performance.
XCTest overhead
Successful testScreenshots executions varied from approximately 172 to 212 seconds. XCTest startup and cleanup add another one to two minutes. This is acceptable for the current end-to-end screenshot flow.
Runner queue time
Some reruns waited roughly ten minutes for a hosted macOS runner. Queue time is separate from job duration and cannot be addressed by workflow code.
Failed no-prewarming experiment
Commit 20a4dfa disabled warm-xcodebuild-settings only in the PR Screenshots workflow.
The first fresh-runner experiment failed after 8m04s:
- Runtime resolution, simulator boot, and
bootstatus succeeded in 3m07s.
- Fastlane resolved Swift packages in approximately 2m02s.
- Fastlane then became the first caller of
xcodebuild -showBuildSettings.
- Its attempts timed out after 3, 6, 12, and 24 seconds.
build-for-testing never started.
This shows that the action prewarm currently protects the workflow from Fastlane's short build-settings timeout. Simply disabling prewarming is not stable. Prewarming was restored in commit 8fdfa56.
Recommended next experiment
Replace Fastlane's run_tests(build_for_testing: true) implementation in build_screenshots with a direct xcodebuild build-for-testing invocation.
The direct build should:
- Use the resolved simulator destination.
- Use
/tmp/screenshot_derived_data.
- Use the existing Debug configuration and
SWIFT_TREAT_WARNINGS_AS_ERRORS=NO setting.
- Allow the real build to wait for Xcode initialization without a separate short
showBuildSettings preflight.
- Preserve the generated
.xctestrun discovery and the existing Fastlane test-without-building retry flow.
After that change, disable the standalone action prewarm in the PR Screenshots workflow and run the same commit three times.
Acceptance criteria:
- Three consecutive successful Screenshots jobs on fresh hosted runners.
- Four screenshots generated and uploaded in every run.
- No simulator readiness regression.
- Screenshot test retries and simulator reboot recovery remain available.
- Median job duration improves materially over the 18m13s prewarmed baseline.
- Inspect logs to confirm that build work starts instead of failing in a Fastlane build-settings probe.
If this succeeds, consider applying the same approach to the beta screenshot workflow. Keep release workflows unchanged until separately validated.
Later options
Only investigate DerivedData caching after the direct-build experiment. Any cache must be keyed tightly to the Xcode version, SDK, Package.resolved, project configuration, and relevant sources. Stale Xcode caches are a stability risk and should not be the first optimization.
It may also be useful to propose upstream improvements:
- Add a separate warm-up timeout to
kula-app/wait-for-services-action.
- Surface whether prewarming succeeded as an action output.
- Avoid or configure Fastlane's short
showBuildSettings timeout when an explicit destination and derived-data path are already known.
Context
The Screenshots GitHub Actions job is reliable but remains slow and highly variable on hosted macOS runners.
Initial evidence came from PR #159 job 99810822044, which completed in approximately 23m50s:
PR #166 implements the first optimization pass.
Improvements validated in PR #166
build-for-testing.test-without-buildinginstead of rebuilding.Three consecutive runs of commit
ce9ad40succeeded without a screenshot test retry or simulator reboot:Average job duration was 18m13s, approximately 5m37s faster than the original job. The optimized screenshot capture step averaged 10m39s instead of 15m47s.
A later restored-prewarming run also succeeded in 17m39s.
Remaining bottlenecks
Simulator readiness and Xcode prewarming
The complete simulator readiness step varied from 5m20s to 7m24s.
Runtime resolution and simulator boot typically consumed 1m37s to 2m22s. The remaining 3m43s to 5m17s was
xcodebuild -showBuildSettingsprewarming. It commonly required three or four attempts and sometimes exhausted its 300-second timeout.The restored-prewarming run spent:
Cold builds
Each GitHub-hosted macOS job and rerun receives a new ephemeral VM.
/tmp/screenshot_derived_dataand Xcode DerivedData therefore start empty. The workflow currently restores Ruby dependencies but does not restore compiled Xcode products.The single-device architecture-specific build still takes roughly 2m28s to 4m15s depending on runner performance.
XCTest overhead
Successful
testScreenshotsexecutions varied from approximately 172 to 212 seconds. XCTest startup and cleanup add another one to two minutes. This is acceptable for the current end-to-end screenshot flow.Runner queue time
Some reruns waited roughly ten minutes for a hosted macOS runner. Queue time is separate from job duration and cannot be addressed by workflow code.
Failed no-prewarming experiment
Commit
20a4dfadisabledwarm-xcodebuild-settingsonly in the PR Screenshots workflow.The first fresh-runner experiment failed after 8m04s:
bootstatussucceeded in 3m07s.xcodebuild -showBuildSettings.build-for-testingnever started.This shows that the action prewarm currently protects the workflow from Fastlane's short build-settings timeout. Simply disabling prewarming is not stable. Prewarming was restored in commit
8fdfa56.Recommended next experiment
Replace Fastlane's
run_tests(build_for_testing: true)implementation inbuild_screenshotswith a directxcodebuild build-for-testinginvocation.The direct build should:
/tmp/screenshot_derived_data.SWIFT_TREAT_WARNINGS_AS_ERRORS=NOsetting.showBuildSettingspreflight..xctestrundiscovery and the existing Fastlanetest-without-buildingretry flow.After that change, disable the standalone action prewarm in the PR Screenshots workflow and run the same commit three times.
Acceptance criteria:
If this succeeds, consider applying the same approach to the beta screenshot workflow. Keep release workflows unchanged until separately validated.
Later options
Only investigate DerivedData caching after the direct-build experiment. Any cache must be keyed tightly to the Xcode version, SDK,
Package.resolved, project configuration, and relevant sources. Stale Xcode caches are a stability risk and should not be the first optimization.It may also be useful to propose upstream improvements:
kula-app/wait-for-services-action.showBuildSettingstimeout when an explicit destination and derived-data path are already known.