Skip to content

Formatter: one helper for the visited set - #44139

Closed
robobun wants to merge 1 commit into
mainfrom
farm/6002b032/formatter-visited-helpers
Closed

robobun wants to merge 1 commit into
mainfrom
farm/6002b032/formatter-visited-helpers

Conversation

@robobun

@robobun robobun commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Behaviour change: none

Problem

  • The console formatter keeps a set of the values that it is rendering (the visited set). The code that takes the pooled map on first use and records a value has two copies: print_as_prelude (src/jsc/ConsoleObject.rs) and the loop that prints the cause of an error (print_error_instance_body, src/jsc/VirtualMachine.rs).
  • error printer: one render per error, [Circular] under the key that closes the cycle #37270 moves the record of an error into the error printer and needs that code again.

Fix

  • Add Formatter::visited_insert and Formatter::visited_remove. visited_insert takes the pooled map on first use. It returns true when the value was not in the set, like HashSet::insert.
  • print_as_prelude, the cause loop, and the three sites that drop the record before print_as changes the tag of a value call them.
  • Verified: the existing suites pass unchanged (Notes). 367 of 380 printed outputs are byte-identical to main. The other 13 run into the end of the stack.

Background

  • The visited set is a hash map from a thread-local pool. A value that the formatter finds in it prints [Circular].
  • print_as recurses once per nesting level, so its frame must stay small. The locals of the record live in an outlined leaf function for that reason.

Downsides

  • print_as_prelude now calls visited_insert: one more call per object, array, Map, Set, class, function, error and event printed. Pool takes and hash operations per print equal main in 15 of 15 measured cases. Release .text: 1280 bytes smaller. Instructions: not measured, perf and valgrind are not installed.
Notes

Status. #44268 covers this PR and lists it under Fixes. This PR stays open as the small fallback until that one lands. I do not push to it any more.

All numbers: linux x64, main a4f1429 and this branch.

Pool takes / hash inserts / hash lookups per print (release build, gdb hit counts of LocalKey::with for the pool, HashMap<JSValue, ()>::get_or_put_slot and ::get_index, difference of 80 and 40 prints):

print main this PR
Bun.inspect(new Error("x")) 1 / 2 / 2 1 / 2 / 2
Bun.inspect of an Error with an object property 1 / 3 / 3 1 / 3 / 3
Bun.inspect of an Error with a cause 1 / 3 / 3 1 / 3 / 3
Bun.inspect({ a: 1, b: [1, { c: 2 }] }) 1 / 3 / 3 1 / 3 / 3
reportError(new Error("x")) 0 / 0 / 0 0 / 0 / 0
reportError of an Error with an object property 1 / 1 / 1 1 / 1 / 1
reportError of an Error with a cause 1 / 1 / 1 1 / 1 / 1

The other 8 cases (primitive properties, assigned cause, AggregateError of 2, console.log) are equal too.

Stack reserved per function (sub ...,%rsp):

function main, release this PR, release main, debug+ASAN this PR, debug+ASAN
Formatter::print_as 56 B 56 B 320 B 320 B
Formatter::print_as_prelude 88 B 16 B 544 B 384 B
Formatter::visited_insert (new leaf) 56 B 288 B
VirtualMachine::print_error_instance_body 376 B 360 B 10816 B 10560 B

print_as is the frame that recurses. print_as_prelude and visited_insert return before the formatter descends.

Binary. size of the release binary: .text 80660492 -> 80659212 bytes. Code of the printer functions (nm -S): 18958 -> 18491 bytes.

Output corpus. 38 values (plain errors, errors with properties, cause chains of 4 and 12, AggregateErrors, errors that reach themselves, objects and arrays that hold errors, primitives) through 10 entries (console.log, console.error, console.log(v, v), Bun.inspect at depth 2, 0 and Infinity, util.inspect, throw, Promise.reject, reportError). One process per output, debug+ASAN build. 367 of 380 outputs are byte-identical.

  • 12 are e.cause = e; e.errors = [e] and a.x = [b]; b.y = b on the six entries where main recurses until the stack ends. The exit status is the same (134 or 139). The recursion prints 7 more renders before that, because the frame of print_error_instance_body is 256 B smaller.
  • 1 is Bun.inspect at depth Infinity of an AggregateError that lists itself: 1014 levels before the stack check stops it, main 1013.

Suites run (debug+ASAN): test/js/bun/util/inspect.test.js, test/js/bun/util/inspect-error.test.js, test/js/node/util/bun-inspect.test.ts, test/regression/issue/circular-error-stack.test.ts, test/js/web/console/console-log.test.ts, test/js/bun/util/reportError.test.ts, test/js/bun/test/stack.test.ts.


no test proof · iteration 2 · the description declares no behaviour change, so there is no failing test to prove; the existing suite in CI is the check

Comment thread src/jsc/ConsoleObject.rs Outdated
Comment thread src/jsc/ConsoleObject.rs Outdated
Comment thread src/jsc/ConsoleObject.rs Outdated
The visited set of the console formatter records the values that are
being rendered, so that a value that reaches itself prints [Circular].
The code that takes the pooled map on first use and records a value had
two copies: print_as_prelude and the loop that prints the cause and the
error-valued own properties of an error.

Move it into Formatter::visited_insert, with visited_remove for the
removal, and call them from both places and from the three sites that
drop the record before print_as changes the tag of a value.

No behaviour change.
@robobun
robobun force-pushed the farm/6002b032/formatter-visited-helpers branch from 18a3f20 to 7bfdf30 Compare September 27, 2026 19:21
@robobun
robobun marked this pull request as ready for review September 28, 2026 19:46
@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: oven-sh/bun/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 97713e77-c458-4596-abd0-d3a8e44163a2

📥 Commits

Reviewing files that changed from the base of the PR and between a4f1429 and 7bfdf30.

📒 Files selected for processing (2)
  • src/jsc/ConsoleObject.rs
  • src/jsc/VirtualMachine.rs

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


Walkthrough

Formatter now provides methods to insert and remove visited values. Console object printing and error-chain cycle tracking use these methods instead of manipulating the visited map directly.

Changes

Visited-map handling

Layer / File(s) Summary
Visited-map handling in console printing
src/jsc/ConsoleObject.rs
Formatter adds visited_insert and visited_remove. Console printing uses them for circular-reference checks and before recursive printing. The node_data_mut safety documentation is shortened; its implementation is unchanged.
Error-chain cycle tracking
src/jsc/VirtualMachine.rs
Error-chain cycle detection uses Formatter methods to insert and remove visited errors. Imports now use direct Tag and TagPayload imports instead of the console_object module alias.

Suggested reviewers: jarred-sumner, dylan-conway

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 7bfdf

No merge-blocking behavior change is identified; this refactor appears ready to merge after normal checks.

🚥 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 is concise and accurately identifies the main change: centralizing visited-set handling in one formatter helper.
Description check ✅ Passed The description explains the problem, fix, behavior impact, verification results, performance data, and test suites. It does not use the exact template headings, but it provides the required informati…

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.

Code review found no issues

No high-confidence issues detected in this change.

@robobun

robobun commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator Author

@robobun wake up!!

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

@robobun wake up!!

@robobun

robobun commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator Author

I am here. The status:

If you want a different shape, for example one PR and not two, tell me and I change it.

@robobun

robobun commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: this change is on main. #46021 merged the helpers of this PR (Formatter::visited_insert and Formatter::visited_remove in src/jsc/ConsoleObject.rs) together with the fix of #37270 that uses them.

@robobun robobun closed this Oct 22, 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