Skip to content

test(prune): serve installs from one warm cache and assert whole listings - #39954

Closed
robobun wants to merge 3 commits into
mainfrom
farm/d3d05131/speed-up-bun-prune-test
Closed

robobun wants to merge 3 commits into
mainfrom
farm/d3d05131/speed-up-bun-prune-test

Conversation

@robobun

@robobun robobun commented Aug 21, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • test/cli/install/bun-prune.test.ts took 16s on the debian x64-asan lane (build 102501). Each of its 169 bun install runs fetched from verdaccio into its own cache. In CI verdaccio runs on the build under test, so on the asan lanes each fetch is slow.
  • Most prune runs did not check stderr. Trees were checked with existsSync on a few paths, so a prune that touched some other entry passed.

Fix

  • beforeAll installs the 19 registry packages the file uses into one shared cache. From a warm cache bun install makes no registry request. The 5 projects whose installs write to the cache (git, tarball, global store) get their own. A last, non-concurrent test asserts that the shared cache is unchanged.
  • installed() installs each distinct starting tree once and copies it per test. Relative links are copied as they are, junctions are re-pointed into the copy. 350 processes instead of 381.
  • Every prune run asserts stderr and the exit code. Every step asserts tree(dir), the sorted listing of each node_modules folder, the store and its hidden hoist folder, with links and dangling links marked. 276 listings replace about 285 single-path checks.
  • Verified: bun bd test test/cli/install/bun-prune.test.ts, 111 pass in 3 runs. Windows x64 on the canary of the base commit: 110 pass, 2 skipped. Timings are in the notes.

Background

  • verdaccio is the install tests' local registry (VerdaccioRegistry in test/harness.ts). In CI it runs on bunExe().
  • BUN_INSTALL_CACHE_DIR holds tarballs and manifests and overrides bunfig.toml. A manifest stays fresh for 300s (src/install/npm.rs:578), so a warm cache serves a fresh install with no request.
  • The hidden hoist folder, node_modules/.bun/node_modules, holds one link per package name of an isolated install. prune unlinks the ones it leaves dangling (housekeeping in src/install/prune.rs).
Notes

Timings. Under ASAN bun test runs at most 5 tests at a time (src/options_types/context.rs:506), so the per-test sum matters there. Local debug build, 16 shared cores, load average about 50, so wall times move by about 1s between runs.

  • Release verdaccio (the local configuration): before 17.8s wall, per-test sum 66s. After 17.2s, 18.2s, 17.6s wall, sums 63s to 69s. Wall time is flat. CPU of the whole run (user+sys) 74.6s before, 68.3s to 69.9s after. The runner process itself uses 12.5s of CPU before and 14.1s after, out of about 17s of wall: it is the bottleneck in both versions. Bun.spawn plus reading both pipes costs it about 18ms per child in the debug build, and the 276 listings add fs calls.
  • Verdaccio on the debug build, which is what CI=1 selects and what the asan lanes run (runs interleaved): before 62.5s and 59.5s wall, per-test sums 127.5s and 120.8s. After 53.0s and 50.8s wall, sums 63.0s and 58.4s. Starting verdaccio alone takes about 34s in this mode, in both versions. That start is the largest fixed cost of every install test file on the asan lanes. It is harness-level and not touched here.
  • Windows x64, release canary: 2.11s before, 2.31s after. The warm-up install runs before the first test, the listings cost a little, and verdaccio is fast there.
  • The CI run of this PR gives the real number for the asan lane.
  • The zero-request claim was checked with a counting proxy in front of verdaccio: a fresh install with a warm cache made no request, the same for an install with a lockfile and for --production.

Process counts. 381 before: 201 prune, 169 install, 11 other. 350 after: 201 prune, 138 install (67 template installs, 36 installs that are part of a scenario, 26 bun install --production cross-checks, 8 projects built directly, 1 warm-up), 11 other. The 102 setups through the helpers build 71 distinct trees.

Assertion changes.

  • expectOk (stderr is "", exit code 0) on every successful prune run, 121 places. Before, most of the 142 exit code checks came without a stderr check.
  • not.toContain("warn:") and toContain(PRUNED_NOTE) became exact stderr checks. The linker refusals, the missing workspace error, the --global rejection (all four runs), bun run prune and the global store prune output are exact now.
  • toEndWith(NOTHING(...)) became a check of the whole output.
  • tree(dir) before and after each step. The state is asserted again after each bun install --production cross-check and after each --frozen-lockfile install. Before, nothing was asserted after them.
  • --help without a lockfile asserts the usage header and an empty stderr. The long flag dry run in the --help test checks its exit code. The global store test asserts the listing of the cache's links folder.
  • The deletion test that is skipped as root was run as nobody and passes.
  • On Windows a bin lists as .bin/<name> only when both of its shims are present, so a listing that expects a kept bin still requires the complete pair, as expectBinInstalled did. A lone shim lists under its file name.
  • The file had no assertion on the absence of "panic".

