Skip to content

fleet: cargo target duplication across worktrees — shared compile cache, seeding, and a storage cap #3448

Closed

Description

@dezren39

Problem

Every fleet worktree compiles the same dependency graph into its own cargo target dir, and the storage + CPU cost keeps biting:

  • 2026-10-26 cleanup: 84 cargo target dirs totalling 1,182.8 GiB deleted from this laptop.
  • 2026-11-01 ~01:26 CST: /home hit 100% full (0 bytes free) — the immediate cause of a failed NixOS switch (SQLite fetcher-cache I/O error). Drew ordered a target/cache cleanup; disk recovered to ~55%.
  • 2026-11-01 ~03:23 CST, roughly two hours after that cleanup deleted every target dir: 12 fleet worktree pkgs/id/target dirs already exist again, 198G total across fleet + primary + worktrees — each one a from-scratch copy of the same ~760-package graph (debug and release profiles multiply it further).
  • Same night: 12 concurrent lane cargo builds drove load to 118 on 16 cores, and the nix side built the id package twice concurrently from divergent checkout sources (different drv/output hashes for identical intent) — one was a stale Oct-30 toplevel build still holding daemon slots; both had to be found and killed by hand during a production switch.
  • Nothing is configured today: no sccache (not installed), no CARGO_TARGET_DIR, no shared cargo config on the laptop.

