You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(apple-runner): evict stale runner cache keys after a build - #3247
Runner cache keys under ~/.agent-device/apple-runner/derived/<platform>/cache-<hash> are never removed. Any runner source or Xcode change mints a new 150-230 MB key and the old one can never match again; one Mac reached 3.45 GB across 20 keys. Details and measurements are in #3246.
After a build (the only event that adds a key), the sweep removes sibling keys in the same platform folder unless they are:
the key just built, or among the 3 most recently used (new key included) by .agent-device-runner-cache.json mtime, which every reuse rewrites;
used within the last day, re-checked under the lock, which covers a runner that resolved its key but has not written its lease yet;
held by a build (cache-<hash>.lock, acquired without waiting);
named, by xctestrun path or cache key, by a live runner lease (owner live or unknown, or the leased runner process still running).
AGENT_DEVICE_IOS_RUNNER_CACHE_KEEP=<n> sets the count, 0 disables. A set AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH is never swept. The sweep runs off the start path, is best-effort, and loads lazily so the eager closure budgets are unchanged.
Not in this PR: trimming build scratch inside a kept key (about 97% of each key), and the un-keyed pnpm build:package output. Both are listed in #3246.
Tested at 0542d32. pnpm check:affected --run passed, plus pnpm check:fallow --base origin/main, the eager-closure and package-closure tests, and all of packages/platform-apple/src/runner. New tests cover keep count, the one-day floor, stubs, non-key entries, a held lock, live, dead and handed-off leases, 0, the path override, and the sweep after a real ensureXctestrunArtifact build with a stubbed xcodebuild.
I ran the sweep against a metadata-only copy of a real 20-key cache and its 44 leases. It kept 3 keys per platform and would free about 2.1 GB of 3.45 GB (1.5 GB iOS simulator, 0.6 GB macOS).
Not run: a live simulator build through the hook (it would delete other agents' keys in the shared cache home), and a physical device.
Automated review (Claude): no data-loss bug on the default path; two narrow races/gaps worth fixing, the rest checked and fine.
Confirmed (low to medium)
Idle check uses a stale snapshot, not re-checked under the lock. runner-cache-retention.ts computes lastUsedMs for every key once (listCacheKeyDirectories, then the 24h filter at the .slice(keep - 1).filter(...) chain, ~L44-47), then deletes candidates one by one, each rm -r taking seconds. A process whose runner key is K (a different checkout/Xcode than the sweeper's) can reuse K after the snapshot: tryReuseExistingXctestrun rewrites the metadata (fresh mtime) and releases the cache lock. Before writeRunnerLease runs (runner-session.ts ~L321, after port allocation, plist rewrite and xcodebuild spawn) the sweeper takes K's lock, finds no lease, and deletes K. The 24h floor exists to cover exactly this gap but is evaluated before the lock, so it does not. Fix: in evictIfUnused, after acquiring the lock, re-stat the metadata mtime and skip if nowMs - mtime < MIN_IDLE_MS (also keeps the same-snapshot ordering for keep ranking out of the picture). Requires K idle more than 24h at snapshot time plus two processes with different keys, so rare.
Lease protection is path-prefix based and misses leases whose xctestrun is outside the cache root. listActiveRunnerLeaseXctestrunPaths is matched with xctestrunPath.startsWith(derived + sep). prepareXctestrunWithEnv (runner-artifact-env.ts L121) writes the per-session .xctestrun into iosXctestEnvDir when set, and that path is what runner-session.ts stores in the lease. With iosXctestEnvDir configured, a live runner (owner alive, daemon up for days, key not current and not reused for >24h) is not recognized as using its key and its products get deleted. The lease already records cacheKey (runner-lease.ts L121/138, equal to basename(derived) via resolveRunnerCacheKey); matching on lease.cacheKey === path.basename(derived) in addition to the prefix would cover this. The unit test seeds leases with paths under derived/Build/Products, so it cannot see this.
(Minor) fs.promises.rm(derived, {recursive}) is not atomic. If the process dies mid-delete and the metadata file is already gone but products remain, the next start gets cache_metadata_missing, which ensureXctestrunUnderCacheLock deliberately does not clean, so xcodebuild builds into the leftover tree and the manifest then certifies whatever is in the product paths. If the metadata file survives, validateRunnerCacheArtifactManifest returns artifact_manifest_missing or artifact_content_mismatch and the tree is cleaned and rebuilt, which is fine. Cheap hardening: unlink the metadata file first, or rename derived to a non-matching sibling before rm. Leaving as-is is defensible since an aborted-build stub already behaves this way.
Checked and fine
(1) Race between lock release and lease write: only the case in item 1 gets through. Same-process concurrent ensure for the same key is safe: the sweeper's acquireRunnerXctestrunCacheLock(K, 0) sees a held claim whose token is in liveClaimTokens (judgeStandingClaim, not a spent own claim) and reports busy, so it skips. Stubs with no metadata (lastUsedMs 0) are only deleted if the lock is free, so an in-progress first build holding the lock is safe. The lock dir is a sibling (<key>.lock), so rm of the key does not touch it. A lease with live owner or unknown liveness counts as active; a dead owner with a verified runner pid (handed-off runner) also counts; runnerPid: null with a dead owner is correctly treated as a leftover.
(2) Keep arithmetic: the current key is filtered out before the sort, so slice(keep - 1) keeps the keep - 1 most recent others: keep=1 keeps none beyond current (all subject only to the 24h floor), keep=2 keeps the newest other, keep=3 the newest two. A current key that is not newest by mtime does not matter. The 24h floor is an AND with the rank cut, as documented. keep=0 or an unparsable value returns early or falls back to 3. OK.
(3) listRunnerLeasesForOwner refactor: filter order changed from per-entry continue to a post-filter on the same predicates; identical results, including the startTime === undefined case. OK.
(4) void evictStaleRunnerCachesBestEffort: the dynamic import and the sweep are inside one try/catch, and per-key errors are swallowed, so no unhandled rejection. Eviction runs in the daemon (prepare ios-runner is a daemon handler), not a short-lived CLI, so exit mid-delete is only the crash case in item 3. Metadata mtime is refreshed on every reuse (writeRunnerCacheMetadata is called from tryReuseExistingXctestrun), so the last-used signal is valid.
(5) acquireProcessLock({timeoutMs: 0}): params.timeoutMs ?? DEFAULT keeps 0 (nullish, not falsy), the do/while makes exactly one tryAcquireProcessLock, remaining <= 0 breaks, and it throws process_lock_timeout; evictIfUnused catches that and returns false. A dead-owner or recycled-pid lock is reclaimable on that single attempt (judgeStandingClaim returns reclaimable), unless the reclaim mutex is momentarily held, in which case it reports busy and the key is simply skipped until the next sweep.
The eviction logic in 0542d32 looks correct, but the build-then-sweep route has no live proof yet. The sweep runs in the daemon right after a real build, and only unit fixtures with a stubbed xcodebuild cover it. Two things are unproven: a real start that builds still answers while the sweep deletes 150-230 MB trees, and the stale_cache_evicted decision fires on the real route. Please run this on a booted iOS simulator from this head. Seed ~/.agent-device/apple-runner/derived/ios-simulator/cache-ffffffffffffffff/Build/Products/ with no metadata file, so it ranks last. Set AGENT_DEVICE_IOS_RUNNER_CACHE_KEEP to the number of real keys other than the one being built, plus 1, so no real key ranks past the cut. Start the daemon with AGENT_DEVICE_IOS_CLEAN_DERIVED=1 to force a rebuild, then run agent-device open <app> --platform ios and a snapshot in the same session. Three outputs prove it: diagnostics showing built_new then stale_cache_evicted for the stub, the stub directory gone with every other cache-* directory intact, and the snapshot succeeding.
Not blocking, take or leave: the comment at runner-cache-retention.ts:100 says the next build cleans a half-deleted key, but the build deliberately skips cleanup on cache_metadata_missing and builds over it, so "never a hit; the next build for this key overwrites it" is accurate. No test reaches the under-lock mtime re-check at line 92 or the metadata-first unlink, because the snapshot filter already excludes fresh keys. A test that touches a key's metadata after the listing and asserts it survives would cover both. listActiveRunnerLeaseArtifacts at runner-lease.ts:479 re-derives the live/unknown owner rule that classifyRunnerLease already owns. The empty catches at runner-artifact.ts:212 and retention.ts:50 swallow every eviction failure, so a debug diagnostic there would help while the start path stays non-failing.
I looked for a smaller design and found none materially smaller, since the PR already reuses the cache lock, the lease store and the decision emitter. Could the liveness filter go through classifyRunnerLease and drop the inline rule?
The Smoke failure is likely unrelated. It fails in the iOS XCTest regression step, which runs raw xcodebuild test-without-building and never reaches the daemon code in this PR. That job also sets AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH, which makes the sweep inert at retention.ts:39. I did not run the unit, eager-closure or fallow checks, so I have not verified the PR body's claims about them. Before merge, the live simulator run above must show the build-then-sweep route working with the session still answering.
Pushed 503e977. Live proof below, then the code changes.
Live proof (this head, booted iOS 27.0 simulator)
Isolation: the shared ~/.agent-device is used by other agents, so every command ran with HOME set to a scratch directory. The runner cache root ($HOME/.agent-device/apple-runner/derived/ios-simulator), the runner leases and the daemon state all derive from os.homedir(), so they all moved into the scratch home. ~/Library/Developer in the scratch home is a symlink to the real one, because simctl and Xcode find simulators there; the simulator was a throwaway I created with simctl and deleted afterwards. I did not set AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH (it makes the sweep inert). I used com.apple.Preferences as the app.
Built one real key first: open com.apple.Preferences --platform ios in the empty scratch home produced cache-817d3c95b86bcf90 (167 MB, with metadata). I then copied that tree to cache-1111111111111111 and cache-2222222222222222 to stand in for two real keys from other Xcode or runner versions (same bytes, fresh metadata), and seeded cache-ffffffffffffffff/Build/Products/stub.txt without metadata. So: 2 real keys other than the one being built, AGENT_DEVICE_IOS_RUNNER_CACHE_KEEP=3.
Fresh daemon with AGENT_DEVICE_IOS_CLEAN_DERIVED=1, then open com.apple.Preferences --platform ios --session s and snapshot -i --session s.
Before:
$ ls $HOME/.agent-device/apple-runner/derived/ios-simulator
cache-1111111111111111 cache-2222222222222222 cache-817d3c95b86bcf90 cache-ffffffffffffffff
(1) Diagnostics from the daemon request log (paths shortened to <home>):
12:45:54.361 info runner_xctestrun_cache {"action":"clean","reason":"forced_clean","derived":"<home>/.../cache-817d3c95b86bcf90"}
12:45:54.455 warn runner_xctestrun_cache {"action":"rebuild","reason":"cache_metadata_missing", ...}
12:46:01.665 info runner_xctestrun_cache {"action":"build","reason":"built_new","derived":"<home>/.../cache-817d3c95b86bcf90", ...}
12:46:01.743 info runner_xctestrun_cache {"action":"clean","reason":"stale_cache_evicted","derived":"<home>/.../cache-ffffffffffffffff"}
built_new for the real key, then stale_cache_evicted for the stub 78 ms later. (With AGENT_DEVICE_IOS_CLEAN_DERIVED=1 every ensure call cleans and rebuilds the key, so a second forced_clean and built_new pair for cache-817d... follows at 12:46:01.668 and 12:46:08.004. That is the existing behaviour of the env var, not the sweep: the sweep never touches the current key.)
(2) After:
$ ls $HOME/.agent-device/apple-runner/derived/ios-simulator
cache-1111111111111111 cache-2222222222222222 cache-817d3c95b86bcf90
Stub gone, both other real keys and the current key intact.
(3) Snapshot, run while the build and sweep were in flight and again after they finished (open returned Opened: com.apple.Preferences):
The daemon, runner and simulator were mine and were stopped and deleted afterwards.
Code changes
runner-cache-retention.ts: the comment now says a half-deleted key is never a hit and the next build for that key overwrites it.
Eviction failures: the empty catches in runner-artifact.ts and runner-cache-retention.ts now emit a warn diagnostic (runner_xctestrun_cache_eviction_failed, with the key and error message); the start path still never fails.
listActiveRunnerLeaseArtifacts now filters through classifyRunnerLease (anything but stale, or a stale lease whose runner process is still intact) instead of the inline owner-liveness rule. The existing lease tests pass unchanged.
New test: a runner reuses key B between the listing and the lock (its metadata is touched when the first eviction's rm runs); B must survive while the other stale key is evicted. I confirmed it fails when the under-lock recheck is removed.
apple-runner project (70 files, 803 tests), format:check, lint and typecheck pass locally. I did not run the smoke or eager-closure suites; CI covers them.
The earlier review at 0542d32 asked for live proof, and that is now in place. The quoted daemon log and directory listing from this head match what that review asked for, though I did not rerun the simulator proof myself. The code still needs two changes before merge, and Compatibility & Provenance is red because of it. There are no conflicts.
emitEvictionFailure is exported, but only its own module calls it, so fallow flags it as an unused export. evictStaleRunnerCachesBestEffort also repeats the same runner_xctestrun_cache_eviction_failed emit with the same payload. That gives one diagnostic two writers that can drift apart. Could the retention module be the only emitter of that diagnostic? evictStaleRunnerCaches catches its own listing, stat and per-key failures, so the outer catch in runner-artifact.ts only covers a failed dynamic import. Please drop export from emitEvictionFailure. Then either remove the duplicate emit or give the import failure its own one-line phase. Moving the whole fail-soft wrapper into the retention module would also work.
This also needs a fix before merge, as the open P1 threads below say: the lease filter in runner-lease.ts still fails open, but eviction should treat a lease as unused only on positive proof. listRunnerLeases returns an empty list on any readdir error, and a null start-time read marks a live runner as not intact when the owner is stale. The rule could be that every lease not proven dead counts as in use, and the sweep aborts when leases cannot be listed. classifyOwnerLiveness from host-kit already reads an unreadable start time as alive, so the runner check can reuse it.
The inline threads on the readdir fallback (Copilot, cubic), the stale-owner lease check (Copilot, cubic) and the null start-time probe (cubic P1) still apply. Six docs-wording threads on configuration.md and commands.md also still apply: r4195359969, r4195414306, r4195414322, r4195414334 and r4195414362. Two threads do not apply and can be resolved. r4195414297 does not apply because the stale key is 30 days before the fixed test time, and the real clock is later, so its idle time only grows. r4195414349 does not apply because the empty catch now emits a warning, and the remaining empty returns are intended for a stub with no metadata.
The fallow failure at 503e977 has two parts. The unused export above comes from this PR's changes since 0542d32. The complexity findings in http-server.ts come from main, and this PR does not touch that file. I did not run the apple-runner unit suite, the eager-closure check or fallow locally. The failure attribution comes from the CI job log. I also did not check how deleting a key's derived tree affects a runner that is already running. The next step is to remove the unused export and the duplicate emit so fallow passes on the PR's own findings.
@thymikee pushed 94bef0c. emitEvictionFailure is no longer exported, and the retention module is the only writer of runner_xctestrun_cache_eviction_failed; the wrapper in runner-artifact.ts now emits runner_xctestrun_cache_eviction_unavailable for an import failure only. Eviction treats a lease as unused only on positive proof: the lease directory is read strictly (only ENOENT is empty, any other error aborts eviction with a warning), owner-state-dir-gone counts as in use, and the leased runner is classified through classifyOwnerLiveness, so an unreadable start time keeps the key. The docs now describe the keep count as the number of most-recent keys guaranteed to survive, name the per-platform-and-kind folders, and mark the sweep best effort. New tests cover an unlistable lease directory, a state-dir-gone owner, and a recycled runner pid. Local pnpm check:affected --run and fallow audit pass. I am waiting on CI for the Compatibility & Provenance result; the earlier failure also listed complexity findings in http-server.ts, which this PR does not touch.
A failure to enumerate the platform cache root is currently converted into “no keys,” so permission or I/O failures silently disable the cleanup that is meant to stop unbounded growth. Emit the eviction-failure diagnostic before returning (or propagate to the best-effort wrapper) so operators can distinguish an empty cache from a broken sweep.
Distinguish lock contention from other lock acquisition failures
This catch treats every acquisition failure as an intentionally held build lock. Errors such as EACCES/EIO or a broken lock path are therefore silently ignored instead of producing the documented runner_xctestrun_cache_eviction_failed warning. Suppress only confirmed lock contention and let other acquisition failures reach the outer diagnostic handler.
Cache lease liveness observations during eviction sweeps
listActiveRunnerLeaseArtifacts() scans and reclassifies every lease for every eviction candidate. Liveness classification can launch synchronous ps probes, so the reported 20-key/44-lease cache can cause hundreds of main-thread probes; the fire-and-forget task still blocks the daemon event loop while these run. Cache/batch liveness observations for the sweep while retaining the per-key under-lock freshness check.
Preserve normalized error details in fallback eviction warnings
This fallback warning also strips the caught error down to its text, losing errno and typed error details such as reasons and hints. Use normalizeError(error) for the diagnostic payload so an unavailable sweep remains actionable.
Preserve normalized filesystem error details in diagnostics
Reducing the caught error to message drops filesystem errno fields and any typed AppError details/hints needed to diagnose why eviction failed. Record normalizeError(error) in the diagnostic data so the repository's normalized failure metadata is preserved.
The eviction fix at 94bef0c is in good shape. Most of what I raised on 503e977 is now fixed, and the fallow unused-export failure is gone because the export was removed. CI is green at 94bef0c with 16 checks and none failing, and there are no conflicts.
One problem remains. listRunnerLeases now wraps the strict read in a catch-all that returns an empty list. A single unreadable lease file (EACCES or EIO) therefore hides every lease, and owner cleanup skips all the readable leases it owns. Those runners can then be orphaned. At the merge base each bad file was skipped on its own. Only eviction should read strictly, so please make every other caller of listRunnerLeases skip unreadable files one by one, for example with a strict option that only listActiveRunnerLeaseArtifacts sets. A test with one unreadable lease next to a readable owned lease would prove it: owner cleanup should still stop the readable one.
On evidence, I did not run the apple-runner unit suite or fallow locally, and I did not rerun the live simulator proof. The live evidence from the earlier review covers the eviction route, and this change only tightens the lease filter. The tests stub classifyOwnerLiveness, so the null start time reading as live comes from reading host-kit, not from a test.
Please resolve the threads that no longer apply. The cubic-dev-ai thread on the stale key anchored 30 days before the fixed time is benign, because the real clock is later and the idle time only grows: #3247 (comment). The cubic-dev-ai thread on the empty returns in the eviction loop is benign too, since the catch now calls emitEvictionFailure and the remaining empty returns are for keys with no metadata: #3247 (comment). The other earlier threads from Copilot and cubic-dev-ai are fixed at this head. The cubic-dev-ai P2 thread on lease listing still stands: #3247 (comment).
Once the lease listing skips each unreadable file on its own, this is ready to merge.
This catch treats every lock-acquisition failure as ordinary contention. An EACCES/EIO while creating or inspecting the lock therefore skips eviction silently instead of reaching the outer runner_xctestrun_cache_eviction_failed diagnostic promised by the new behavior. Only the typed process_lock_timeout case should mean “held”; rethrow other failures for the outer best-effort handler.
This rescans and classifies every lease once per eviction candidate. classifyRunnerLease performs synchronous process-liveness probes (including ps for live foreign owners), so a cache with K stale keys and L leases can issue O(K×L) blocking probes on the daemon event loop; the 20-key/44-lease case motivating this PR can already produce hundreds. Add a candidate-targeted lease query that filters by cache key/path before liveness classification while retaining the under-lock check.
@thymikee pushed 4c240c6. listRunnerLeases is gone. readAllRunnerLeases now takes { strict }: only listActiveRunnerLeaseArtifacts (eviction) reads strictly, where only ENOENT means no leases and any other error aborts the sweep with the existing warning. Owner cleanup reads non-strictly again and skips each unreadable file on its own, as at the merge base. New tests in runner-cache-retention.test.ts: owner cleanup still stops a readable owned lease next to a chmod 000 lease file (fails on 94bef0c), and eviction removes nothing when one lease file is unreadable. Local: format:check, lint, typecheck, fallow audit --base origin/main, and the runner-lease, runner-session, retention and owner-cleanup tests pass; I did not run the full check:affected. All review threads here were already resolved.
This PR is ready. At 4c240c6 the lease-listing problem from the earlier review (#3247 (comment)) is fixed. Owner cleanup now skips each unreadable lease file on its own, and a lease directory that cannot be listed evicts nothing. The new tests cover both cases. CI is green: 16 checks, none failing at this commit. There are no conflicts, and nothing else stands between this PR and merge.
On the open review threads, the ones below are fixed at this commit and can be resolved: #3247 (comment) (owner cleanup now reads with strict:false, so each unreadable file is skipped on its own), #3247 (comment) (the strict read throws on any error except ENOENT, covered by the test "evicts nothing when the lease directory cannot be listed"), #3247 (comment) (the real clock is later than NOW_MS, so idle time only grows and the stale key still qualifies), and #3247 (comment) (the catch calls emitEvictionFailure, and the remaining empty returns are for keys with no metadata). No P1 or P2 thread still applies, and no lower-priority thread still applies.
I did not run the apple-runner unit suite or fallow locally, so the passing claims rest on the green CI. I did not rerun the live simulator proof. This change only touches lease-listing error handling, which is off the device-facing build path, so the live evidence from the earlier review still covers the eviction route. I also did not re-trace the lock ordering for concurrent leases in this change.
This PR is ready. The lease-listing finding from the earlier review (#3247 (comment)) is now fixed at both readers in 4c240c6, and no new problems came up in the delta. Not blocking: the two new chmod 000 tests in runner-cache-retention.test.ts (line 220) only exercise the error path when the suite runs as a non-root user, since root can still read the file. You can skip them when process.getuid?.() === 0, or leave them as they are.
The earlier inline thread on owner cleanup in runner-lease.ts (#3247 (comment)) is fixed at this head: cleanup now skips unreadable lease files, and the owner-cleanup test covers it. You can resolve that thread.
All 16 checks pass at 4c240c6, and there are no conflicts. I did not run the tests myself. That they pass at 4c240c6 and fail at 94bef0c comes from reading the code. I also did not check on a live device whether the post-build eviction route runs during a real runner build, but this change only touches lease reading, and the earlier review covered that route. Nothing else stands in the way, so it can go to the maintainer for merge.
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
ready-for-humanValid work that needs human implementation, judgment, or maintainer merge
3 participants
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
Runner cache keys under
~/.agent-device/apple-runner/derived/<platform>/cache-<hash>are never removed. Any runner source or Xcode change mints a new 150-230 MB key and the old one can never match again; one Mac reached 3.45 GB across 20 keys. Details and measurements are in #3246.After a build (the only event that adds a key), the sweep removes sibling keys in the same platform folder unless they are:
.agent-device-runner-cache.jsonmtime, which every reuse rewrites;cache-<hash>.lock, acquired without waiting);AGENT_DEVICE_IOS_RUNNER_CACHE_KEEP=<n>sets the count,0disables. A setAGENT_DEVICE_IOS_RUNNER_DERIVED_PATHis never swept. The sweep runs off the start path, is best-effort, and loads lazily so the eager closure budgets are unchanged.Not in this PR: trimming build scratch inside a kept key (about 97% of each key), and the un-keyed
pnpm build:packageoutput. Both are listed in #3246.Closes #3246
Validation
Tested at 0542d32.
pnpm check:affected --runpassed, pluspnpm check:fallow --base origin/main, the eager-closure and package-closure tests, and all ofpackages/platform-apple/src/runner. New tests cover keep count, the one-day floor, stubs, non-key entries, a held lock, live, dead and handed-off leases,0, the path override, and the sweep after a realensureXctestrunArtifactbuild with a stubbedxcodebuild.I ran the sweep against a metadata-only copy of a real 20-key cache and its 44 leases. It kept 3 keys per platform and would free about 2.1 GB of 3.45 GB (1.5 GB iOS simulator, 0.6 GB macOS).
Not run: a live simulator build through the hook (it would delete other agents' keys in the shared cache home), and a physical device.