Skip to content

test(bundler): run bundler_compile_prelinked.test.ts three links at a time and assert stderr - #42358

Merged
Jarred-Sumner merged 1 commit into
mainfrom
robobun/abf69eaf/speed-up-prelinked-test
Sep 12, 2026
Merged

Jarred-Sumner merged 1 commit into
mainfrom
robobun/abf69eaf/speed-up-prelinked-test

Conversation

@robobun

@robobun robobun commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • test/bundler/bundler_compile_prelinked.test.ts takes 81 s on the Windows 11 aarch64 lane (build 114078). It links 26 executables one after the other and runs each one 3 times.
  • About 62 s of that is Smart App Control: the first CreateProcess of a new executable blocks about 2.4 s. Bun.spawn makes that call on the JS thread, so describe.concurrent alone cannot overlap it (74.5 s to 69 s).

Fix

  • The block is describe.concurrent. A beforeEach/afterEach pair lets 3 cases link at once on release builds and 1 on ASAN and debug builds (the split test(bundler): overlap bundler_compile.test.ts links on release builds and pin more outputs #39649 measured).
  • On Windows the executables start through cmd.exe /d /c. The wait happens in cmd.exe, and the cases overlap it.
  • Every run asserts an empty stderr (75 new checks). GeneratedGraph+splitting pins each loader hook count: resolve is 5 with the graph, 252 without (was <= 6, > 63). The module-load Bun.spawnSync probe is gone.
  • Verified, main to branch: Windows 11 arm64 74.5 s to 27 s, Windows Server 2019 x64 11.9 s to 5.8 s, Linux x64 release 6.5 s to 2.9 s. bun bd test is unchanged (1 slot). All 27 tests and 3 loader modes still run.

Background

  • Smart App Control is a Windows 11 code integrity policy, in evaluation mode by default. ci(windows): turn off Smart App Control on the Windows 11 test runners #41550 (open) turns it off on the runners.
  • Bun runs concurrent tests 20 at a time with no per-describe limit. 20 links at once ran CI out of memory before.
  • A loader mode is one environment for the same executable: graph in use, graph cross-checked, graph off.
Notes

Where the time goes. Probe on a Windows 11 arm64 VM with Smart App Control in evaluation mode (Get-MpComputerStatus reports SmartAppControlState: Eval, the same as the CI image): bun build --compile --bytecode 260 to 330 ms, first launch of each new executable 2.4 to 2.7 s, second and third launch 16 ms. The Bun.spawn() call itself returns after 2.3 to 4.0 s for a new executable and after 2 to 3 ms for a known one. Eight cases started at once take 22.0 s with a direct spawn (the same as serial) and 5.8 s through cmd.exe /d /c, where the Bun.spawn() call returns in 7 ms. During one first launch MsMpEng uses about 1.7 s of CPU. Pinning the test process tree to 4 or to 2 cores does not change the file's time (28.0 s and 27.2 s), so the 4 vCPU runners should see a similar gain.

Whole file, bun test with a release build, main to this branch.

machine main branch
Windows 11 arm64, 16 vCPU, Smart App Control in evaluation mode 74.5 s 26.2 to 35.2 s (7 runs, median 28.0 s)
the same, without the cmd.exe launcher 69.0 s
Windows Server 2019 x64, 16 vCPU 11.9 s 5.8 s (6.4 s without the launcher)
Linux x64, release, 5 interleaved pairs 6.3 to 6.9 s 2.4 to 3.4 s
Linux x64, bun bd test (debug + ASAN, 1 slot) 107.5 s, 127.8 s 114.0 s, 99.4 s, 99.0 s

Slot count on the Windows 11 VM: 2 slots 38.3 s, 3 slots 27 s, 4 slots 22.9 s, 6 slots 17.0 s. The file uses 3 on every release build, the number #39649 checked on the 4 vCPU, 8 GB Linux lanes.

Per lane in build 114078 (all in the parallel bucket): windows 11 aarch64 80.55 s, darwin x64 44.52 s, debian x64-asan 37.19 s, windows 2019 x64 17.27 s, debian x64 9.68 s, ubuntu x64 9.60 s, ubuntu aarch64 7.20 s, alpine x64 7.18 s, debian aarch64 7.08 s, alpine aarch64 5.00 s, darwin aarch64 4.31 s. In that batch of 189 files (91 s) this file was the critical path on Windows 11.

ASAN and debug stay serial. Locally the link is 2.5 to 2.9 s of a 3.5 s case with the 812 MB debug binary, and each run is about 0.3 s. #39649 tried 3 links at once on the x64-asan lane: the file got slower there (170 s against 107 s), because the link is three passes over the binary and the lane is bound by writeback. One slot under describe.concurrent behaves like today.

The three runs of one executable do not overlap. After the first launch a run takes 5 ms on Linux and 16 ms on Windows, so there is nothing to gain on release builds. On debug builds it would save about 0.6 s per case, and it needs a change to the run loop in expectBundled.ts, which every bundler test file shares.

bunArgs. expectBundled builds the command as [...bunArgs, file, ...args] for a compiled executable, so the launcher needs no change to the helper. cmd.exe /d /c <path> passes stdio and the exit code through. It works for paths with spaces and + (probed). It does not work for a temp directory whose path contains &, (, ), ^ or %VAR%.

Loader counts. With BUN_JSC_dumpModuleLoadingState=1 JSC prints one Loader [hook] key line per host hook call. Linux x64 release, Linux x64 debug, Windows 11 arm64 and Windows Server 2019 x64 all print the same table: resolve 5 (bun:main twice, the entry, m24 twice) with the graph, 252 without it, fetch 3, evaluate 63, import 1, and no other line on stderr.

The probe. hasPrelinkOptions let an older bun fail only in the loader log check. That was for the fail-before run of #42002. A bun without the options now fails the second and third run of every case with invalid JSC environment variable, which is also a clear failure.

Slots. 45 concurrent tests with 3 slots, one test that throws and one that times out: 45 ran, at most 3 held a slot, no slot leaked. afterEach runs after a failed or timed out test. The per-test timeout starts when the test callback starts (on_entry_started in src/runtime/test_runner/Execution.rs), so the wait in beforeEach does not count against it. The reported duration of a test does include the wait.


no test proof · iteration 1 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bundler/bundler_compile_prelinked.test.ts

@robobun

robobun commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. The diff is green. Both CI builds are red only for a test that this PR does not touch (see the end).

How the slow run was reproduced: bun test test/bundler/bundler_compile_prelinked.test.ts on a Windows 11 arm64 VM with Smart App Control in evaluation mode (the state of the CI image) takes 74.5 s on main, against 80.55 s on the lane in build 114078. A probe shows where it goes: each new executable costs 2.4 to 2.7 s on its first launch and 16 ms after that, and the Bun.spawn() call itself blocks for that time.

With this branch the same machine takes 26 to 35 s (7 runs). The file passes with bun bd test on Linux x64 (27 pass, 156 expect() calls) and with release builds on Linux x64, Windows 11 arm64 and Windows Server 2019 x64.

Time of this file in CI: main (build 114078) against the two builds of this PR. The file passed on every lane that ran it, 27 pass each time.

lane main 114383 114408 (rebased)
windows 11 aarch64 80.55 s 29.10 s 36.05 s
darwin x64 44.52 s 26.26 s 2.45 s
debian 13 x64-asan (1 slot) 37.19 s 38.78 s 34.04 s
windows 2019 x64 17.27 s 11.71 s 11.29 s
debian 13 x64 9.68 s 9.96 s 4.15 s
ubuntu 25.04 x64 9.60 s 9.27 s 4.24 s
ubuntu 25.04 aarch64 7.20 s 7.83 s 2.90 s
alpine 3.23 x64 7.18 s 5.34 s 9.96 s
debian 13 aarch64 7.08 s 7.79 s 8.84 s
alpine 3.23 aarch64 5.00 s 3.54 s 4.15 s
darwin aarch64 4.31 s job timed out before the runner started 8.09 s

The Linux and darwin aarch64 columns are not like for like. A modified file runs first in its shard, next to the start of the shard's docker services and the dependency install. On main the file runs later, in the parallel bucket. The last build of #42002 (113552) is the like for like case, because the file was new there and also ran first: 25.1 s on debian 13 x64 and 18.2 s on debian 13 aarch64, against 4.2 to 10.0 s and 7.8 to 8.8 s here.

Facts for the three choices a reviewer may want to check:

  • The slot queue is the one test/cli/install/bun-install-lifecycle-scripts.test.ts:35 and bun-workspaces.test.ts:39 already use. Here it sits in beforeEach/afterEach because itBundled owns the test body. The Infinity is the timeout of the hook, not of a test: a queued case waits there for the cases ahead of it.
  • The loader hook counts are the same on every lane above, and with the WebKit of Upgrade WebKit to cf1b36ec8703 #42319 (module loader change): the branch is rebased onto 158ff6c and bun bd test gives 27 pass there.
  • The cmd.exe launcher can go once ci(windows): turn off Smart App Control on the Windows 11 test runners #41550 turns Smart App Control off on the runners. Without it the Windows 11 VM takes 69 s.

The red tests: test/js/bun/http/serve-pending-promise-abort-leak.test.ts on x64-asan in 114383, which also fails on main. test/cli/install/migration/complex-workspace.test.ts on debian 13 aarch64 in 114408, where the install script of sharp got 500 Internal Server Error from github.com on all four attempts. Every other failure passed on a retry.

@coderabbitai

coderabbitai Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 81a5b446-0596-4cfa-9189-5070b3d24a2f

📥 Commits

Reviewing files that changed from the base of the PR and between 158ff6c and 0bdaed0.

📒 Files selected for processing (1)
  • test/bundler/bundler_compile_prelinked.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.


Walkthrough

Changes

The prelinked bundler tests now run all loader modes, support platform-aware executable launching, limit concurrent links, require empty stderr, and validate exact generated-graph hook counts.

Changes

Prelinked bundler test execution

Layer / File(s) Summary
Loader modes and concurrent execution
test/bundler/bundler_compile_prelinked.test.ts
The tests run graph, graph-validation, and by-name modes unconditionally. Windows uses cmd.exe. The suite merges per-mode environments, requires empty stderr, and limits concurrent executable linking.
Exact generated-graph hook counts
test/bundler/bundler_compile_prelinked.test.ts
Generated-graph validation now requires exact resolve, fetch, evaluate, and import hook counts. The by-name mode expects 252 resolve calls; other modes expect 5.

Suggested reviewers: jarred-sumner

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to e7bcc

No confirmed merge-blocking risk remains in these test changes.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the test file and the two main changes: limiting concurrent links to three and asserting stderr.
Description check ✅ Passed The description explains the problem, the implementation, verification results, platform-specific behavior, and preserved test coverage. It does not use the template headings exactly, but it provides …

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

@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.

I reviewed this and didn't find any bugs. A human look is still worthwhile: the hand-rolled beforeEach/afterEach semaphore with an Infinity hook timeout is a new pattern in the suite, and the pinned loader-hook counts (252/5/3/63/1) plus the cmd.exe /d /c launcher are cross-platform choices worth a maintainer sign-off.

What was reviewed:

  • Semaphore correctness — acquire/release balanced on every path; afterEach runs after throw/timeout so no slot leaks; waiting.shift() is fine at ~26 entries.
  • bunArgs placement in expectBundled.ts — for compile: true the command becomes [...bunArgs, file, ...args], so ["cmd.exe","/d","/c", exe] is a valid argv[0] launcher.
  • stderr: undefined in splitRun correctly suppresses the new stderr: "" default (expectBundled filters v !== undefined), and the line-by-line hookCalls parse turns any non-Loader [...] line into a failing key.
  • Removing hasPrelinkOptions strengthens rather than weakens coverage — all three loader modes now always run.
Extended reasoning...

Overview

This PR reworks a single test file, test/bundler/bundler_compile_prelinked.test.ts, for concurrency and stricter assertions. It switches to describe.concurrent gated by a hand-rolled 3-slot (1 under ASAN/debug) semaphore in beforeEach/afterEach, adds a Windows-only cmd.exe /d /c launcher via bunArgs to move Smart App Control's first-CreateProcess stall off the JS thread, adds stderr: "" to every graphCase run, drops the hasPrelinkOptions probe so all three loader modes always run, and replaces the loose count("evaluate")===63 / greater-than / less-than-or-equal checks in GeneratedGraph+splitting with an exact .toEqual on a parsed hook→count map.

Security risks

None. Test-only change; no production code, no network, no credentials, no untrusted input parsing beyond stderr from a locally-built executable.

Level of scrutiny

Medium. It's test-only and additive in coverage, but it introduces a concurrency pattern (beforeEach(..., Infinity) semaphore) that appears nowhere else in the suite and sits at the edge of test/CLAUDE.md's "do not set a timeout on tests" rule — the Infinity is on a hook, not a test, and is load-bearing (queued beforeEach waits can exceed the 5 s default), but a maintainer should confirm this pattern is acceptable versus, say, a harness helper. The 3-slot cap references prior measurement (#39649) on Linux lanes; the PR verified Windows and Linux but a maintainer familiar with all CI lanes' memory headroom should confirm. The exact loader-hook counts were verified on four platforms per the PR notes and follow REVIEW.md's "assert the strongest invariant," though they will need updating if JSC's dumpModuleLoadingState output ever changes.

Other factors

I traced the semaphore through failure/timeout paths and it releases correctly. I confirmed in expectBundled.ts that bunArgs prepends to the compiled executable path when compile: true, so the cmd.exe launcher works as described, and that run.stderr === undefined skips the stderr equality check (so splitRun's stderr: undefined correctly overrides the new default). Removing the probe does not weaken coverage — an older bun without the JSC options now hard-fails on invalid JSC environment variable instead of silently running only one mode. The cmd.exe /d /c launcher's known limitation (paths containing &, (, ), ^, %VAR%) is acknowledged in the PR notes and is unlikely to bite CI temp dirs. Bug-hunt exit reason was dry_streak with no findings.

@robobun

robobun commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:24 PM PT - Sep 11th, 2026

❌ @robobun, your commit e7bcca2 has 1 failures in Build #114408 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 42358

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

bun-42358 --bun

…e and pin stderr

The 26 compile cases ran one after the other. They now run under
describe.concurrent, with a beforeEach/afterEach pair that lets three
links be in flight on release builds and one on ASAN and debug builds.

On Windows the executables start through cmd.exe. The first
CreateProcess of a new executable blocks its caller while Windows looks
up the file's reputation, and Bun.spawn makes that call on the JS
thread, so without this the cases cannot overlap that wait.

Every run now also asserts an empty stderr. GeneratedGraph+splitting
asserts the exact count of each loader hook call. The module-load probe
for the JSC options is gone: every bun that can run this file has them.
@robobun
robobun force-pushed the robobun/abf69eaf/speed-up-prelinked-test branch from 0bdaed0 to e7bcca2 Compare September 11, 2026 19:37

@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.

LGTM — test-only change that tightens assertions and bounds link concurrency.

What was reviewed:

  • Semaphore in beforeEach/afterEach: acquire/release balanced on all paths, Infinity is a valid HookOptions per test.d.ts, slot count branches on isASAN/isDebug.
  • bunArgs launcher: expectBundled.ts builds [...bunArgs, file, ...args] when compile is true, so cmd.exe /d /c <exe> is argv[0] as intended; non-Windows gets [].
  • stderr: "" added to every mode; splitRun correctly overrides to undefined via spread order so the loader-log validate sees the output; the histogram .toEqual strengthens the prior loose bounds.
  • Dead hasPrelinkOptions probe and its unused imports removed alongside the unconditional mode list.
Extended reasoning...

Overview

This PR touches a single test file, test/bundler/bundler_compile_prelinked.test.ts. It converts the suite to describe.concurrent with a hand-rolled 3-slot (1 under ASAN/debug) semaphore in beforeEach/afterEach, wraps compiled-executable launches in cmd.exe /d /c on Windows to move the Smart App Control first-run stall off the JS thread, adds stderr: "" to every loader-mode run, replaces the loose loader-log bounds with an exact per-hook .toEqual histogram, and deletes the hasPrelinkOptions spawnSync probe together with its now-unused bunEnv/bunExe imports. No production code changes.

Security risks

None. The change is confined to a test file; it spawns only the locally built executable (via expectBundled) and cmd.exe on Windows CI runners. No network, credentials, or untrusted-input parsing is introduced.

Level of scrutiny

Low-to-moderate. This is test infrastructure with no runtime surface. The parts worth checking were mechanical: the semaphore's acquire/release pairing (correct — afterEach wakes one waiter or increments freeSlots, never both), the beforeEach(fn, number) overload (HookOptions = number | { timeout?: number } in packages/bun-types/test.d.ts), and that expectBundled places bunArgs ahead of the compiled file when compile is set (it does, line ~1799). The spread order in graphCase puts splitRun's stderr: undefined after the default stderr: "", so the loader-log case is exempted from the empty-stderr check and validated line-by-line instead.

Other factors

The change follows the repo's stated test conventions closely: describe.concurrent for independent subprocess-spawning suites, resource bounds branched on isASAN/isDebug, exact .toEqual on a whole object over loose comparisons, dead code removed in the same change, and a why-comment on the Windows-only branch. A prior COMMENTED review under this app's identity was followed by a force-push; no CHANGES_REQUESTED review or unresolved third-party objection is outstanding. The PR description's timing measurements are consistent with the shape of the change but are not load-bearing for correctness.

@Jarred-Sumner
Jarred-Sumner merged commit 1a126e4 into main Sep 12, 2026
4 of 5 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the robobun/abf69eaf/speed-up-prelinked-test branch September 12, 2026 03:52
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