What to evaluate (measure, then adopt what the data supports)

  1. sccache (or equivalent content-addressed compile cache) shared across all worktrees — kills the repeated dependency compilation without target-dir locking between concurrent lanes.
  2. Shared CARGO_TARGET_DIR per repo across worktrees — proven manually on 2026-10-26 (a worktree pointed at the primary's target reuses the warm cache); the trade-off is cargo's target-dir lock serializing concurrent lane builds. May suit a hybrid: shared for deps-heavy phases, per-lane for final artifacts, if the tooling allows.
  3. Cache seeding for fresh worktrees (reflink/copy-on-write copy of a warm target where the filesystem supports it) so a new lane never starts cold.
  4. Profile discipline for lanes: build only the profile the task needs (check vs test vs release), instead of several profiles per worktree by default.
  5. Nix side: a shared binary cache for the fleet's own builds (the id package and the system toplevel) so identical checkouts substitute instead of rebuilding; plus keeping fleet checkouts converged on the same main so drv hashes dedupe in the store.
  6. A storage cap with a janitor: a bounded, automatic pruning rule for target dirs (age/size based, never touching a target with a build in flight) so /home can never silently refill to 100% again — tonight's disk-full broke a production switch.

Acceptance

  • Measured before/after on the laptop: total target-dir storage across the fleet after a representative day of lane work, and cold-start build time for a fresh worktree.
  • The adopted setup is the default for new fleet worktrees (encoded wherever lanes are provisioned), not a per-lane manual trick.
  • No lane workflow regression: concurrent lanes can still build without corrupting each other's artifacts.

Related: #3195 (pkgs/id build-time review — its build-hygiene line names sccache / shared target), and the pkgs/id crate-split issue filed alongside this one (splitting reduces per-build work; this issue removes the duplicated work).

Activity

  1. developing-today commented on Nov 1, 2026

    @developing-today
    Contributor
  2. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    Filed from Drew's question in main chat 2026-11-01 ~03:23 CST ('is there anything in all the targets that we can avoid duplicating'). Triage task 5369 queued at priority 20, DB-verified. Headline numbers are in the issue body: 12 worktree target dirs / 198G regrown within ~2h of a full cleanup; 1,182.8 GiB deleted in the 2026-10-26 precedent.

  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  4. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  5. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (triage)

    Fresh laptop census (2026-11-01 ~09:35Z, this run):

    • Target-dir total: ~233 GiB (du sum 244,585,580 KB) over 12 worktree pkgs/id/target dirs + the primary clone + ad-hoc target-* dirs — regrown within hours of this morning's cleanup (issue reported 198G at ~03:23 CST).
    • Largest: primary ~/git/code/pkgs/id/target 67G; worktrees 5197/5140/3688 at 31G/31G/30G. The big worktree targets are debug-profile only (5197: 31G, all under debug/) — lanes compile debug/test almost exclusively; the dependency graph is what multiplies.
    • Config state: sccache not installed; CARGO_TARGET_DIR and RUSTC_WRAPPER unset; ~/.cargo/config.toml does not exist. Nothing is configured, as filed.
    • Disk: /home at 55% post-cleanup. Laptop load ~97 at census time (fleet builds running) — the CPU side of duplication is live, not theoretical.

    Evaluation → pinned design:

    1. sccache — ADOPT (Packet A, task 5374). Content-addressed, shared across worktrees, no target locking; it caches exactly what duplicates (the ~760-package dependency graph). It does not cache the local crate — that's pkgs/id: split the monolith crate into sub-crates along module seams (prioritized by Drew 2026-11-01) #3447's split, so the two are complementary. Provisioning: dedicated SCCACHE_DIR, SCCACHE_CACHE_SIZE=50G, ~/.cargo/config.toml rustc-wrapper, CARGO_INCREMENTAL=0 (sccache can't cache incremental builds).
    2. Shared CARGO_TARGET_DIR — REJECTED as the primary mechanism. One target dir serializes ~12 concurrent lanes on cargo's target lock and thrashes when profiles/flags differ between lanes; the failure mode is lanes blocking each other, which is the resource this fleet is shortest on.
    3. Warm-target seeding — REJECTED. Copying a 30G target per worktree is the storage disease (and reflink availability is not established); sccache gets the same cold-start win without the copies.
    4. Target-dir janitor with a storage cap — ADOPT (Packet B, sequenced). A user-level scheduled janitor: enumerate fleet worktree target dirs (+ target-*), delete those whose worktree's task is terminal / mtime older than 48h, keep the fleet total under a 150 GiB cap (LRU by mtime), never touch the primary clone's target or a live-claimed worktree; dry-run + log on its first runs. This is the safety net that makes another 100%-disk night structurally impossible while sccache reduces regrowth. Packet B is enqueued by Packet A's worker on completion (the queue's fix dedupe key admits one live fix task per issue).
    5. Lane profile discipline — noted, mostly already true: the census shows lanes already build debug-only; Packet A's verification keeps it that way (no lane release builds; release goes through nix/CI per standing policy).

    Before/after measurements named for the fix packets: Packet A — two fresh-worktree pkgs/id debug builds back to back (wall times + sccache --show-stats hit rate + per-worktree target size) and a re-run of this census total. Packet B — fleet target total after a representative lane day under the cap, and fresh-worktree cold-start time unchanged-or-better.

  6. added
    work/assignedQueue worker lifecycle state
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 1, 2026
  7. 192 remaining items

  8. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    event: done
    task: 6194 (repo-health-code-pilot, OpenChamber scheduled lane)
    state: Triage complete. Verdict posted above; scoped follow-up task 6443 queued (install janitor timer, verify cap, record option 2-5 decisions, close with evidence). No code, branch, worktree, or config changes in this task.
    labels: queue/queued, status/needs-triage for live follow-up 6443.

  9. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  10. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    event: picked_up
    task: 6443 (repo-health-code-pilot, OpenChamber scheduled lane)
    state: Claimed atomically; first checkpoint records origin=openchamber-scheduled-lane. Fresh GitHub state: #3448 is OPEN with no hard-stop labels; PR #4175 is MERGED, so there is no active competing PR.
    next: Fetch and fast-forward ~/git/code main, inspect the merged janitor install docs, then create the dedicated task worktree and revalidate laptop timer/load state before applying anything.

  11. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    event: progress
    task: 6443 (repo-health-code-pilot, OpenChamber scheduled lane)
    state: Janitor tests pass (11/11). Installed and enabled the hourly user timer from the updated worktree; systemd unit verification passes (with an unrelated pre-existing invalid proxy-supervisor Wants symlink warning). The first reviewed dry-run measured 372.20 GiB against 150 GiB; the 20-candidate batch projected 266.17 GiB. The bounded apply run completed successfully, reclaimed 106.78 GiB, and reported 276.49 GiB remaining / over_cap=true. More eligible targets remain beyond the 20-target batch; active/open-PR/recent targets remain protected, so the cap is not yet met.
    option_decisions:

    • Option 2, shared CARGO_TARGET_DIR: reject fleet-wide; per-worktree isolation avoids cargo target-lock serialization/cross-branch artifacts, while sccache shares compilation without a shared target.
    • Option 3, cache seeding: reject; the live shared sccache cache fills organically, while seeding large target trees adds disk/I/O duplication.
    • Option 4, profile discipline: adopt as lane guidance; run only the profile needed for the task acceptance (check/test/release), not multiple profiles by default.
    • Option 5, Nix binary cache: defer; dedicated cache infrastructure and measured Nix-store duplication are not established by this cap task.
      next: Timer is enabled and active; next scheduled run is 16:03 CST. Re-measure after subsequent bounded hourly passes; keep fleet: cargo target duplication across worktrees — shared compile cache, seeding, and a storage cap #3448 open until under cap or remaining excess is only protected paths. Current issue owner remains dezren39.
  12. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    event: done
    task: 6443 (repo-health-code-pilot, OpenChamber scheduled lane)
    state: Completed the scheduled-lane close-out. Janitor tests passed (11/11); the hourly user timer is enabled/active and its apply service succeeded. Two bounded applies reclaimed 106.78 GiB and 11.54 GiB. Final dry-run at 2026-11-02T21:54:45Z reports 268.06 GiB / 150 GiB, over_cap=true, with candidates=[]; all remaining paths are protected by the janitor policy (open PR, live task/build, younger than 48h, or unmapped worktree). No protected target was removed.
    evidence: PR #4175 merged: #4175 ; dry-run log /home/user/.local/state/cargo-target-janitor/runs/run-20261102T215445Z-1084333.jsonl ; apply logs /home/user/.local/state/cargo-target-janitor/runs/run-20261102T215104Z-1065270.jsonl and /home/user/.local/state/cargo-target-janitor/runs/run-20261102T215426Z-1083022.jsonl. Option 2-5 dispositions are recorded in #3448 (comment).
    next: Hourly timer continues re-evaluating protected paths; next scheduled trigger is 16:07 CST. Closing this umbrella under its accepted exception: remaining excess is only on protected paths.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

priority/highImportant and time-sensitive

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions