Skip to content

perf(router): compile dynamic parameter extraction - #32

Merged
Upd4ting merged 1 commit into
mainfrom
perf/compiled-dynamic-parameter-extraction
Aug 19, 2026
Merged

perf(router): compile dynamic parameter extraction#32
Upd4ting merged 1 commit into
mainfrom
perf/compiled-dynamic-parameter-extraction

Conversation

@Upd4ting

@Upd4ting Upd4ting commented Aug 19, 2026

Copy link
Copy Markdown
Member

🔗 Linked issue

N/A

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • ⚠️ Breaking change

📚 Description

Compile the dynamic-segment extraction shape when a handler is registered:

  • keep a registration-ordered dynamicRouteList, avoiding Object.values(dynamicRoutes) allocation on every lookup;
  • compile the common single-capture shape separately from multi-capture patterned segments, without special-casing a parameter name;
  • mutate one request-local parameter object while descending and restore it when a branch backtracks, instead of spreading at every dynamic segment;
  • snapshot parameters only when each multi-handler result is emitted, preserving independent prefix/postfix/monitor parameter objects;
  • reuse compiled entries through unregister/re-register.

This head is rebased on combined main at 04f8294 and preserves:

Static → dynamic → catch-all precedence, method → any fallback, regex generation, URL encoding behavior, middleware ordering, and response/context behavior are unchanged.

Rebased benchmark

Environment: Node 22.19.0, one isolated orb, autocannon 8.0.0.

Micro-router — 8 alternating runs

The fixture registers static, simple/complex/concurrent/catch-all routes and 1,000 late-match dynamic routes. Timed loops use 20m operations for exact static, 3m simple, 1m complex/concurrent/catch-all, and 5k for the 1,000-route late match, after 100k warm-up operations. CV is across the 8 process-level runs. Allocation values are V8 statistical sampling estimates and are comparative, not a full allocation census.

Scenario main ops/s candidate ops/s Δ mean ns/op before → after CV before → after sampled B/op before → after
exact static, mixed fixture 56.41m 52.84m −6.3% 17.73 → 19.10 1.9% → 9.2% 0.31 → 0.36
simple /users/:id 2.95m 5.37m +82.1% 339.64 → 186.48 3.4% → 3.5% 53.20 → 0.15
complex, 5 captures 0.98m 1.04m +6.0% 1020.11 → 966.80 1.4% → 6.5% 186.28 → 133.53
12 concurrent dynamic alternatives, late match 1.23m 1.72m +39.5% 810.19 → 586.57 1.9% → 10.5% 76.10 → 23.60
catch-all 1.72m 2.94m +70.8% 581.68 → 344.27 2.6% → 9.7% 92.56 → 38.67
1,000 dynamic routes, late match 3,958 9,528 +140.7% 252,778 → 104,998 2.3% → 1.9% 78.23 → 23.59

A separate static-only exact-index benchmark (no dynamic fixture, 50m operations, 10 alternating runs) measures +2.3% for the candidate: 54.01m → 55.23m ops/s, 18.59 → 18.14 ns/op, CV 5.8% → 4.3%. The −6.3% mixed-fixture result above is therefore reported as a fixture/cache-pressure anomaly, not hidden. The full HTTP static result below is also positive.

Full HTTP stack — 6 alternating runs

50 connections, 1 s warm-up + 2 s measured per scenario/run. This includes exact-index lookup where applicable, sockets, URL parsing, context/response creation, handler execution, and response serialization.

Scenario main req/s candidate req/s Δ p99 before → after CV before → after
exact static 19,532 20,364 +4.3% 3.67 → 3.17 ms 6.5% → 3.0%
simple /users/:id 19,979 20,031 +0.3% 3.33 → 3.50 ms 1.5% → 3.0%
complex, 5 captures 19,096 19,052 −0.2% 3.50 → 3.50 ms 1.9% → 2.0%
12 concurrent alternatives, late match 19,124 19,517 +2.1% 3.50 → 3.17 ms 2.3% → 1.9%
catch-all 19,505 19,701 +1.0% 3.00 → 3.17 ms 1.4% → 2.3%
1,000 dynamic routes, late match 2,526 4,007 +58.7% 21.17 → 14.50 ms 2.4% → 2.8%

Does #32 recover #31's ~4–7% dynamic regression?

  • Yes in the isolated router: simple +82.1%, complex +6.0% (directly covering the reported range), concurrent +39.5%, and 1,000 routes +140.7%.
  • Only partially / below resolution in ordinary full-stack routes: simple +0.3%, complex −0.2%, concurrent +2.1%, catch-all +1.0%. Socket/context/response cost dominates these short route matches.
  • Clearly in the 1,000-route full-stack stress case: +58.7% and p99 improves by 6.67 ms.
  • perf(router): index exact static handlers #31 static gain is not cancelled in representative paths: static-only micro +2.3% and full HTTP +4.3%. The loaded mixed micro fixture does show −6.3% with 9.2% candidate CV and is retained above as a risk signal.

Memory / allocation limits

