Speed up Debug builds: fix CI cache key + trim type-checker hotspots#308
Merged
Conversation
Profile diagnostics from `xcodebuild ... OTHER_SWIFT_FLAGS=-Xfrontend -warn-long-function-bodies=100 -Xfrontend -warn-long-expression-type-checking=100` surfaced ~14s of cumulative type-checking concentrated in a handful of files, plus the project-level compilation cache being limited to the main target only. This pass attacks both. Type-checker hotspots refactored: - ArchivedWorktreesDetailView body (4312ms -> 346ms): hoist data prep into a typed snapshot, split branches/sections into helpers, and promote the inline (repository:, worktrees:) tuple to a nominal ArchivedWorktreeGroup struct. - CanvasView body (1639ms -> below threshold): pull the per-card view out into a private `cardView(for:in:activeStates:)` helper. - WorktreeTerminalTabsView / ShelfOpenBookView bodies: factor out the tab bar, content stack, and icon-picker sheet into private helpers. - ListRuntimeSnapshotBuilder / TargetResolver / ToolbarNotificationGroup: replace `Dictionary(uniqueKeysWithValues:)` and chained higher-order closures with typed for-loops that the type-checker resolves cheaply. - supacodeApp.init: extract MemoryWatchdog wiring into a static helper to break the long property chain. Build settings: - COMPILATION_CACHE_ENABLE_CACHING = YES at the project Debug level so SPM packages (TCA, Sharing, Dependencies, PostHog, Sentry, ...) hit the same Xcode compilation cache the main target already uses. With a warm cache this drops `make build-app` clean wall time from ~44s to ~26s on an M2 Pro (~40% reduction). - EAGER_LINKING = YES for Debug to overlap linking with compilation. Verified: `make build-app`, `make lint`, swift-format strict lint, and ToolbarNotificationGroupingTests all pass.
…gers `actions/cache@v4` only saves a new cache when the primary key has no exact match. The previous static `xcode-compilation-cache-v0` key meant the very first CI run that populated the cache became permanent: every subsequent run restored that same snapshot but never updated it. In practice the saved cache is ~152MB (verified via `gh api /repos/onevcat/Prowl/actions/caches`), frozen since the workflow was added back in February 2026. Most of the project's compile work doesn't find its content hash in there, so CI keeps cold-compiling everything except a few stable SDK Clang modules — even though the `hits / cacheable tasks` metric looks high (the metric only counts tasks Xcode bothered to look up, not the silent misses). Rotate the key by Package.resolved + project.pbxproj hashes so it only churns on dep upgrades or target/build-setting edits, not on every PR. That avoids paying the ~30s upload cost on routine code-only PRs while still letting the cache evolve when its content profile actually changes. restore-keys keeps the old `-v0` cache reachable as a one-time fallback until the new `-v1` snapshot lands.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two threads of work to shorten Debug clean-build wall time, especially on CI:
CI cache key fix —
~/Library/Developer/Xcode/DerivedData/CompilationCache.noindexwas being cached with a static
xcode-compilation-cache-v0key.actions/cache@v4only saves a new cache when the primary key has noexact match, so the cache was frozen at the very first snapshot ever
written (~152MB, verified via
gh api /repos/onevcat/Prowl/actions/caches).Most compile work didn't find its content hash in there, so CI kept
cold-compiling almost everything. New key rotates only when
Package.resolvedorsupacode.xcodeproj/project.pbxprojchanges,so routine code-only PRs don't pay the ~30s upload cost but the
cache still gets refreshed when its content profile actually shifts.
restore-keyskeeps the old-v0snapshot reachable as a one-timefallback until the new
-v1cache lands.Type-checker hotspots — profiling with
-Xfrontend -warn-long-function-bodies=100 -Xfrontend -warn-long-expression-type-checking=100surfaced ~14s of cumulative type-checker time concentrated in a
handful of files. The biggest mechanical wins:
Dictionary(uniqueKeysWithValues: arr.map { ($0.id, $0) })with typed
for-loops (huge generic-resolution reduction).(repository:, worktrees:)tuple to a nominalArchivedWorktreeGroupstruct.result-builder accumulates less state per scope.
Type-checker hotspots (Debug, M2 Pro, threshold 100ms)
ArchivedWorktreesDetailView.bodyCanvasView.bodyWorktreeTerminalTabsView.bodyShelfOpenBookView.bodysupacodeApp.initToolbarNotificationGroup.toolbarNotificationGroupsListRuntimeSnapshotBuilder.makeSnapshotTargetResolver.makeSnapshot* The Dictionary-init hotspot is gone; the remaining time has shifted
to the surrounding outer body, which is a smaller per-file cost.
Build settings
COMPILATION_CACHE_ENABLE_CACHING = YESat the project Debuglevel (was target-only). In practice SPM packages already inherit
the cache via Xcode defaults so this is mostly belt-and-braces, but
explicit is better than implicit.
EAGER_LINKING = YESfor Debug to overlap link with compile.Wall-clock impact (M2 Pro 12-core, Debug)
Measured locally, two states each. The PR's biggest single CI lever is
the cache-key fix — local numbers conflate it with the existing warm
local cache, so the delta below is the realistic shape of what CI
should see once the new
-v1cache populates.Code-level refactoring on its own buys ~5% on cold; the dramatic
drop comes from the cache actually being usable, which is what the CI
fix unlocks.
CI impact
Once the new
-v1key populates after merge, subsequent runs shouldrestore a much fuller cache and skip most SwiftCompile work. The key
rotates only on dep / project-file changes, so routine PRs hit primary
key → restore → build → no re-upload (avoids per-run ~30s save cost
and CI minute charges). Stale or harmful caches can be wiped via the
GitHub Actions cache UI; the next CI run rebuilds a fresh
-v1fromscratch.
Test plan
make build-appmake lint(SwiftLint strict)make format-lint(swift-format strict)xcodebuild test -only-testing:supacodeTests/ToolbarNotificationGroupingTests-v1-<hash>entry)