Skip to content

perf(http): reuse response results through request lifecycle - #35

Merged
Upd4ting merged 3 commits into
mainfrom
perf/response-lifecycle
Aug 19, 2026
Merged

perf(http): reuse response results through request lifecycle#35
Upd4ting merged 3 commits into
mainfrom
perf/response-lifecycle

Conversation

@Upd4ting

@Upd4ting Upd4ting commented Aug 19, 2026

Copy link
Copy Markdown
Member

🔗 Linked issue

Consumes the published @antelopejs/interface-api@0.0.11 release produced by AntelopeJS/interface-api#14.

❓ Type of change

  • 📖 Documentation (updates to the documentation or readme)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality like performance)
  • ✨ New feature (a non-breaking change that adds functionality)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

The HTTP listener previously created a 404 HTTPResult before route matching, then commonly replaced it after the handler and cloned it again even when no monitor existed. Those replacements called getHeaders(), which also forced the header store to exist.

This change:

  • creates the 404 response only after a route miss;
  • reuses the matched route's context response for ordinary handler, prefix, and postfix bodies;
  • skips monitor snapshots when no monitor matches;
  • keeps snapshots isolated when monitors do match;
  • preserves prefix/handler/postfix headers when an HTTPResult replaces the context response;
  • calls the published HTTPResult.peekHeaders() API directly; the compatibility cast and allocating getHeaders() fallback are no longer needed.

No response bypass was added: status, content type, body, HEAD, stream, and error paths still finish through HTTPResult.sendResponse() / sendHeadResponse(). The package now requires @antelopejs/interface-api >=0.0.11 <1.0.0. @antelopejs/interface-core deliberately remains unchanged because its pending #9 work is expected to require another release. Promise sync behavior, static routes, URL/context construction, and dynamic extraction are deliberately outside this PR.

Cumulative end-to-end benchmark

Baseline: origin/main at 0bf67c2b93615a2c427e205fa3cd7a04a582b210, containing #29#33 and explicitly excluding #34. Candidate: bbd94e4 (#29#33 + this PR). Both variants use published @antelopejs/interface-api@0.0.11.

Node 22.19.0, real loopback requestListener, 50 connections, pipelining 10, 1 s warm-up + 3 s measured, six alternating repetitions. HEAD uses a 50-connection Node loader because autocannon 8 does not count completed HEAD responses. Values are mean req/s; p99 and population CV are reported for both variants. Transport errors and timeouts were zero throughout; non-2xx responses are expected for the error scenario.

Scenario Baseline req/s Candidate req/s Delta p99 before → after CV before / after
GET, no headers 44,487 45,166 +1.5% 20.8 → 23.3 ms 4.5 / 2.1%
GET, 4 headers 41,493 44,992 +8.4% 19.0 → 21.0 ms 2.4 / 1.7%
returned HTTPResult 43,290 44,704 +3.3% 23.5 → 20.3 ms 6.0 / 2.2%
dynamic route 39,570 41,198 +4.1% 25.8 → 21.3 ms 6.5 / 5.1%
POST + body 24,966 27,039 +8.3% 41.3 → 37.2 ms 8.2 / 3.4%
prefix + postfix + monitor 36,697 38,700 +5.5% 24.5 → 24.2 ms 2.6 / 1.4%
HEAD 8,071 8,221 +1.9% 12.5 → 11.0 ms 4.4 / 1.6%
stream 32,946 32,677 -0.8% 24.2 → 25.7 ms 3.9 / 2.5%
thrown error 25,180 25,128 -0.2% 38.0 → 40.2 ms 2.8 / 3.0%
1,000 routes 39,160 39,730 +1.5% 26.2 → 26.7 ms 4.7 / 2.8%

The direct response paths improve by 1.5–8.4%. Stream and error are neutral within run variance. The incremental diff introduces no async, await, or Promise on #33's synchronous HTTP path.

Allocation and GC profile

Five alternating fresh-process repetitions per scenario, 2,000-request warm-up, forced GC, server-only V8 allocation sampling at 4 KiB, then 10,000 external HTTP requests. Sampled bytes include Node internals and have 3.8–16.4% CV, so they are directional rather than exact byte accounting.

