Skip to content

perf: make nested serialization 5x faster by skipping stack captures - #10

Merged
thetutlage merged 4 commits into
2.xfrom
perf/lazy-debugging-error
Oct 15, 2026
Merged

thetutlage merged 4 commits into
2.xfrom
perf/lazy-debugging-error

Conversation

@RomainLanz

@RomainLanz RomainLanz commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

transform() and paginate() create a RuntimeException on each call. This error is only for one case: Item.resolve() throws it when the resource is undefined, and its stack then points to the transformer that called transform(). But each Exception from @poppinss/exception captures the stack two times (the Error constructor, then Error.captureStackTrace), which costs ~3 µs per call. With one call per row and per relation, this was ~72% of the CPU on a paginated response with nested relations.

Fix

  • transform() creates the error only when the value is undefined. It creates it at the same call site, so the stack still points to X.transform → Y.toObject.
  • Collection and paginate() do not create an error. Collection never read it.
  • debuggingError is now optional in the exported Item and Collection constructors, so existing calls continue to work. If an Item has no error, Item.resolve() creates one when the resource is undefined. Before, this case threw TypeError: Cannot set properties of undefined.

Benchmark

Node 26.10, tsdown build. One copy of build/ before the fix and one after. Each run is a separate process, before and after interleaved, 3 rounds. Each value is the median of the 3 round medians, in µs/op (lower is better).

Workload Before After Change
Paginated, 20 users with profile, posts and comments (840 transformers) 1481.5 275.7 5.4× faster
Single item with relations (42 transformers) 77.6 14.6 5.3× faster
Flat list of 500 rows, no relations 591.6 591.1 no change (one transform() call)

Verification

  • The serialized JSON is byte-identical before and after for the 3 workloads.
  • For ProfileTransformer.transform(undefined) in UserTransformer.toObject, the stack trace is the same before and after: ProfileTransformer.transform → UserTransformer.toObject.
  • npm run typecheck passes.
  • Tests: 61/61 pass.

npm run lint fails with 30 Prettier errors in src/types.ts. This file is not changed by this PR, and the same 30 errors occur on 2.x. Because npm test runs lint first, the CI test jobs also stop before the tests run, as they do on 2.x (e2d6339).

Not covered by tests: the existing tests check the "Cannot transform undefined value" error message, but no test checks its stack trace or the fallback for an Item created directly without an error. I checked both with a script against the built output.

transform() and paginate() created a RuntimeException on every call to
get a useful stack trace if the resource turned out to be undefined.
Capturing that stack costs a few microseconds per call, and with one
call per row and per relation it dominated the CPU profile of nested
responses.

The error is only thrown by Item.resolve() for an undefined resource,
and Collection never reads it. Item now receives the error only when
the value is undefined, still created at the transform() call site so
the stack keeps pointing to the calling transformer. Collection and
paginate() no longer receive one.

debuggingError becomes optional in the Item and Collection
constructors. Item.resolve() creates the error itself when none was
given.

On a paginated response with 840 nested transformers, a serialization
went from ~1480 µs to ~276 µs (Node 26.10, tsdown build).
@RomainLanz
RomainLanz force-pushed the perf/lazy-debugging-error branch from cbe8e14 to 3ab90b3 Compare October 15, 2026 07:05
Cover nested items, items inside collections and paginators, nested collections, variants, and items created without a debugging error.
Define classes and data inside each test and assert on the stack with an inline try/catch, matching the other spec files.
Cover user -> posts -> comments -> author -> profile, where the author is missing a profile four levels down.
@thetutlage
thetutlage merged commit 613cb06 into 2.x Oct 15, 2026
10 checks passed
@thetutlage

Copy link
Copy Markdown
Member

Thanks 👍🏽

@RomainLanz
RomainLanz deleted the perf/lazy-debugging-error branch October 16, 2026 11: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