POC: reduce per-row cost in the TDS row decode path - #238
POC: reduce per-row cost in the TDS row decode path#238Srinivasan S A (NivasSA) wants to merge 1 commit into
Conversation
Proof of concept, not for merge. Shows five changes and what each is worth. Measured on 1,499,000 rows of 48 columns. Wall clock 7.45s -> 3.92s. CPU on the reading process 7.35s -> 2.58s. CPU inside mssql-tds 4.69s -> 0.97s. The server stopped blocking on ASYNC_NETWORK_IO, from 4.54s to none. The five changes: 1. Resolve each column's type, precision, scale and collation once per result set into a DecodeOp plan, instead of rederiving it from ColumnMetadata on every row. 2. Add a synchronous row path that decodes a whole ROW or NBCROW token from one borrow of the reader's buffer. It runs two passes, the first to measure and bounds check and the second to decode without checks. It declines when the row crosses a packet boundary, and the async path handles that case. 3. Remove the two tracing callsites that fired once per row. This was done to measure their cost, not as a proposal. 4. Offer decoded strings and binaries to the RowWriter as borrows, so a consumer that does not need to own them avoids a copy. 5. Let the RowWriter supply the destination for a PLP value whose length is declared up front, so large values are read into consumer storage directly. Known gaps are listed in POC_README.md. The largest is that NOT NULL columns arrive as fixed length TDS types, which are not in the decode plan, so a table of NOT NULL columns gets none of this.
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
|
Srinivasan S A (@NivasSA) please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.
Contributor License AgreementContribution License AgreementThis Contribution License Agreement (“Agreement”) is agreed to by the party signing below (“You”),
|
There was a problem hiding this comment.
Pull request overview
Proof of concept for reducing per-row TDS decoding overhead through precomputed decode plans, buffered row decoding, borrowed values, and direct PLP destinations.
Changes:
- Adds a two-pass buffered ROW/NBCROW decoder.
- Extends
RowWriterwith borrowed-value and destination-buffer APIs. - Adds non-blocking packet reads and an unboxed row API.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
POC_README.md |
Documents goals, benchmarks, and limitations. |
mssql-tds/src/token/tokens.rs |
Adds cached per-column decode plans. |
mssql-tds/src/token/parsers/row_parser.rs |
Initializes decode-plan state in tests. |
mssql-tds/src/token/parsers/nbcrow_parser.rs |
Initializes decode-plan state in tests. |
mssql-tds/src/token/parsers/colmetadata_parser.rs |
Initializes decode plans during metadata parsing. |
mssql-tds/src/io/token_stream.rs |
Implements buffered two-pass row decoding. |
mssql-tds/src/io/packet_reader.rs |
Adds synchronous buffered-read APIs. |
mssql-tds/src/datatypes/sql_string.rs |
Supports borrowed UTF-8 decoding. |
mssql-tds/src/datatypes/row_writer.rs |
Adds borrowed writes and PLP destinations. |
mssql-tds/src/datatypes/decoder.rs |
Integrates buffered reads and direct PLP storage. |
mssql-tds/src/connection/transport/network_transport.rs |
Implements buffered reads for network transport. |
mssql-tds/src/connection/tds_client.rs |
Removes row tracing and adds an unboxed API. |
mssql-tds/src/connection/metadata_retriever.rs |
Initializes decode-plan state in tests. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| The server stopped waiting on us too. Before the change it spent 4.54 seconds | ||
| blocked on `ASYNC_NETWORK_IO`. After the change it spent none. | ||
|
|
||
| The unit tests pass. Run them with `cargo test -p mssql-tds --lib`. |
| if let Some(destination) = writer.string_destination(col, length, encoding_type) { | ||
| let result = Self::read_known_plp_bytes(reader, destination).await; | ||
| writer.finish_string_destination(col, result.is_ok()); |
| fn try_receive_row_into_buffered<R: TdsPacketReader + Send + Sync>( | ||
| reader: &mut R, | ||
| context: &ParserContext, | ||
| writer: &mut (dyn RowWriter + Send), | ||
| ) -> TdsResult<Option<RowReadResult>> { |
| if let Some(destination) = writer.bytes_destination(col, length) { | ||
| let result = Self::read_known_plp_bytes(reader, destination).await; | ||
| writer.finish_bytes_destination(col, result.is_ok()); |
| | Measure | Before | After | | ||
| | --- | ---: | ---: | | ||
| | Wall clock | 7.45 s | 3.92 s | | ||
| | CPU on the reading process | 7.35 s | 2.58 s | | ||
| | CPU inside `mssql-tds` | 4.69 s | 0.97 s | |
| pub async fn next_row_into_unboxed( | ||
| &mut self, | ||
| writer: &mut (dyn RowWriter + Send), | ||
| ) -> TdsResult<bool> { |
Proof of concept, not for merge. Shows five changes and what each is worth.
Measured on 1,499,000 rows of 48 columns. Wall clock 7.45s -> 3.92s. CPU on the reading process 7.35s -> 2.58s. CPU inside mssql-tds 4.69s -> 0.97s. The server stopped blocking on ASYNC_NETWORK_IO, from 4.54s to none.
The five changes:
Resolve each column's type, precision, scale and collation once per result set into a DecodeOp plan, instead of rederiving it from ColumnMetadata on every row.
Add a synchronous row path that decodes a whole ROW or NBCROW token from one borrow of the reader's buffer. It runs two passes, the first to measure and bounds check and the second to decode without checks. It declines when the row crosses a packet boundary, and the async path handles that case.
Remove the two tracing callsites that fired once per row. This was done to measure their cost, not as a proposal.
Offer decoded strings and binaries to the RowWriter as borrows, so a consumer that does not need to own them avoids a copy.
Let the RowWriter supply the destination for a PLP value whose length is declared up front, so large values are read into consumer storage directly.
Known gaps are listed in POC_README.md. The largest is that NOT NULL columns arrive as fixed length TDS types, which are not in the decode plan, so a table of NOT NULL columns gets none of this.
Description
Related Issues
Checklist
cargo bfmtpassescargo bclippypassescargo btestpasses