perf: parse incoming messages with a push based token parser - #1782
perf: parse incoming messages with a push based token parser#1782arthurschreiber wants to merge 1 commit into
Conversation
fa403d6 to
b5e711a
Compare
ReviewThis is a well-engineered change with a clear rationale and solid benchmarking. The core idea — a Correctness / robustness
Test coverage
Nits
OverallSolid, carefully-reasoned perf work with good measurements and a design that keeps the "retry from scratch" vs. "resumable with committed state" tradeoff explicit per token type. My comments above are mostly "please double check" rather than "this is broken" — I did not find a concrete correctness bug in the row/NBC-row/PLP/return-value resumption logic or in the new backpressure plumbing. This review was generated by Claude Code (Sonnet 5) via automated PR review. |
Incoming message data used to flow through a `Message` PassThrough stream into an async generator that awaited the message's async iterator for every chunk and, inside token readers, whenever a token was incomplete. This cost dozens of promises per response and delayed delivery by several event loop turns. Incoming messages are now `IncomingMessage` objects: still readable streams for consumers that want raw data (the TLS handshake, the prelogin response, tests), but with a `MessageSink` that receives each chunk synchronously as the `IncomingMessageStream` frames it. The token stream parser attaches itself as that sink and parses whatever complete tokens the buffered data holds right away. The stream parser is push based: readers are synchronous and throw `NotEnoughDataError` when a token is incomplete, after which the parser rewinds to the last committed position and waits for the amount of data the reader asked for before trying again. Rows, NBC rows and return values keep their progress on the parser and commit after every column and every PLP chunk, so large values spanning many packets are parsed linearly. All other tokens are retried from their start. The async variants of the column metadata, row, NBC row, return value and PLP readers, as well as the unused async `metadataParse`, are gone. Backpressure is preserved: a paused request makes the sink's `push` return `false`, which stops the incoming message stream from framing further packets until the request is resumed. Data written while the sink is stalled is queued behind data buffered before the sink attached, so ordering is kept. `Parser.parseTokens` is kept as an adapter for the token parser tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DZCoWULmkdZ9hXnVoBAsj5
b5e711a to
e97bda9
Compare
ReviewThis is a well-structured perf change — the Performance:
|
ReviewI traced through the buffer/backpressure state machine in detail ( What I verified as correct
Suggestion: add unit tests for multi-packet resumable stateThis is the one gap I'd actually want closed before/after merging. The whole point of this PR is that rows, NBC rows, return values, and PLP (
The PR description says these were exercised manually against a real SQL Server, which is good, but that's not repeatable in CI. Given this is precisely the new logic (vs. the mechanical Minor / non-blocking observations
Performance & securityThe benchmark numbers in the description (removing dozens of promises per response, and the async_hooks counts) are consistent with the structural change (synchronous push-based parsing replacing an async generator awaited per chunk/per incomplete token), and match what I'd expect from this refactor. I don't see any new security-relevant surface here — this is purely internal parsing of already-trusted-transport (TDS/TLS) data, with no new external input trust boundary, and bounds-checking is still centralized in the same Nice work — this is a substantial, well-reasoned rewrite of a hot path, and the design choices (committing progress per-column/per-PLP-chunk rather than per-token, bypassing the |
Summary
Incoming message data used to flow through a
MessagePassThrough into an async generator that awaited the message's async iterator for every chunk and, inside the token readers, whenever a token was incomplete. A single-row response cost ~80 promises and a 3-packet response ~60; delivery lagged the socket by several event loop turns. This replaces that with a synchronous, push based parser.IncomingMessage(new) replacesMessagefor incoming data. It is still aReadablefor consumers that want raw bytes (TLS handshake, prelogin, tests), but aMessageSinkcan be attached that receives each chunk synchronously asIncomingMessageStreamframes it.TokenStreamParserattaches itself as that sink and parses whatever complete tokens are buffered right away. Pausing a request makespushreturnfalse, which stops framing further packets until the request is resumed, so backpressure to the socket is preserved. Data that arrives while the sink is stalled is queued behind data buffered before it attached.StreamParseris push based:push(chunk)thenparseNext()per token. Readers are synchronous and throwNotEnoughDataErrorfor an incomplete token; the parser rewinds to the last committed position and waits for the byte count the reader asked for before retrying. Rows, NBC rows and return values keep their progress on the parser (RowState,ReturnValueState) and commit after every column and every PLP chunk, so a value spanning many packets is parsed linearly. All other tokens are retried from their start.metadataParse.Parser.parseTokensremains as an adapter for the token parser tests.Measurements
Local SQL Server 2025, loopback, Node 22, default 4 KB packets, warm connection (
niterations). Ops/s formasterand for this branch, measured in the same session.Async resources per request (async_hooks): a 3-packet response goes from 61 promises to 9, and per-request cost now rises smoothly with response size instead of stepping up at the packet boundary.
Testing
npm test: 538 passing.npm run test-integrationagainst a local SQL Server 2025: all passing except two pre-existing environment specific tests (a connect timeout test whose target address this sandbox rejects immediately, and the TDS 8 strict TLS test).npm run lintclean.execSql, error tokens, PLP and NULL (NBC) values, output parameters, prepared statements and multi result set responses.IncomingMessagecover sink attach ordering, stalling and end delivery.🤖 Generated with Claude Code
https://claude.ai/code/session_01DZCoWULmkdZ9hXnVoBAsj5