Skip to content

perf(runtime): stop rebuilding per-route data on every request - #1875

Open
cesco69 wants to merge 1 commit into
lukeautry:masterfrom
cesco69:perf/avoid-per-request-allocations
Open

perf(runtime): stop rebuilding per-route data on every request#1875
cesco69 wants to merge 1 commit into
lukeautry:masterfrom
cesco69:perf/avoid-per-request-allocations

Conversation

@cesco69

@cesco69 cesco69 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

perf(runtime): stop rebuilding per-route data on every request

What

getValidatedArgs does two things on every request that do not depend on the request:

  1. Object.values(args) rebuilds the parameter list. But the generated routes build
    one args object per route at registration time and hand back that very same object on
    every request, so the derived list is stable. It is now computed once and cached in a
    WeakMap keyed by that object.

  2. Object.keys(fieldErrors).length > 0 allocates an array of keys only to find out
    whether the object is empty. Replaced with an allocation-free for...in check.

Both helpers live on TemplateService, so the express, koa and hapi services share them.
The formData branch, which calls Object.values(args).filter(...), uses the cached list
too.

The change is deliberately small: .map() and the switch are untouched.

Measurements

Node 26, best of 5 rounds, 300k iterations per measurement. One route with four parameters
({in:'path',dataType:'double'}, {in:'request'}, {in:'query',dataType:'string'},
{in:'query',dataType:'double'}):

ns/request
current 310
this PR 170

45% faster, on a route of typical shape.

Measured with the validator fix from
validatorjs/validator.js applied, because
that one dominates otherwise: isFloat compiles a RegExp per call and toFloat calls
isFloat again, which alone accounts for 85% of getValidatedArgs. Without that fix the
numbers here are the same in absolute terms but a much smaller share of the total.

Correctness

  • Same returned values, same ValidateError, same fieldErrors contents.
  • The WeakMap is keyed by the args object identity, so routes never share a list, and
    entries are collected with the route.
  • If a caller ever passes a fresh args object per request (a hand-written template, for
    instance) the cache simply never hits: behaviour is unchanged, cost is one failed
    WeakMap lookup.

A caveat on verification

I could not run tsoa's own test suite in my environment: yarn install cannot reach the
registry from here, and npm install in packages/runtime triggers the prepare script,
which needs lerna. Please run yarn test before merging. The change is mechanical and
the semantics are argued above, but I would not want that to stand in for the suite.

Possible follow-up

Once these allocations are gone, the remaining per-request cost is the switch (param.in)
plus ValidateParam's switch (property.dataType) — string dispatch over metadata that is
constant per parameter. Building one closure per parameter at registration time, instead of
interpreting the schema on each request, would be the structural fix. That is a much larger
change and I have not attempted it here.

getValidatedArgs did two things on every request that do not depend on the
request:

- Object.values(args) rebuilt the parameter list, although the generated routes
  create one args object per route at registration time and pass back the very
  same object every time. It is now derived once and cached in a WeakMap keyed
  by that object.
- Object.keys(fieldErrors).length > 0 allocated an array of keys only to find
  out whether the object was empty. Replaced with an allocation-free check.

Both helpers live on TemplateService so express, koa and hapi share them.

No behavioural change: same values, same ValidateError, same field errors.
iffa added a commit to iffa/tsoa that referenced this pull request Aug 24, 2026
The generated routes build one args object per route at registration and
hand back that same object on every request, so Object.values(args) was
rebuilt per request for a list that never changes.

Measured on a four-parameter route: 956 -> 932 ns per getValidatedArgs
call, best of five. Upstream lukeautry#1875
reports 45%, which does not reproduce; its second change, a for..in over
fieldErrors, is worth about 1ns and is left out.
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.

1 participant