What the listings pinned down. prune leaves an emptied .bin folder and emptied nested or workspace folders in place, and removes emptied scope dirs. bun install --production prints "no changes" but recreates the hidden hoist links. After bun install replaces a root copy, the nested copy it made redundant stays until prune removes it. A file: folder dependency gets no hidden hoist link.

Observation, not changed here. When two versions of one package are both direct dependencies (an npm: alias plus the real name), or when a full install follows a production install, the version that gets the hidden hoist link differs between installs of the same package.json (12 fresh installs of the alias case gave both). Two tests read the link before they prune and branch on it. With one direct and one transitive version the direct one gets the link every time (12 of 12).

Own caches. Two git tests create repos with the same content in the same second, so they share one cache key, and two tests install the same left-pad tarball. With the shared cache both pairs would write the same entry at the same time. The final test catches any new case of this.

#38952 and #39216 also edit this file. Whichever lands second needs a rebase.

…e listings

Every test in bun-prune.test.ts installed its starting tree from the
registry into a cache of its own. beforeAll now installs every registry
package the file uses once into one shared cache, and the installs the
tests run are served from it without registry traffic. Identical
starting trees are installed once and copied per test. The projects
whose installs write to the cache (git and tarball dependencies, the
global store) keep a cache of their own, and a final test checks that
nothing else wrote to the shared cache.

Each prune run now asserts stderr and the exit code, and each step
asserts the whole listing of every node_modules folder, the isolated
store and its hidden hoist folder, instead of the presence or absence
of a few paths.
@coderabbitai

coderabbitai Bot commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • test/cli/install/bun-prune.test.ts
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a1f430f4-b175-4fe4-a4d2-ee9cbc7ab13e

📥 Commits

Reviewing files that changed from the base of the PR and between 4448a2e and 6f87762.

📒 Files selected for processing (1)
  • test/cli/install/bun-prune.test.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 21, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: closed, not merged.

The speed-up this PR set out to make does not show in CI (comparison in the comment below: five baselines at 14.8s to 16.2s on x64-asan, this PR at 20.4s to 20.7s in the modified-first slot, flat locally), and the file is not on the lane's critical path. The assertion half (tree() listings, bun-prune.test.ts:145-209 on the branch) is worth re-applying after the open prune PRs land. The branch stays for that.

Comment thread test/cli/install/bun-prune.test.ts
@robobun

robobun commented Aug 21, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 6:23 AM PT - Aug 21st, 2026

✅ @robobun, your commit 6f87762b848e2438c10da2572e3edec9915f43a6 passed in Build #102651! 🎉


🧪   To try this PR locally:

bunx bun-pr 39954

That installs a local version of the PR into your bun-39954 executable, so you can run:

bun-39954 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the Windows .bin shim-pair precision — shimPairName now only collapses to the base name when both .exe and .bunx are present, so a lone shim surfaces as an unexpected entry. LGTM.

Extended reasoning...

Overview

Test-only refactor of test/cli/install/bun-prune.test.ts (~1,800 lines changed, single file). Three pieces: (1) a shared warm BUN_INSTALL_CACHE_DIR populated in beforeAll so concurrent installs don't hit verdaccio, with per-project caches for the handful that write to the cache and a final non-concurrent test asserting the shared cache is untouched; (2) installed() template-and-copy so identical starting trees are installed once and copied per test, with copyTree re-pointing Windows junctions through the resolved path; (3) every prune run now asserts stderr + exit code via expectOk, and every step asserts a full tree(dir) listing (folders, store, hidden hoist, links marked, dangling links marked) instead of spot-checking a few paths.

Prior feedback addressed

My previous review flagged that the .bin branch of listFolder() collapsed a lone .exe or lone .bunx to the same entry a full pair would produce, weakening the kept-bin assertion on Windows relative to the removed expectBinInstalled. Commit 6f87762 adds shimPairName(files, file) which returns the base name only when files contains both ${name}.exe and ${name}.bunx, and the raw filename otherwise — a half-pair now shows up as .bin/foo.exe and fails equality. This restores (and slightly exceeds) the original precision.

Security risks

None. Test-only; no production code, no new dependencies, no network beyond the local verdaccio harness.

Level of scrutiny

