diff --git a/src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/ConnectionPoolChurnRunner.cs b/src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/ConnectionPoolChurnRunner.cs
new file mode 100644
index 0000000000..a69015a702
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/ConnectionPoolChurnRunner.cs
@@ -0,0 +1,104 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+
+namespace Microsoft.Data.SqlClient.PerformanceTests
+{
+ ///
+ /// Measures the raw per-checkout overhead of the connection pool: a single thread
+ /// repeatedly opens and closes a connection against a warm pool, with no contention.
+ ///
+ /// With only one caller and a pre-warmed pool, every open is a pure pool checkout
+ /// (no physical connect, no waiting), so this benchmark isolates the CPU and
+ /// allocation cost of the pool's acquire/return path itself. It is the low-noise
+ /// counterpart to the parallel and contention runners and is the most sensitive
+ /// measure of per-operation allocations (watch the Allocated / Gen0 columns) — a key
+ /// concern for the new ChannelDbConnectionPool, which aims to avoid extra allocations
+ /// on the hot path (issue #3356).
+ ///
+ /// The pool implementation (legacy vs V2) is a process-level choice — see the remarks
+ /// on . Run twice (UseConnectionPoolV2 false
+ /// then true) to compare.
+ ///
+ /// Related issue: #3356
+ ///
+ public class ConnectionPoolChurnRunner : BaseRunner
+ {
+ public enum AsyncBehavior
+ {
+ Sync,
+ Async
+ }
+
+ ///
+ /// Whether the connection is opened synchronously or asynchronously.
+ ///
+ [ParamsAllValues]
+ public AsyncBehavior Async { get; set; }
+
+ ///
+ /// Number of sequential open/close operations performed per invocation. Kept high
+ /// so each measured invocation captures many pool checkouts, yielding a stable
+ /// per-operation cost.
+ ///
+ [Params(1000)]
+ public int OpsPerInvocation { get; set; }
+
+ private string _connectionString;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ Console.WriteLine(
+ "[ConnectionPoolChurnRunner] Pool implementation: " +
+ (s_config.UseConnectionPoolV2
+ ? "ChannelDbConnectionPool (V2)"
+ : "WaitHandleDbConnectionPool (legacy)"));
+
+ var builder = new SqlConnectionStringBuilder(s_config.ConnectionString)
+ {
+ Pooling = true,
+ MaxPoolSize = 100,
+ // Pre-warm a single connection so the very first checkout is served from
+ // the pool rather than establishing a physical connection.
+ MinPoolSize = 1
+ };
+ _connectionString = builder.ConnectionString;
+
+ // Force the pool to exist and hold at least one idle connection.
+ using var warm = new SqlConnection(_connectionString);
+ warm.Open();
+ warm.Close();
+ }
+
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ using var conn = new SqlConnection(_connectionString);
+ SqlConnection.ClearPool(conn);
+ }
+
+ [Benchmark]
+ public async Task RapidOpenCloseSingleThread()
+ {
+ for (int i = 0; i < OpsPerInvocation; i++)
+ {
+ using var conn = new SqlConnection(_connectionString);
+
+ if (Async is AsyncBehavior.Async)
+ {
+ await conn.OpenAsync();
+ }
+ else
+ {
+ conn.Open();
+ }
+ // Dispose returns the connection to the pool.
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/ConnectionPoolContentionRunner.cs b/src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/ConnectionPoolContentionRunner.cs
new file mode 100644
index 0000000000..384ebca71c
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/ConnectionPoolContentionRunner.cs
@@ -0,0 +1,153 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+
+namespace Microsoft.Data.SqlClient.PerformanceTests
+{
+ ///
+ /// Measures steady-state pool throughput under concurrent load — the hot path for
+ /// real applications such as web servers, where many workers repeatedly check a
+ /// connection out of a warm pool, do a small unit of work, and return it.
+ ///
+ /// This is where the legacy WaitHandleDbConnectionPool and the new
+ /// ChannelDbConnectionPool differ most (issue #3356):
+ ///
+ /// - When the pool is large enough to serve every worker, the benchmark isolates
+ /// checkout/return overhead and lock contention on the hot path.
+ /// - When the pool is smaller than the worker count ( less
+ /// than ), workers must wait for a connection to be
+ /// returned. This exercises the waiter wake path: the legacy pool parks on
+ /// WaitHandle.WaitAny (a blocking wait that consumes a threadpool thread for async
+ /// callers), while the new pool awaits the idle channel asynchronously. The
+ /// ThreadingDiagnoser's "Completed Work Items" and "Lock Contentions" columns make
+ /// the difference visible.
+ ///
+ /// The pool implementation (legacy vs V2) and SNI are process-level choices — see the
+ /// remarks on . Run twice (UseConnectionPoolV2
+ /// false then true) to compare.
+ ///
+ /// Related issues: #601, #979, #3356
+ ///
+ public class ConnectionPoolContentionRunner : BaseRunner
+ {
+ public enum AsyncBehavior
+ {
+ Sync,
+ Async
+ }
+
+ ///
+ /// Whether workers check connections out synchronously or asynchronously.
+ ///
+ [ParamsAllValues]
+ public AsyncBehavior Async { get; set; }
+
+ ///
+ /// Number of concurrent workers competing for pooled connections.
+ ///
+ [Params(50)]
+ public int Parallelism { get; set; }
+
+ ///
+ /// Maximum pool size. A value greater than or equal to
+ /// means no contention for physical connections; a smaller value forces workers to
+ /// wait for connections to be returned (pool exhaustion / back-pressure).
+ ///
+ [Params(50, 10)]
+ public int MaxPoolSize { get; set; }
+
+ ///
+ /// Number of open/query/close operations each worker performs per invocation.
+ ///
+ [Params(20)]
+ public int OpsPerWorker { get; set; }
+
+ private string _connectionString;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ Console.WriteLine(
+ "[ConnectionPoolContentionRunner] Pool implementation: " +
+ (s_config.UseConnectionPoolV2
+ ? "ChannelDbConnectionPool (V2)"
+ : "WaitHandleDbConnectionPool (legacy)"));
+
+ var builder = new SqlConnectionStringBuilder(s_config.ConnectionString)
+ {
+ Pooling = true,
+ MaxPoolSize = MaxPoolSize,
+ MinPoolSize = 0
+ };
+ _connectionString = builder.ConnectionString;
+ }
+
+ [IterationSetup]
+ public void IterationSetup()
+ {
+ // Warm the pool up to MaxPoolSize so the measured run reflects steady-state
+ // checkout/return rather than first-time physical connection establishment.
+ WarmPool(MaxPoolSize);
+ }
+
+ [IterationCleanup]
+ public void IterationCleanup()
+ {
+ using var conn = new SqlConnection(_connectionString);
+ SqlConnection.ClearPool(conn);
+ }
+
+ [Benchmark]
+ public async Task SteadyStateOpenQueryClose()
+ {
+ var tasks = new Task[Parallelism];
+ for (int i = 0; i < Parallelism; i++)
+ {
+ tasks[i] = Task.Run(async () =>
+ {
+ for (int op = 0; op < OpsPerWorker; op++)
+ {
+ using var conn = new SqlConnection(_connectionString);
+
+ if (Async is AsyncBehavior.Async)
+ {
+ await conn.OpenAsync();
+ using var cmd = conn.CreateCommand();
+ cmd.CommandText = "SELECT 1";
+ _ = await cmd.ExecuteScalarAsync();
+ }
+ else
+ {
+ conn.Open();
+ using var cmd = conn.CreateCommand();
+ cmd.CommandText = "SELECT 1";
+ _ = cmd.ExecuteScalar();
+ }
+ // Dispose returns the connection to the pool.
+ }
+ });
+ }
+
+ await Task.WhenAll(tasks);
+ }
+
+ private void WarmPool(int count)
+ {
+ var conns = new SqlConnection[count];
+ for (int i = 0; i < count; i++)
+ {
+ conns[i] = new SqlConnection(_connectionString);
+ conns[i].Open();
+ }
+ // Return them all to the pool so subsequent checkouts are served from idle.
+ for (int i = 0; i < count; i++)
+ {
+ conns[i].Close();
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/ConnectionPoolStressRunner.cs b/src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/ConnectionPoolStressRunner.cs
index c40b3f5b71..b5358e66a2 100644
--- a/src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/ConnectionPoolStressRunner.cs
+++ b/src/Microsoft.Data.SqlClient/tests/PerformanceTests/BenchmarkRunners/ConnectionPoolStressRunner.cs
@@ -3,281 +3,180 @@
// See the LICENSE file in the project root for more information.
using System;
-using System.Collections.Generic;
+using System.Collections.Concurrent;
+using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
namespace Microsoft.Data.SqlClient.PerformanceTests
{
///
- /// Stress-tests the connection pool with randomized parallel access patterns:
- /// - Massive concurrent open/close churn
- /// - Randomized hold durations simulating real workloads
- /// - Mixed sync/async callers competing for pooled connections
- /// - Connection reuse with interleaved queries
- /// - Pool exhaustion and recovery under pressure
+ /// Measures the throughput of opening connections in parallel — the core scenario
+ /// the channel-based connection pool (ChannelDbConnectionPool / UseConnectionPoolV2)
+ /// is designed to improve (see issue #3356). The design goals are parallel connection
+ /// opening, reduced thread contention, and lower managed threadpool pressure, so this
+ /// runner sweeps the dimensions that expose those characteristics:
+ ///
+ /// - : cold pool (physical connects dominate) vs. warm pool
+ /// (checkout throughput dominates). The warm-pool case is where the new pool shows
+ /// the largest wins.
+ /// - : sync vs. async callers. The new pool follows async best
+ /// practices, so async checkout is a primary target for improvement.
+ /// - : pooling on vs. off. Pooling off is the no-pool baseline.
+ /// - : how many connections are opened in parallel.
+ ///
+ /// The pool implementation itself (legacy WaitHandleDbConnectionPool vs. new
+ /// ChannelDbConnectionPool) is NOT a benchmark parameter. The UseConnectionPoolV2
+ /// AppContext switch is read and cached the first time a pool is created, and every
+ /// benchmark case runs in the same process under the in-process toolchain, so the
+ /// implementation cannot be toggled per iteration. It is selected once per process via
+ /// the "UseConnectionPoolV2" flag in runnerconfig.jsonc. To compare the two pools, run
+ /// the benchmark twice: once with UseConnectionPoolV2=false (legacy) and once with
+ /// UseConnectionPoolV2=true (new). Likewise, native vs. managed SNI is selected once
+ /// per process via the "UseManagedSniOnWindows" config flag (Windows-only).
+ ///
+ /// Pair this with the ThreadingDiagnoser (enabled in BenchmarkConfig) to observe
+ /// threadpool completed-work-item counts and lock contention across the two pools.
///
/// Related issues: #601, #979, #3356
///
public class ConnectionPoolStressRunner : BaseRunner
{
- private string _connectionString;
- private string _tableName;
+ public enum AsyncBehavior
+ {
+ Sync,
+ Async
+ }
///
- /// Number of concurrent tasks hammering the pool.
+ /// Whether the pool is pre-warmed (connections opened and returned) before the
+ /// measured run. A warm pool isolates checkout/return throughput; a cold pool
+ /// includes the cost of establishing physical connections.
///
- [Params(10, 20, 25)]
- public int Parallelism { get; set; }
+ [ParamsAllValues]
+ public bool PoolIsWarm { get; set; }
///
- /// Max pool size — controls how many physical connections the pool can hold.
- /// When Parallelism exceeds this, tasks must wait for a free connection.
+ /// Whether callers open connections synchronously or asynchronously.
///
- [Params(50, 100)]
- public int MaxPoolSize { get; set; }
+ [ParamsAllValues]
+ public AsyncBehavior Async { get; set; }
- [GlobalSetup]
- public void Setup()
- {
- _connectionString = s_config.ConnectionString +
- $";Pooling=True;Max Pool Size={MaxPoolSize};Min Pool Size=5;Connect Timeout=60";
+ ///
+ /// Whether connection pooling is enabled. When disabled, every open establishes a
+ /// new physical connection — the no-pool baseline.
+ ///
+ [Params(true, false)]
+ public bool Pooling { get; set; }
- // Create a small table for query workloads.
- // Hash the machine name instead of using it verbatim: hostnames can be long enough
- // to push the identifier past SQL Server's 128-character limit. Cast to uint rather
- // than using Math.Abs, which throws OverflowException when the hash is int.MinValue.
- string machineHash = ((uint)Environment.MachineName.GetHashCode()).ToString("x8");
- _tableName = $"[perf_PoolStress_{machineHash}_{Guid.NewGuid():N}]";
- using var conn = new SqlConnection(_connectionString);
- conn.Open();
- using var cmd = new SqlCommand(
- $"CREATE TABLE {_tableName} (Id INT IDENTITY PRIMARY KEY, Val INT)", conn);
- cmd.ExecuteNonQuery();
+ ///
+ /// Number of connections opened in parallel per measured invocation.
+ ///
+ [Params(100)]
+ public int NumConnectionsToOpen { get; set; }
- // Seed a few rows so SELECT queries return data
- using var insert = new SqlCommand(
- $"INSERT INTO {_tableName} (Val) VALUES (1),(2),(3),(4),(5)", conn);
- insert.ExecuteNonQuery();
- }
+ private string _connectionString;
+ private IProducerConsumerCollection _connections = new ConcurrentBag();
- [IterationCleanup]
- public void IterationCleanup()
+ [GlobalSetup]
+ public void Setup()
{
- SqlConnection.ClearAllPools();
+ // Report which pool implementation is active for this process so the results
+ // summary is self-describing (the implementation is a process-level choice,
+ // not a benchmark parameter — see the class remarks).
+ Console.WriteLine(
+ "[ConnectionPoolStressRunner] Pool implementation: " +
+ (s_config.UseConnectionPoolV2
+ ? "ChannelDbConnectionPool (V2)"
+ : "WaitHandleDbConnectionPool (legacy)"));
}
- [GlobalCleanup]
- public void Cleanup()
+ [IterationSetup]
+ public void IterationSetup()
{
- using var conn = new SqlConnection(_connectionString);
- conn.Open();
- using var cmd = new SqlCommand($"DROP TABLE IF EXISTS {_tableName}", conn);
- cmd.ExecuteNonQuery();
- conn.Close();
- SqlConnection.ClearAllPools();
- }
+ _connections = new ConcurrentBag();
- ///
- /// Pure open/close churn — every task opens a pooled connection, immediately closes it,
- /// and repeats. Measures raw pool checkout/return throughput under contention.
- ///
- [Benchmark]
- public async Task RapidFireOpenClose()
- {
- var tasks = new Task[Parallelism];
- for (int i = 0; i < Parallelism; i++)
+ // A fresh WorkstationID per iteration produces a distinct connection string,
+ // and therefore a fresh, isolated pool — so a "cold" iteration truly starts
+ // with an empty pool and iterations don't share pooled connections.
+ var builder = new SqlConnectionStringBuilder(s_config.ConnectionString)
{
- tasks[i] = Task.Run(async () =>
- {
- for (int j = 0; j < 20; j++)
- {
- using var conn = new SqlConnection(_connectionString);
- await conn.OpenAsync();
- // immediate return to pool
- }
- });
+ MaxPoolSize = 1000,
+ WorkstationID = Guid.NewGuid().ToString(),
+ Pooling = Pooling
+ };
+ _connectionString = builder.ConnectionString;
+
+ if (PoolIsWarm && Pooling)
+ {
+ // Open and return NumConnectionsToOpen connections so the measured run is
+ // served from a warm pool.
+ OpenConnectionsCore(_connectionString, NumConnectionsToOpen, returnToPool: true);
}
- await Task.WhenAll(tasks);
}
- ///
- /// Randomized hold — each task opens a connection, holds it for a random duration
- /// (0-50ms), optionally runs a query, then returns it. Simulates realistic mixed
- /// workloads where some connections are held briefly and others longer.
- ///
- [Benchmark]
- public async Task RandomizedHoldAndQuery()
+ [IterationCleanup]
+ public void Cleanup()
{
- var tasks = new Task[Parallelism];
- for (int i = 0; i < Parallelism; i++)
+ // Close every connection opened during setup and the measured run, then clear
+ // the pool so pooled connections are physically closed before the next iteration.
+ if (_connections.TryTake(out var first))
{
- int seed = i;
- tasks[i] = Task.Run(async () =>
- {
- var rng = new Random(seed);
- for (int j = 0; j < 10; j++)
- {
- using var conn = new SqlConnection(_connectionString);
- await conn.OpenAsync();
+ first.Close();
- // ~50% of the time, execute a lightweight query while holding the connection
- if (rng.Next(2) == 0)
- {
- using var cmd = new SqlCommand($"SELECT TOP 1 Val FROM {_tableName}", conn);
- _ = await cmd.ExecuteScalarAsync();
- }
+ while (_connections.TryTake(out var conn))
+ {
+ conn.Close();
+ }
- // Random hold time: 0-50ms
- int holdMs = rng.Next(51);
- if (holdMs > 0)
- {
- await Task.Delay(holdMs);
- }
- }
- });
+ SqlConnection.ClearPool(first);
}
- await Task.WhenAll(tasks);
}
///
- /// Mixed sync and async — half the tasks use sync Open/ExecuteReader,
- /// the other half use async. Stresses the pool lock paths that differ
- /// between sync and async checkout.
+ /// Opens connections in parallel and holds them
+ /// open (does not return them to the pool) so the measurement captures the cost of
+ /// acquiring that many connections concurrently.
///
[Benchmark]
- public async Task MixedSyncAsyncContention()
+ public int OpenConnectionsInParallel()
{
- var tasks = new Task[Parallelism];
- for (int i = 0; i < Parallelism; i++)
- {
- bool useAsync = i % 2 == 0;
- tasks[i] = Task.Run(async () =>
- {
- for (int j = 0; j < 10; j++)
- {
- using var conn = new SqlConnection(_connectionString);
- if (useAsync)
- {
- await conn.OpenAsync();
- using var cmd = new SqlCommand($"SELECT COUNT(*) FROM {_tableName}", conn);
- _ = await cmd.ExecuteScalarAsync();
- }
- else
- {
- conn.Open();
- using var cmd = new SqlCommand($"SELECT COUNT(*) FROM {_tableName}", conn);
- _ = cmd.ExecuteScalar();
- }
- }
- });
- }
- await Task.WhenAll(tasks);
+ return OpenConnectionsCore(_connectionString, NumConnectionsToOpen, returnToPool: false);
}
- ///
- /// Connection reuse with multiple commands — each task opens one connection and
- /// executes many sequential queries before returning it. Measures pool efficiency
- /// when connections are held for multi-step operations (like EF SaveChanges).
- ///
- [Benchmark]
- public async Task MultiCommandReuse()
+ private int OpenConnectionsCore(
+ string connectionString,
+ int numConnectionsToOpen,
+ bool returnToPool)
{
- var tasks = new Task[Parallelism];
- for (int i = 0; i < Parallelism; i++)
+ var tasks = new Task[numConnectionsToOpen];
+
+ for (int i = 0; i < numConnectionsToOpen; i++)
{
- int seed = i;
tasks[i] = Task.Run(async () =>
{
- var rng = new Random(seed);
- using var conn = new SqlConnection(_connectionString);
- await conn.OpenAsync();
+ var conn = new SqlConnection(connectionString);
- // Execute a burst of 5-15 commands on the same connection
- int commandCount = rng.Next(5, 16);
- for (int c = 0; c < commandCount; c++)
+ if (Async is AsyncBehavior.Async)
{
- using var cmd = new SqlCommand($"SELECT Val FROM {_tableName} WHERE Id = @id", conn);
- cmd.Parameters.AddWithValue("@id", rng.Next(1, 6));
- using var reader = await cmd.ExecuteReaderAsync();
- while (await reader.ReadAsync()) { }
+ await conn.OpenAsync(SqlConnectionOverrides.OpenWithoutRetry, CancellationToken.None);
+ }
+ else
+ {
+ conn.Open(SqlConnectionOverrides.OpenWithoutRetry);
}
- });
- }
- await Task.WhenAll(tasks);
- }
-
- ///
- /// Pool exhaustion and recovery — spawns more tasks than MaxPoolSize so some must
- /// wait. Measures how well the pool handles back-pressure when all connections are
- /// checked out and callers are queued.
- ///
- [Benchmark]
- public async Task PoolExhaustionRecovery()
- {
- // Ensure we exceed pool capacity
- int taskCount = Math.Max(Parallelism, MaxPoolSize * 2);
- var tasks = new Task[taskCount];
- for (int i = 0; i < taskCount; i++)
- {
- int seed = i;
- tasks[i] = Task.Run(async () =>
- {
- var rng = new Random(seed);
- using var conn = new SqlConnection(_connectionString);
- await conn.OpenAsync();
- // Hold the connection for 10-100ms to create pool pressure
- using var cmd = new SqlCommand($"SELECT TOP 1 Val FROM {_tableName}", conn);
- _ = await cmd.ExecuteScalarAsync();
+ _connections.TryAdd(conn);
- await Task.Delay(rng.Next(10, 101));
+ if (returnToPool)
+ {
+ conn.Close();
+ }
});
}
- await Task.WhenAll(tasks);
- }
- ///
- /// Bursty traffic pattern — sends waves of connections with pauses between bursts,
- /// simulating real web server traffic patterns where requests cluster.
- ///
- [Benchmark]
- public async Task BurstyTrafficPattern()
- {
- int burstCount = 5;
- int tasksPerBurst = Parallelism / burstCount;
- if (tasksPerBurst < 1)
- {
- tasksPerBurst = 1;
- }
-
- for (int burst = 0; burst < burstCount; burst++)
- {
- var tasks = new Task[tasksPerBurst];
- for (int i = 0; i < tasksPerBurst; i++)
- {
- int seed = burst * tasksPerBurst + i;
- tasks[i] = Task.Run(async () =>
- {
- var rng = new Random(seed);
- using var conn = new SqlConnection(_connectionString);
- await conn.OpenAsync();
-
- // Each connection in the burst does 1-5 queries
- int queryCount = rng.Next(1, 6);
- for (int q = 0; q < queryCount; q++)
- {
- using var cmd = new SqlCommand($"SELECT Val FROM {_tableName}", conn);
- using var reader = await cmd.ExecuteReaderAsync();
- while (await reader.ReadAsync()) { }
- }
- });
- }
- await Task.WhenAll(tasks);
-
- // Brief pause between bursts (simulates request clustering)
- await Task.Delay(5);
- }
+ Task.WaitAll(tasks);
+ return tasks.Length;
}
}
}
diff --git a/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Config/Config.cs b/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Config/Config.cs
index 7a93f65090..2bbd1f1c23 100644
--- a/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Config/Config.cs
+++ b/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Config/Config.cs
@@ -12,6 +12,20 @@ public class Config
public string ConnectionString;
public bool UseManagedSniOnWindows;
public bool UseOptimizedAsyncBehaviour;
+
+ ///
+ /// When true, selects the new channel-based connection pool
+ /// (ChannelDbConnectionPool) by enabling the
+ /// Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2 AppContext switch.
+ /// When false (the default), the legacy WaitHandleDbConnectionPool is used.
+ ///
+ /// This is a process-level setting: the switch is read and cached the first time
+ /// a pool is created, so it cannot be toggled per benchmark iteration. To compare
+ /// the two implementations, run the benchmark once with this flag false and once
+ /// with it true.
+ ///
+ public bool UseConnectionPoolV2;
+
public bool WaitForProfiler;
public bool UseNativeMemoryAndETWProfiler;
public Benchmarks Benchmarks;
@@ -54,6 +68,8 @@ public class Benchmarks
public RunnerJob JsonVsVarcharReadRunnerConfig;
public RunnerJob BeginTransactionRunnerConfig;
public RunnerJob ConnectionPoolStressRunnerConfig;
+ public RunnerJob ConnectionPoolContentionRunnerConfig;
+ public RunnerJob ConnectionPoolChurnRunnerConfig;
}
public class RunnerJob
diff --git a/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Program.cs b/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Program.cs
index 8f64c7448a..0a1cb1e6d1 100644
--- a/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Program.cs
+++ b/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Program.cs
@@ -33,6 +33,8 @@ public Program()
Run_JsonVsVarcharReadBenchmark();
Run_BeginTransactionBenchmark();
Run_ConnectionPoolStressBenchmark();
+ Run_ConnectionPoolContentionBenchmark();
+ Run_ConnectionPoolChurnBenchmark();
// TODOs:
// Prepared/Regular Parameterized queries
@@ -48,6 +50,14 @@ private void SetupConfigurations()
AppContext.SetSwitch("Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows", true);
}
+ // If the config file specifies to use the new channel-based connection pool,
+ // enable the UseConnectionPoolV2 AppContext switch. This must be set before any
+ // connection is opened because the switch is read and cached when a pool is first
+ // created; it cannot be changed later in the process lifetime.
+ AppContext.SetSwitch(
+ "Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2",
+ _config.UseConnectionPoolV2);
+
// If the config file specifies to use optimized async behavior,
// enable packet multiplexing feature and other optimizations in SqlClient
// by setting the appropriate AppContext switches.
@@ -186,6 +196,22 @@ private void Run_ConnectionPoolStressBenchmark()
}
}
+ private void Run_ConnectionPoolContentionBenchmark()
+ {
+ if (_config.Benchmarks.ConnectionPoolContentionRunnerConfig?.Enabled == true)
+ {
+ BenchmarkRunner.Run(BenchmarkConfig.s_instance(_config.Benchmarks.ConnectionPoolContentionRunnerConfig));
+ }
+ }
+
+ private void Run_ConnectionPoolChurnBenchmark()
+ {
+ if (_config.Benchmarks.ConnectionPoolChurnRunnerConfig?.Enabled == true)
+ {
+ BenchmarkRunner.Run(BenchmarkConfig.s_instance(_config.Benchmarks.ConnectionPoolChurnRunnerConfig));
+ }
+ }
+
///
/// The main entry point for the performance tests program.
///
diff --git a/src/Microsoft.Data.SqlClient/tests/PerformanceTests/runnerconfig.jsonc b/src/Microsoft.Data.SqlClient/tests/PerformanceTests/runnerconfig.jsonc
index 34d7a511aa..5777db9652 100644
--- a/src/Microsoft.Data.SqlClient/tests/PerformanceTests/runnerconfig.jsonc
+++ b/src/Microsoft.Data.SqlClient/tests/PerformanceTests/runnerconfig.jsonc
@@ -2,6 +2,12 @@
"ConnectionString": "Server=tcp:localhost; Integrated Security=true; Initial Catalog=sqlclient-perf-db;",
// Enable this flag to enable managed SNI on Windows.
"UseManagedSniOnWindows": false,
+ // Enable this flag to select the new channel-based connection pool
+ // (ChannelDbConnectionPool) instead of the legacy WaitHandleDbConnectionPool.
+ // This is a process-level setting (the pool implementation switch is cached the
+ // first time a pool is created), so comparing the two pools requires two runs:
+ // once with this flag false (legacy) and once true (new). See issue #3356.
+ "UseConnectionPoolV2": false,
// Enable this flag to enable optimized async behavior in SqlClient.
// This is useful for benchmarking packet multiplexing and async performance improvements.
"UseOptimizedAsyncBehaviour": true,
@@ -120,6 +126,22 @@
"InvocationCount": 1,
"WarmupCount": 1,
"RowCount": 0
+ },
+ "ConnectionPoolContentionRunnerConfig": {
+ "Enabled": true,
+ "LaunchCount": 1,
+ "IterationCount": 15,
+ "InvocationCount": 1,
+ "WarmupCount": 1,
+ "RowCount": 0
+ },
+ "ConnectionPoolChurnRunnerConfig": {
+ "Enabled": true,
+ "LaunchCount": 1,
+ "IterationCount": 15,
+ "InvocationCount": 1,
+ "WarmupCount": 1,
+ "RowCount": 0
}
}
}