Skip to content

ci: add a schema-3 ci.toml bounding the cache budget (85 entries / 8.48 GB = 83%) - #603

Merged
zackees merged 1 commit into
mainfrom
ci/cache-policy
Oct 5, 2026
Merged

zackees merged 1 commit into
mainfrom
ci/cache-policy

Conversation

@zackees

@zackees zackees commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Opened as a DRAFT because this repository already ships a local-gate.toml and adding a ci.toml changes where ci-lint local-gate reads gate config from. I did not add any [local.gate.*] key — ci_lint/local_gate.py:293-306 only prefers ci.toml [local.gate] when that table is present, so with it absent local-gate.toml stays the sole declaration. Verified: local-gate lint reports the identical result with and without this file (1 violation, 3 other findings — all pre-existing, and three of them are just "PyYAML not importable" parser gaps).

Why

mimalloc-pprof runs at 85 cache entries / 8.48 GB = 83% of GitHub's 10 GiB per-repo cap, with no ci.toml to bound it. A toolchain bump lands on top of that.

The critical constraint, and how it is met

ci-lint cache janitor LRU-evicts each evict = "lru" family down to max x cardinality(per). Under-stating that product makes the janitor delete live caches. So every family's product is at or above its measured live footprint. Verified against all 85 live entries:

family           live_n   live_MB   lru_budget_MB   safe
compile              14       207            700   YES
cook                 10      1584           4200   YES
cook-delta            3         6             30   YES
registry             15        67            210   YES
soldr-mini            8        82            280   YES
solo-toolchain        8      2475           2800   YES

ALL FAMILIES SAFE (janitor LRU deletes nothing live): True

shapes deliberately not used, despite #334

shapes was merged in forge-ci/ci.yml#334 and works in ci_lint.rules.cache_static.family_footprint — but ci_lint.cache.ops._lru_evictions (line 210) still computes max x cardinality(per) and ignores it. Declaring shapes here would make CACHE-004's arithmetic look correct while the janitor evicted down to a smaller number — precisely the truncation hazard this PR is meant to avoid. max + per keeps both paths in agreement. This looks like a gap in #334 worth its own issue: ops._lru_evictions should route through family_footprint.

Budget arithmetic

compile: 100MB x 7 (platform) = 734003200 B (lockfile peak counted again)
cook: 600MB x 7 (platform) = 4404019200 B (lockfile peak counted again)
cook-delta: 10MB x 3 (os) = 31457280 B
registry: 30MB x 7 (platform) = 220200960 B (lockfile peak counted again)
soldr-mini: 40MB x 7 (platform) = 293601280 B
solo-toolchain: 400MB x 7 (platform) = 2936012800 B (lockfile peak counted again)
steady total            = 8619294720 B
+ [cache.pr].budget     = 0 B
formula applied: worst = steady + [cache.pr].budget (every writer flow pre-prunes: lockfile peak waived)
= worst case            = 8619294720 B
budget ([cache].budget) = 10200547328 B

8.02 GiB worst case vs a 9.5 GiB budget. It fits, with ~1.4 GiB of headroom for the lockfile generations a setup-soldr bump produces before the janitor reclaims them.

per = "platform" (7) rather than "os" (3) for the four families that need it: cardinality() has no per-lockfile axis, and the live check (_check_family_live_excess) compares live entry count against cardinality(per) x2. With per = "os" four families fired CACHE-004 needs_review at 10/14/15/8 live entries against a model of 6.

Live audit (ci-lint cache audit, ci.yml@92106df)

ci-lint cache audit: 85 live cache entrie(s), 8482877804B total
family                    count        bytes
compile                      14    217458483
cook                         10   1660730489
cook-delta                    3       6660187
registry                     15     70357011
soldr-mini                    8     86216756
solo-toolchain                8   2595186496
undeclared                   27   3846268382
budget: 8482877804B / 10200547328B (83.2%)

Counts: 27 CACHE-001, 2 CACHE-004 needs_review, 16 CACHE-005, 12 CACHE-006, 1 GEN-009. None of these are static-proof failures of this document — load_ci_toml is 0 findings and CACHE-004/029/030 are each 0 findings statically. They describe the existing cache state.

