perf(router): replace matchit with a registration-ordered route table - #137
perf(router): replace matchit with a registration-ordered route table#137RomainLanz wants to merge 2 commits into
Conversation
|
Thanks for the PR! I was going through the implementation and found some edge cases where the new matcher diverges from test('do not match "/users" against "/users/:id"', ({ assert }) => {
const router = new RouterFactory().create()
router.get('/users/:id', async () => {})
router.commit()
assert.isNull(router.match('/users', 'GET', false))
})
test('match "/users" route registered after "/users/:id"', ({ assert }) => {
async function indexHandler() {}
const router = new RouterFactory().create()
router.get('/users/:id', async () => {})
router.get('/users', indexHandler)
router.commit()
assert.strictEqual(router.match('/users', 'GET', false)?.route.handler, indexHandler)
})
test('do not match "/posts" against "/posts/:slug?.json"', ({ assert }) => {
const router = new RouterFactory().create()
router.get('/posts/:slug?.json', async () => {})
router.commit()
assert.isNull(router.match('/posts', 'GET', false))
})All three pass on However, I think even if we fix these edge cases, we might run into others and risk breaking existing apps. Therefore, I think we must first check whether we have a good test suite for |
Note
AI assistance was used to draft parts of the implementation, tests, benchmarks, and PR description.
Hey! 👋🏻
This PR replaces the runtime
@poppinss/matchitdependency with an internal, registration-ordered route table.It addresses the same linear route-matching cost as #135, but explores a different architecture. Instead of keeping
matchitbehind multiple indexes, this implementation moves matching itself into a persistent route table.How it works
The route table combines:
Mapfast path for exact static routes;matching route always wins;
(
global,sticky, or overriddenexec/testmethods).The structural index only eliminates impossible routes. It never changes route precedence.
matchRoute, which receives a transient list of patterns, keeps a flat matching path instead of constructing a temporary index.The public routes tree and
StoreMethodNodeshape remain unchanged.Compatibility
Registration order remains part of the routing semantics, including precedence between:
Compatibility was checked with 1,603,716 differential comparisons against
matchit, covering generated route patterns, route permutations, domains, wildcards, optionals, casts, decoding, stateful regexes, and custom regex methods.matchitremains a dev dependency only, as the differential-test oracle. No production source or built output imports it.Benchmarks
All measurements below compare the previous matcher and this implementation on the same machine and environment.
Full-stack HTTP: dynamic and wildcard routes
Protocol: 5 rounds, 3 seconds warmup, 5 seconds measurement, 100 connections, pipelining 10.
Both scenarios passed functional validation with no errors or timeouts.
Full-stack HTTP: static routes
Protocol: 5 rounds, 10 seconds warmup, 10 seconds measurement, 100 connections, pipelining 10.
The first-route result is within the relatively high variance of this exploratory run and should not be treated as a demonstrated regression.
These static measurements were collected after the static-index commit. The final matcher preserves that exact static
Mapfast path, but a final-state 40s/40s run is still planned.Matcher microbenchmarks
With 1,000 routes:
The overlapping-regex case is intentionally included as an adversarial scenario where the tree cannot eliminate any candidate.
Costs
Measured while building indexes for 10,000 routes:
The additional work happens when routes are committed and is amortized across requests.