Skip to content

perf(server): create request URLs lazily - #34

Closed
Upd4ting wants to merge 4 commits into
mainfrom
perf/lazy-request-context-url
Closed

perf(server): create request URLs lazily#34
Upd4ting wants to merge 4 commits into
mainfrom
perf/lazy-request-context-url

Conversation

@Upd4ting

@Upd4ting Upd4ting commented Aug 19, 2026

Copy link
Copy Markdown
Member

🔗 Linked issue

N/A

❓ Type of change

  • 👌 Enhancement (performance)

📚 Description

RequestContext.url reste une propriété propre, énumérable, assignable et de type URL, mais l'objet WHATWG n'est construit qu'au premier accès. La cible et le Host sont figés à l'entrée du listener. Le routage extrait directement les pathnames origin-form sûrs et retombe sur WHATWG pour les cibles absolues/protocol-relative, les dot segments, backslashes, caractères non sûrs et hosts ambigus. Aucun cache inter-requêtes n'est ajouté.

Après fusion de #29#33, la branche a été rebasée sur main 0bf67c2. Le diff incrémental conserve le dispatch statique (#31), l'extraction dynamique compilée (#32), le skip middleware (#30) et le chemin HTTP synchrone (#33). Le lazy URL s'applique aux chemins HTTP sync/async et WebSocket.

Compatibilité vérifiée

  • host, protocol, query, encodage, dot-segment normalization, cibles absolues et protocol-relative, Host absent ;
  • identité stable lors d'accès répétés, assignation publique et snapshot de req.url/Host ;
  • handler synchrone et asynchrone, route dynamique, POST, middleware/monitor et WebSocket ;
  • propriété propre/énumérable et spread du monitor (le spread matérialise une data property URL) ;
  • host numérique invalide rejeté avant le handler ;
  • décorateurs et contextes dérivés couverts par la suite d'intégration complète.

Benchmark cumulatif (#29#33 vs #29#34)

  • Base : 0bf67c2b93615a2c427e205fa3cd7a04a582b210
  • Candidat : 9020499d5655a9ddc73b6402a69a1be0a1c9b9e0
  • Orb 8 vCPU Intel Xeon 2.60 GHz, Node 20.9.0, pnpm 10.6.5, serveur HTTP + routeur + réponse complets
  • autocannon 8.0.0, 100 connexions, 3 s, 6 répétitions alternées ; processus redémarrés à chaque répétition
  • req/s et p99 : moyennes arithmétiques ; CV : coefficient de variation population
Scénario Main req/s (CV) #34 req/s (CV) Delta p99 main / #34 Erreurs
GET sans accès URL 36 163 (11,21 %) 36 737 (13,73 %) +1,6 % 5,67 / 4,67 ms 0
GET pathname lu 1× 38 491 (5,27 %) 33 080 (7,57 %) −14,1 % 4,33 / 5,33 ms 0
GET URL lue plusieurs fois 36 566 (3,24 %) 32 055 (4,78 %) −12,3 % 4,50 / 5,50 ms 0
Route dynamique 37 401 (4,16 %) 38 540 (2,92 %) +3,0 % 4,33 / 4,33 ms 0
POST 34 107 (3,79 %) 35 390 (3,23 %) +3,8 % 4,67 / 4,83 ms 0
Pipeline prefix/handler/postfix/monitor 37 124 (2,79 %) 31 955 (1,86 %) −13,9 % 4,50 / 5,00 ms 0
1 000 routes statiques 38 007 (4,44 %) 39 543 (4,00 %) +4,0 % 4,17 / 4,00 ms 0

Le gain no-read est faible et bruyant dans cette campagne (CV 11–14 %), mais la suppression de construction est exacte : sur 10 000 requêtes, new URL passe de 1 à 0 par requête sans lecture et reste 1 à 1 avec une ou plusieurs lectures. La pénalité URL-read antérieure (~9–12 %) est reproductible ici à −12–14 %. Le pipeline contient un monitor : son spread lit nécessairement la propriété énumérable url, donc il appartient au cas URL-read.

Des variantes locales (remplacement du getter par une data property après le premier accès et stockage WeakMap) avaient été mesurées pendant la campagne initiale ; elles ne supprimaient pas clairement la pénalité et certaines étaient plus lentes. La campagne cumulée confirme que le coût est concentré sur l'accès à l'accesseur. Aucun mécanisme plus complexe n'est ajouté pour masquer ce compromis.

Allocations, GC et profil CPU

Profil complet en processus (50 000 requêtes mesurées, client inclus, une observation par cas) :

Scénario Octets alloués main / #34 Delta Cycles GC Pause GC main / #34
Sans accès URL 1 156 304 592 / 1 139 316 720 −1,5 % 114 / 113 553,6 / 539,5 ms
Pathname 1× 1 161 347 040 / 1 166 383 560 +0,4 % 114 / 115 554,4 / 582,9 ms
URL plusieurs fois 1 196 687 088 / 1 201 456 152 +0,4 % 116 / 116 573,3 / 611,3 ms

Ces totaux incluent client, warm-up et GC forcés : ils sont directionnels, pas une comptabilité server-only. Les pauses sont trop bruitées pour conclure à un effet stable. Sur 100 000 requêtes no-read, le profil CPU passe de 18 143 à 18 060 échantillons et les échantillons URL de 135 à 0. Le candidat répartit le travail dans createRequestContext (212), getPathname (47) et isCommonHost (62), tandis que processRequest baisse de 348 à 217.

Référence framework de la campagne initiale (Node 22, six rotations, non feature-equivalent) : Fastify 48 521 req/s, candidat Antelope 36 240, Nest/Fastify 29 924, Adonis 10 609.

✅ Validation

  • pnpm install --frozen-lockfile
  • pnpm build
  • pnpm test — 162 passing
  • pnpm lint
  • CI Ubuntu et GitGuardian verts sur 9020499
  • Review Greptile traitée et thread résolu
  • Review humaine demandée à Thomasims

Limites

Le benchmark partage CPU client/serveur dans la même orb et le cas no-read a une variance élevée. Les chiffres expriment ce run local, pas une garantie de production. La PR reste ouverte et non mergée.

@Upd4ting
Upd4ting requested a review from Thomasims August 19, 2026 12:20
Comment thread src/server.ts

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.

ampagent and others added 4 commits August 19, 2026 20:36
Avoid parsing common request targets until consumers access the URL while preserving enumerable context and WHATWG fallback semantics. Skip monitor context cloning when no monitors match.

Amp-Thread-ID: https://ampcode.com/threads/T-01a019cf-8c38-777d-8af1-1f42f02050a9
Co-authored-by: Upd4ting <upd4ting@gmail.com>
Preserve eager rejection for numeric Host values that the WHATWG parser rejects while retaining the common valid-host path.

Amp-Thread-ID: https://ampcode.com/threads/T-01a019cf-8c38-777d-8af1-1f42f02050a9
Co-authored-by: Upd4ting <upd4ting@gmail.com>

Copy link
Copy Markdown
Member Author

Fermée après benchmark sur la chaîne cumulée #29#33 : le gain sans lecture de RequestContext.url est faible et bruité (+1,6 %), tandis que les chemins qui lisent l’URL régressent de 12 à 14 %. Nous conservons donc la création eager actuelle et n’intégrons pas cette optimisation.

@Upd4ting Upd4ting closed this Aug 19, 2026
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