Two things the task brief got wrong, and one real finding

1. soldr-prepare-* is NOT setup-soldr:cross-targets. The brief said it was. It isn't:

setup-soldr:cross-targets -> ('setup-soldr-prepare-v3-',)

The live keys are soldr-prepare-<triple>-<sha> — no v3 segment, so no via value classifies them. All 1.87 GB is therefore correctly reported undeclared, not silently bound to a family. Declaring it as cross-targets would have been a lie the audit couldn't catch.

2. msys2-pkgs-upd:* and benchmark-* are hand-written actions/cache keys, not any sanctioned action's output. Same conclusion: undeclareable via via.

3. The undeclared row is 3.58 GiB — 42% of the repository's cache. Breaking it down:

prefix entries note
setup-soldr-targetcache-thin-v2- 11 no via exists for it
soldr-prepare-* 4 1.87 GB; prefix mismatch above
benchmark-allocators-* 4 900 MB, hand-written
msys2-pkgs-upd:* 4 338 MB, hand-written
benchmark-references-* / benchmark-fork-* 3 223 MB
solo-toolchain-v2-* 1 older major, not a declared legacy_prefix

So no ci.toml can currently get this repository under budget, and I'm not claiming otherwise. ci-lint needs via entries for setup-soldr-targetcache-thin-v2- and the soldr-prepare-* shape before 3.58 GiB can be declared rather than left unbounded. That's a ci.yml follow-up, not something to fudge here.

