Skip to content

perf: implement Phase 1-7 optimizations to improve performance toward Fastify parity#6

Merged
rjoydip merged 14 commits into
mainfrom
perf/optimize-core-pipeline-2
Jul 3, 2026
Merged

perf: implement Phase 1-7 optimizations to improve performance toward Fastify parity#6
rjoydip merged 14 commits into
mainfrom
perf/optimize-core-pipeline-2

Conversation

@rjoydip

@rjoydip rjoydip commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

Performance optimization implementing multiple phases to improve Rune's HTTP throughput.

Changes

  • Phase 1: Fast path for simple controllers — Controllers without deps/guards/interceptors/DTOs pre-instantiate at init time, skipping DI resolution, guard checks, and interceptor chains per request.
  • Phase 2: Pre-compiled JSON serializer — Lazily compiles optimized serializers per response shape using new Function(), replacing JSON.stringify() on the hot path.
  • Phase 3: Dual Context eliminationfetch() stores Context on ctx.state('__ctx') so route handlers reuse it instead of creating a second one.
  • Phase 4: Middleware pipeline fast pathcompose() short-circuits when no middleware is registered.
  • Phase 5: Object.values cache on ContextparamsArray and queryValues lazy getters cache results.
  • Phase 7: Router static cache — Routes without : or * patterns cached in a Map for O(1) hash-based lookup.

Testing

  • 20 new tests added covering all optimization paths
  • 644 tests pass, 0 failures
  • Lint: 0 warnings, 0 errors

rjoydip added 5 commits July 2, 2026 23:15
…unner

- Fix double handler execution (module-loader.ts:160)
- Fix duplicate body JSON parse (module-loader.ts:176)
- Cache middleware compose, seal pipeline in init()
- Optimize URL pathname extraction (eliminate duplicate string scan)
- Clean up query/param resolution (remove dead code paths)
- Use Set.has() instead of includes() for route method validation
- Remove regex-based DI fallback; require @deps()
- Remove dead providerCache, dependencyMap, requestScope, instantiate()
- Skip JSON.stringify in adapters when body is already a string
- Fix container initChild() type safety (remove (this as any))
- Fix benchmark runner: use fetch instead of node:http agent
- Fix server.listen() callbacks and add error handlers
- Update runner to write results to docs/benchmarks.md
- Phase 1: Fast path for simple controllers (skip DI/guards/interceptors)
- Phase 2: Pre-compiled JSON serializer via lazy new Function()
- Phase 3: Dual Context elimination via ctx.state('__ctx') sharing
- Phase 4: Middleware pipeline fast path for 0-middleware case
- Phase 5: paramsArray and queryValues lazy caches on Context
- Phase 7: Router static cache for O(1) hash-based route lookup
- 20 new tests covering all optimizations
- Updated AGENTS.md and docs/performance.md with optimization details
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Bundle Sizes

Package Baseline Raw Gzip Brotli Status
@rune/adapter-aws-lambda 897 B 897 B 489 B 412 B
@rune/adapter-bun 94 B 94 B 105 B 92 B
@rune/adapter-cloudflare-pages 101 B 101 B 111 B 95 B
@rune/adapter-cloudflare-workers 118 B 118 B 122 B 94 B
@rune/adapter-deno 445 B 445 B 325 B 261 B
@rune/adapter-elysia 374.2 KB 374.2 KB 104.3 KB 88.4 KB
@rune/adapter-express 438 B 458 B 345 B 288 B 🔴
@rune/adapter-fastify 402 B 402 B 303 B 244 B
@rune/adapter-hono 19.9 KB 19.9 KB 8.1 KB 7.3 KB
@rune/adapter-koa 399 B 427 B 305 B 251 B 🔴
@rune/adapter-lambda-edge 1016 B 1016 B 546 B 466 B
@rune/adapter-netlify 602 B 602 B 381 B 322 B
@rune/adapter-node 599 B 599 B 397 B 343 B
@rune/adapter-service-worker 99 B 99 B 113 B 90 B
@rune/adapter-vercel 86 B 86 B 100 B 84 B
@rune/cache 365 B 365 B 229 B 207 B
@rune/config 553 B 553 B 336 B 282 B
@rune/container 1.2 KB 1.2 KB 533 B 458 B
@rune/core 41.2 KB 42.2 KB 12.2 KB 10.7 KB 🔴
@rune/create-rune 6.7 KB 6.7 KB 2.8 KB 2.5 KB
@rune/database 8 B 8 B 28 B 12 B
@rune/decorators 2.3 KB 2.5 KB 1.1 KB 962 B 🔴
@rune/events 252 B 252 B 190 B 161 B
@rune/graphql 180.8 KB 180.8 KB 47.0 KB 39.5 KB
@rune/logger 299 B 299 B 182 B 148 B
@rune/mail 8 B 8 B 28 B 12 B
@rune/middleware 10.4 KB 10.2 KB 3.7 KB 3.3 KB 🟢
@rune/openapi 1.8 KB 1.8 KB 1021 B 829 B 🟢
@rune/queue 8 B 8 B 28 B 12 B
@rune/router 2.6 KB 2.6 KB 1.2 KB 1.1 KB 🔴
@rune/socket 1.2 KB 1.2 KB 548 B 465 B
@rune/telemetry 180 B 180 B 157 B 143 B
@rune/tsconfig
@rune/validation 31.1 KB 31.1 KB 8.2 KB 7.3 KB

📊 Size Changes

Package Δ Raw Δ Gzip Δ Brotli
@rune/adapter-express +20 B (+4.6%) 🟡 +13 B (+3.9%) 🟡 +5 B (+1.8%) 🟡
@rune/adapter-koa +28 B (+7.0%) 🔴 +13 B (+4.5%) 🟡 +12 B (+5.0%) 🔴
@rune/container 0 B (+0.0%) 🟡 -2 B (-0.4%) 🟡 +2 B (+0.4%) 🟡
@rune/core +963 B (+2.3%) 🟡 +224 B (+1.8%) 🟡 +181 B (+1.7%) 🟡
@rune/decorators +144 B (+6.0%) 🔴 +81 B (+7.9%) 🔴 +73 B (+8.2%) 🔴
@rune/middleware -211 B (-2.0%) 🟡 +23 B (+0.6%) 🟡 +20 B (+0.6%) 🟡
@rune/openapi -3 B (-0.2%) 🟡 -2 B (-0.2%) 🟡 +14 B (+1.7%) 🟡
@rune/router +59 B (+2.2%) 🟡 +19 B (+1.6%) 🟡 +15 B (+1.4%) 🟡

Generated by scripts/bundle-size.ts at 2026-07-03T18:42:52.821Z

@pkg-pr-new

pkg-pr-new Bot commented Jul 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

@rune/adapter-aws-lambda

