Skip to content

perf(server): add synchronous HTTP request path - #33

Merged
Upd4ting merged 2 commits into
mainfrom
perf/synchronous-http-request-path
Aug 19, 2026
Merged

perf(server): add synchronous HTTP request path#33
Upd4ting merged 2 commits into
mainfrom
perf/synchronous-http-request-path

Conversation

@Upd4ting

@Upd4ting Upd4ting commented Aug 19, 2026

Copy link
Copy Markdown
Member

Résumé

  • exécute prefix → handler → postfix → monitor → send dans la même pile lorsque toutes les étapes sont synchrones ;
  • bascule vers la continuation Promise dès qu'une Promise ou un thenable apparaît, sans modifier le chemin WebSocket ;
  • conserve les réponses anticipées, erreurs/rejets, priorités, HEAD/OPTIONS, streams, monitors et hot reload ;
  • est rebasée sur db7869f, donc évaluée avec les optimisations perf(api): compile request controller resolution #29perf(router): compile dynamic parameter extraction #32 sans les dupliquer dans le diff de cette PR.

Diff incrémental contre le nouveau main : 2 fichiers, 617 ajouts, 75 suppressions (src/server.ts et la suite ciblée). Head : 0088869.

Cause mesurée

Le profil async_hooks a été rejoué après rebase sur 50 000 GET passant par les contrôleurs compilés, avec prefix, postfix et monitor synchrones :

Le profil CPU initial de ce même listener sous charge montrait aussi runMicrotasks à 1,78 % avant contre 0,04 % après. Les microtasks imposées par les await du listener sont donc bien la cause supprimée ; aucun cas spécial fixture n'est utilisé.

Benchmark HTTP après rebase

Même orb, Node 22.14.0, autocannon 8.0.0, serveur HTTP Node réel, contrôleurs compilés, résolution de paramètres, routeur, RequestContext, HTTPResult, prefix/postfix/monitor synchrones et envoi réel. Baseline exacte db7869f (#29#32), candidat exact 0088869 (#29#33). Chauffe 1 s avant chaque mesure, mesure 3 s, 5 répétitions alternées baseline/candidat. Le POST consomme un corps JSON ; le pipeline async contient un contrôleur async et un await. p99 en ms ; CV calculé sur req/s.

Scénario #29#32 req/s #29#33 req/s Écart p99 avant→après CV avant→après Erreurs/timeouts
GET statique c=10 30 019 38 408 +27,9 % 0→0 17,00→5,74 % 0/0
GET statique c=100 30 035 38 292 +27,5 % 5,8→4,4 14,08→3,59 % 0/0
GET dynamique 28 442 36 004 +26,6 % 0→0 17,72→5,89 % 0/0
POST/body 23 784 27 340 +15,0 % 0,8→0,2 14,87→13,84 % 0/0
Pipeline sync ×10 35 977 39 698 +10,3 % 4,4→4,0 11,13→12,02 % 0/0
Pipeline async ×10 36 402 41 531 +14,1 % 3,8→3,2 9,60→10,42 % 0/0
Erreur sync (HTTP 500) 17 098 19 448 +13,7 % 1,0→0,8 11,99→13,98 % 0/0*
1 000 routes 27 923 35 836 +28,3 % 0→0 14,95→16,55 % 0/0

* Le scénario erreur a produit les HTTP 500 attendus (256 458 réponses non-2xx baseline, 291 696 candidat), sans erreur réseau ni timeout.

Lecture prudente : l'orb était bruitée, avec des CV de 3,6 à 17,7 %. Les moyennes et médianes vont toutes dans le même sens, mais un run pairé sur cinq était marginalement négatif pour le pipeline sync (−0,1 %) et les 1 000 routes (−1,5 %). Il n'y a aucune régression agrégée, y compris sur le chemin comportant un handler async, mais les pourcentages précis ne doivent pas être lus comme des microbenchmarks à faible variance.

Comparaison frameworks

La campagne comparative précédente, exécutée dans cette même orb avec la même pile et cinq répétitions, plaçait le candidat pré-rebase à 37 721 req/s (GET c=10) et 37 228 (c=100), contre Adonis HTTP 7.7 à 39 978 et 38 314, Nest/Fastify à 41 106 et 41 806, et Fastify seul à 45 482 et 48 242. La nouvelle campagne rebasée mesure 38 408 et 38 292, soit au niveau de l'ancien résultat Adonis, mais ces séries n'ont pas été alternées ensemble : cette comparaison reste indicative, pas une preuve de dépassement. Le benchmark avant/après ci-dessus est la comparaison causale retenue.

Tests et validation

src/test/server-synchronous-path.test.ts couvre :

  • pile entièrement synchrone et absence d'awaitable ;
  • callbacks async, Promises et thenables sur prefix/handler/postfix/monitor ;
  • getter then stateful lu exactement une fois ;
  • rejets et throws synchrones/asynchrones, puis isolation des erreurs monitor ;
  • ordre, priorités, paramètres dynamiques et réponse anticipée ;
  • HEAD, OPTIONS, streams, hot reload et WebSocket.

Validation locale après pnpm install --frozen-lockfile :

  • pnpm build
  • pnpm lint
  • pnpm test ✅ — 149 tests

La review valide sur la double lecture du getter then reste corrigée et couverte par régression. Aucun thread de review ouvert au moment de cette mise à jour. CI post-force-push : Ubuntu ✅, GitGuardian ✅.

Limites et risques

  • Toute Promise/thenable conserve une continuation asynchrone ; seules les piles réellement synchrones évitent les microtasks.
  • L'assimilation capture le getter then une seule fois et conserve les rejets/throws.
  • Les monitors gardent leur snapshot de réponse et isolent leurs erreurs.
  • Le type interne de retour devient void | PromiseLike<void> ; requestListener n'est pas exporté par l'API publique du package et le server factory ignore déjà sa valeur de retour.
  • Les erreurs exceptionnelles du listener restent converties en Promise rejetée, comme avec l'ancienne fonction async.

Comment thread src/server.ts Outdated

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.

@Upd4ting
Upd4ting force-pushed the perf/synchronous-http-request-path branch from f6b4e73 to 0088869 Compare August 19, 2026 20:16
@Upd4ting
Upd4ting merged commit 0bf67c2 into main Aug 19, 2026
2 checks passed
@Upd4ting
Upd4ting deleted the perf/synchronous-http-request-path branch August 19, 2026 20:34
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