Other choices

  • [cache.promote] mode = "off" — nearest-ancestor promotion is unreleased (setup-soldr#566); claiming it would be a promise nothing implements (CACHE-030).
  • cook-delta declared as a family, not under [cache].retired: setup-soldr 0.9.27 still writes it, so retiring would fire CACHE-009 against 3 live entries.
  • [cache.pr] mode = "none", budget 0MB — no PR-scoped cache is declared today.
  • platform-selector = "src/prim/prim.c", platform-code = ["src/prim/{osx,unix,windows,wasi,emscripten}/**"] — the C library's real OS-branch layout.
  • allow.setup-soldr.only-in = ".github/actions/soldr" names a directory that does not exist: setup-soldr is called directly from ~15 workflows with no composite wrapper. It's deliberately non-matching rather than naming a fiction. Creating the wrapper is a follow-up.
  • 7 platforms, from cross.yml's build+test matrices and macos-bundles.yml's cross matrix. aarch64-pc-windows-msvc is excluded: cross.yml builds it but has no runner for it.

What I did NOT do

  • Did not delete any cache. The 16 CACHE-005 and 12 CACHE-006 findings are pre-existing and left in place — cleaning them up is a separate, deliberate cache janitor run, not a side effect of adding a policy file.
  • Did not touch [local.gate], local-gate.toml, or any workflow.
  • Did not add [[exceptions]]. The 3 GEN-003 needs_review findings on ci.toml are the known "required suite's run command isn't in a literal run: line" case; they resolve to needs_review by design, not a defect to suppress.
  • Did not merge.

Reviewer checklist

  1. Confirm the shapes gap in ops._lru_evictions is worth its own ci.yml issue (it makes bench: hole-purging chart under jemalloc, upstream mimalloc, Bun mimalloc and mimalloc-pprof (Performance section) #334 unusable for exactly the families it was built for).
  2. Decide whether undeclared families should block adopting a budget at all — 42% of this repo's cache is currently unbounded by any document.
  3. Confirm [allow] should be populated this loosely for a repo with no ci.toml history, or whether this should land as a minimal cache-only document first.

zackees added a commit to forge-ci/ci.yml that referenced this pull request Oct 5, 2026
…t blocks it (#344)

**Merged:** forge-ci/reld#218 (ac71f0b), worst case 5.92 GiB against a 9.5 GB
budget. 7 entries stay undeclared -- no via exists for msys2-pkgs,
llvm-ld-coff, linkers-Linux or setup-soldr-targetcache-thin-v2.

**Open drafts:**
- forge-ci/bosn#513 -- draft because adding a ci.toml surfaces **461
  pre-existing findings** (LAYOUT-001 x208, SEC-004 x148) that were
  invisible while there was no ci.toml. The agent also corrected my
  premise: bosn is NOT 221 distinct builds. It is 13 live build-cache
  entries under 4 toolchain digests -- setup-soldr#237 deliberately
  dropped the per-commit SHA so caches survive across commits -- plus 196
  tiny ci-lint attestation caches. A janitor dry-run plans 9.86 -> 2.07 GB.
- zackees/mimalloc-pprof#603 -- draft because a local gate exists.
  Declared families fit at 8.02 GiB vs 9.5 GB, but 3.58 GiB (42%) is
  UNDECLARABLE, see below.
- FastLED/fbuild#1652 -- fits at 9.75 GB against 9.5 GB, 448 MB headroom.

**Structural:** FastLED/FastLED#4703 applies `save: 'false'` on PR board
jobs so they restore without writing. It does NOT apply
`cache-mode: 'split'` -- verified available (it is on fbuild main and
FastLED pins @main, a floating ref) but genuinely broken: setup runs
before the compile step synthesises platformio.ini, and fbuild install
hard-fails on apollo3_red, clone and kitchensink. Shipping it would turn
board jobs red. Stops the leak at ~120 GiB; does not reach 10 GiB.

**A bug in my own #334**, found independently by the reld, fbuild and
mimalloc-pprof agents: `shapes` made CACHE-004 sum the shape maxima
while `_lru_evictions` still used `max x cardinality(per)`. Where they
diverge the janitor is stricter, so it evicted live entries precheck had
approved. Fixed in #343; both paths now call `family_footprint`.

**Next blocker, now the top ci.yml item:** `ALLOWED_VIA_VALUES` has no
entry for shapes setup-soldr actually emits, so they can never be
declared and are reported undeclared forever --
setup-soldr-targetcache-thin-v2-, setup-soldr-prepare-* without its v3
segment, setup-soldr-tarball-*, setup-soldr-cargo-registry-v2-*,
cook-base-v1-*. That is 42% of mimalloc-pprof's cache and blocks reld
and fbuild too.

Refs #153, #334, #343

Local-Gate: v1 tree=d3697bb3c8052b2e0a9ad339e012e0772ca4c929 secs=73
mimalloc-pprof runs at 85 cache entries / 8.48 GB = 83% of GitHub's 10 GiB
per-repo cap, with no ci.toml to bound it.

Every family's `max x cardinality(per)` is at or above its measured live
footprint, because `ci-lint cache janitor` LRU-evicts down to exactly that
product. Verified live against all 85 entries on 2026-10-05: every declared
family's budget exceeds its live bytes, so the janitor reclaims nothing live.

`shapes` (forge-ci/ci.yml#334) is deliberately not used -- `family_footprint`
honours it but `cache.ops._lru_evictions` still computes `max x cardinality`
and ignores it, so declaring shapes would make CACHE-004's arithmetic look
right while the janitor evicted down to a smaller number.

`[cache.promote] mode = "off"`: nearest-ancestor promotion is unreleased.

No [local.gate] key: this repository already ships local-gate.toml, and
adding a ci.toml makes ci-lint read gate config from ci.toml, so an
incomplete [local.gate] would be worse than none.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Local-Gate: v1 tree=607f48cac3d49c6d9f5efe265d422468921271f9 secs=35 lanes=lint:run
Ci-Attestation: {"at":1791193741,"gate":"python/all/lint","host":"linux-x86_64","key":"ee5a79f4784ef8fc9f863999530b31520321ac19d0a8f5dccb995bb5ef035d03","lane":"lint","parents":["1118b0674b127f9f2c88646da5359bc582b6d132"],"secs":35,"stamp":"b2a4065d7bbd4785ddcf17c38edbf4ef","tree":"607f48cac3d49c6d9f5efe265d422468921271f9","v":1,"via":"run"}
@zackees
zackees marked this pull request as ready for review October 5, 2026 10:23
@zackees
zackees merged commit 19a4029 into main Oct 5, 2026
64 checks passed
@zackees
zackees deleted the ci/cache-policy branch October 5, 2026 10:24
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