Introduce new performance benchmark runners#4447
Conversation
There was a problem hiding this comment.
Pull request overview
Adds new BenchmarkDotNet runners to the Microsoft.Data.SqlClient.PerformanceTests harness to measure previously reported performance hot spots (large async reads, MARS overhead, parallel OpenAsync, cancellation token overhead, sequential XML reads, JSON vs VARCHAR reads, transaction latency, and connection pool stress), along with configuration and wiring updates to run them via runnerconfig.json.
Changes:
- Registers 8 new benchmark runners and gates each by a per-runner
RunnerJob.Enabledconfig. - Extends the perf test config model (
Config) and defaultrunnerconfig.jsonwith new runner entries plus new top-level flags. - Updates BenchmarkDotNet
ManualConfigoptions/environment for more consistent perf runs (e.g., dont overwrite results, joined summary, server GC).
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Microsoft.Data.SqlClient/tests/PerformanceTests/runnerconfig.json | Adds new top-level flags and runner job entries; enables new runners by default and disables older ones. |
| src/Microsoft.Data.SqlClient/tests/PerformanceTests/Program.cs | Loads config, applies AppContext switches / optional profiler wait, and invokes each runner when enabled. |
| src/Microsoft.Data.SqlClient/tests/PerformanceTests/Config/Config.cs | Adds new config fields and per-runner RunnerJob entries. |
| src/Microsoft.Data.SqlClient/tests/PerformanceTests/Config/BenchmarkConfig.cs | Updates BDN config options and sets server GC env var; joins summaries. |
| src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/AsyncLargeDataReadRunner.cs | New runner to compare sync vs async large VARBINARY streaming reads. |
| src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/MarsOverheadRunner.cs | New runner comparing query execution with MARS enabled/disabled (sync + async). |
| src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/ParallelAsyncConnectionRunner.cs | New runner opening many connections concurrently with pooling on/off. |
| src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/CancellationTokenReadAsyncRunner.cs | New runner measuring ReadAsync iteration overhead with/without CancellationToken. |
| src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/SequentialXmlReadRunner.cs | New runner targeting sequential XML read scaling across payload sizes. |
| src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/JsonVsVarcharReadRunner.cs | New runner comparing JSON vs VARCHAR read performance (disabled by default). |
| src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/BeginTransactionRunner.cs | New runner comparing transaction vs no-transaction latency (sync + async). |
| src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/ConnectionPoolStressRunner.cs | New stress runner for pooled open/close churn, mixed sync/async contention, and exhaustion/recovery. |
apoorvdeshmukh
left a comment
There was a problem hiding this comment.
These benchmarks will be very helpful!
Wondering if it would be possible to establish a baseline for native memory consumption when running these benchmarks and monitor memory growth if it drifts too much from the established baseline? This may be beyond the current scope of the PR but worth looking into.
Added optionally, could you try and confirm as well? |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| using var conn = new SqlConnection(_connectionString); | ||
| conn.Open(); | ||
|
|
||
| using var createCmd = new SqlCommand( |
There was a problem hiding this comment.
We should be able to use the RAII types here. Can be added in the future.
There was a problem hiding this comment.
Yes, this is just bringing over test cases to public repo that were missing, to give a kickstart to unix async perf work. All goodness is welcome to enhance the test suite :)
Just don't change too much on configuration front as I will be setting up pipelines soon for running benchmarks internally :)
|
I'm in favor of merging these to give us easy access to the existing tests that were previously locked away in ADO. We can improve these and add on to them as we go. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4447 +/- ##
==========================================
- Coverage 65.83% 63.95% -1.89%
==========================================
Files 287 284 -3
Lines 43763 66786 +23023
==========================================
+ Hits 28812 42712 +13900
- Misses 14951 24074 +9123
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
- AsyncLargeDataReadRunner: seed VARBINARY(MAX) row via CRYPT_GEN_RANDOM T-SQL instead of allocating a client-side byte[] and shipping it over the wire, and add [Params] for ReadBufferBytes so we can observe how the client read buffer size interacts with payload size. - CancellationTokenReadAsyncRunner: move CancellationTokenSource allocation to [IterationSetup] and disposal to [IterationCleanup] so only the per-row ReadAsync(CancellationToken) overhead is measured. - ConnectionPoolStressRunner: scale the RapidFireOpenClose per-task iteration count with MaxPoolSize/Parallelism so total checkouts stay proportional to pool capacity, and drop the unused System.Collections.Generic import. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 62d8a5ff-8bd4-4780-9715-748ea0d950aa
| int burstCount = 5; | ||
| int tasksPerBurst = Parallelism / burstCount; | ||
| if (tasksPerBurst < 1) | ||
| { | ||
| tasksPerBurst = 1; | ||
| } |
There was a problem hiding this comment.
Fixed in 8787fce — BurstyTrafficPattern now spins up Parallelism concurrent tasks per burst (instead of Parallelism / burstCount), so the parameter drives actual observed concurrency.
| while (reader.Read()) | ||
| { | ||
| _ = reader.GetString(0); | ||
| } |
There was a problem hiding this comment.
Good catch — switched the JSON sync path to reader.GetSqlJson(0).Value in 8787fce so we exercise the SqlJson accessor rather than the shared string path. VARCHAR still uses GetString(0).
| while (await reader.ReadAsync()) | ||
| { | ||
| _ = await reader.GetFieldValueAsync<string>(0); | ||
| } |
There was a problem hiding this comment.
Same treatment on the async path in 8787fce: JSON uses reader.GetSqlJson(0).Value (JSON handling cost), VARCHAR keeps GetFieldValueAsync<string>(0).
- JsonVsVarcharReadRunner: use SqlDataReader.GetSqlJson(0).Value for the JSON case (sync and async) so the benchmark exercises the SqlJson accessor path implicated in #3499 instead of the shared GetString / GetFieldValueAsync<string> path used for VARCHAR. - ConnectionPoolStressRunner.BurstyTrafficPattern: spin up Parallelism concurrent tasks per burst instead of Parallelism/burstCount, so the configured Parallelism parameter actually drives observed concurrency. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 62d8a5ff-8bd4-4780-9715-748ea0d950aa
| var behavior = UseSequentialAccess ? CommandBehavior.SequentialAccess : CommandBehavior.Default; | ||
|
|
||
| using var conn = new SqlConnection(_connectionString); | ||
| conn.Open(); | ||
| using var cmd = new SqlCommand(_query, conn); | ||
| using var reader = cmd.ExecuteReader(behavior); | ||
| while (reader.Read()) | ||
| { | ||
| _ = reader.GetString(0); | ||
| } | ||
| } |
Summary
Adds 8
BenchmarkDotNetrunners toMicrosoft.Data.SqlClient.PerformanceTestscovering previously-reported hot spots, plus config/wiring updates to support them.New runners
VARBINARY(MAX)(1–20 MB)OpenAsync(10/50/100), pooling on/offCancellationTokenoverhead inReadAsyncSequentialAccessXML reads at increasing sizes (O(N²) check)JSONvsVARCHAR(MAX)reads (disabled by default, needs SQL 2025+)BeginTransaction/Asynclatency vs baselineSupporting changes
RunnerJob.EnabledRunnerJobper runnerDontOverwriteResults,JoinSummary,COMPlus_gcServer=1Validation
Ran all new runners locally (.NET 9.0 Release, Apple M3 Pro, Docker
azure-sql-edge) — all completed successfully.Checklist