Repository navigation
Conversation
…ss of its text A Log keeps one LineColumnTracker, so that many diagnostics in one source cost one scan. The tracker knew its source by the address and length of the contents and of the path. A read buffer that is used again puts the next file at the same address, so a file of the same length was taken for the last one. Its diagnostics got the line, the column and the excerpt bounds of the file before it. Each Source now has an id, drawn when the Source is constructed. The tracker keys on the id and on the address and length of the contents. A clone keeps the id only if it borrows the same bytes. A debug build scans the first 4096 bytes again and asserts that a resumed cursor agrees.
|
Status: ready for review. This PR is #46277. How I reproduced it (debug build and release build of main):
|
|
Updated 12:17 AM PT - Oct 27th, 2026
✅ @robobun, your commit 7503d263e040ecc139a20b25b2c1c6c697473789 passed in 🧪 To try this PR locally: bunx bun-pr 46277That installs a local version of the PR into your bun-46277 --bun |
WalkthroughThe change adds ChangesDiagnostic source identity
Suggested reviewers: Priority: Merge Risk:
|
|
Merged main (8a0e49e) into this branch.
|
The drop was there because the tracker knew a source by its address. A new Source now has a new id, so the next manifest does not resume the scan of this one.
There was a problem hiding this comment.
I re-reviewed after b88ac25 and found no bugs; the earlier nit about the now-redundant tracker drop in src/install/npm.rs is addressed by that commit. A human look is still worthwhile because the PR adds a new field to bun_ast::Source and a process-wide id scheme, which the description itself asks a maintainer to sign off on.
What was reviewed:
SourceId::new/next_block: ids from a per-thread block of 2^32 with one Relaxedfetch_addper block; checked that multiples ofBLOCKare skipped so 0 stays "no source" and blocks cannot overlap across threads.Clone for Source: borrowed contents keep the id, owned contents mint a new one; thebundle_v2.rscall site now using.clone()only loses scan reuse, not correctness.LineColumnTracker::matches/reset_forand the debug-only rescan assertion inerror_position; the path ptr/len checks were dropped, which is fine since the id already distinguishes sources.- The four new install tests drain stderr and exit concurrently and assert exact line:column lists; the bunfig case uses
withoutAsanQuarantineso the address reuse it depends on actually occurs.
Extended reasoning...
The change touches the shared Source struct in src/ast/lib.rs (new id: SourceId field, manual Clone, a #[thread_local] counter plus a global AtomicU64 for block allocation), rekeys LineColumnTracker on that id, updates the three struct-literal/clone sites in the bundler and resolver, removes the per-producer tracker reset in src/install/npm.rs, and adds unit tests plus four install tests. It touches no auth, crypto, or input-parsing surface; the only security-adjacent effect is that the bunfig token redaction now sees the correct line, which a test asserts. The latest commit addresses the one inline nit from the prior run and the hunt exited on dry_streak with no findings. Defer rather than approve because the PR introduces a new cross-cutting identity mechanism on a core type and the author explicitly requests a maintainer decision between this design and a tracker drop in each producer.
There was a problem hiding this comment.
🔇 Additional comments: 1
src/ast/lib.rs-2292-2292: 1
2292-2292:Trivial · Maintainability & Code Quality · Quick win
⚠️ Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.Store the tracker ID as
SourceId.
source_id: u64removes the type distinction introduced bySourceId. Keep the field typed asSourceId, deriveDefaultand equality forSourceId, and compare or assign the newtype directly. This keeps future byte offsets or counts from being assigned as source IDs. Based on learnings, “avoid using bare primitive types ... for domain identifiers” and use newtype structs instead.Source: Learnings

Review info
-
Run configuration-
- Source mode: Auto
- Repository: oven-sh/bun
- Branch: robobun/4e81506c/stale-line-column-tracker
- File: .coderabbit.yaml
- Commit: b88ac25
- Layers: Repository YAML
- Review profile: ASSERTIVE
- Plan: Advanced
- Run ID:
5257c1e9-bdfa-4929-8943-f76aad632fcc
Files selected for processing: 5-
src/ast/lib.rssrc/bundler/bundle_v2.rssrc/bundler/linker_context/generateCodeForFileInChunkJS.rssrc/install/npm.rssrc/resolver/package_json.rs
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.
-
|
On the CodeRabbit note about
|
Problem
bun installwith twofile:folders printsb/package.json:4:105for a syntax error at 1:138. Two 5000-byte bunfig files print 29tokencharacters unmasked.LineColumnTracker::matches(src/ast/lib.rs:2259) knows a source by addresses and lengths. A reused buffer gives the next file the same address.Fix
Sourcedraws aSourceIdat construction. The tracker keys on it. A clone keeps it only for borrowed bytes.BuildMessage.position.file: real lifetimes forSource,Path,ImportRecordand resolve results #46834 added ininstall/npm.rsis removed.test/cli/install/bun-install.test.ts(4 new cases fail on main), unit tests under Miri. Self-reviewed: 21 concerns raised, 20 addressed. Rejected: a private tracker field (the bundler needs a public one).Background
Logkeeps one tracker, so N diagnostics in one source cost one scan (Compute diagnostic positions incrementally to fix quadratic hang in Bun.JSONC.parse #31273).Sourceacceptable? perf: cache thread_id::current() in a #[thread_local] slot #30971 uses a stamp,bun lint,bun fmt: the first run, oxlint's order, Prettier's Tailwind plugin, native JavaScript plugins, a fasterBun.Glob#45043 and Fix use-after-free ofBuildMessage.position.file: real lifetimes forSource,Path,ImportRecordand resolve results #46834 a drop.Downsides
Sourcegrows from 112 to 120 bytes. Each construction adds 6 instructions and no lock prefix.Bun.JSONC.parse('{}'): +0.24% instructions (control -0.05%). Release.text: +0 B, function bytes +115 B.Sourcerewritten in place still resumes the scan. No producer does that.Notes
Repro. A project with
"a": "file:./a"and"b": "file:./b". Bothpackage.jsonfiles are 260 bytes.ahas a duplicate key on line 4.bis one line with a syntax error at column 138. The test casea syntax error after a warningwrites these files. Runbun installwith nobun.lock. main prints this forb:The file
bhas one line. With this change the place is1:138and the gutter says1. On main the excerpt is the text ofbbetween the line start and the line end that the scan kept froma. A cursor is only resumed at an equal or later offset, so the second finding must be further into its file than the first.When it shows. For
file:folders: on a run that resolves the folders (nobun.lock, orbun update). Abun installwith abun.lockprints no located diagnostic for them. One byte of difference in the length of the file or of the path hides it.Producers, on release builds (main
6f58f6b894against this diff on that commit, mimalloc92ef6587in both, measured before #46834 merged):file:folders, pooled read buffer (folder_resolver.rs:226), the 3 folder cases of the test~/.bunfig.tomlthen./bunfig.toml, 5000 and 20000 bytes, 5 runs each (bunfig/arguments.rs:100)9:1, the file says3:1, 10 of 10 runsPackage/Scripts.rs:346)processDependencyList.rs:252)The last three free the first text and allocate the next one. mimalloc gives the same address back for these sizes. ASAN's quarantine hides that, so the bunfig case of the test spawns bun with
withoutAsanQuarantine. That case fails on a debug build of main and on a release build of main.On main
8a0e49eedb, after #46834, the key and these producers are the same by reading, and the 4 cases of the test fail on a debug build of it.The token. In the bunfig case, the stale line start is inside the value of
token. The redactor masks a value when it sees its key, and the excerpt starts after the key. main prints2 | 4e5f6g7h8i9j0k1l2m3n4o5p6q7r8", url = 5 }. This change prints the line with the value masked. The 120-byte excerpt window can cut a key off in the same way for one long line. That is #43316 and this PR does not change it.Measurements.
size_of(rustc -Zprint-type-sizes, main8a0e49eedb):Source112 to 120 B,Log48 to 48 B,LineColumnTracker256 to 248 B. Each type that holds aSourceby value grows by 8 B (before Fix use-after-free ofBuildMessage.position.file: real lifetimes forSource,Path,ImportRecordand resolve results #46834:Graph::InputFile192 to 200 B,transpiler::ParseResult1368 to 1376 B).llvm-objdumpof the release build: load of the thread-local,test,je,lea, store of the thread-local, store into theSource. The cold path callsSourceId::next_block, which has the onlylock xaddfor ids in the binary.bun buildof 2,001 modules drew 6,052 ids on 13 threads and took 13 blocks (read with gdb at exit).perf_event_open, 10 interleaved rounds, a second copy of the PR binary as control, release builds of6f58f6b894with and without this diff):Bun.JSONC.parse('{}')transformSync('1')bun buildof 2,001 modulestransformSyncrow. The diff adds only the draw on that path.Transpiler::parse_maybe_return_file_only_allow_shared_bufferis 2,256 B smaller in the PR build, so I think that ThinLTO inlines differently there. An earlier build of this branch gave the same -6.8%.llvm-size):.text81,695,461 B in both (the section is aligned to 256 B). Function bytes, as the sum of unique symbol sizes fromllvm-nm -S: +115 B..tbss+8 B,.data+64 B,.bss+8 B.valgrindandbloatyare not installed on the machine that I used. The instruction counts are from a hardware counter and the byte counts fromllvm-sizeandllvm-nm.Designs that I weighed.
read_package_json_from_disk. It fixes one of the four producers, and a new producer must know the rule. ASAN does not reuse an address, so the debug lane does not catch a producer that forgets.Sourcehas a lifetime since Fix use-after-free ofBuildMessage.position.file: real lifetimes forSource,Path,ImportRecordand resolve results #46834, so this is possible as a later step.Source, which takesnoaliasfrom every&Sourceparameter.AtomicU64::fetch_addfor eachSource, as perf: cache thread_id::current() in a #[thread_local] slot #30971 does for each thread pool. That is alock xaddfor each construction. The thread-local block has one for each thread.Source.Sourcestays at 112 bytes. A 24-bit id repeats after 16,777,215 sources on one thread, and two threads draw the same ids.contents_is_recycledis not the marker. Only the runtime transpiler sets it, and none of the four producers does.If an id on
Sourceis not wanted, the other fix is a tracker drop in the four producers of the table. I can change this PR to that.What the id does not cover. One
Sourcevalue whose bytes are written again in place. I found no producer that does it. The two places that give a cachedSourcenew contents keep the old buffer (stale_contents), so the address differs. The debug assertion catches this case for the first 4096 bytes.#46834. It merged while this PR was open. It gave
Sourcea lifetime, and it put a tracker drop inPackageManifest::parse(src/install/npm.rs) with the comment "It knows a source by its address". This PR removes that drop: the next manifest is a newSourcewith a new id. The branch has a merge of main for the one conflict, atpub struct Source.Other open PRs. #35550, #44811 and #45554 rewrite the body of
LineColumnTracker::error_positionand leavematchesas it is. The one that merges second has a small conflict in that function.Self-review. One concern rejected: to make
Log.line_column_trackerprivate. The bundler builds aLogwith a struct literal, so every field has to stay public. The others are in this diff or in this text: the cold refill is out of line, the comments are one line each, the debug assertion, the bunfig case, and the measurements on release builds of main.Audit. A
Sourcethat is built again for each diagnostic over one buffer would now restart the scan each time. An audit of every construction site ofSource(init_*,to_source, struct literals, clones) found none. The closest isexpr_from_blobinMacro.rs, which parses the whole blob again for each call anyway.Suites. On the merge with main
8a0e49eedb:bun bd test test/cli/install/bun-install.test.tsgives 776 pass, 0 fail, and the 4 new cases fail whensrc/is that of main.bun run rust:miri -p bun_ast: 26 pass.cargo clippyforbun_astandbun_install. Before the merge, on this diff:bun run rust:clippy,cargo check --workspace --all-targets,rust:check-allforx86_64-pc-windows-msvcandaarch64-apple-darwin, and with the debug assertion on, no failure in:redacted-config-logs,config-precedence,bad-workspace,bun-install-patch,overrides,catalogs,frozen-lockfile-pruned,bun-workspaces,bun-lock,bun-update,test/cli/format/bunfig,test/bundler/transpiler/transpiler,jsonc,resolve-error,parse-error-column,test/bake/dev/{bundle,css,esm}andtest/cli/hot/hot. Several of these need a longer timeout than 5 s on a debug build, with and without this change.no test proof · iteration 1 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/install/bun-install.test.ts