feat(http/unstable): add radix tree router; keep linear scan as routeLinear#7075
feat(http/unstable): add radix tree router; keep linear scan as routeLinear#7075esroyo wants to merge 1 commit intodenoland:mainfrom
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #7075 +/- ##
==========================================
- Coverage 94.41% 94.40% -0.01%
==========================================
Files 630 630
Lines 50490 50617 +127
Branches 8949 8988 +39
==========================================
+ Hits 47669 47786 +117
- Misses 2249 2255 +6
- Partials 572 576 +4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…Linear Add routeRadix(), a radix tree router that provides O(segments) dispatch for static, parametric, and wildcard routes. Routes with complex URLPattern syntax (regex constraints, optional groups, inline wildcards, modifier suffixes) fall back to linear matching while preserving insertion order. - routeRadix: radix tree with fallback to linear for complex patterns - routeLinear: the original linear scan, extracted as its own export - route: re-exported alias for routeRadix (backward compatible) The radix router matches routeLinear semantics exactly — insertion order is always respected, even when static and parametric routes overlap at the same tree depth. Benchmarks show 1.5–9x improvement on static/parametric/wildcard routes, with negligible overhead on complex fallback patterns.
786873d to
d522ffa
Compare
| } | ||
| return routeMethod.toUpperCase() === requestMethod; | ||
| } | ||
| export { routeRadix as route }; |
There was a problem hiding this comment.
This silently changes the semantics of route() for existing callers — they go from a simple linear scan to a radix tree with fallback. While behavior should be equivalent, if there's any divergence it becomes a regression for current users.
Would it be safer to keep route as the linear implementation (preserving current behavior) and offer routeRadix as an explicit opt-in? That gives the radix path time to mature without risking surprises for existing consumers.
| const segments = parseSegments(pathname); | ||
| const radixCandidates: IndexedRoute[] = []; | ||
| collectCandidates(root, segments, 0, radixCandidates); | ||
| radixCandidates.sort((a, b) => a.index - b.index); |
There was a problem hiding this comment.
Every request allocates radixCandidates and sorts it, then the merge with fallbackRoutes may allocate yet another array. For the common happy path of a single candidate, the sort is wasted work.
This partly explains the ~1.2x slowdown for "static route — first in small table" in the benchmarks. Consider skipping the sort when radixCandidates.length <= 1, and/or pre-sorting fallback routes at build time so the merge can be avoided when radixCandidates is empty.
| * Extract pathname from a URL string without allocating a URL object. | ||
| * Handles both `http://host/path?query` and `http://host/path` forms. | ||
| */ | ||
| function parsePathname(url: string): string { |
There was a problem hiding this comment.
This custom parser assumes the URL always has an authority (//). It works for http(s):// URLs from Request objects, but would break for schemes like file:///path (double // is part of the scheme, not authority) or URLs with userinfo containing /.
Since this is an internal function only called with request.url, it's safe in practice, but a short comment documenting the assumption ("only handles http(s):// URLs from Request objects") would prevent future misuse.
|
|
||
| function insert(r: Route): void { | ||
| const indexed: IndexedRoute = { route: r, index: insertionCounter++ }; | ||
| const segments = parseSegments(r.pattern.pathname); |
There was a problem hiding this comment.
The radix tree indexes routes solely by pathname segments, but URLPattern can also constrain hostname, search, protocol, etc. Routes like new URLPattern({ hostname: "api.example.com", pathname: "/data" }) get inserted in the tree under /data, so a request to /data on a different hostname will produce a tree candidate that pattern.exec() then rejects.
This is correct (the tree is a pre-filter, pattern.exec is authoritative), but it means the tree provides no pruning benefit for multi-component patterns — and may produce more candidates than a linear scan would test. Worth documenting this trade-off.
|
|
||
| /** | ||
| * Route configuration for {@linkcode route}. | ||
| * Route configuration for {@linkcode routeRadix}. |
There was a problem hiding this comment.
Route is shared by both routeRadix and routeLinear, so linking it exclusively to routeRadix is misleading. Consider linking to both, or reverting to the more general route.
| function isComplexSegment(segment: string): boolean { | ||
| if (segment.includes("{") || segment.includes("(")) return true; | ||
| if (segment.includes("*") && segment !== "*") return true; | ||
| if (segment.endsWith("?") || segment.endsWith("+")) return true; |
There was a problem hiding this comment.
The endsWith("+") check correctly catches :param+ (one-or-more repeating), but it would also flag a hypothetical literal segment like foo+bar as complex. In practice URLPattern pathnames won't have unencoded + in static segments, so this is fine — but it's worth noting in the doc comment that this heuristic assumes well-formed URLPattern syntax.
Also: does this catch all modifier suffixes? URLPattern also supports * as a modifier (e.g., :id*), but that's already handled by the includes("*") check above. Looks complete to me, just want to confirm that was intentional.
Add
routeRadix(), a radix tree router that provides O(segments) dispatch for static, parametric, and wildcard routes. Routes with complex URLPattern syntax (regex constraints, optional groups, inline wildcards, modifier suffixes) fall back to linear matching while preserving insertion order.routeRadix: radix tree with fallback to linear for complex patternsrouteLinear: the original linear scan, extracted as its own exportroute: re-exported alias forrouteRadix(backward compatible)The radix router matches routeLinear semantics exactly; insertion order is always respected, even when static and parametric routes overlap at the same tree depth.
Benchmarks show 1.5–9x improvement on static/parametric/wildcard routes, with negligible overhead on complex fallback patterns.
--- config: themeVariables: xyChart: plotColorPalette: "#e05c5c, #4a90d9" --- xychart-beta title "route: 🔴 linear vs 🔵 radix - Avg latency (µs)" x-axis [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] y-axis "Avg latency (µs)" 0 --> 7 line [0.91, 2.04, 6.1, 1.63, 5.54, 1.49, 2.84, 1.08, 3.15, 0.9, 1.45, 1.98] line [1.08, 1.38, 1.39, 0.62, 0.61, 1.55, 1.66, 0.91, 1.73, 1.46, 1.62, 2.1]