Medium — the diff is large and introduces new test infrastructure, but it's confined to one test file with no runtime impact. Failure modes are CI-visible (either the file fails or flakes). The author verified 111 pass on the debug build across 3 runs and 110 pass / 2 skip on Windows canary, plus a manual check of the junction branch of copyTree.

Other factors

  • Shared-cache concurrency: reads of a warm cache are safe; the design routes every cache-writing project (git deps, tarballs, global store, the mid-test tarball override via ownCache.add(dir)) to its own cache, and the trailing non-concurrent test catches any missed case.
  • copyTree: relative symlinks copied verbatim (stay valid inside the copy); absolute-target junctions re-pointed via realpathSync.native + relative(template, ...) so the copy's junction resolves inside the copy — verified on Windows per the PR notes.
  • The two hiddenHoistTarget branches accommodate a documented install-order nondeterminism rather than masking a prune bug; both arms assert the resulting link is never dangling.
  • Net assertion strength is up: 276 whole-tree listings replace ~285 single-path checks, and toEndWith/toContain on stdout/stderr became exact matches.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

@robobun compare CI timings

@robobun

robobun commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

CI timings for test/cli/install/bun-prune.test.ts, taken from the job logs (gap between the file's --- [k/N] header and the next one; bun's own Ran ... [Ns] line agrees within 0.1s).

debian 13 x64-asan, the lane the sweep flagged

build branch file time position in shard docker services still starting during the file
102838 unrelated PR 14.8s 9/290 no
102843 unrelated PR 15.0s 9/290 no
102850 unrelated PR 16.1s 9/290 no
102842 unrelated PR 16.2s 9/290 no
102501 sweep baseline 16.2s 9/290 no
102642 this PR, 4239216 20.7s 3/290 yes, the 4 services came up at +8.5s to +12.1s
102651 this PR, 6f87762 20.4s 3/290 yes, at +12.0s to +14.6s

Main builds have no asan lane, so the baselines are other PR builds. The runner runs modified files first, so in this PR's builds the file ran third in its shard, while squid, minio, redis and mysql were still starting. That inflates both PR numbers by an unknown amount (#39431 is about the same artifact). Even so, nothing here shows a win: the best case is "hidden by the noise", and the serial warm-up install in beforeAll is a cost the old file did not have.

All lanes, one sample each (same caveat: this PR's runs were in the modified-first slot)

lane 102501 102642 102651
alpine 3.23 aarch64 5.7s 6.8s 5.9s
alpine 3.23 x64 5.1s 6.3s 5.0s
darwin aarch64 1.7s 1.2s 9.4s
darwin x64 2.4s 3.0s 3.1s
debian 13 aarch64 10.1s 6.5s 6.3s
debian 13 x64 9.4s 6.8s 8.2s
debian 13 x64-asan 16.2s 20.7s 20.4s
ubuntu 25.04 aarch64 5.3s 4.6s 6.5s
ubuntu 25.04 x64 8.0s 8.2s 8.0s
windows 11 aarch64 6.9s 10.0s 12.0s
windows 2019 x64 3.5s 12.0s 9.6s

Local numbers from the PR body, for the record: flat with a release verdaccio (17.8s before, 17.2s to 18.2s after), 2.11s to 2.31s on Windows, and faster only when verdaccio itself runs on the debug build.

Conclusion. The speed-up does not show in CI, and the file is 16s out of about 4950s of test time on that lane (build 102501, 20 shards). Its shard runs 253s of tests while the slowest shard runs 415s, so the lane's duration does not depend on this file at all. The per-process ASAN cost of the 201 prune runs and the verdaccio start dominate this file, and this PR cannot move either. The review I ran on the diff came to the same verdict and added that five open PRs (#38952, #39216, #38856, #38797, #39232) edit this file, so the rewrite would also cost each of them a rebase. I am closing this PR.

Two things from the work that may still be useful:

  • The tree(dir) listing (bun-prune.test.ts:145-209 on the branch) is a compact way to assert a whole node_modules, store and hidden hoist folder. It is worth re-applying once the open prune PRs have landed, as an assertion change with no speed claim.
  • The listings exposed an installer nondeterminism. With the isolated linker and a package present at two versions as direct dependencies, for example { "dependencies": { "aliased": "npm:no-deps@1.0.0" }, "devDependencies": { "no-deps": "2.0.0" } }, the hidden hoist link node_modules/.bun/node_modules/no-deps points at either version from one install to the next (12 fresh installs of the same package.json gave both). With one direct and one transitive version it is stable. The same happens when a full install follows a --production install. That is a bun install matter in src/install/isolated_install.rs, not a prune one.

@robobun robobun closed this Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants