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 } } }