Skip to content

test(aot): run the five builds of errors-made-by-a-constructor-that-the-build-cannot-name.test.ts concurrently and pin every line - #47850

Open
robobun wants to merge 3 commits into
mainfrom
robobun/0c1bc9c0/faster-errors-made-by-a-constructor-test
Open

robobun wants to merge 3 commits into
mainfrom
robobun/0c1bc9c0/faster-errors-made-by-a-constructor-test

Conversation

@robobun

@robobun robobun commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • test/aot/errors-made-by-a-constructor-that-the-build-cannot-name.test.ts takes 344 s on debian 13 x64-asan (build #137158). One test runs five bun test --aot builds in turn.
  • It pins one of a run's 45 lines, by substring, and does not check that both inner test files ran. A run as one program that ignores --aot passes.
  • bun bd test fails on main: a debug build lists a private frame in plain Bun's stacks.

Fix

  • Each build is its own test, and the five are concurrent. A debug build runs them in turn and hides private frames, as a release does.
  • A snapshot pins plain Bun's 45 lines, both inner tests, the totals and the exit code. Each compiled run must equal it and say how it was built.
  • Verified, the file alone: release 15 to 20 s before, 7 s after. release-asan 57 to 97 s before, 42 to 47 s after. bun bd test: 1 fail before, 6 pass in 265 to 338 s after.

Background

  • bun test --aot compiles the test files and what they import, then runs them. In layers, the lowest program holds all of Bun's own JavaScript.
  • Processor time stays: 66 s of user time (release). In a CI batch that keeps every processor busy, the file takes as long as before (Notes).
  • Considered fewer builds. A validating run needs no build of its own, but bun test --aot builds and runs in one command.
  • AOT: hangs, wrong answers, failed builds; bun test --aot follows #44833's mocks #47605 splits this file the same way. Its test/aot/every-build.txt leaves the file out of CI.
Notes

What stays as it was. The project of the test is the same text, byte for byte. The six commands are the same: plain bun test --isolate, then --aot as one program, over one layer, over two layers, and as one program and over two layers with BUN_JSC_validateAOTRuntime=1. The words of BUN_DEBUG_TEST_AOT_WORDS are the same. run() of harness/run.ts still fails a run that was not built in the layers that it asked for. The one new variable is BUN_JSC_showPrivateScriptsInStackTraces=0, on a debug build only.

Assertions, before and after.

Before After
Lines of the plain run 45 counted. Line 2 checked by substring All 45 in an inline snapshot
Lines of a compiled run Equal to the plain run The same
Test files of the project Not checked (pass) for both tests, 2 pass, 0 fail, 4 expect() calls, Ran 2 tests across 2 files. for every run
That a run was compiled Only for the runs in layers, by run() Every compiled run says AOT test run | one program or | layers. The runs as one program must say one program
Exit code First, alone, for the plain run In the same comparison as the output, for every run
A run that fails Shows the exit code Shows its stderr too
A plain run that fails One failure, and no build starts The same: beforeAll fails with the exit code and the stderr of the plain run
Limit 360 s for five builds The default of test/aot/supported.ts for one build: 120 s, or 270 s with assertions

On debian 13 x64-asan the one test of main takes 344 s of its 360 s.

Three faults show the difference. Each was put into both versions of the file and run with the release build.

Fault Main's file This PR
b.test.js skips its test 1 pass 1 fail: (skip), 1 pass, 1 skip, 3 expect() calls
The two runs as one program leave --aot out 1 pass 2 fail: "built": undefined
The project drops the frames of the package from every trace 1 pass 1 fail: the snapshot

A fourth fault makes the plain run fail: b.test.js fails its test. Main's file and this PR each report one failure in 0.1 s and start no build. The second commit of this PR reported six failures in 8.0 s and started five builds.

Where the time goes. One run at a time, processor time of the run and of all that it starts:

Run release, 12 CPUs release-asan, 8 CPUs debug, 12 CPUs
plain 0.04 s, 0.1 s CPU 0.8 s, 0.7 s CPU 1.0 s, 1.6 s CPU
one program 2.7 s, 16.8 s CPU 15.9 s, 79.7 s CPU 75.1 s, 474 s CPU
one layer 2.4 s, 13.5 s CPU 19.4 s, 77.3 s CPU 58.3 s, 333 s CPU
two layers 2.4 s, 13.5 s CPU 17.3 s, 70.1 s CPU 52.0 s, 313 s CPU
one program that validates 2.4 s, 16.1 s CPU 16.2 s, 80.0 s CPU 61.2 s, 424 s CPU
two layers that validate 3.1 s, 15.1 s CPU 21.3 s, 82.3 s CPU 52.0 s, 310 s CPU
all 13.0 s, 75 s CPU 90.8 s, 390 s CPU 299.6 s, 1855 s CPU
  • One build keeps 5 or 6 processors busy when it has them. So five at once end sooner only where processors are free.
  • A run in layers costs 11 s of processor time for any project (release): a project of two empty tests takes 11.0 s over one layer and 11.8 s over two, and 1.1 s as one program. With BUN_JSC_logAOTPhaseTimes=1, the lowest layer takes 2.3 s of the 2.9 s of a run over two layers.
  • As one program, this project takes 16.8 s. Without its two import("data:text/javascript,..") lines it takes 5.3 s. The build cannot read what such an import loads, and the program then holds most of Bun's library. Both lines stay: they are cases of the test.

Why not fewer builds. The two runs that validate repeat the builds of the runs beside them: validateAOTRuntime is an option of the run time. bun test --aot writes the program into an unnamed file and starts itself again with it (start_second_process, src/runtime/cli/run_compiled.rs). A test has no way to run that program a second time. A Bun that is built with --compiledLibrary=on does not compile its library for a run in layers, but then the package is a program of its own above the library. In the Bun under test the two are one program.

All local timings. Linux x64, 12 vCPUs, 15.6 GB, taskset for fewer CPUs. Before and after were taken in turn. The kernel time of this machine changes from run to run, and wall time follows it.

release (bun run build:release, bun-profile), the file alone, 12 CPUs:

Wall User Sys
Before 18.1, 19.4, 14.6, 17.9, 20.0, 16.3 s 69.7, 65.6, 66.8, 65.3 s 18.5, 19.4, 12.3, 17.7 s
After 7.5, 7.0, 7.5, 7.5, 7.3, 6.7, 6.5 s 68.8, 66.7, 68.1, 66.7, 63.8 s 7.7, 6.2, 7.0, 7.7, 5.0 s

release, the file alone, 4 CPUs, the environment that scripts/runner.node.ts gives a file:

Wall User
Before 21.2, 19.6 s 60.9, 59.6 s
After, --max-concurrency=4 17.5, 17.1 s 61.3, 60.0 s
After, --max-concurrency=2 18.1, 17.6 s 61.8, 60.5 s

release, inside the batch of CI's x64 lane: the 28 files that build #137158 ran beside this one, bun test --parallel=3 --max-concurrency=2 --timeout=90000, 4 CPUs:

The file The batch
Before 37.2, 35.9 s 151.2, 143.9 s
After 38.3, 35.6 s 153.3, 139.6 s

One other test of that batch, in test/aot/perf/threads-of-a-build.test.ts, failed in three of these four runs, before and after. It counts the threads of a build, and taskset changes what it sees.

release-asan (bun run build:asan, bun-asan), the file alone, 8 CPUs, the environment of the lane with AddressSanitizer:

Wall User Sys
Before 69.5, 56.6, 96.8 s 272, 265, 283 s 60, 32, 131 s
After, --max-concurrency=8 42.1, 46.5, 47.2, 42.5 s 286, 304, 303, 292 s 28, 33, 35, 28 s
After, --max-concurrency=2 46.9, 71.5, 52.3, 53.6 s 280, 304, 287, 287 s 30, 87, 45, 44 s
  • --max-concurrency=8 is what the runner passes when the file runs in a process of its own. --max-concurrency=2 is what a batch gets there.
  • User time with AddressSanitizer is 7% higher with five builds at once: 273 s before, 296 s after, on average.
  • The batch of that lane (37 files, 7 at a time) was not run locally: it needs more memory than this machine has. The first release-asan build ended when the kernel ran out of memory. A build with -j4 passed.

debug (bun bd test), 12 CPUs:

  • Before: 1 fail in 63.7 s (67.8 s with the check of the build). The first comparison with a compiled run fails at line 106: the plain run has at <anonymous> (a.test.js:54:2) at the end of 22 of its 45 lines.
  • After: 6 pass in 265.4 s, 335.2 s, 337.5 s and 276.2 s (266.8 s, 336.9 s, 338.8 s and 277.0 s with the check of the build). The longest test took 57.3 s, 87.7 s, 77.2 s and 62.6 s of its 270 s.

Most memory in use at once, beyond what was in use before the run:

Before After
release 0.24, 0.23 GB 0.84, 0.94 GB
release-asan 1.74, 1.73 GB 6.10, 6.14 GB with --max-concurrency=8, 2.87, 2.86 GB with --max-concurrency=2

The lane with AddressSanitizer has 64 GB.

The debug build. A debug build sets showPrivateScriptsInStackTraces (src/jsc/bindings/ZigGlobalObject.cpp:436). Plain Bun then lists one more frame, at <anonymous> (a.test.js:54:2), below the test function until its first await. A compiled run does not list it. With BUN_JSC_showPrivateScriptsInStackTraces=0 the debug build prints the same 45 lines as the release build and the release-asan build, plain and compiled.

Why a debug build runs the builds in turn. One build keeps 6 of 12 processors busy there. Five at once took 160 s, and each run took 153 to 160 s of the 270 s that a test has. In turn the five took 300 s, and each run took 52 to 75 s.

One folder for six runs. The runs share the project folder, as in #47605. A build writes nothing into it: the program goes into an unnamed file (create_unnamed_file, src/runtime/cli/run_compiled.rs:1509), and the text that stands in for the test files stays in memory (set_stand_in, line 91). The folder holds the same files after five runs at once.

A test that runs out of time. bun test ends the child of a test that ran out of time only where one test runs at a time (kill_dangling_processes_on_timeout, src/runtime/test_runner/Execution.rs:442). So a compiled run whose test runs out of time goes on until the file ends, where the one test of main ended it at once. It does not go on behind the file:

  • In a batch, the worker ends what a file left running before it loads the next file (swap_global_for_test_isolation, src/jsc/VirtualMachine.rs:6471).
  • In a process of its own, the runner sets BUN_FEATURE_FLAG_NO_ORPHANS=1 for a file of test/aot, and the child ends with the test process.
  • I checked both with a concurrent test that leaves sleep 300 behind its timeout. The next file of the same worker finds the child gone. So does the shell after a run of the one file with that variable. Without the variable, in a run by hand, the child outlives bun test.

On main, 72 test files of test/aot import run() and declare concurrent tests. A limit for one run belongs in run() of harness/run.ts, which this PR does not change.

The snapshot. It holds what plain Bun prints today. Two of its lines say undefined where the stack is read: Reflect.construct(Error, [..], Subclass) and an error made under Error.stackTraceLimit = 0. Node gives both a stack. The snapshot does not say that this is right. It says that a compiled run does the same.

CI, before. Build #137158, from the logs of the test jobs:

Lane The file Its batch
debian 13 x64-asan 343.55 s 37 files, 7 at a time: 536.08 s. This file is the second longest
debian 13 x64 65.86 s 29 files, 3 at a time: 199.16 s. This file is the second longest
debian 13 aarch64 44.58 s 29 files, 3 at a time

CI, this PR. Builds #137257, #137264 and #137268 ran the first, the second and the third commit. The second commit adds a comment. The third makes a plain run that fails one failure.

Lane Main, in its batch (#137158) This PR, in a process of its own (#137257) The same (#137264) The same (#137268)
debian 13 x64-asan 343.55 s 64.96 s 64.25 s 73.27 s
debian 13 x64 65.86 s 29.63 s 28.48 s 30.26 s
debian 13 aarch64 44.58 s 21.53 s not reached before the third push 20.85 s
  • The file passed on the first try each time.
  • The columns of main and of this PR are not like for like. CI runs a file that a PR changes in a process of its own, in front of the other files that build many executables: beside two of them with --max-concurrency=8 on x64-asan, beside one with --max-concurrency=4 on the others. On main the file runs inside its batch with --max-concurrency=2.
  • On x64-asan the whole file took 64 to 73 s, and the limit of one test there is 270 s.

On top of #47605. That PR has beforeAll for the plain run and test.each with the same five rows. test of its test/aot/supported.ts is concurrent with a sanitizer only. To combine the two, keep this file's version. test.serial and test.concurrent pass through the proxies of that supported.ts. The snapshot test has no await, so the snapshot also works where that test is concurrent.


[auto-merge] gate passed · iteration 1 · 1 files touched

passes on PR (with fix)
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/aot/errors-made-by-a-constructor-that-the-build-cannot-name.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/aot/errors-made-by-a-constructor-that-the-build-cannot-name.test.ts
bun test v1.4.4 (367d939d9)

test/aot/errors-made-by-a-constructor-that-the-build-cannot-name.test.ts:
(pass) plain Bun says where the engine's own constructor made an error [9.02ms]
(pass) an error made by the engine's own constructor says what plain Bun says: one program [59202.71ms]
(pass) an error made by the engine's own constructor says what plain Bun says: one layer [50112.10ms]
(pass) an error made by the engine's own constructor says what plain Bun says: two layers [52398.69ms]
(pass) an error made by the engine's own constructor says what plain Bun says: one program that validates [53738.97ms]
(pass) an error made by the engine's own constructor says what plain Bun says: two layers that validate [50028.33ms]

 6 pass
 0 fail
 1 snapshots, 9 expect() calls
Ran 6 tests across 1 file. [268.82s]
Exit: 0
diff hotspot
...-constructor-that-the-build-cannot-name.test.ts | 142 ++++++++++++++++++---
 1 file changed, 121 insertions(+), 21 deletions(-)

gate history · 2 passed · 0 rejected · iteration 1

evidence per changed file
file                                                      reads  edits  tests
…ade-by-a-constructor-that-the-build-cannot-name.test.ts      5      2     33

…he-build-cannot-name.test.ts concurrently and pin every line

One test ran five `bun test --aot` builds one after another under a
limit of 360 s. Each build is now a test of its own, and the five are
concurrent. A debug build runs them one by one.

A snapshot pins the 45 lines that plain Bun prints, the result of both
inner tests, the totals and the exit code. Each compiled run must equal
it and must say that it was built in the form that its row names.

On a debug build every run hides the frames of private scripts, as a
release does. Without that the plain run lists a frame that compiled
code does not have, and the file fails under `bun bd test`.
@robobun

robobun commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. The diff is green, and it needs a maintainer to merge.

  • The one file that this PR changes passed on the first try on every lane that ran it, in each build of this PR: #137257, #137264 and #137268.
  • Build #137268, the last one, finished red in 28 test files that this PR does not change:
    • 27 of them also fail on main. 26 are under test/aot, most of them about mocks under bun test --aot. The other is test/bundler/compile-aot-size.test.ts.
    • test/aot/engine/native-5.test.ts fails on macOS and Windows with the native assertion that fails test/aot/engine/native-6.test.ts on Linux on main.
  • 39 more files failed once and passed on a retry. Most are timeouts inside the parallel batches on debian 13 x64-asan: 59 batch failures in this build, 54 in build #137158 of main's code.
  • The three review threads are answered and resolved.

How the slowness was measured:

  • CI: the file took 344 s on debian 13 x64-asan, 66 s on x64 and 45 s on aarch64 in build #137158. The times are in the logs of the test jobs.
  • Local: release, release-asan and debug builds of main at 3bf1a06. Before and after were run in turn. Every run is in the description.

Result, the file alone, local:

Build Before After
release, 12 CPUs 15 to 20 s 7 s
release-asan, 8 CPUs 57 to 97 s 42 to 47 s
debug (bun bd test) 1 fail in 64 s 6 pass in 265 to 338 s

Result in CI:

Lane main, in its batch (#137158) This PR, in a process of its own (#137257) The same (#137264) The same (#137268)
debian 13 x64-asan 343.55 s 64.96 s 64.25 s 73.27 s
debian 13 x64 65.86 s 29.63 s 28.48 s 30.26 s
debian 13 aarch64 44.58 s 21.53 s not reached before the next push 20.85 s

The columns of main and of this PR are not like for like: CI runs a file that a PR changes in a process of its own. Processor time is the same before and after. Inside a batch that keeps every processor busy, the file takes as long as before.

@robobun

robobun commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:26 AM PT - Nov 3rd, 2026

❌ @robobun, your commit 6fe88b4 has 40 failures in Build #137268 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 47850

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

bun-47850 --bun

@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Walkthrough

The test captures and snapshots normalized plain-Bun output, including traces, test results, totals, and exit status. It compares five AOT configurations with that baseline and runs the configurations serially in debug builds or concurrently in other builds.

Changes

Stack trace tests

Layer / File(s) Summary
Capture and snapshot the plain-Bun baseline
test/aot/errors-made-by-a-constructor-that-the-build-cannot-name.test.ts
The test configures debug-build stack trace behavior, captures normalized output and test results, and snapshots the plain-Bun baseline. Lifecycle hooks create and dispose of the temporary directory.
Run and compare AOT configurations
test/aot/errors-made-by-a-constructor-that-the-build-cannot-name.test.ts
The test compares five AOT configurations with the baseline. Debug builds run the cases serially; other builds run them concurrently.

Suggested reviewers: jarred-sumner

Merge Risk: Minimal · up to 6fe88

The change strengthens output checks and runs AOT configurations concurrently outside debug builds. No concrete merge-blocking regression was identified; it is mergeable subject to normal checks.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title names the main change: the five builds of the AOT test now run concurrently and the output is pinned by a snapshot. The title is specific and matches the changeset. It is 129 characters, whi…
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.
Description check Passed The description clearly explains the problem, the implementation, the unchanged behavior, performance impact, and verification results. It does not use the exact template headings, but it provides the…
Full details: Title check

Explanation

The title names the main change: the five builds of the AOT test now run concurrently and the output is pinned by a snapshot. The title is specific and matches the changeset. It is 129 characters, which is long, but it does not use vague terms. The only concern is length, which does not make the title unrelated, vague, or misleading.

Full details: Description check

Explanation

The description clearly explains the problem, the implementation, the unchanged behavior, performance impact, and verification results. It does not use the exact template headings, but it provides the required information in the Problem, Fix, Background, and evidence sections.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 532d8f6 and 6fe88b4.

Files selected for processing: 1
  • test/aot/errors-made-by-a-constructor-that-the-build-cannot-name.test.ts

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

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Two of the 45 pinned lines say `undefined` where Node has a stack: an
error that `Reflect.construct` makes for another `new.target`, and one
made under `Error.stackTraceLimit = 0`. A comment above the snapshot
says that this is what plain Bun does today, so that a change of plain
Bun updates the text knowingly.

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

…tructor fails

`beforeAll` kept the plain run without a look at its exit code. When
plain Bun failed the project, the five rows still built and each failed
against a result that holds the raw stderr of the plain run: six
failures and five builds, where the one test of main failed at once.

`beforeAll` now holds the plain run to exit code 0 and shows its stderr
when it fails. That is one failure, and no build starts.

@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 — the latest commit adds the fail-fast in beforeAll, and the rest of the rewrite checks out against the runtime and harness.

What was reviewed:

  • BUN_JSC_showPrivateScriptsInStackTraces=0 is live: the debug default is set in ZigGlobalObject.cpp before the BUN_JSC_ env loop, so the override wins; AOT test run | one program|layers|programs without layers is printed by log_form_of_test_run in src/runtime/cli/run_compiled.rs whenever a TestRun* word is set, which every row sets.
  • A throwing beforeAll skips straight to afterAll (Execution.rs failure_skip_to), so no build starts when plain Bun fails; afterAll tolerates the unsupported path where beforeAll is a no-op and dir stays undefined.
  • Snapshot totals match the fixture (3 expect() calls in a.test.js + 1 in b.test.js, 2 files); normalizeBunSnapshot strips the [N ms] suffixes the (pass)/Ran filters rely on; test.serial.each/test.concurrent.each exist in bun:test and pass through supported.ts's skip proxy.
Extended reasoning...

The diff rewrites one test file under test/aot: the plain run moves into a beforeAll, its full output is pinned in an inline snapshot, and the five --aot builds become a test.each table run concurrently on release and serially on debug. It touches no production code and no security-sensitive surface; the only env knobs it sets are read by src/ as verified above. The one commit since the prior run addresses the fail-fast point raised then, no CODEOWNER covers test/aot, and the bug hunt ran dry, which decided approve over defer.

This branch has not been deployed

No deployments
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