Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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 <see cref="ConnectionPoolStressRunner"/>. Run twice (UseConnectionPoolV2 false
/// then true) to compare.
///
/// Related issue: #3356
/// </summary>
public class ConnectionPoolChurnRunner : BaseRunner
{
public enum AsyncBehavior
{
Sync,
Async
}

/// <summary>
/// Whether the connection is opened synchronously or asynchronously.
/// </summary>
[ParamsAllValues]
public AsyncBehavior Async { get; set; }

/// <summary>
/// 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.
/// </summary>
[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.
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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 (<see cref="MaxPoolSize"/> less
/// than <see cref="Parallelism"/>), 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 <see cref="ConnectionPoolStressRunner"/>. Run twice (UseConnectionPoolV2
/// false then true) to compare.
///
/// Related issues: #601, #979, #3356
/// </summary>
public class ConnectionPoolContentionRunner : BaseRunner
{
public enum AsyncBehavior
{
Sync,
Async
}

/// <summary>
/// Whether workers check connections out synchronously or asynchronously.
/// </summary>
[ParamsAllValues]
public AsyncBehavior Async { get; set; }

/// <summary>
/// Number of concurrent workers competing for pooled connections.
/// </summary>
[Params(50)]
public int Parallelism { get; set; }

/// <summary>
/// Maximum pool size. A value greater than or equal to <see cref="Parallelism"/>
/// means no contention for physical connections; a smaller value forces workers to
/// wait for connections to be returned (pool exhaustion / back-pressure).
/// </summary>
[Params(50, 10)]
public int MaxPoolSize { get; set; }

/// <summary>
/// Number of open/query/close operations each worker performs per invocation.
/// </summary>
[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();
}
Comment on lines +146 to +150
}
}
}
Loading
Loading