Repository navigation
perf(router): make fewer calls per part when building the tree - #195
Conversation
A cold start builds the tree in V8's interpreter, where each call and each property load runs a builtin. `expand` now decodes only parts with `%` instead of calling `decode` for each, and lowers `literals` with a comparison instead of loading and calling `Math.min`. A late-add test now covers a route sharing a param part with the route before it, which needs `literals` to stay at the first param. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7Sutbw6xMTMyDFeNfw
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Merging this PR will not alter performance1 warning
Comparing Footnotes
|
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
expandskipsdecodefor%-free parts —part.includes('%') ? decode(part) : part;decodealready returns its input unchanged when it holds no%, so the value is identical and only the call frame is saved. Same shape as the existingsplitbranch.literals = Math.min(...)→if (index < literals) literals = index(both the wait and param sites) — equivalent for the finite valuesliterals/indexever hold; drops a globalMathload, property load, and call per waiting route/param.- New late-add case —
/s/{x}/t/{y}then/s/{x}/t/uqueued after a request, asserting/s/1/t/ustill yields{ x: '1' }.
I verified the two edits are behavior-preserving by reading decode (its %-guard makes the ternary a no-op) and the literals value domain. I also mutated the param-branch guard to always assign (if (index >= 0)) and ran late-adds: it fails exactly as the PR describes, with /s/1/t/u returning params: undefined because the over-long shared-prefix walk skips the {x} param — so the added case is non-vacuous and genuinely pins that regression. Reverted; router suite is 73/73 green and the tree is clean.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…201) <!-- claude-projects-attribution: {"github_login":"dinwwwh"} --> _Requested by **dinwwwh** · [project thread](https://claude.ai/code/project/chan_01UcKMz91tMarPV1kWLVgLYE?thread=cmsg_01UcKMz91tMarPV1kWLVgLYE6swgSUM9SyULmBVdRjY2t7)_ ## What `expand` tracks how many of the previous route's parts the next route can reuse (`literals`). It used to lower that count in two places: once where a route waits, and once where it adds a param. Both happen only for a part that starts with `{` once decoded, so the count is now lowered in one place, right after that check. That check also catches a part such as `{a.b}`, which starts with `{` but is literal text. The count now drops for that part too, so the next route shares fewer parts with this one and parses them again. Matching is unaffected. The comment on `shared` and `literals` now describes the rule this way. ## Measurements Instruction counts from callgrind, `node --single-threaded`, averaged over hash seeds 1 and 2: | | before | after | |---|---|---| | min / min+gzip | 5012 / 2092 B | 5001 / 2091 B | | REST cold+3 match | 3.498M | 3.485M (−0.35%) | | REST cold+3, one request per method | 5.075M | 5.064M (−0.2%) | | REST cold+10 | 6.010M | 5.998M (−0.2%) | `expand` only runs while the tree is being built, so steady-state matching does not change. ## Checks - Tests pass with 100% coverage. - The model fuzzers, including the late-add ones, find 0 diffs. - Stryker finds 66 survivors, down from 67, and none of them are new. The `if (true)` mutant of the merged check is killed by the late-add test added in #195. --- _Generated by [Claude Code](https://claude.ai/code/session_01VVaw7Sutbw6xMTMyDFeNfw)_ Co-authored-by: Claude <noreply@anthropic.com>

Requested by dinwwwh · project thread
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 inexpand's part loop:decode(part)becamepart.includes('%') ? decode(part) : part, so a part without%(nearly all of them) skips the call.literals = Math.min(literals, index)becameif (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,literalsmoves to the last param, and a late-added route sharing a param part with the route before it skipped that param. A case inlate-adds.test.tsnow 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:--minify, gzip -9): 5068 B to 5076 B minified, 2112 B to 2114 B gzip.expandonly runs while building the tree.Checks
pnpm run checkgreen; router tests 73/73 with 100% coverage.index <= literalsat both checks (assigning an equal value), andpart.includes(""), which only sends every part throughdecode, which checks for%itself. (An earlier count of 54 here came from a run that shared the CPU with benchmarks, where test timeouts counted as kills.)🤖 Generated with Claude Code
https://claude.ai/code/session_01VVaw7Sutbw6xMTMyDFeNfw