Ten alternating --expose-gc registration runs for 1,000 dynamic routes measured retained heap delta of 2,901,366 bytes on main versus 2,964,190 bytes on the candidate: +62,824 bytes / +2.2% (about 63 bytes per registered route in this fixture). This is the cost of the additional ordered route arrays/references.

V8 sampled allocation estimates fall substantially on every dynamic lookup (for example 53.20 → 0.15 B/op simple and 78.23 → 23.59 B/op at 1,000 routes), but sampling is statistical and these values must not be read as exact total allocations.

Validation after rebase

  • pnpm install --frozen-lockfile
  • pnpm build
  • pnpm test140 passing
  • pnpm lint — clean, no warnings
  • incremental diff checked against origin/main: only src/server.ts and src/test/routing-parameters.test.ts

Coverage includes static/dynamic/catch-all precedence, multiple and patterned captures, encoded values, concurrent branch backtracking, any, multi-handler object isolation, prefix/postfix/monitor parameters, and unregister/re-register lifecycle. Existing #29#31 test suites remain green.

Limits / risks

  • Dynamic alternatives remain linear by design to preserve registration precedence.
  • A request-local object is mutated only during synchronous lookup and restored before another branch; no user callback runs during lookup.
  • Retained registration memory rises about 2.2% in the 1,000-route fixture.
  • Candidate CV is higher in several micro scenarios; full-stack CV remains below 3% except the main static control.
  • The mixed-fixture exact-static micro regression is not reproduced in static-only or full HTTP measurements, but remains documented rather than dismissed.
  • No changes to promise handling, response semantics, URL decoding, controller plans, middleware fast paths, or exact static indexing.

📝 Checklist

  • I have linked an issue or discussion. (N/A)
  • I have updated the documentation accordingly. (N/A: internal optimization; benchmark and limits documented here)

Copy link
Copy Markdown
Member Author

Benchmark d’intégration — toutes les optimisations HTTP

J’ai assemblé localement, sans pousser de branche d’intégration, les têtes de api#29 à api#35 avec interface-api#14 (7602636). Build OK et 174 tests passants avec l’interface optimisée.

Méthode : même orb et même suite que le baseline initial, Node 24.19, serveur CPU 2, autocannon CPU 4/6, 3 répétitions, chauffe 2 s + mesure 5 s, ordre randomisé. Tous les runs retenus ont 0 erreur, 0 timeout et 0 non-2xx.

Scénario Baseline Antelope Toutes optis Gain p99 avant → après
GET JSON c=10 22 062 32 170 +45,8 % 0 → 0 ms
GET JSON c=100 22 696 31 771 +40,0 % 7 → 6 ms
Pipeline 10 31 186 34 504 +10,6 % 48 → 42 ms
Route dynamique 22 085 30 616 +38,6 % 8 → 5 ms
POST JSON 15 173 19 768 +30,3 % 9 → 8 ms
1 000 routes 22 434 30 664 +36,7 % 6 → 5 ms

Comparaison dans le même run final :

Scénario Antelope Adonis 7.4 Nest/Fastify Position Antelope
GET c=10 32 170 32 304 35 728 ≈ Adonis (−0,4 %), −10,0 % Fastify
GET c=100 31 771 31 507 36 061 +0,8 % Adonis, −11,9 % Fastify
Pipeline 10 34 504 37 866 44 880 −8,9 % Adonis, −23,1 % Fastify
Dynamique 30 616 31 451 33 133 −2,7 % Adonis, −7,6 % Fastify
POST JSON 19 768 11 350 12 431 +74,2 % / +59,0 %
1 000 routes 30 664 10 799 36 534 +183,9 % Adonis, −16,1 % Fastify

Conclusion honnête : le cumul apporte un gros saut et dépasse Adonis dans plusieurs cas, mais ne dépasse pas encore Nest/Fastify hors POST. Par rapport à l’intégration précédente #29 + #30, les cinq nouvelles PR apportent +4,7 à +7,5 % sur les GET usuels/dynamiques/1 000 routes, mais le pipeline recule de 6,3 % ; c’est le prochain point critique à profiler. Le cold start reste à 1 133 ms car cette campagne utilise encore le CLI standard ; le launcher de production est traité séparément dans AntelopeJS/antelopejs#96.

Reuse registration-order route arrays and backtrack over one parameter object instead of allocating intermediate copies during dynamic matching. Snapshot parameters only when handlers are emitted so middleware and monitor handlers remain isolated.

Amp-Thread-ID: https://ampcode.com/threads/T-01a019cf-991c-74c8-9d37-1a285cd5f912
Co-authored-by: Upd4ting <upd4ting@gmail.com>
@Upd4ting
Upd4ting force-pushed the perf/compiled-dynamic-parameter-extraction branch from aa93ca3 to 70fb83b Compare August 19, 2026 19:50
@Upd4ting
Upd4ting merged commit db7869f into main Aug 19, 2026
2 checks passed
@Upd4ting
Upd4ting deleted the perf/compiled-dynamic-parameter-extraction branch August 19, 2026 20:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants