Skip to content

Attribution costs ~12µs per query, and ~4× that under instrumentDrizzle #23

Open

Description

@Truta446

Now that npm run bench exists (#16), the cost of attribution is a number rather than a claim:

node v24 · 20,000 queries × 9 samples (median)

  detector disabled                    20 ns/query            —   the floor
  captureStack: false                1488 ns/query     +1468 ns   detection without attribution
  captureStack: true (default)      12476 ns/query    +12456 ns   stackDepth 30, the shipped default
  captureStack, stackDepth: 8        9869 ns/query     +9849 ns   a shallower walk
  instrumentDrizzle, 4-call chain   48579 ns/query    +48559 ns   one capture per chained call

Attribution is roughly 90% of what the detector costs, and instrumentDrizzle pays it four times over because the #12 fix captures on every chained call rather than once.

None of this is a crisis — it is development-only, and fifty queries in a request still costs under a millisecond. But an order of magnitude is worth attacking, and the numbers point at something specific.

Where the time goes

captureCallSite() reads new Error().stack, and that getter formats every frame into a string before we parse it back apart with a regex. V8 can hand over structured frames instead, via Error.prepareStackTrace.

I measured that, because it is the obvious first idea:

Error().stack (string) depth 30        10158 ns
prepareStackTrace (structured) d30      5752 ns
prepareStackTrace, read 1 app frame     6848 ns

About a third off, not the order of magnitude the trick is usually credited with. Worth having, and worth knowing the ceiling before anyone spends a weekend on it.

The catch, which is the real design problem

Error.prepareStackTrace is a global. source-map-support, Sentry and several test runners install their own, and that is how a TypeScript project gets original .ts line numbers out of compiled .js. Swapping it out for the duration of a capture — even synchronously, even restoring it afterwards — bypasses theirs, so the reported line would become the compiled position.

For this library that trade is close to unacceptable: pointing at dist/routes/orders.js:412 instead of src/routes/orders.ts:38 gives back in confusion more than it saves in microseconds. Any PR here has to say what it does about that. Detecting whether another prepareStackTrace is installed and falling back to the string path is one answer; there may be better ones.

Other angles, possibly better

  • Cache by call site. A loop hits the same line thousands of times. If a cheap key could identify "same place as last time" without a full walk, the capture could be skipped — but finding that key without walking the stack is the whole problem.
  • Reduce the chain captures. instrumentDrizzle captures per chained call because the last one before execution is the useful one. Capturing only the first and the last, or capturing lazily only when a finding is actually created, might get the same answer for a quarter of the cost. Note that a finding is created after the fact, when the stack is long gone — so "lazily" needs an idea.
  • Lower stackDepth. The table shows depth 8 saves ~20%. It is already the chain default (CHAIN_DEPTH); making it the global default would break attribution for deeply-wrapped drivers.

Acceptance

  • A measured improvement, shown with npm run bench before and after on the same machine
  • Source-mapped line numbers still correct in a TypeScript project — a test or a demonstration, not an assurance
  • No change to which line is reported in any existing test

Please post numbers before writing much code. It is entirely possible the honest outcome here is "measured, not worth the complexity", and that is a useful result too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is wanted

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions