Skip to content

Commit 7a4e0bd

Browse files
dinwwwhclaude
andauthored
perf(router): make fewer calls per part when building the tree (#195)
<!-- claude-projects-attribution: {"github_login":"dinwwwh"} --> _Requested by **dinwwwh** · [project thread](https://claude.ai/code/project/chan_01UcKMz91tMarPV1kWLVgLYE?thread=cmsg_01UcKMz91tMarPV1kWLVgLYE6swgSUM9SyULmBVdRjY2t7)_ ## Summary A cold start builds the tree in V8's interpreter and baseline code, where each call and each property load goes through a builtin. In the profile of the first request for a method (PATCH in the REST set, 100 routes), loads and calls are most of the time outside `split`. Two changes in `expand`'s part loop: - `decode(part)` became `part.includes('%') ? decode(part) : part`, so a part without `%` (nearly all of them) skips the call. - `literals = Math.min(literals, index)` became `if (index < literals) literals = index`, which drops a global load, a property load and a call per waiting route and per param. Stryker showed a gap the second change exposed: with the comparison mutated to `true`, `literals` moves to the last param, and a late-added route sharing a param part with the route before it skipped that param. A case in `late-adds.test.ts` now covers it (`/s/{x}/t/{y}` then `/s/{x}/t/u`, added after a request). ## Measurements Callgrind instructions, `--single-threaded`, hash seeds 1 and 2 (averaged), main vs this PR: | Stage | main | PR | Change | | --- | --- | --- | --- | | REST, 3 requests | 3.892M | 3.757M | -3.5% | | REST, 3 requests on 3 methods | 5.697M | 5.487M | -3.7% | | REST, 10 requests | 6.615M | 6.452M | -2.5% | | 100 real nouns, 10 requests | 6.588M | 6.426M | -2.5% | | RPC (statics only), 10 requests | 420k | 420k | same | | Router import | | | +6k (+0.4% of the router's share) | - Size (esbuild `--minify`, gzip -9): 5068 B to 5076 B minified, 2112 B to 2114 B gzip. - Steady state: unchanged, since `expand` only runs while building the tree. ## Checks - `pnpm run check` green; router tests 73/73 with 100% coverage. - All fuzzers at 3 seeds: 0 diffs. - Stryker: 54 survivors (61 on main). The new one, `part.includes("")`, only makes every part go through `decode`, which checks for `%` itself. --- _Generated by [Claude Code](https://claude.ai/code/session_01VVaw7Sutbw6xMTMyDFeNfw)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent b2a5f7d commit 7a4e0bd

2 files changed

Lines changed: 12 additions & 3 deletions

File tree

‎packages/router/src/index.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -384,15 +384,17 @@ function expand<T>(node: Node<T>, pending: unknown[], start: number, whole: bool
384384

385385
for (; index < parts.length; index++) {
386386
const part = parts[index]!
387-
let segment: Segment = decode(part)
387+
let segment: Segment = part.includes('%') ? decode(part) : part
388388

389389
// Only a part starting with `{` once decoded can be a param. Past its
390390
// first part, a route waits before one, and before literal text like it,
391391
// which keeps the first of two that read the same. A route added whole
392392
// goes after the routes waiting there.
393393
if (segment.charCodeAt(0) === 123) {
394394
if (!whole && index > start) {
395-
literals = Math.min(literals, index)
395+
if (index < literals) {
396+
literals = index
397+
}
396398
;(child.pending ??= []).push(parts, data, params)
397399
continue next
398400
}
@@ -408,7 +410,9 @@ function expand<T>(node: Node<T>, pending: unknown[], start: number, whole: bool
408410
continue
409411
}
410412

411-
literals = Math.min(literals, index)
413+
if (index < literals) {
414+
literals = index
415+
}
412416
// An array literal holds just its items, where a push to `[]` reserves 17.
413417
if (params === undefined) {
414418
params = [segment]

‎packages/router/tests/late-adds.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ describe('routes added after requests', () => {
1515
router.add('GET', '/d/{+r}/e', 'duplicate')
1616
router.add('GET', '/d/f', '/d/f')
1717
router.add('GET', '/d/{x}', '/d/{x}')
18+
// shares its parts up to `t` with the route before it
19+
router.add('GET', '/s/{x}/t/{y}', '/s/{x}/t/{y}')
20+
router.add('GET', '/s/{x}/t/u', '/s/{x}/t/u')
1821

1922
expectMatches(router, {
2023
'/a/1/b/2': ['/a/{x}/b/{y}', { x: '1', y: '2' }],
@@ -26,6 +29,8 @@ describe('routes added after requests', () => {
2629
'/d/f/e': ['/d/{+p}/e', { p: 'f' }],
2730
'/%64/f': ['/d/f'],
2831
'/a/1/c': ['/a/{x}/c', { x: '1' }],
32+
'/s/1/t/2': ['/s/{x}/t/{y}', { x: '1', y: '2' }],
33+
'/s/1/t/u': ['/s/{x}/t/u', { x: '1' }],
2934
})
3035
})
3136

0 commit comments

Comments
 (0)