Scenario sampled B/req before → after API-owned B/req before → after heap delta before → after GC count before → after GC ms before → after
GET, no headers 37.47 → 37.85 2.50 → 1.48 353,822 → 357,053 23.0 → 22.8 17.80 → 16.95
GET, 4 headers 41.70 → 44.05 3.35 → 3.89 392,922 → 377,514 25.4 → 24.2 19.63 → 19.02
returned HTTPResult 41.12 → 39.34 3.69 → 1.48 368,386 → 360,082 23.0 → 23.0 18.11 → 18.51
dynamic route 39.47 → 39.07 2.88 → 1.98 362,648 → 358,958 25.0 → 24.0 20.47 → 20.79
POST + body 41.74 → 41.85 2.49 → 1.96 360,520 → 388,411 25.0 → 25.0 23.87 → 25.57
prefix + postfix + monitor 39.37 → 43.42 3.15 → 3.58 344,360 → 364,187 27.0 → 27.0 21.76 → 21.83
HEAD 67.29 → 74.29 3.24 → 3.41 692,098 → 710,534 41.4 → 40.8 31.66 → 32.31
stream 55.05 → 55.25 4.60 → 2.27 427,053 → 426,578 34.0 → 34.0 28.55 → 28.09
error 39.16 → 39.20 7.06 → 5.96 347,291 → 340,771 24.8 → 24.2 19.52 → 19.56
1,000 routes 40.43 → 41.38 2.11 → 1.86 370,651 → 367,795 23.0 → 22.4 19.14 → 19.63

API-owned sampled allocation falls on seven of ten scenarios, including -40.8% for headerless GET, -59.9% for a returned headerless HTTPResult, and -50.7% for stream. Total sampled allocation and retained heap are noisy and do not show a universal reduction; headers, middleware, and HEAD remain dominated by their required stores/snapshots and Node internals.

A separate instrumented contract request for a returned headerless HTTPResult observed:

Variant peekHeaders() calls returned undefined getHeaders() calls
baseline #29#33 0 0 1
candidate + #35 1 1 0

This confirms the candidate directly observes the absent header store without materializing an empty object.

Contract and validation coverage

📝 Checklist

  • I have linked an issue or discussion.
  • I have documented the dependency and benchmark limits in this PR.

Greptile Summary

The PR reduces HTTP response allocation by reusing response objects through the request lifecycle while retaining replacement and monitor-snapshot behavior.

  • Defers creation of the 404 response until route lookup confirms a miss.
  • Reuses matched-route responses for ordinary prefix, handler, and postfix results.
  • Preserves existing headers when replacing a response and calls the published peekHeaders API directly.
  • Avoids cloning responses when no monitor matches and adds real-HTTP lifecycle coverage.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete blocking or non-blocking defects identified in the changed behavior.

The revised lifecycle retains response sending through HTTPResult, preserves headers across replacements, isolates monitor snapshots, and is covered across the principal response paths.

Important Files Changed

Filename Overview
src/server.ts Reworks response creation, reuse, header preservation, error replacement, and monitor snapshot allocation without an established correctness defect.
src/test/response-lifecycle.test.ts Adds end-to-end HTTP tests covering response identity, headers, replacement results, route misses, monitors, errors, HEAD requests, and streams.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Incoming HTTP request] --> B[Resolve route]
  B -->|No match| C[Create 404 HTTPResult]
  B -->|Match| D[Create reusable HTTPResult]
  C --> E[Execute applicable lifecycle]
  D --> E
  E --> F[Prefix handlers]
  F --> G[Route handler]
  G --> H[Postfix handlers]
  H --> I{Matching monitors?}
  I -->|Yes| J[Clone isolated response snapshot]
  I -->|No| K[Skip snapshot]
  J --> L[Send final response]
  K --> L
Loading

Reviews (1): Last reviewed commit: "perf(http): reuse response results throu..." | Re-trigger Greptile

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/response-lifecycle branch from 26512bb to 42ea1c2 Compare August 19, 2026 20:58
Co-authored-by: Upd4ting <upd4ting@gmail.com>
@Upd4ting
Upd4ting merged commit ce95f5b into main Aug 19, 2026
2 checks passed
@Upd4ting
Upd4ting deleted the perf/response-lifecycle branch August 19, 2026 22:02
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