Heap-allocate the PLP drain scratch buffer - #226
Open
Saurabh Singh (saurabh500) wants to merge 2 commits into
Open
Heap-allocate the PLP drain scratch buffer#226Saurabh Singh (saurabh500) wants to merge 2 commits into
Saurabh Singh (saurabh500) wants to merge 2 commits into
Conversation
The 8 KiB scratch array in drain_active_plp is live across an await, so rustc stores it inline in the generated future. That size propagates into every caller in the await chain, making the row-fetch hot path construct and move ~8.5 KB of state for a cleanup path that rarely runs. Fixes #225 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot started reviewing on behalf of
Saurabh Singh (saurabh500)
August 12, 2026 00:44
View session
Contributor
There was a problem hiding this comment.
Pull request overview
Heap-allocates the PLP drain buffer to reduce row-fetch future sizes and hot-path overhead.
Changes:
- Replaces the 8 KiB stack buffer with a
Vec. - Documents the allocation rationale.
- Adds regression tests for future sizes.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The public doc promised an allocation-free drain, which no longer holds when the drain abandons a partially read PLP column. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
📊 Code Coverage Report
Diff CoverageDiff: main...HEAD, staged and unstaged changes
Summary
🔗 Quick Links |
Saurabh Singh (saurabh500)
marked this pull request as ready for review
August 12, 2026 02:08
Saurabh Singh (saurabh500)
enabled auto-merge (squash)
August 12, 2026 02:08
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
drain_active_plpheld an 8 KiB scratch array on the stack across an.await, so rustc stored it inline in the generated future and that size propagated into every caller in the await chain. The row-fetch hot path therefore paid to construct and move ~8.5 KB of state per row for a cleanup path that only runs when a caller abandons a partially read PLP column.The fix is one line —
[0u8; 8192]→vec![0u8; 8192]— plus a doc comment ondrain_active_plprecording why the buffer is heap-allocated, so it doesn't get "optimized" back into a stack array later.#225 has the full explanation, the standalone criterion repro, and the measured numbers. Please read it there rather than expecting the analysis restated here.
Measured impact
Per-row future size and cost, from the standalone repro in the issue (5000 rows per sample, drain branch never taken):
Measured in the driver itself:
next_row_cursor8520 B → 928 B,read_row_column8464 B → 432 B. Fetch cost fell 20% on the ODBC path and 33% on the TDS column path.The change is entirely inside
mssql-tds, somssql-js,mssql-py-core,mssql-tds-cliandmssql-odbcall benefit. No API change, no growth in steady-state memory.Regression guard
The issue floated a
size_of_valassertion to keep this from silently regressing. It came out clean, so it's included:row_fetch_futures_stay_smallbuilds each of the four hot-path futures via the existingcreate_test_client()helper and asserts each stays under 4096 B. Constructing anasync fn's future runs none of its body, so the futures are built and dropped unpolled — no I/O, no mock traffic.Measured sizes under the test profile are 928 / 408 / 1208 / 960 B, giving ~3.4× headroom while still catching an 8 KiB regression. Verified it works by temporarily reverting the one-liner: the test fails with
next_row_cursor future is 8480 B, expected <= 4096 B.Doc correction
Per Copilot review feedback, the public doc on
next_row_cursorpromised an "allocation-free" drain, which no longer holds once the PLP drain path allocates. It now states that the drain is allocation-free unless it abandons a partially read PLP column, which allocates a single scratch buffer.Scope
mssql-tds/src/connection/transport/win_tls/stream.rs:387has an identical-looking[0u8; 8192]and is deliberately left alone — it sits in a synchronouspoll_readreturningPollwith no await spanning it, so it is an ordinary stack local and is correct as written. Perf: 8 KiB stack scratch in drain_active_plp inflates every row-fetch future to ~8.5 KB (8.6x per-row cost) #225 calls this out explicitly;drain_active_plpwas the only live instance.Related Issues
Fixes #225
Checklist
cargo bfmtpassescargo bclippypassescargo btestpasses — see note belownext_row_cursordoc correctedValidation was run via
.\scripts\bfmt.ps1and.\scripts\bclippy.ps1(both also covermssql-py-core, which is excluded from the workspace) and both are green.Tests were scoped with
cargo nextest run --workspace --lib --no-fail-fastbecause integration tests require a.envthat isn't present locally. Result: 2216 passed, 7 failed — the 7 are the pre-existing expired-certificate fixture tests incertificate_validatorandwin_tls::validate. Confirmed identical failures on a stashed, unmodified tree, so they are unrelated to this change.