Skip to content

Heap-allocate the PLP drain scratch buffer - #226

Open
Saurabh Singh (saurabh500) wants to merge 2 commits into
mainfrom
dev/saurabh/fix-plp-drain-scratch-buffer
Open

Heap-allocate the PLP drain scratch buffer#226
Saurabh Singh (saurabh500) wants to merge 2 commits into
mainfrom
dev/saurabh/fix-plp-drain-scratch-buffer

Conversation

@saurabh500

@saurabh500 Saurabh Singh (saurabh500) commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Description

drain_active_plp held 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 on drain_active_plp recording 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):

before after
per-row future 8248 B 80 B
per-row cost 136.1 ns 15.8 ns (8.6×)

Measured in the driver itself: next_row_cursor 8520 B → 928 B, read_row_column 8464 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, so mssql-js, mssql-py-core, mssql-tds-cli and mssql-odbc all benefit. No API change, no growth in steady-state memory.

Regression guard

The issue floated a size_of_val assertion to keep this from silently regressing. It came out clean, so it's included: row_fetch_futures_stay_small builds each of the four hot-path futures via the existing create_test_client() helper and asserts each stays under 4096 B. Constructing an async 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_cursor promised 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

Related Issues

Fixes #225

Checklist

  • cargo bfmt passes
  • cargo bclippy passes
  • cargo btest passes — see note below
  • New/changed functionality has tests
  • Public API changes are documented — no API change; stale next_row_cursor doc corrected

Validation was run via .\scripts\bfmt.ps1 and .\scripts\bclippy.ps1 (both also cover mssql-py-core, which is excluded from the workspace) and both are green.

Tests were scoped with cargo nextest run --workspace --lib --no-fail-fast because integration tests require a .env that isn't present locally. Result: 2216 passed, 7 failed — the 7 are the pre-existing expired-certificate fixture tests in certificate_validator and win_tls::validate. Confirmed identical failures on a stashed, unmodified tree, so they are unrelated to this change.

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 AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread mssql-tds/src/connection/tds_client.rs
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>
@github-actions

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

100%

🎯 Overall Coverage

91.2%

📦 Project: mssql-tds + mssql-odbc + mssql-py-core
ℹ️ Note: diff coverage is reported, not enforced.


Diff Coverage

Diff: main...HEAD, staged and unstaged changes

  • mssql-tds/src/connection/tds_client.rs (100%)

Summary

  • Total: 17 lines
  • Missing: 0 lines
  • Coverage: 100%

🔗 Quick Links

View Azure DevOps Build · Coverage Report

@saurabh500
Saurabh Singh (saurabh500) marked this pull request as ready for review August 12, 2026 02:08
@saurabh500
Saurabh Singh (saurabh500) requested a review from a team as a code owner August 12, 2026 02:08
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.

Perf: 8 KiB stack scratch in drain_active_plp inflates every row-fetch future to ~8.5 KB (8.6x per-row cost)

2 participants