npm i https://pkg.pr.new/rjoydip/rune/@rune/adapter-aws-lambda@f59dc3e

@rune/adapter-bun

npm i https://pkg.pr.new/rjoydip/rune/@rune/adapter-bun@f59dc3e

@rune/adapter-cloudflare-pages

npm i https://pkg.pr.new/rjoydip/rune/@rune/adapter-cloudflare-pages@f59dc3e

@rune/adapter-cloudflare-workers

npm i https://pkg.pr.new/rjoydip/rune/@rune/adapter-cloudflare-workers@f59dc3e

@rune/adapter-deno

npm i https://pkg.pr.new/rjoydip/rune/@rune/adapter-deno@f59dc3e

@rune/adapter-elysia

npm i https://pkg.pr.new/rjoydip/rune/@rune/adapter-elysia@f59dc3e

@rune/adapter-express

npm i https://pkg.pr.new/rjoydip/rune/@rune/adapter-express@f59dc3e

@rune/adapter-fastify

npm i https://pkg.pr.new/rjoydip/rune/@rune/adapter-fastify@f59dc3e

@rune/adapter-hono

npm i https://pkg.pr.new/rjoydip/rune/@rune/adapter-hono@f59dc3e

@rune/adapter-koa

npm i https://pkg.pr.new/rjoydip/rune/@rune/adapter-koa@f59dc3e

@rune/adapter-lambda-edge

npm i https://pkg.pr.new/rjoydip/rune/@rune/adapter-lambda-edge@f59dc3e

@rune/adapter-netlify

npm i https://pkg.pr.new/rjoydip/rune/@rune/adapter-netlify@f59dc3e

@rune/adapter-node

npm i https://pkg.pr.new/rjoydip/rune/@rune/adapter-node@f59dc3e

@rune/adapter-service-worker

npm i https://pkg.pr.new/rjoydip/rune/@rune/adapter-service-worker@f59dc3e

@rune/adapter-vercel

npm i https://pkg.pr.new/rjoydip/rune/@rune/adapter-vercel@f59dc3e

@rune/openapi

npm i https://pkg.pr.new/rjoydip/rune/@rune/openapi@f59dc3e

@rune/config

npm i https://pkg.pr.new/rjoydip/rune/@rune/config@f59dc3e

@rune/container

npm i https://pkg.pr.new/rjoydip/rune/@rune/container@f59dc3e

@rune/core

npm i https://pkg.pr.new/rjoydip/rune/@rune/core@f59dc3e

@rune/cache

npm i https://pkg.pr.new/rjoydip/rune/@rune/cache@f59dc3e

@rune/database

npm i https://pkg.pr.new/rjoydip/rune/@rune/database@f59dc3e

@rune/graphql

npm i https://pkg.pr.new/rjoydip/rune/@rune/graphql@f59dc3e

@rune/logger

npm i https://pkg.pr.new/rjoydip/rune/@rune/logger@f59dc3e

@rune/mail

npm i https://pkg.pr.new/rjoydip/rune/@rune/mail@f59dc3e

@rune/middleware

npm i https://pkg.pr.new/rjoydip/rune/@rune/middleware@f59dc3e

@rune/queue

npm i https://pkg.pr.new/rjoydip/rune/@rune/queue@f59dc3e

@rune/socket

npm i https://pkg.pr.new/rjoydip/rune/@rune/socket@f59dc3e

@rune/telemetry

npm i https://pkg.pr.new/rjoydip/rune/@rune/telemetry@f59dc3e

@rune/decorators

npm i https://pkg.pr.new/rjoydip/rune/@rune/decorators@f59dc3e

@rune/events

npm i https://pkg.pr.new/rjoydip/rune/@rune/events@f59dc3e

@rune/router

npm i https://pkg.pr.new/rjoydip/rune/@rune/router@f59dc3e

@rune/validation

npm i https://pkg.pr.new/rjoydip/rune/@rune/validation@f59dc3e

@rune/create-rune

npm i https://pkg.pr.new/rjoydip/rune/@rune/create-rune@f59dc3e

commit: f59dc3e

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Fallow combined report

Found 7 findings.

Dependencies (1)
Severity Rule Location Description
minor fallow/unused-dev-dependency packages/infrastructure/middleware/package.json:24 Package '@rune/container' is in devDependencies but never imported; imported in other workspaces: packages/foundation/core
Duplication (1)
Severity Rule Location Description
minor fallow/code-duplication packages/foundation/core/src/rune-app.ts:129 Code clone group 1 (9 lines, 2 instances)
Health (5)
Severity Rule Location Description
major fallow/high-crap-score packages/foundation/container/src/container.ts:81 'resolve' has CRAP score 56.0 (threshold: 30.0, cyclomatic 7)
minor fallow/high-crap-score packages/foundation/core/src/json-serializer.ts:24 '<arrow>' has CRAP score 42.0 (threshold: 30.0, cyclomatic 6)
minor fallow/high-crap-score packages/foundation/core/src/module-loader.ts:149 'extractors' has CRAP score 42.0 (threshold: 30.0, cyclomatic 6)
major fallow/high-crap-score packages/foundation/core/src/module-loader.ts:184 'handler' has CRAP score 56.0 (threshold: 30.0, cyclomatic 7)
minor fallow/high-crap-score packages/foundation/core/src/module-loader.ts:202 'handler' has CRAP score 30.0 (threshold: 30.0, cyclomatic 5)

Generated by fallow.

@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

@@ -0,0 +1,44 @@
export type JsonSerializer = (obj: unknown) => string;

export function compileObjectSerializer(keys: string[]): (obj: Record<string, unknown>) => string {
};
}

export function isSerializableObject(val: unknown): val is Record<string, unknown> {
return typeof val === "object";
}

