Repository navigation
Conversation
|
Updated 9:47 PM PT - Oct 1st, 2026
✅ @robobun, your commit 68c47e6c8e896a16a71c2580c321ec18ddc39648 passed in 🧪 To try this PR locally: bunx bun-pr 35248That installs a local version of the PR into your bun-35248 --bun |
|
Found 2 issues this PR may fix:
🤖 Generated with Claude Code |
|
Neither #8643 nor #19363 use a |
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: oven-sh/bun/.coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (2)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review. WalkthroughChanges
ChangesRouter tree refactor
Suggested reviewers: Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to No actionable issue identified in the reviewed change; it is mergeable after normal checks. 🚥 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 `@test/js/bun/http/bun-serve-routes.test.ts`:
- Around line 1063-1091: Remove the explicit 30_000 timeout argument from the
“route registration time scales linearly with route count” test, while
preserving the test body and relying on the runner’s default timeout.
🪄 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: 2f09a044-be58-47f9-a6ff-5a4bfbbc7cf7
📒 Files selected for processing (2)
packages/bun-uws/src/HttpRouter.htest/js/bun/http/bun-serve-routes.test.ts
There was a problem hiding this comment.
No bugs found, but this is a structural rewrite of the uWS HTTP router's child storage and match loop — every Bun.serve request flows through executeHandlers()/route(). The precedence semantics look preserved and the new tests are solid, but it's worth a human pass given how hot this path is.
What was reviewed:
- Traced that the new match order (prio=1→0, static→
:→*) reproduces the old sorted-vector iteration order fromgetNode'supper_boundcomparator. - Checked
route()'s dropped root-children sort is safe: method lookup is now hashed andANY_METHOD_TOKEN(*) lands inspecialChildren[0]and is looked up explicitly after the method-specific tree. - Checked
cullNode's inverted erase contract (parent erases ontruereturn) andmap.erase(it)iterator handling — no UAF or skipped entries. - Verified
map.emplace(ptr->name, std::move(newNode))is safe:std::moveis a cast, the Node storage is stable, and the key is copied.
Extended reasoning...
Overview
The PR replaces uWS::HttpRouter::Node's single sorted std::vector<unique_ptr<Node>> children with two containers per priority tier: a transparent-hash unordered_map<string, unique_ptr<Node>> for literal segment names and a small ordered vector for :/* segments. getNode, executeHandlers, findHandler, route, add, and cullNode are all rewritten to use the new layout. Three new tests in bun-serve-routes.test.ts cover linear-scaling registration, large-table correctness, and precedence preservation.
Security risks
None identified. The router already treats URL segments as opaque strings; the change swaps a linear scan for a hash lookup on the same string_view. The transparent hash uses std::hash<string_view> consistently for both key types, so heterogeneous find() cannot mismatch. Untrusted URL segments cannot land in specialChildren because executeHandlers only ever hash-looks-up the incoming segment against staticChildren — it never classifies request bytes with isSpecialName.
Level of scrutiny
High. HttpRouter::route() and executeHandlers() sit on the request path of every Bun.serve connection, and the old code's sorted-vector iteration encoded a specific precedence contract (high-priority before normal; static before : before *; method-specific before ANY). I walked through the old upper_bound comparator and the post-add() root sort and confirmed the new two-tier loop reproduces the same visit order, but the number of interacting axes (priority × segment class × method vs ANY × backtracking on subtree miss) makes this worth a maintainer's eyes.
Other factors
- The dropped per-
add()std::sortof root children served two purposes: put GET first (a linear-scan micro-opt, now moot with hashing) and put ANY last soroot.children.back()was the ANY node. The newroute()looks ANY up by name inspecialChildren[0], which is arguably more correct than the oldback()(which could point at the wrong node ifcullNodeever removed ANY). cullNodenow returnstrueto signal the caller to erase, rather than erasing itself from the parent's container mid-iteration — cleaner and equivalent; theunordered_map::erase(it)return is used correctly.- Test coverage: existing precedence/reload/method tests in this file plus
serve.test.ts/websocket-server.test.ts(which exercises HIGH_PRIORITY upgrade routes) reportedly pass. The new ratio-based perf test uses best-of-three for the small sample and a 3× slack over the linear ratio, which should be reasonably CI-stable, though timing-ratio tests always carry some flake risk. - This is a vendored/forked upstream file (
packages/bun-uws), so a maintainer should also weigh divergence from upstream uWS.
There was a problem hiding this comment.
I didn't find any correctness issues, but this is a non-trivial data-structure refactor of the hot-path HTTP router in vendored uWS code, so it's worth a human look — particularly the per-node memory delta (two unordered_maps + two vectors vs one vector on every leaf) and the flagged overlap with #30410.
What was reviewed:
- Traced match ordering in
executeHandlers— high-prio→normal, static→:→*matches the old sorted-vector order for every combination I could construct. cullNodereturn-value contract change (caller erases instead of callee) andunordered_map::eraseiterator handling look correct.route()method dispatch: real methods land instaticChildren[0],ANY_METHOD_TOKENinspecialChildren[0]; the removed root sort was order-only, not semantic.getNodeemplace:ptr->namestays valid through theunique_ptrmove; no UAF.
Extended reasoning...
Overview
Restructures uWS::HttpRouter::Node child storage from a single sorted std::vector<unique_ptr<Node>> into staticChildren[2] (an unordered_map per priority tier) plus specialChildren[2] (a tiny ordered vector per tier for :/*). getNode, executeHandlers, findHandler, route, and cullNode are all rewritten around the new layout; the per-add() sort of root children is removed. Three new tests cover scaling, large-table dispatch, and precedence.
Security risks
None identified. Routing precedence is preserved (static before param before wildcard, high-priority before normal), URL segments that literally start with : or * still cannot match static children (they're classified into specialChildren at registration and looked up via the same param/wildcard branches at request time), and empty segments still route as static.
Level of scrutiny
High. This is the per-request routing hot path for Bun.serve, it modifies vendored third-party code (packages/bun-uws), and it changes the Node memory layout — every leaf node now carries two empty unordered_maps plus two empty vectors instead of one vector, which for a 36-method × N flat table is on the order of ~100 extra bytes × 36N nodes. That speed/memory tradeoff isn't discussed in the PR and is the kind of call a maintainer should sign off on.
Other factors
- The duplicate-PR bot flagged #30410 as solving the same problem the same way; a human should reconcile which one lands.
- The scaling test asserts a timing ratio (< 24 for 8×N). It's warmed up and best-of-three on the small sample, and the pre-fix ratio was 50-80×, so the margin is wide — but timing assertions are inherently something a maintainer should be comfortable with before merging.
- Existing precedence/reload/method tests in the same file plus
serve.test.tsandwebsocket-server.test.ts(per the PR description) exercise the rewritten paths, which gives reasonable coverage that ordering didn't regress.
|
Two points from review: Overlap with #30410. That PR fixes the same registration-time O(N^2) by adding a non-owning Per-node memory. Pushed 7e0dcae: the two hash maps and two vectors now live behind a lazily-allocated
So ~9% more RSS than main for ~44x faster registration, and lookup is O(1) per segment instead of O(siblings). |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/bun-uws/src/HttpRouter.h (1)
361-382: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winRe-check
childrenafter a handler can mutate the treeexecuteHandlers()can run user code before the finalchildren.specialChildren[0]scan, so if that code removes the last child andcullNode()resetsroot.children, the cached reference becomes dangling.🤖 Prompt for 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. In `@packages/bun-uws/src/HttpRouter.h` around lines 361 - 382, Update the routing flow around executeHandlers so it does not access the cached children reference after handler execution; re-check root.children before scanning specialChildren[0], return false if it was cleared, and use the refreshed children container for the ANY_METHOD_TOKEN lookup.Source: Coding guidelines
🤖 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 `@packages/bun-uws/src/HttpRouter.h`:
- Around line 239-256: The route traversal around executeHandlers must not
retain a reference to *parent->children across synchronous route callbacks,
because handlers may reset it via reload/remove. Re-fetch and validate
parent->children before each subsequent static/special-child access and each
priority iteration, returning false if it becomes null; update the local access
pattern without changing the existing match-order behavior.
---
Outside diff comments:
In `@packages/bun-uws/src/HttpRouter.h`:
- Around line 361-382: Update the routing flow around executeHandlers so it does
not access the cached children reference after handler execution; re-check
root.children before scanning specialChildren[0], return false if it was
cleared, and use the refreshed children container for the ANY_METHOD_TOKEN
lookup.
🪄 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: 5ca19ca9-6dc8-41e0-baed-a07a2e90f8be
📒 Files selected for processing (1)
packages/bun-uws/src/HttpRouter.h
There was a problem hiding this comment.
Beyond the inline nit, I traced a few invariants that check out: the new executeHandlers priority loop reproduces the old sorted-vector order (high-prio static → : → *, then normal-prio); map.emplace(ptr->name, std::move(newNode)) is safe since ptr is captured before the move and the heap Node doesn't relocate; cullNode's erase-during-iteration uses it = erase(it) / index-based erase correctly. Deferring to a maintainer given this rewrites the per-request routing hot path and the author has already flagged the #30410 overlap for a maintainer call.
Extended reasoning...
This restructures the core Bun.serve router (~250 lines in vendored uWS), touches every request's dispatch path, carries a memory/speed tradeoff, and overlaps with an open PR the author explicitly asked a maintainer to arbitrate — not a change to auto-approve. The ruled-out items above are the non-obvious correctness checks a human reviewer would otherwise re-derive.
There was a problem hiding this comment.
I didn't find any bugs, but this is a ~300-line restructure of the per-request routing hot path (precedence now encoded structurally instead of via sorted-vector iteration), and there's an open design choice vs #30410 — worth a maintainer's eyes.
What was reviewed:
- Traced that the new
prio=1→0/ static→:→*iteration order reproduces the oldupper_bound-sorted vector's match precedence. route()method-node lookup: verifiedANY_METHOD_TOKEN("*") lands inspecialChildren[0]and real methods instaticChildren[0], replacing the removed root sort.cullNodeerase-during-iterate: recursion mutates the child's containers, not the one being iterated; return-true-to-caller replaces the old self-erase correctly.map.emplace(ptr->name, std::move(newNode)):ptrpoints to the heap Node,std::moveis a cast — key is copied before ownership transfers, no dangling read.
Extended reasoning...
Overview
This PR rewrites uWS::HttpRouter::Node child storage from a single sorted std::vector<unique_ptr<Node>> to a lazily-allocated Children block holding unordered_map<string, unique_ptr<Node>> staticChildren[2] (hashed literal segments, one per priority tier) plus vector<unique_ptr<Node>> specialChildren[2] (ordered :/* segments). Every consumer — getNode, executeHandlers, findHandler, route, cullNode, add — is rewritten to index by container instead of linear-scanning. The per-add() root sort is deleted. Four new tests cover registration-time scaling, large-table dispatch, precedence preservation, and reload().
Security risks
None identified. The router operates on already-parsed method/URL segments; no new untrusted-input parsing. Heterogeneous find(string_view) avoids per-request allocation. The children reference cached across the handler callback in executeHandlers is the same reentrancy hazard that existed on main (a synchronous server.reload() inside a handler already invalidated the whole tree via move-assign) — the author and CodeRabbit already discussed this and agreed it's pre-existing and out of scope.
Level of scrutiny
High. This is the dispatch path for every Bun.serve HTTP request and WebSocket upgrade, and precedence is now encoded structurally (container index + iteration order) rather than by an explicit comparator. I traced each of the old comparator's ordering rules to its new structural equivalent and they line up: high-priority tier before normal (the prio=1→0 outer loop), static before param before wildcard within a tier (hash lookup then ordered specialChildren), ANY_METHOD_TOKEN last at the root (now via specialChildren[0] scan instead of children.back()). findHandler's :/* pattern-segment matching preserves the old (seg.starts_with(':') && child.starts_with(':')) || child == seg semantics via the segIsParam branch.
Other factors
All three prior review threads are resolved (per-test timeout dropped in 778a36a, dead isHighPriority field removed in 0f38761, reentrancy concern withdrawn as pre-existing). The full bun-serve-routes/serve/websocket-server/node-http suites pass on the PR. Deferring rather than approving because (a) this is a hot-path data-structure rewrite where a maintainer should sanity-check the RSS/latency tradeoff and any precedence edge cases they know of, and (b) the author explicitly flagged an open choice between this approach and #30410's non-owning side-index — that's a design call for a maintainer, not a bot.
|
@robobun is it faster than node with these changes |
0f38761 to
95adcbf
Compare
|
This PR now has a different design. The head is 95adcbf. The hash map version is still at 0f38761 for comparison. What changed. The router stores its tree in flat arrays and sorts each child list once, when route registration ends. The description has the measurements for main, the hash map version and this version. Correction to my comment of 23 July. I wrote that the hash map version cost about 9% more memory than main. That figure is wrong. It compared two different bun versions, with single runs. Measured on builds of the same commit, the hash map router uses about 25% more router memory than main, and 15% more RSS at 8,000 routes. The flat version uses 59% less router memory than main. Two behaviours to check in review.
Not done yet. My self-review of this version is still pending. I will post the result here. @7heMech I did not compare with Node. This PR changes how long |
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline findings, I also checked two things in the new flat router and ruled them out: a handler that calls server.reload() mid-route (HttpContextData::clearRoutes move-assigns a fresh router in place) — uWS's handler wrappers return true right after running user code and only the ws-upgrade path yields before any JS runs, so executeHandlers never touches the replaced vectors afterwards, same exposure as the base; and name-buffer sharing in compact() — a nameOffset is only shared via the per-add() stored[] array for identical segment text, so every node at a given offset has the same nameLength and the relocation keyed on offset alone is sound.
Extended reasoning...
The change rewrites uWS HttpRouter into a flat index-addressed tree with hashed child lookup and lazy sorting, threads a new sortRoutes() through App/H2/H3, the C shims, the Rust FFI wrappers and NewServer, and adds five route-table tests; it touches no auth, crypto or data-exposure surface but is core request-routing native code. The three inline findings (ANY-node culling changing fallback behaviour, remaining quadratic cost on duplicate registrations, and a wall-clock ratio test) already signal a human look is needed; this note only records two additional candidates examined and ruled out this run.
|
Update: the head is now 702a770.
The description has the measurements and the method. The three review threads are answered. My self-review is still pending and will cover this head. |
There was a problem hiding this comment.
Re-reviewed after 702a770 and found no new bugs in this revision; because the change is a ~800-line rewrite of the uWS routing data structure on the request hot path, a human look at HttpRouter.h is still worthwhile before merge.
What was reviewed:
- The follow-up commit's changes:
lastMethodselection viaisLaterMethodnow reproduces the base'sroot.children.back()comparator (GET first, ANY last, rest by name), andremove()walks only the pattern under each method node instead of the whole tree. - Pointer/reference lifetimes into growable vectors (
insertHandler'sslot,unlinkHandler,lookupInsert/lookupErase,dropIfEmpty): no push_back/erase occurs while a reference is held. compact()'s forward-pass renumbering relies on dead nodes having no live children and children having higher indices than parents; both hold givendropIfEmptyonly kills nodes withchildren == 0and nodes are only appended.- The
edgeslayout consistency betweenbuildEdges,sortedFind, andexecuteHandlers(static hash run then child indices, special children in insertion order), and the special-name treatment of*at the root method level.
Extended reasoning...
The PR replaces the pointer-tree HttpRouter in packages/bun-uws/src/HttpRouter.h with flat index-based arrays, a registration-time hash index, and sorted per-node child lists, and threads a new sortRoutes() through App.h, the H2/H3 contexts, the C shims in src/uws_sys/libuwsockets*.cpp, the Rust FFI wrappers, and the route-registration path in src/runtime/server/mod.rs. It touches no auth, crypto, or injection surface, but it is the request-dispatch hot path for every Bun.serve server and involves hand-rolled open addressing, bitfield counters, and intrusive free-lists. The new commit since the last review addresses the ANY-node fallback and the per-removal tree walk raised earlier, and this run's hunt found nothing new. The size and manual index arithmetic of the change, plus one earlier inline thread that remains open, are why a human review is still recommended rather than approval.
|
Self-review result for 702a770. I put the PR back in draft, because the review found two defects in this head. I reproduced both. 1. The router is slower for the common case. Exact instruction counts per
The description said a GET lookup on 1 to 3 routes was "2 to 13 ns slower" and "below the noise" end to end. That came from timings on a loaded machine, and the table I measured did not have these shapes. The counts show a cost on every server. I also wrote that instruction counts were not available here. They are: a tracer that single-steps the process between two markers gives exact counts. 2. The scaling test fails on the fixed build when the machine is busy. Other findings that I accept
What I will do before this leaves draft
The review found no request that is routed differently from main. Its own differential run and mine both show 0 differences. |
…or-testing httpRouterScript() runs add / remove / route / sort / reset lines against a router with no server around it and returns which handlers each request reached, with their parameters. A steps line returns the number of loop iterations the router made, counted only for a user data type that has a routerSteps member, so a server's router is unchanged.
The cases of uWebSockets' tests/HttpRouter.cpp, and fixed pseudo-random sequences of add, remove, route and sort calls whose output digests were recorded from the router on main.
702a770 to
45ae57b
Compare
…fallbacks Nothing in this branch reads the step counter, and no router here has a sortRoutes() for the `sort` line to call. Both go, and HttpRouter.h is the file on main again. They come back with the router that uses them. An `add` line with an empty method name made a method node with no name. A line with an empty field or an empty method name is now the same TypeError as any other line that is not a router call. Two answers of the router follow from how it keeps its methods: a request with the method * runs the routes of ANY twice, and once a removal dropped the empty ANY node, the last method by name gets the requests that no route answered. Each has a named test now. The random sequences are in two tables: the first keeps one ANY route and never requests with *, so its digests do not depend on either answer. The second does, and says so.
The router reads the first 100 segments of a pattern and of a URL, and it steps over the first byte of a URL without a look at it. No sequence has a URL that reaches either limit, so each limit has a named case with the output of the router on main.
… once Route registration scanned the siblings of every node it passed, a repeated registration walked every node, and route() scanned the same lists. Bun.serve registers an any-method route under each of 36 methods, so a table of N routes cost about 36*N^2 string compares on serve() and on every reload(). Nodes now live in one array and name their parent by index. add() finds a child through a hash index. sortRoutes() lays every child list out once in the order route() tries the children, and frees the index: a static child is one item that route() compares by name, and more than 8 static children are one run that route() searches by name hash. set_routes() ends with sortRoutes() on the HTTP/1, HTTP/2 and HTTP/3 routers. set_routes() also looks the paths that have a user HEAD route up in a sorted list, and does not scan every user route for each static route. The router counts its loop iterations for a user data type that has a routerSteps member. httpRouterScript has one, and gains a `steps` line that prints the count and a `sort` line that calls sortRoutes().
The gates count the router's loop iterations through httpRouterScript, so they do not depend on the machine. With the router on main, registration takes 8 times the steps per route at 8 times the routes, and a request for the last route takes 8 times the steps. A `sort` line before every call of a short random sequence, and before one call in 7 of a long one, changes no digest.
0ac89ac to
2f5fd2f
Compare
|
Update: the head is now 2f5fd2f. The PR is still a draft.
The description has the tables, and a list of what is open before this leaves draft. |
With bun's release flags (ThinLTO), main inlines the segment parse into its one recursive match function. The split into executeHandlers and executeChildren left the parse out of line: 16 to 50 more instructions per request than main on tables of up to 8 routes. The match is one function again, static children are tested first, and the calls that walk a pattern share one out-of-line copy of the parse. A child name must be shorter than the lowest flag bit of a list item.
The router had its own word-at-a-time hash. The name hash is now the in-tree string hash, and the index key mixes it with the parent through WTF::pairIntHash. A lookup in a list of more than 8 children runs 21 more instructions for a 5-byte name. The name hash has 24 bits. A new test registers 12,000 sibling names, checks that each reaches its own handler, and checks through the step counts that some of them shared a hash.
Co-authored-by: Ciro Spaciari <6379399+cirospaciari@users.noreply.github.com>
|
Update: the head is now 68c47e6. The PR is still a draft.
The description has the tables. |
Fixes #44177
Problem
Bun.serve({ routes })andserver.reload()are quadratic in the number of sibling routes. A request is linear. On main, 8,000 routes take 10.9 s.uWS::HttpRouter(packages/bun-uws/src/HttpRouter.h).getNodescans siblings, and a repeated registration walks every node. Bun registers an any-method route under 36 methods.Fix
add()finds a child through a hash index.sortRoutes()lays each child list out once, in match order, and frees the index.route()searches a list of more than 8 static children by name hash.set_routes()ends with it, soreload()is covered.test/js/bun/http/bun-serve-routes.test.ts(10 new tests, 100 pass). Self-reviewed: 6 findings, none asks to close. Notes list them.Background
HttpRouteris a tree of URL segments under one node per method.reload()swaps in an empty router and registers again.:param, then*.Downsides
Notes
How the numbers were taken. Release builds (ThinLTO) of #44323 (3c52061, main's router) and of this branch (68c47e6), linux x64, same machine, each process pinned to one core. The machine was under heavy load, so times are the fastest of 5 or a range. Instruction counts are exact and do not depend on load.
Instructions per request in the release binary. A
ptracetracer single-steps the server's main thread from the return of thereadthat delivers a request to the entry of thewritethat sends the response. StaticResponseroutes, so no JavaScript runs. Each count repeats exactly./leaf0/leaf0,/leaf2/leaf0,/leaf1,/leaf7/leaf0,/leaf1,/leaf99/leaf0,/leaf1,/leaf999Each server also has one function route,
/quit, from the script.Instructions per
route()call, router alone. The header compiled into a harness with bun's release flags, ThinLTO included. Main, then this branch:Bun.serve({ fetch })fetchandwebsocket/a/:xand/a/:x/:y/a/1//a/1/2GETand any-method nodes onlyOf 57 rows, the branch is slower in 13: the first or second of 9 or more siblings (10 rows, 104 to 194 more), the two-parameter request (2 more), and a request whose method has no node on a router with few method nodes (41 more).
bench/snippets/serve-routes.mjs, fastest of 5, main's router then this branch:serve()reload()fnfn{ GET: fn }Response8,000 routes (taken at 2f5fd2f, before the last three commits, which do not change registration):
"/x": fntakes 10,863 to 10,982 ms on main and 69 to 103 ms on the branch, and adds 35.8 MB and 15.4 to 16.3 MB of RSS."/x": { GET: fn }takes 204 ms and 12.9 ms, and adds 6.1 MB and 6.2 MB.Loop steps (the new tests, 200 routes then 1,600 routes). Main: 508.5 then 4,008.5 steps per registered route, and 202 then 1,602 steps for a request for the last route.
Binary size, text bytes of the stripped release binary. main 80,676,555. #44323 80,692,222. This branch 80,730,878. By symbol, the test hook is 14,980 bytes on #44323 and 23,955 bytes here. HTTP/1 plain, HTTP/1 TLS, HTTP/2 and HTTP/3 each instantiate the router template.
Self-review. A review of 2f5fd2f kept 6 findings. None asks to close the PR.
WTF::StringHasher(432f2fe). It costs 21 more instructions per lookup of a 5-byte name in a list of more than 8 children.bench/snippets/serve-routes.mjs, and a test for sibling names with equal hashes.currentRouter, whichsortRoutes()uses, so the second of the two to land needs a rebase. perf(serve): O(n²) → O(n) route registration when routes share a prefix #30410 (open) and perf(serve): O(n) Bun.serve({ routes }) registration #30411 (closed) index registration only.Bun.servetests for several TLS server names with HTTP/2 and forwebsocketwith many routes. Peak memory during registration is not measured.Behaviour that stays as on main. The method fallbacks and URL limits that #44323 names keep their tests unchanged. A route handler can call
reload()on the table it was matched in, as on main, and a test covers it.set_routes(). A static route looked for a userHEADroute of the same path with a scan of all user routes. It is now a binary search in a sorted list.Corrections to earlier statements on this PR. Earlier descriptions gave per-request instruction counts that showed the previous heads at or below main on small tables. Those counts were taken without ThinLTO. With bun's release flags, main inlines more, and the previous head (2f5fd2f) was slower than main on small tables. The tables above replace them. A comment of 23 July said the hash map version cost 9% more memory than main. That figure compared two different bun versions and is wrong.
[human-review] gate passed · iteration 3 · 17 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 6 passed · 1 rejected · iteration 3
evidence per changed file