resolver: dedupe DirnameStore interns across bust+reread and Route::parse - #34284
resolver: dedupe DirnameStore interns across bust+reread and Route::parse#34284robobun wants to merge 4 commits into
Conversation
…arse BSSStringList::append does not dedupe by content. dir_info_cached_miss re-interned each directory's safe_path and DirEntry.dir on every bust_dir_cache -> read_dir_info cycle, and Route::parse re-interned each route's public/abs/base/lowercased path on every reload. Both grew the never-freed DirnameStore unboundedly until its slot capacity was exhausted (panic: unreachable: AllocError). Add DirnameStore::intern_slice/intern_parts/intern_lower_case: a pointer-range fast path plus a thread-local content map that appends each distinct value exactly once. Route the bust-cycle-sensitive call sites in the resolver (dir_info_cached_miss, dir_info_for_resolution) and the router (Route::parse) through them. Expose DirnameStore::append_count via bun:internal-for-testing so the test can assert zero new appends over repeated reloads.
|
Updated 7:15 PM PT - Jul 15th, 2026
❌ @robobun, your commit 87d758b has 2 failures in
🧪 To try this PR locally: bunx bun-pr 34284That installs a local version of the PR into your bun-34284 --bun |
|
Status: reproduced (delta=3450 over 50 reloads without the fix, 0 with it). Review follow-ups addressed in 87d758b: CI build 73486: Holding for @Jarred-Sumner's reply on the overflow design question above before pushing anything further. |
WalkthroughChangesDirnameStore interning
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/resolver/lib.rs`:
- Around line 140-199: Make the DIRNAME_INTERN index process-wide rather than
thread-local, using a shared synchronized map accessible by all DirnameStore
users. Update intern_slice so lookup, append_slice, and insertion occur under
the same lock, preventing concurrent workers from appending duplicate paths to
the shared backing store; preserve the existing exists fast path and
intern_parts/intern_lower_case behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 547d76e8-ca1f-4f18-b1b6-e1a4afd11660
📒 Files selected for processing (7)
src/codegen/generate-js2native.tssrc/js/internal-for-testing.tssrc/resolver/lib.rssrc/resolver/resolver.rssrc/router/lib.rssrc/runtime/api/filesystem_router.rstest/js/bun/util/filesystem_router.test.ts
Jarred-Sumner
left a comment
There was a problem hiding this comment.
No, this is a bad solution. We shouldn't have a situation where DirnameStore can overflow. The memory should be ref counted when it would overflow.
|
The bug here is specifically that the same N directory/route paths are re-appended to the store on every Happy to close this and pursue ref-counted overflow instead, but want to make sure I understand the scope you have in mind:
Which of these are you after? If it's (2) or (3) I'll close this and #34276 and open a fresh PR scoped to the allocator + handle type. |
There was a problem hiding this comment.
No bugs found on 87d758b — the earlier append_count aliasing nit and the thread-local-vs-process-global dedupe concern are both addressed. Deferring to a human because this adds a new process-wide Mutex<HashMap> on the resolver's dir-info cache-miss path and reroutes several hot DirnameStore call sites across resolver.rs / router/lib.rs; a maintainer should sign off on the locking layering and the design choice to put dedup on DirnameStore (this supersedes #34276).
What was reviewed:
intern_slicelock ordering:DIRNAME_INTERN→BSSStringList.mutexis one-way (nothing under the inner mutex reaches back intointern_*), so no deadlock.append_countnow uses raw-place reads under the inner mutex, matching theappend/append_mutablepattern.exists()fast-path lifetime widen is sound (pointer-range check into the process-lifetime backing buffer).- Call-site swaps in
Route::parse(incl. Windowscfgarm),dir_info_cached_miss,dir_info_for_resolution, andread_directory_with_iteratorpreserve return type and'staticcontract.
Extended reasoning...
Overview
Adds content-deduplicating intern_slice / intern_parts / intern_lower_case wrappers on DirnameStore backed by a process-wide LazyLock<Mutex<HashMap<&'static [u8], ()>>>, plus an append_count() accessor exposed via bun:internal-for-testing. Reroutes ~9 call sites in src/resolver/resolver.rs, src/resolver/lib.rs (read_directory_with_iterator), and src/router/lib.rs (Route::parse, both platform arms) from append* to intern*. Adds a regression test asserting 50 reloads over 21 routes perform zero new appends.
Security risks
None identified. No user-controlled input reaches new parsing; the intern map is keyed by resolver-derived filesystem paths already trusted at these call sites.
Level of scrutiny
High. The resolver's dir_info_cached_miss / dir_info_for_resolution and read_directory_with_iterator are on the module-resolution hot path for the runtime, bundler, and --hot/--watch. The change introduces a new process-wide mutex acquired on every cache miss (held across the inner BSSStringList append), and the surrounding code has a documented Stacked-Borrows aliasing discipline (*mut Self + raw-place projections) that new code must respect. This is not a mechanical change.
Other factors
- Two prior review threads (mine on
append_countaliasing UB, CodeRabbit on thread-local vs. process-global dedupe) were both addressed in 87d758b and verified against the current diff. - Lock ordering checked:
DIRNAME_INTERNis only ever taken by the newintern_*wrappers, which then callappend_slice→ innerBSSStringList.mutex; nothing under that inner mutex calls back intointern_*, so no inversion. - Nested locking means all
intern_*callers now serialize onDIRNAME_INTERNin addition to the existing inner mutex. The PR description argues callers already holdRESOLVER_MUTEXor run on the JS thread, but bundler workers reachread_directory_with_iteratortoo — a maintainer should confirm the contention profile is acceptable. - This supersedes #34276 with a different design (dedup on
DirnameStorevs. router-local); that architectural choice deserves human sign-off. - Tests: 30/30 filesystem_router, 43/43 resolve,
rust:check-allclean per PR description; new test assertsdelta === 0and route matching still works.
Repro
Run long enough and it panics:
Cause
BSSStringList::appenddoes not dedupe by content, so the same bytes can be appended repeatedly into the never-freedDirnameStore. Two paths do that on everyreload()(and on every--hot/--watchfile change):Resolver::dir_info_cached_miss(and its siblingdir_info_for_resolution): afterbust_dir_cacheremoves a directory from both caches, the nextread_dir_infomiss re-interns the directory's_safe_path(append_parts/append_slice) and, because the orphanedDirEntryslot is unreachable,DirEntry.dir(append_slice). 2 appends for the root dir and 4 for each subdir per reload.Route::parse: re-interns each route'spublic_path,abs_path,basename, and lowercasedmatch_name. 3 to 4 appends per route per reload.RealFS::read_directory_with_iterator: sameDirEntry.dirshape as the resolver path, reached viaload_as_fileafter a bust.For the repro above that is 69 appends per reload, independent of anything but route/dir counts. The store's
slice_buf(4096 slots) plus overflow list cap out around 8.4M appends, after whichappendreturnsAllocErrorand the.expect("unreachable")panics; every overflow append before that leaks a heap buffer.Fix
Add content-deduplicating wrappers on
DirnameStore:intern_slice(value):exists()pointer-range fast path (slice already lives in the store's inline buffer), else probe a process-wideMutex<HashMap<&'static [u8], ()>>keyed by content; hit returns the prior intern, miss appends once and records it. The lookup, append, and insert happen under the same lock so concurrent resolver workers cannot race past each other.intern_parts(parts)/intern_lower_case(value): stack-scratch concat/lowercase then delegate tointern_slice.Route the bust-cycle-sensitive call sites through them:
resolver.rs:_safe_pathindir_info_cached_miss, andDirEntry.dirin bothdir_info_cached_missanddir_info_for_resolution.lib.rs:DirEntry.dirinread_directory_with_iterator.router/lib.rs: all sixDirnameStoreappends inRoute::parse(including the Windowscfgarm).After the first reload, every subsequent
reload()over an unchanged tree performs zero newDirnameStoreappends.Also expose
DirnameStore::append_countviabun:internal-for-testing; readsslice_buf_used/overflow_list.countas raw-place projections under the store's inner mutex (no whole-struct&BSSStringListbefore locking).Why this fix
The invariant that the process-lifetime
DirnameStoreshould not grow when no new paths are seen belongs onDirnameStoreitself, not in each caller. The dedup map is process-wide to match the backing store's lifetime; the callers this PR touches already holdRESOLVER_MUTEXor run on the JS thread, so there is no new contention.Verification
reload() does not re-intern directory paths into DirnameStore on every bust+rereadintest/js/bun/util/filesystem_router.test.ts: 21 routes over 2 dirs, 50 reloads, assertsdirnameStoreAppendCount()delta is exactly 0 andmatch()still works.filesystem_router.test.ts(30/30),resolve.test.ts(43/43),import-meta.test.js+require.test.ts(42/42) all green underbun bd.bun run rust:check-allclean on all targets including the Windows arm inRoute::parse.Relationship to other PRs
Supersedes #34276 (which added a router-local
intern_route_paththread-local; this PR puts the dedup onDirnameStoreso the resolver's own re-interns are covered in the same place). #29919 addresses theDirEntry/EntryStorereuse side of the same bust cycle and is complementary.