export function fastStringify(val: unknown): string {
@rjoydip rjoydip self-assigned this Jul 2, 2026
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

⚡ Benchmark Results

Framework Benchmarks

Rune

Route ops/sec ms/op
GET /hello 21181 0.047
GET /user/:id 19620 0.051
GET /search 22352 0.045
POST /echo 11377 0.088

Hono

Route ops/sec ms/op
GET /hello 28109 0.036
GET /user/:id 31888 0.031
GET /search 31511 0.032
POST /echo 8893 0.112

Elysia

Route ops/sec ms/op
GET /hello 59839 0.017
GET /user/:id 60769 0.016
GET /search 55310 0.018
POST /echo 40269 0.025

Fastify

Route ops/sec ms/op
GET /hello 33308 0.030
GET /user/:id 38525 0.026
GET /search 38069 0.026
POST /echo 20815 0.048

Node.js HTTP

Route ops/sec ms/op
GET /hello 41682 0.024
GET /user/:id 46221 0.022
GET /search 43235 0.023
POST /echo 28466 0.035

Koa

Route ops/sec ms/op
GET /hello 28227 0.035
GET /user/:id 33487 0.030
GET /search 30124 0.033
POST /echo 18636 0.054

Express

Route ops/sec ms/op
GET /hello 24620 0.041
GET /user/:id 27719 0.036
GET /search 24019 0.042
POST /echo 13667 0.073

Combined Results

Framework GET /hello GET /user/:id GET /search POST /echo
Elysia 59,839 ops/sec 60,769 ops/sec 55,310 ops/sec 40,269 ops/sec
Node.js HTTP 41,682 ops/sec 46,221 ops/sec 43,235 ops/sec 28,466 ops/sec
Fastify 33,308 ops/sec 38,525 ops/sec 38,069 ops/sec 20,815 ops/sec
Koa 28,227 ops/sec 33,487 ops/sec 30,124 ops/sec 18,636 ops/sec
Hono 28,109 ops/sec 31,888 ops/sec 31,511 ops/sec 8,893 ops/sec
Express 24,620 ops/sec 27,719 ops/sec 24,019 ops/sec 13,667 ops/sec
Rune 21,181 ops/sec 19,620 ops/sec 22,352 ops/sec 11,377 ops/sec

Updated ../docs/benchmarks.md

Rune Adapter Benchmarks

Runtime Adapters

Bun Adapter

Route ops/sec ms/op
GET /hello 51040 0.020
GET /user/:id 49078 0.020
GET /search 43661 0.023
POST /echo 6244 0.160

Node Adapter

Route ops/sec ms/op
GET /hello 23734 0.042
GET /user/:id 24153 0.041
GET /search 21631 0.046
POST /echo 11652 0.086

Framework Adapters

Elysia Adapter

Route ops/sec ms/op
GET /hello 50751 0.020
GET /user/:id 49588 0.020
GET /search 44077 0.023
POST /echo 6043 0.165

Hono Adapter

Route ops/sec ms/op
GET /hello 9304 0.107
GET /user/:id 9915 0.101
GET /search 9554 0.105
POST /echo 4960 0.202

Express Adapter

Route ops/sec ms/op
GET /hello 18677 0.054
GET /user/:id 18877 0.053
GET /search 15610 0.064
POST /echo 14401 0.069

Koa Adapter

Route ops/sec ms/op
GET /hello 20109 0.050
GET /user/:id 20047 0.050
GET /search 19937 0.050
POST /echo 14658 0.068

Fastify Adapter

Route ops/sec ms/op
GET /hello 21812 0.046
GET /user/:id 22524 0.044
GET /search 20894 0.048
POST /echo 10144 0.099

Combined Results

Adapter GET /hello GET /user/:id GET /search POST /echo
Bun 51,040 ops/sec 49,078 ops/sec 43,661 ops/sec 6,244 ops/sec
Elysia 50,751 ops/sec 49,588 ops/sec 44,077 ops/sec 6,043 ops/sec
Node.js 23,734 ops/sec 24,153 ops/sec 21,631 ops/sec 11,652 ops/sec
Fastify 21,812 ops/sec 22,524 ops/sec 20,894 ops/sec 10,144 ops/sec
Koa 20,109 ops/sec 20,047 ops/sec 19,937 ops/sec 14,658 ops/sec
Express 18,677 ops/sec 18,877 ops/sec 15,610 ops/sec 14,401 ops/sec
Hono 9,304 ops/sec 9,915 ops/sec 9,554 ops/sec 4,960 ops/sec

Updated at 2026-07-03T18:45:06.620Z by Benchmark CI

…ers compatibility

- Extract timingSafeEqual to shared helpers.ts in @rune/middleware (fixes dup:7fe71bb2)
- Extract joinPaths to @rune/decorators for use by @rune/core and @rune/openapi (fixes dup:3f24b414)
- Refactor trailing-slash.ts with createUrlParts + isGetOrHead helpers (fixes dup:7a5c1714, dup:39694973)
- Remove unused devDependencies: class-transformer/validator from @rune/core, @rune/container from @rune/middleware
- Add fallow-ignore-next-line comments with correct singular rule names (unused-export, unused-enum-member, unused-class-member)
- Fix json-serializer: wrap new Function() in try-catch to fallback to JSON.stringify for Cloudflare Workers (new Function blocked in V8 isolates)
- Update fallow.toml configuration
@rjoydip

rjoydip commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

APIError: Insufficient Balance

opencode session  |  github run

@rjoydip

rjoydip commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

Now I have all the information needed for a thorough review. Here is my analysis:

Status Category Description Location Suggestion
🔴 Bugs Benchmark claim contradicts data — PR title/docs claim "beat Fastify" but benchmarks show Rune at 19.5k ops/sec vs Fastify at 31.6k ops/sec (62%) AGENTS.md, PR title Update documentation to accurately reflect: "improved performance toward Fastify parity"
⚠️ Bugs OpenAPI in field uses "headers" instead of "header" — For p.type === "headers", maps to p.type which is "headers" (plural), but OpenAPI 3.0 spec requires "header" (singular) scanner.ts:130-134 Fix: change the mapping to produce "header" for headers type
⚠️ Bugs Fast path singleton instance shared across requestsisSimple controllers are pre-instantiated once (new (controller as any)()). Any instance state mutation (e.g., this.counter++) becomes a race condition module-loader.ts:139 Document that simple controllers must be stateless, or deep-clone the instance per request
⚠️ Bugs Dead code exports flagged by CodeQLcompileObjectSerializer, isSerializableObject, fastStringify are exported but never imported. fastStringify also re-compiles via new Function() on every call (worse than JSON.stringify) json-serializer.ts:4,33,40 Remove dead exports or mark internal; remove fastStringify or implement caching
⚠️ Performance compose() called per request — Every fetch() call creates a new closure via this.pipeline.compose(handler), even in the no-middleware fast path rune-app.ts:124 Cache the composed handler after pipeline.seal() — it's immutable after init
⚠️ Security new Function() code generationcompileObjectSerializer uses new Function() with JSON.stringify(obj.keys). While keys come from Object.keys() (not user input), new Function() is inherently risky json-serializer.ts:8-11 Add input validation/assertion that keys are strings; consider a Map-based pre-compiled cache instead
🔧 Quality Unnecessary ctx null check(ctx ? ctx.paramsArray : Object.values(params))[param.index]ctx is always defined module-loader.ts:148,151 Remove dead ternary branch
🔧 Quality moduleGuards heuristic is fragile — Filters providers by p.name.endsWith("Guard") or "canActivate" in p.prototype. Arrow-function methods won't have prototype methods; naming convention is unreliable module-loader.ts:64-66 Use explicit metadata from @UseGuard on module-level instead of heuristic
💡 Design fastStringify always re-compiles — Each call creates a new new Function() via compileObjectSerializer, which has JIT compilation overhead. For repeated calls with the same shape, this is slower than JSON.stringify json-serializer.ts:40-49 Remove or cache per-shape — createLazySerializer already does this correctly
Testing 20 new tests added covering serialization (67 lines), fast path (4 tests), guards (3 tests), interceptors, static cache, context caches Multiple test files Good coverage — consider adding nested-object edge case for serializer
Quality timingSafeEqual extracted to shared helper — DRY improvement across basic-auth, bearer-auth, trailing-slash helpers.ts, basic-auth.ts, bearer-auth.ts Clean refactoring
Fix Express/Koa adapter body handling — Handles pre-parsed body objects vs raw strings express/src/index.ts:63, koa/src/index.ts:29-30, fastify/src/index.ts:58-60 Correct defensive fix
Design Router static cache (Phase 7) — O(1) Map lookup for static routes, bypassing radix tree router/src/router.ts:69,86-88,106-107 Clean, well-integrated

Final Verdict

⚠️ Review needed. While the optimizations (fast path, serializer, static cache, middleware bypass, context caches) are well-implemented and tested, the PR contains 3 issues that should be addressed before merging: (1) benchmark claim mismatch — title/docs should not claim "beat Fastify" when data shows 62% of Fastify's throughput; (2) OpenAPI spec bug — "headers" should be "header"; (3) fast path singleton shared state risk should be documented. The new Function() serializer, while functionally correct, warrants a security note. Several dead exports should be cleaned up.

New%20session%20-%202026-07-03T16%3A22%3A18.964Z
opencode session  |  github run

@rjoydip rjoydip changed the title perf: implement Phase 1-7 optimizations to beat Fastify perf: implement Phase 1-7 optimizations to improve performance toward Fastify parity Jul 3, 2026
- Fix OpenAPI spec: 'headers' → 'header' (singular) per OpenAPI 3.0 spec
- Remove unnecessary ctx null check in module-loader.ts fast path
- Cache composed middleware handler after pipeline.seal() to avoid per-request closure allocation
- Update benchmark claim in AGENTS.md and PR title to accurately reflect Fastify parity progress
@rjoydip

rjoydip commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

Code Review: perf/optimize-core-pipeline-2main

Changeset Summary: 752 additions, 501 deletions, 47 files


Status Category Description Location Suggestion
⚠️ Security new Function() code generation with dynamic stringscompileObjectSerializer constructs a function body using JSON.stringify(keys) and new Function(...). While keys come from Object.keys(obj) (not user input), new Function() eschews CSP protections and is flagged by security tooling json-serializer.ts:8-10 Consider a Map-based string-builder or pre-compiled template instead. At minimum, assert keys are safe strings
⚠️ Bugs fastStringify re-compiles on every call — Unlike createLazySerializer (which caches per shape), fastStringify calls compileObjectSerializer(keys) on each invocation. This means new Function() JIT compilation on every serialization — likely slower than JSON.stringify to begin with json-serializer.ts:40-49 Remove fastStringify or add per-shape caching. It's also flagged as dead export (see below)
⚠️ Bugs Fast path singleton shared across requestsisSimple controllers are instantiated once at registration time (new (controller as any)()). If any instance property is mutated (e.g., this.count++), it becomes a race condition across concurrent requests module-loader.ts:139 Document that simple-controller instances must be stateless, or add a structuredClone / factory per request
⚠️ Bugs compose() called per request instead of cachedthis.pipeline.compose(handler) in rune-app.ts:124 creates a new closure on every fetch() call. After seal(), the composed field is pre-built and could be reused with just handler substitution rune-app.ts:124, middleware-pipeline.ts:62-89 Either cache the composed pipeline per app lifecycle or make composed a method that accepts the handler and returns a shared pre-bound dispatch
🔴 Bugs OpenAPI in field still produces "headers" — The ternary at scanner.ts:134 has an unreachable fallback "header" case. When p.type === "headers", maps to "header" which is correct. But the fallback "header" is only reached when p.type is none of "param", "query", or "headers" — then the fallback is "header" regardless, which means unknown types silently become "header". Actually, looking more carefully: The code maps p.type === "param" → "path", p.type === "query" → "query", p.type === "headers" → "header", else → "header". The bug from the original review was that it used to say "headers" (plural) for the p.type === "headers" case. The diff shows this was already fixed to "header". But the test at line 119 checks for "header". So this is already correct. Let me re-examine... scanner.ts:129-136 The "headers""header" mapping is correctly fixed. The duplicate fallback is just dead code. This is a non-issue.
💡 Performance compose() non-sealed path still allocates a new closure per call — The non-sealed compose (line 64-89) creates a new dispatch closure per invocation. For high-throughput scenarios, this allocation overhead matters middleware-pipeline.ts:62-89 Call seal() before first fetch() — is already called in rune-app.ts:87. The unsealed path is dead code in practice. Consider removing it or making it throw
⚠️ Dead Code 3 exports never importedcompileObjectSerializer, isSerializableObject, fastStringify are exported but never imported outside json-serializer.ts. The fallow-ignore-next-line pragmas confirm these are known-dead exports json-serializer.ts:4,33,40 Either un-export them (make internal), or document the intended external use. Remove fastStringify entirely — it's both dead and slower than JSON.stringify
🔧 Quality Inconsistent error handling in extractors — The fast path uses await Promise.all(extractors.map(...)) wrapped in try/catch that yields args = []. A single extractor failure silently drops all arguments, potentially producing confusing behavior module-loader.ts:165-169 Consider per-extractor error handling, or let the exception propagate as a 500
🔧 Quality instance parameter called _c but unuseduseFactory: (_c: IContainer) => { ... } discards the container reference, then manually calls this.container.resolve(dep) on the ModuleLoader instance instead of the injected _c module-loader.ts:77,105 Use the injected container _c instead of capturing this.container — makes the factory independent of the loader instance
🔧 Quality moduleGuards heuristic is fragile — Filters providers by p.name.endsWith("Guard") or "canActivate" in p.prototype. Arrow-function class properties won't have prototype methods; naming convention is fragile module-loader.ts:64-66 Use explicit metadata from @UseGuard at module-level instead of convention-based heuristic. Document the limitation
🔧 Quality ctx is always defined in fast-path extractors(ctx ? ctx.paramsArray : Object.values(params))[param.index]ctx is always a Context instance, never undefined module-loader.ts:148,151 Remove the ternary, use ctx directly
🔧 Quality Express/Koa adapter: typeof req.body === "string" is fragile — When body parsing is disabled or Content-Type is not JSON, req.body could be undefined or a Buffer. JSON.stringify(undefined) returns undefined (not a string), which causes issues express/src/index.ts:63, koa/src/index.ts:29-30 Check req.body !== undefined first; fallback to empty string if body is undefined
Performance Phase 7: Router static cache — O(1) Map lookup for !path.includes(":") && !path.includes("*") routes, bypassing radix tree router/src/router.ts:69,86-88,106-107 Clean, well-integrated. Tests confirm path and param routes both work
Performance Phase 3: Dual Context eliminationctx.state.set("__ctx", ctx) in rune-app.ts:120 allows route handlers to reuse the same Context rune-app.ts:120, module-loader.ts:162-163 Good — avoids creating a second Context per request
Performance Phase 5: Object.values cache on ContextparamsArray and queryValues lazy getters with private cache fields context.ts:22-24,83-103 Correct lazy caching pattern
Quality timingSafeEqual extracted to shared helper — DRY improvement across 3 middleware files helpers.ts, basic-auth.ts, bearer-auth.ts Good refactoring
Quality trailing-slash.ts simplified — Was ~72 lines, now ~15 lines via createUrlParts / isGetOrHead helpers trailing-slash.ts Clean functional decomposition
Testing 20 new tests covering fast path (4), guards (3), interceptors, static cache (2), serialization (11), context cache Multiple test files Good coverage. Consider adding nested-object edge cases for serializer
🏗️ Design createLazySerializer correctly caches per shape — Compiles only once per first-encountered object shape, then reuses json-serializer.ts:17-30 This is the right pattern. The fastStringify exported function should use this same approach

Final Verdict

⚠️ Review needed. The core optimization infrastructure (fast path, static cache, middleware seal, context caching) is sound and well-tested. The PR delivers genuine throughput improvements. However, 3 issues should be addressed before merging:

  1. fastStringify is counter-productive — re-compiles via new Function() on every call with no caching, making it slower than JSON.stringify. Also a dead export. Either cache per-shape or remove.
  2. Fast path singleton riskisSimple controllers are pre-instantiated once; document that they must be stateless, else concurrent requests share mutable state.
  3. compose() called per request — After seal(), the composed dispatch with the no-middleware shortcut is already pre-built. But rune-app.ts:124 still calls this.pipeline.compose(handler) which allocates a new wrapper closure on every fetch(). Cache the composition.

The OpenAPI "headers""header" fix is already correct in the diff — the previous review's concern about it was stale. The dead-code exports (compileObjectSerializer, isSerializableObject) are minor — safe to clean up but not blocking.

New%20session%20-%202026-07-03T16%3A29%3A43.086Z
opencode session  |  github run

- Cache compiled serializers per shape in fastStringify (Map keyed by joined keys)
- Use injected container c.resolve() instead of this.container in useFactory
- Guard Express/Koa body serialization against undefined body
- Remove silent extractor error catch; let errors propagate as 500s
- Document fast path singleton stateless requirement
@rjoydip

rjoydip commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

Now I have a complete picture of all changed files. Here is my code review:

Code Review: perf/optimize-core-pipeline-2main

Changeset: 791 additions, 513 deletions, 47 files

Status Category Description Location Suggestion
⚠️ Bug Risk Fast path singleton shared across requestsisSimple controllers are new (controller as any)() at init time. Any instance property mutation (e.g., this.count++) becomes a race condition module-loader.ts:142 Document that simple controllers must be stateless; consider a factory function or Object.freeze on the instance
⚠️ Security new Function() code generationcompileObjectSerializer constructs function bodies from JSON.stringify(keys). While keys come from Object.keys(obj) (not raw user input), this bypasses CSP and is flagged by CodeQL json-serializer.ts:8-10 Add input validation that keys are safe strings; alternatively use a Map-based string builder without eval/new Function
⚠️ Dead Code 3 exports never importedcompileObjectSerializer, isSerializableObject, fastStringify are exported but never imported elsewhere. fallow-ignore-next-line pragmas confirm these are known dead exports json-serializer.ts:4,33,42 Remove dead exports or mark internal with @internal JSDoc. fastStringify has its own cache but duplicates createLazySerializer's concern
⚠️ Edge Case Fast path createLazySerializer caches per-first-object-shape — If the first response is an array (falls back to JSON.stringify), all subsequent serializations use JSON.stringify even for plain objects json-serializer.ts:20-27 Edge case is unlikely (first response typically sets the pattern), but worth noting. Consider resetting compiled on type mismatch
⚠️ Maintainability moduleGuards heuristic is fragile — Filters providers by p.name.endsWith("Guard") or "canActivate" in p.prototype. Arrow-function class properties lack prototype; naming convention is brittle module-loader.ts:64-66 Use explicit metadata from @UseGuard at module level instead of convention-based heuristics
🔧 Minor Fast path extractors accept but ignore _paramscase "body", case "query", and others declare _params: Record<string, string> parameter but never use it module-loader.ts:148-161 Remove unused parameter to reduce closure size
🔧 Minor useFactory parameter c shadows injected container — Line 77 passes (c: IContainer) but the surrounding scope also has this.container. The code correctly uses c in the factory, but _c would clarify intent module-loader.ts:77 Rename to _c or just verify the correct container is used (it is)
🔧 Minor Compose closure created per requestthis.pipeline.compose(handler) at rune-app.ts:124 creates a new wrapper closure on every fetch() call, even in the sealed no-middleware fast path rune-app.ts:124 In the seal() fast path, the composed wrapper is minimal (1 async call + 1 instanceof check). Acceptable but worth noting for future optimization
🔧 Minor ctx.state mutation via Mapctx.state.set("__ctx", ctx) at rune-app.ts:120 stores the context on itself. The handler middleware receives ctx as its parameter, so __ctx retrieval in module-loader.ts:165 is always the same reference rune-app.ts:120, module-loader.ts:165 This is functionally correct but the round-trip is unnecessary — the ctx is already available as the handler's first argument. Not a bug, just an indirect pattern
Performance Phase 7: Router static cache — Routes without : or * get O(1) Map lookup bypassing radix tree router.ts:69,86-88,106-107 Clean integration; tests cover both static and param routes
Performance Phase 5: Object.values cache on ContextparamsArray and queryValues lazy getters with private cache fields context.ts:22-24,83-103 Correct lazy caching pattern
Performance Phase 4: Middleware pipeline sealseal() pre-builds this.composed to avoid conditionals on hot path middleware-pipeline.ts:39-62 Clean design; the fallback compose() path at lines 64-91 is only reachable if seal() wasn't called
Bug Fix OpenAPI "headers""header" — Corrects OpenAPI 3.0 spec violation (spec requires singular "header") scanner.ts:134 Fixed correctly; test expects "header" at scanner.test.ts:119
Bug Fix Adapter body handling — Handles pre-parsed body objects vs raw strings in Express, Koa, Fastify adapters express/src/index.ts:63-68, koa/src/index.ts:29-35, fastify/src/index.ts:56-61 Correct defensive fix with typeof + != null check
Quality timingSafeEqual extracted to shared helper — DRY improvement across basic-auth, bearer-auth, trailing-slash helpers.ts, basic-auth.ts, bearer-auth.ts Clean refactoring
Quality trailing-slash.ts simplified — ~72 lines → 71 lines via createUrlParts / isGetOrHead helpers trailing-slash.ts Good functional decomposition
Quality Container.createScope takes parent referenceinitChild now receives parent for clearer chain management container.ts:31-39 Clean improvement
Testing 20 new tests across 4 test files — Fast path (4), guards (3), interceptors, static cache (2), serialization (11), context cache rune-app.test.ts:725-840, json-serializer.test.ts, router.test.ts:108-122, context.test.ts Good coverage — consider adding nested-object edge case for serializer, and a test verifying fastStringify shape cache distinctness

Final Verdict

⚠️ Review needed. The optimization infrastructure (fast path, pre-compiled serializer, static cache, middleware seal, context caching) is sound, well-tested, and delivers genuine throughput improvements. The OpenAPI spec fix and adapter body handling are correct. However, 3 issues warrant attention — the new Function() CSP bypass in compileObjectSerializer (document / mitigate), the 3 dead exports with fallow-ignore pragmas (fastStringify should either be used or removed), and the fast path shared-instance risk (documented but no enforcement). The moduleGuards heuristic and compose per-request allocation are minor design notes. Overall, the code quality is high and the performance gains are meaningful.

New%20session%20-%202026-07-03T18%3A09%3A25.993Z
opencode session  |  github run

- Remove export from compileObjectSerializer and isSerializableObject (internal)
- Add SAFE_KEY_RE validation to compileObjectSerializer (falls back to JSON.stringify for non-safe keys)
- Remove compileObjectSerializer direct tests (covered by createLazySerializer/fastStringify)
- Add Object.freeze to fast path singleton to enforce statelessness at runtime
@rjoydip

rjoydip commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

Now I have a thorough understanding of all changes. Here is my review:

Status Category Description Location Suggestion
🔴 Bug Serializer shape mismatch corrupts outputcreateLazySerializer compiles based on first response's keys and never re-evaluates. If a route returns objects with different shapes across requests (e.g., conditional { admin: true } vs { id }), output becomes {"id":"42","admin":undefined} — invalid JSON json-serializer.ts:23-31 Recompile when shape changes, or verify shape matches before using compiled function. Fall back to JSON.stringify on mismatch
⚠️ Bug Fast path vs normal path behavioral inconsistency — Fast path propagates body parse errors as 500 (via fetch catch). Normal path (executeHandler:242-246) silently swallows extraction errors and sets args = [], producing potentially confusing behavior module-loader.ts:242-246 vs handler in fast path Make error handling consistent — either let both propagate or both swallow; at minimum document the difference
⚠️ Bug Fast path Object.freeze is shallow — Only protects own properties from reassignment. Nested objects, Maps, Arrays referenced by the instance can still be mutated (e.g., this.cache.set(key, val)). Object.freeze also throws in strict mode if constructor assigns to this in certain ways module-loader.ts:142 Add a comment noting shallow limitations, or use deepFreeze if practical
⚠️ Bug moduleGuards heuristic includes unrelated providers — Filters by p.name.endsWith("Guard") which can match non-guard classes named SomeGuard. Also, arrow-function methods won't have canActivate on prototype module-loader.ts:64-66 Use explicit metadata via a @UseGuard at module-level, or require guards to extend a base class
⚠️ Security new Function() bypasses CSPcompileObjectSerializer uses new Function() with generated code strings. While keys from Object.keys() are safe, environments with strict CSP (script-src 'self') will block execution json-serializer.ts:12-15 Consider a Map-based string builder without eval/new Function, or document the CSP requirement
⚠️ Dead Code 3 dead exports with fallow-ignorecompileObjectSerializer, isSerializableObject, and fastStringify are exported but never imported. CodeQL confirms as "unused export" json-serializer.ts:4,36,45 Remove or @internal annotate. If fastStringify is kept, wire it into the fast path instead of createLazySerializer which duplicates its concern
⚠️ Performance compose() allocates closure per requestthis.pipeline.compose(handler) in rune-app.ts:124 creates a new async (context) wrapper on every fetch(), even in the sealed no-middleware fast path rune-app.ts:124 Since handler is always the same local function, cache the wrapper after first call; or make compose return a reusable function
🔧 Minor _context?.get?.("__ctx") unnecessary null-safe chaining_context is ctx.state (a Map), always defined per RouteHandler signature. ?. implies it could be null/undefined module-loader.ts:166,183 Use _context.get("__ctx") directly
🔧 Minor Non-fast path executeHandler uses JSON.stringify — Could benefit from createLazySerializer for consistency with fast path module-loader.ts:254 Replace JSON.stringify(result) with a shared serializer for parity
🔧 Minor Fast path extractors use _params param but never read it — Several extractors declare _params: Record<string, string> but only context or req are used module-loader.ts:149-162 Remove unused _params param from extractors
🔧 Minor Express/Koa body: req.body could be undefinedtypeof undefined === "undefined", falls to req.body != null (false), correctly returns undefined. But Buffer bodies also fall to JSON.stringify(buffer) which produces {} express/src/index.ts:63-68, koa/src/index.ts:29-35 Add Buffer.isBuffer(req.body) check or check typeof req.body === "object"
🔧 Minor Container initChild parameter namingparent: Container param added but previously the method may not have taken a parent ref container.ts:31-39 The naming is clear — no change needed, just noting the improvement
💡 Observation Router static cache registers to both staticCache and radix treeadd() always calls both staticCache.set and addRoute, so static routes are in both structures router.ts:86-89 Brief comment explaining dual registration is intentional (static cache fast path, radix tree for correctness when iterating)
Quality timingSafeEqual extracted to shared helpers.ts — DRY improvement across 3 middleware files helpers.ts, basic-auth.ts, bearer-auth.ts Good refactoring
Quality Trailing-slash refactored with createUrlParts/isGetOrHead — Clear functional decomposition, reduces duplication trailing-slash.ts Clean
Performance Phase 5: paramsArray/queryValues lazy caching — Private cache fields + lazy getters; test verifies toBe() identity (same reference on second call) context.ts:83-103 Correct pattern
Performance Phase 7: Router static cache — O(1) Map lookup for routes without : or * router.ts:69,86-88,106-107 Clean integration
Fix OpenAPI "headers""header" — Corrects OpenAPI 3.0 spec violation scanner.ts:134 Correctly fixed; test confirms "header"
Fix Adapter body handling — Handles pre-parsed body objects vs raw strings express, koa, fastify adapters Correct defensive fix
Quality Container initChild now takes explicit parent — Cleaner chain management container.ts:31-39 Good improvement
Testing 20 new tests across 4 files — fast path (4), guards (3), serializer (11), context caching (2), router static cache (2) Various test files Good coverage. Consider adding: serializer shape-mismatch test, fast-path Object.freeze mutation attempt test, paramsArray/queryValues identity caching test already present

Final Verdict: 🔴 Changes requested. The optimizations are well-designed and the code quality is generally high, but there is one correctness bug that must be fixed: createLazySerializer corrupts output when the response shape varies across requests (serializes with first-call keys only, producing undefined literals for missing/new keys). Additionally, the 3 dead exports should be cleaned up, and the fast-path vs normal-path error-handling inconsistency should be reconciled. The remaining items (CSP concern, shallow Object.freeze, per-request closure allocation) are less critical but worth addressing before merge.

New%20session%20-%202026-07-03T18%3A15%3A27.269Z
opencode session  |  github run

…router cache

- Inline handler directly in fetch() when pipeline has no middleware
  (eliminates 2 closure allocations per request)
- Skip Promise.all when all param extractors are synchronous
  (most common case: param/query/headers only)
- Use two-level Map (method -> pathname) in router static cache
  (avoids per-request template literal string allocation)
- Share JSON headers constant across modules
  (avoids per-response object allocation)
@rjoydip

rjoydip commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

Code Review: perf/optimize-core-pipeline-2main

Changeset: 791 additions, 513 deletions, 47 files

Status Category Description Location Suggestion
🔴 Bug createLazySerializer doesn't recompile on shape mismatch — Compiles keys from first response object, then reuses forever. If a route returns {id:1} then later {name:"x"}, the output becomes {"id":undefined} — invalid JSON with literal undefined json-serializer.ts:23-31 Compare current object keys against compiled keys; fall back to JSON.stringify on mismatch
⚠️ Performance fastStringify dead export with fallow-ignore — Exported but never imported. Uses per-shape caching but duplicates createLazySerializer's concern. Adds 200B+ to bundle json-serializer.ts:45 Remove dead export; the fast path already uses createLazySerializer internally
⚠️ Security new Function() code generationcompileObjectSerializer constructs function body from JSON.stringify(keys). While keys come from Object.keys() (not user input), this bypasses CSP and is flagged by CodeQL json-serializer.ts:12-15 Document CSP restriction, or use a Map-based string builder without eval/new Function
⚠️ Bug Risk Fast path singleton mutable via nested referencesObject.freeze is shallow: this.cache.set(k,v) or this.items.push(x) still mutates shared state across concurrent requests module-loader.ts:145 Document strongly that simple controllers must be stateless (including nested collections). Consider deepFreeze or a factory
⚠️ Bug Risk moduleGuards heuristic is fragile — Filters by p.name.endsWith("Guard") or "canActivate" in p.prototype. Misses arrow-function guards (no prototype), false-positives on classes coincidentally named *Guard module-loader.ts:66-67 Use explicit metadata from @UseGuard at module-level instead of convention-based heuristic
⚠️ Maintainability Error handling asymmetry — fast path vs normal path — Fast path (line 188) lets extraction errors propagate as 500. Normal executeHandler (line 262) silently swallows errors and sets args = []. Same intent, different failure modes module-loader.ts:188,262 Normalize: either both propagate or both fall back. Document the behavior
⚠️ Performance compose() allocates closure per requestthis.pipeline.compose(handler) in rune-app.ts:145 creates a new wrapper function on every fetch(), even when sealed with no middleware middleware-pipeline.ts:45-50, rune-app.ts:145` Pre-assign the no-middleware wrapper to avoid per-request compose call, or inline the handler directly like the sealed fast path already does at line 125
🔧 Minor __ctx round-trip indirection — Handler receives context.state (Map) as 3rd arg, then does _context.get("__ctx") to get the Context back. The Context is already available as the first arg to the middleware module-loader.ts:184,203, rune-app.ts:120-122 Pass context directly (via state.set) is only needed because RouteHandler signature is Map-typed. Consider changing handler signature — or keep as is since it's a minor indirection
🔧 Minor Fast path extractors declare unused _params parameter — Several extractors like "body", "headers", "context" cases declare _params but never read it module-loader.ts:153,159,164,170,176 Remove unused parameter from those extractors
🔧 Minor Adapter body: req.body could be undefined or Buffertypeof undefined === "undefined" falls through to req.body != null (false → undefined). But Buffer bodies pass typeof check as "object" and JSON.stringify(buffer) produces {} — data loss express/src/index.ts:63-68, koa/src/index.ts:29-35, fastify/src/index.ts:56-61 Add Buffer.isBuffer(req.body) check, or handle undefined explicitly before the typeof branch
🔧 Minor compose() fallback path (not sealed) still creates closures — The unsealed compose path at line 70-94 creates a new dispatch closure each call. In practice, seal() is always called before first fetch() middleware-pipeline.ts:68-95 Consider making unsealed compose throw, or document that seal() must be called
💡 Observation Non-fast path still uses JSON.stringifyexecuteHandler line 274 uses JSON.stringify(result) instead of createLazySerializer module-loader.ts:274 Could use the lazy serializer for consistency with the fast path
Performance Phase 7: Router static cache — O(1) Map lookup for !path.includes(":") && !path.includes("*"), clean integration router.ts:69,86-93,111-115 Well-implemented
Performance Phase 5: paramsArray/queryValues lazy caching — Private cache fields + lazy getters; test verifies reference identity context.ts:22-24,83-103 Correct pattern
Performance Phase 4: Middleware pipeline seal — Pre-builds composed dispatch, sealedNoMiddleware getter bypasses chain middleware-pipeline.ts:35-66 Clean, efficient
Quality timingSafeEqual extracted to helpers.ts — DRY across basic-auth, bearer-auth, trailing-slash helpers.ts, basic-auth.ts, bearer-auth.ts Good refactoring
Quality trailing-slash.ts refactored with createUrlParts/isGetOrHead — Clean functional decomposition trailing-slash.ts Much cleaner
Quality Container initChild takes explicit parent param — Clearer chain management container.ts:31-39 Good improvement
Fix OpenAPI "headers""header" — Correct OpenAPI 3.0 spec compliance scanner.ts:129-136 Correctly fixed; test confirms
Fix Adapter body handling — Pre-parsed body objects vs raw strings, correct defensive pattern express, koa, fastify adapters Correct
Testing 20 new tests — Fast path (4), guards (3), serializer (11), cache (2), context caching (2) Multiple test files Good coverage. Consider adding: serializer shape-mismatch test, fast-path concurrent state mutation test, Buffer body edge case

Final Verdict

⚠️ Review needed. The optimization infrastructure (Phase 1-7) is well-designed, correctly implemented, and delivers genuine throughput improvements. The router static cache, middleware seal, context caching, and extracted helpers are clean.

Required before merging:

  1. 🔴 createLazySerializer shape-mismatch bug — produces invalid JSON when response keys change between requests
  2. ⚠️ Remove dead fastStringify export (200B+ bundle savings, confirmed dead by fallow)
  3. ⚠️ Normalize error handling between fast path (500) and normal path (silent args = [])

Strongly recommended:
4. ⚠️ Per-request compose() closure allocation — cache the wrapper
5. ⚠️ Document fast path statelessness requirement (including nested mutability caveat)
6. ⚠️ Fix moduleGuards heuristic fragility

The remaining items (CSP concern, adapter Buffer edge case, unused params) are minor.

New%20session%20-%202026-07-03T18%3A37%3A11.842Z
opencode session  |  github run

…ndling, heuristic

- Fix createLazySerializer shape-mismatch: recompile serializer when
  object keys change between calls (tracks shapeKey via join("\0"))
- Remove dead fastStringify export (confirmed unused, saves bundle size)
- Normalize error handling: remove silent try/catch in executeHandler
  (let param extraction errors propagate like the fast path now does)
- Document nested mutability caveat in fast path comment
- Fix moduleGuards heuristic: remove fragile p.name.endsWith("Guard")
  check, rely solely on canActivate in prototype
@rjoydip

rjoydip commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

Now I have a complete view of all changes. Let me produce the review.

Code Review: perf/optimize-core-pipeline-2main

Changeset: 14 commits, 796 additions, 533 deletions, 47 files

Status Category Description Location Suggestion
⚠️ Bug createLazySerializer produces invalid JSON on type switch — When compiled for objects ({msg:"hello"}), then called with an array/primitive, the old compiled function runs against the wrong type producing {"msg":undefined} (literal undefined in JSON). E.g., serialize([1,2,3]) after serialize({msg:"x"}) yields {"msg":undefined} json-serializer.ts:23-37 Reset compiled = null in the else branch so arrays/primitives fall through to JSON.stringify, and subsequent objects recompile
⚠️ Breaking Change Container.useClass removed — Throws "useClass is not supported". All existing useClass registrations must be migrated to useFactory. This was not mentioned in the PR description or docs container.ts:103-108 Document in AGENTS.md or the PR body. The test suite migration shows the pattern (c.register({ token: Service, useFactory: () => new Service() }))
🔧 Maintainability _context?.get?.("__ctx") unnecessary optional chaining_context is typed as Map<string, unknown> (per RouteHandler signature), always defined. The ?. operators are misleading and suggest _context could be nullish module-loader.ts:165,183 Use _context.get("__ctx") directly — the Map is always present
💡 Minor hasSyncExtractors check short-circuits error handling — When hasSyncExtractors is true, a throwing extractor (e.g., ctx.body on a non-body request) propagates synchronously as an uncaught exception instead of a rejected promise module-loader.ts:171-173 Wrap the map in a try block or use Promise.all consistently regardless of sync/async — currently the sync path will crash rather than produce a 500
Bug Fix createLazySerializer recompiles on shape change — Latest commit added shapeKey tracking, correctly re-compiling when object keys differ between requests json-serializer.ts:27-29 Fix verified — test confirms round-trip through different shapes
Bug Fix Dead exports removedfastStringify deleted, compileObjectSerializer and isSerializableObject made non-exported functions json-serializer.ts Clean
Bug Fix Error handling unified — Both fast path and normal path now propagate extraction errors as 500s (try/catch removed from executeHandler) module-loader.ts Consistent behavior
Bug Fix moduleGuards heuristic simplified — Changed from `p.name.endsWith("Guard") "canActivate" in p.prototypeto just"canActivate" in p.prototype`
Fix OpenAPI "headers""header" — Correct OpenAPI 3.0 spec compliance scanner.ts:134 Correct; test expects "header"
Fix Adapter body handling — Handles pre-parsed body objects vs raw strings across Express, Koa, Fastify express/koa/fastify adapters Correct defensive pattern
Performance Router static cache (Phase 7) — O(1) Map lookup for routes without : or * router.ts:69,86-93,111-115 Clean, well-integrated
Performance Middleware pipeline seal (Phase 4)seal() pre-builds composed dispatch; sealedNoMiddleware getter bypasses chain allocation in fetch() middleware-pipeline.ts:35-66, rune-app.ts:125-146 Inline no-middleware path avoids per-request compose() call
Performance paramsArray/queryValues lazy caching (Phase 5) — Private paramsArrayCache/queryValuesCache fields with lazy getters on Context context.ts:83-103 Tests verify reference identity
Performance Dual Context elimination (Phase 3)ctx.state.set("__ctx", ctx) so handlers reuse the same Context instance rune-app.ts:132, module-loader.ts:165,183 Correct
Performance Fast path for simple controllers (Phase 1) — Pre-instantiates controllers without deps/guards/interceptors/DTOs at init time, skipping DI resolution per request module-loader.ts:132-178 Correct; Object.freeze with statelessness comment
Performance Pre-compiled JSON serializer (Phase 2)createLazySerializer compiles optimized serializers per response shape json-serializer.ts Correct pattern with lazy compilation
Quality timingSafeEqual extracted to shared helpers.ts — DRY across basic-auth.ts, bearer-auth.ts, trailing-slash.ts helpers.ts Good refactoring
Quality trailing-slash.ts decomposedcreateUrlParts/isGetOrHead helpers reduce duplication trailing-slash.ts Clean
Quality joinPaths extracted to @rune/decorators — Shared between ModuleLoader and OpenAPIScanner metadata.ts:138-145 Good DRY improvement
Quality Container initChild uses proper assignments — Replaced (this as any).parent = parent with this.parent = parent container.ts:38-40 Type-safe
Quality isSerializableObject handles undefined — Checks val === undefined in addition to null json-serializer.ts:40 Correct
Testing 20 new tests across 5 test files — Fast path (4), guards (1), serializer (4), static cache (2), context caching (2), container migration (7) Various Good coverage
📝 Docs Benchmark title still says "Phase 1-7 optimizations to beat Fastify" — The commit message a3755f3 and benchmark docs still reference "beating Fastify" Commit message, docs/benchmarks.md Update to "toward Fastify parity" — actual benchmarks show Rune at ~62% of Fastify throughput

Final Verdict

✅🎯 LGTM (minor). The 14-commit optimization PR delivers genuine throughput improvements across all phases. The three previous rounds of review feedback have been addressed in the latest commits (shape-mismatch fix, dead export removal, error handling unification, heuristic cleanup). The remaining issues are a minor edge case in createLazySerializer (type-switch produces invalid JSON — unlikely in practice since routes return consistent types) and an undocumented breaking change (useClass removed from Container). Both are worth fixing but not blocking. The optional chaining on _context.get() is cosmetic.

New%20session%20-%202026-07-03T18%3A42%3A55.829Z
opencode session  |  github run

@rjoydip
rjoydip merged commit 334e604 into main Jul 3, 2026
8 checks passed
@rjoydip
rjoydip deleted the perf/optimize-core-pipeline-2 branch July 3, 2026 18:45
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