Repository navigation
Conversation
…LocalStorage A debug build gave every AsyncLocalStorage an own `__id__` property and gave the prototype a `Symbol(nodejs.util.inspect.custom)` method. A release build has neither. test/js/node/visible-surface.test.ts records the surface of a release build, so two of its tests failed on every debug build. The debug log also read `__id__` from the storage of every frame on each get() and set(). The storage of a frame can be a module mock. A mock of vi.mock() or vi.doMock() throws on a read of an export that its factory did not return, so on a debug build AsyncLocalStorage threw in a job that such a factory scheduled. Remove the id, the inspect hook, and the frame dump. The `[async_hooks]` debug log keeps its lines, without the storage ids.
|
Updated 11:05 PM PT - Nov 10th, 2026
❌ @robobun, your commit fe6edab has some failures in 🧪 To try this PR locally: bunx bun-pr 48532That installs a local version of the PR into your bun-48532 --bun |
|
Status: reproduced, fix is in this PR (#48532). How I reproduced it, on a debug build of main 89452d2: bun bd test test/js/node/visible-surface.test.ts
const { AsyncLocalStorage } = require("node:async_hooks");
const storage = new AsyncLocalStorage();
console.log(Reflect.ownKeys(storage), require("node:util").inspect(storage));
CI: no failure comes from this diff. The diff changes only code that a debug build runs, and CI runs release builds. The release bundle of the built-in modules is byte-identical to the one of main. Build 140025 on fe6edab (an empty commit on top of the diff), not finished yet:
Build 139937 on fea61ad (the same diff), finished:
|
|
No actionable comments were generated in the recent review. WalkthroughThe changes remove context formatting, per-instance debug IDs, and the custom inspector representation from ChangesAsyncLocalStorage debug output and tests
Suggested reviewers: Merge Risk:
|
Problem
AsyncLocalStoragean own__id__key and aSymbol(nodejs.util.inspect.custom)prototype method. Sobun bd test test/js/node/visible-surface.test.tsfails: 1 pass, 2 fail.__id__from the storage of every frame. Since bun test: Vitest compatibility #44833 that can be avi.mock()mock, which throwsNo "__id__" export is defined on the mock.src/js/node/async_hooks.ts(lines 85, 248, 375). No issue reports it. My PRs TextEncoder: set thelengthofencodeto 0, as its IDL requires #48395, worker_threads: make the MessagePort inspect hook native and return a non-port receiver only to util.inspect #47313 and node: give ChildProcess and FSWatcher node's_handle#35841 note it.Fix
debugFormatContextValue). The$debugcalls keep their text.AsyncLocalStorage {}fromutil.inspect().test/js/node/async_hooks/async_hooks.node.test.ts: 4 pass / 3 fail, then 7 pass.visible-surface.test.ts: 1 pass / 2 fail, then 3 pass.Background
IS_BUN_DEVELOPMENTcode and$debug(...)arguments run only in a debug build. They run also when the debug log is off.WeakMap: debug-only state that no CI lane runs.Downsides
[async_hooks]debug log lose the storage ids and the frame dump. Before:set { zs2l7z@hooks: number; }. After:set.Notes
The new tests
All three are in
test/js/node/async_hooks/async_hooks.node.test.ts. Each fails on a debug build of main and passes on a debug build of this branch. A release build passes them with and without the change, because only a debug build has the defect.util.inspect()__id__write in the constructor, the inspect hookdisable()with a receiver that is no storagethis.__id__read in the log call ofdisable()(aTypeErrorforundefined)test/js/node/visible-surface.test.tsis not changed. Its snapshot already has the release surface.Measurements
Debug builds of main 89452d2 and of this branch, idle machine.
async_hooks.node.test.tsvisible-surface.test.tsnew AsyncLocalStorage():Math.randomcalls,__id__sets,__id__getsrun()+getStore():__id__getsReflect.ownKeys(storage)["__id__"][]util.inspect(storage)AsyncLocalStorage { [debug id]: "3f2j5w@hooks" }AsyncLocalStorage {}node:async_hooksbuiltin_sources.bin), debugRelease code generators (
bun scripts/build.ts --profile=release --target=codegen) on main 89452d2 and on this branch:node:async_hooks3d8299c3918c54105d85af39b5ad8e16InternalModuleRegistryConstants.bin)16c943149c5feb72e05c4a5d86d88f27builtin_sources.bin)Release builds and Node.js v26.3.0 already give the "this PR" answers of rows 3 to 6.
--aot, measured on the earlier base 2596ac5: the program that--aot-outwrites for a file that usesAsyncLocalStoragegoes from 1,754,040 B to 1,751,632 B. The code map has the same 55node:async_hooksfunctions with the same sizes (16,202 B).The debug log
BUN_DEBUG_ASYNC_HOOKS=1turns it on. Two nestedrun()calls print 20 lines before and after. 17 of them had a storage id. The part of the id after@was meant to name the file that created the storage. It is the constanthookstoday, becausecallerSourceOrigin()answersbuiltin://node/async/hooks.console.log(storage)on a debug build printed the one-line[debug id]form. It now prints the member list: 13 lines, where a release build prints 11 without the two private fields (measured on the earlier base 2596ac5).Other suites on the debug build of this branch
With
--timeout=270000, the per-test limit of the ASAN lanes in CI (scripts/runner.node.ts:1536).bun bd testhas a 5 s default limit, and many of these tests start a debugbunof their own. Runs "with the bundle of main" use the debug binary of this branch with main's bundle of built-in JS. A debug binary reads that bundle from disk.test/js/node/async_hooks/: all 5 files pass (63, 76, 5, 5, 7 tests).mock-fn.test.js(848),mock/mock-module.test.ts(314),mock/mock-hoist.test.ts(134),vi-utils.test.ts(191),vitest-flavor.test.ts(191),require-bun-test.test.ts(9),mock/mock-module-non-string.test.ts(5),mock/mock-module-resolve-log.test.ts(1),mock/6874/A.test.ts(1),mock/6874/B.test.ts(1),mock/6879/6879.test.ts(2). All pass.Bun.ModuleGraph, whose frames have a graph or the global object as storage:module-graph.test.ts(297),module-graph-matrix.test.ts(893),module-graph-workers.test.ts(479),module-graph-gc.test.ts(38),module-graph-io.test.ts(28),module-graph-callbacks.test.ts(1). All pass.module-graph-isolation.test.ts: 413 pass in 4 of 6 runs. In the other 2 runs, 4 tests and 1 test of one concurrent group ran past their own poll deadline. With the bundle of main: 413 pass in 5 of 6 runs, and 5 tests of that group failed in the other run.module-graph-compile.test.ts: 76 pass in 7 of 9 runs. In 2 runs the first build hook ran past its 60 s limit. The file writes four debug executables of 1.6 GB at once. With the bundle of main: the same failure in 1 of 8 runs.Not changed by this PR
These make a debug build answer differently from a release build through other code. Each needs a change of its own.
$debug(...)arguments.src/codegen/replacements.ts:280expands the call to(IS_BUN_DEVELOPMENT?$debug_log(...):void 0), so a debug build evaluates the arguments with the log off. A guard on the log switch there would stop that for every module.src/js/node/async_hooks.ts.enterWith()andrun()callthis.getStore()inside$assert(lines 262 and 313), andassertValidFrame(line 74) requires the storage of a frame to be an object. With a receiver that is not a storage,enterWith()throws on a debug build and returnsundefinedon a release build.test/js/node/members-of-prototype-objects-for-any-receiver.test.tsis 27 pass / 5 fail on a debug build before and after. Itsnode:async_hookscase goes from 32 differing lines to 20. The 20 lines are the answers ofenterWith()andrun()for receivers that are not storages. Itsnode:events,node:stream,node:httpandnode:zlibcases also fail. This PR does not look at them.test/js/node/async_hooks/AsyncLocalStorage.test.tsat the 5 s default limit: 62 pass / 1 fail before and after. "re-entering a storage inside run() does not grow the context" takes 12.5 s before and 6.6 s after.stream.debugId(src/js/internal/streams/native-readable.ts:68) andglobalThis.__lastId/__getId(src/js/node/child_process.ts:73).