Skip to content

Commit b016409

Browse files
Add benchmarking app to probe memory leak with encrypt modes
1 parent be95ca2 commit b016409

7 files changed

Lines changed: 814 additions & 0 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using BenchmarkDotNet.Attributes;
2+
using BenchmarkDotNet.Columns;
3+
using BenchmarkDotNet.Configs;
4+
using BenchmarkDotNet.Jobs;
5+
using BenchmarkDotNet.Reports;
6+
using Microsoft.Data.SqlClient;
7+
8+
namespace StrictEncryptMemoryBenchmark;
9+
10+
[MemoryDiagnoser(displayGenColumns: true)]
11+
[Config(typeof(Config))]
12+
public class ConnectionBenchmarks
13+
{
14+
private class Config : ManualConfig
15+
{
16+
public Config()
17+
{
18+
AddJob(Job.Default
19+
.WithWarmupCount(3)
20+
.WithIterationCount(20)
21+
.WithId("SqlClientMemory"));
22+
23+
AddColumn(StatisticColumn.AllStatistics);
24+
AddDiagnoser(new NativeMemoryDiagnoser());
25+
WithSummaryStyle(SummaryStyle.Default.WithMaxParameterColumnWidth(50));
26+
}
27+
}
28+
29+
[Params("Strict", "Mandatory", "Optional")]
30+
public string EncryptMode { get; set; } = "Strict";
31+
32+
[Params(true, false)]
33+
public bool Pooling { get; set; }
34+
35+
private string _connectionString = null!;
36+
37+
[GlobalSetup]
38+
public void Setup()
39+
{
40+
var server = Environment.GetEnvironmentVariable("BENCHMARK_SERVER")!;
41+
var database = Environment.GetEnvironmentVariable("BENCHMARK_DATABASE") ?? "master";
42+
var user = Environment.GetEnvironmentVariable("BENCHMARK_USER")!;
43+
var password = Environment.GetEnvironmentVariable("BENCHMARK_PASSWORD")!;
44+
var serverCert = Environment.GetEnvironmentVariable("BENCHMARK_SERVER_CERTIFICATE");
45+
var hostInCert = Environment.GetEnvironmentVariable("BENCHMARK_HOSTNAME_IN_CERTIFICATE");
46+
47+
var builder = new SqlConnectionStringBuilder
48+
{
49+
DataSource = server,
50+
InitialCatalog = database,
51+
UserID = user,
52+
Password = password,
53+
Encrypt = EncryptMode switch
54+
{
55+
"Strict" => SqlConnectionEncryptOption.Strict,
56+
"Mandatory" => SqlConnectionEncryptOption.Mandatory,
57+
"Optional" => SqlConnectionEncryptOption.Optional,
58+
_ => SqlConnectionEncryptOption.Strict
59+
},
60+
TrustServerCertificate = EncryptMode != "Strict", // Not supported with Strict
61+
Pooling = Pooling,
62+
ConnectTimeout = 30,
63+
Authentication = SqlAuthenticationMethod.SqlPassword,
64+
};
65+
66+
if (!string.IsNullOrEmpty(serverCert))
67+
{
68+
builder.ServerCertificate = serverCert;
69+
}
70+
71+
if (!string.IsNullOrEmpty(hostInCert))
72+
{
73+
builder.HostNameInCertificate = hostInCert;
74+
}
75+
76+
_connectionString = builder.ConnectionString;
77+
78+
// Validate connectivity
79+
using var conn = new SqlConnection(_connectionString);
80+
conn.Open();
81+
}
82+
83+
[Benchmark(Description = "Open+Close connection")]
84+
public void OpenClose()
85+
{
86+
using var connection = new SqlConnection(_connectionString);
87+
connection.Open();
88+
}
89+
90+
[Benchmark(Description = "Open+Query+Close connection")]
91+
public object? OpenQueryClose()
92+
{
93+
using var connection = new SqlConnection(_connectionString);
94+
connection.Open();
95+
using var cmd = connection.CreateCommand();
96+
cmd.CommandText = "SELECT 1";
97+
return cmd.ExecuteScalar();
98+
}
99+
100+
[GlobalCleanup]
101+
public void Cleanup()
102+
{
103+
SqlConnection.ClearAllPools();
104+
}
105+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using BenchmarkDotNet.Attributes;
2+
using BenchmarkDotNet.Configs;
3+
using BenchmarkDotNet.Jobs;
4+
using Microsoft.Data.SqlClient;
5+
6+
namespace StrictEncryptMemoryBenchmark;
7+
8+
/// <summary>
9+
/// A stress-oriented benchmark that opens many connections per invocation.
10+
/// This amplifies per-connection native memory overhead (SChannel TLS session cache)
11+
/// and makes leaks visible. Uses InvocationCount=1 so each iteration is a single
12+
/// batch of N connections — process memory grows cumulatively across iterations.
13+
/// </summary>
14+
[MemoryDiagnoser(displayGenColumns: true)]
15+
[Config(typeof(Config))]
16+
public class ConnectionStressBenchmarks
17+
{
18+
private class Config : ManualConfig
19+
{
20+
public Config()
21+
{
22+
AddJob(Job.Default
23+
.WithWarmupCount(1)
24+
.WithIterationCount(15)
25+
.WithInvocationCount(1)
26+
.WithUnrollFactor(1)
27+
.WithId("NativeMemoryStress"));
28+
29+
AddDiagnoser(new NativeMemoryDiagnoser());
30+
}
31+
}
32+
33+
[Params("Strict", "Mandatory")]
34+
public string EncryptMode { get; set; } = "Strict";
35+
36+
[Params(1000, 5000)]
37+
public int ConnectionCount { get; set; }
38+
39+
private string _connectionString = null!;
40+
41+
[GlobalSetup]
42+
public void Setup()
43+
{
44+
var server = Environment.GetEnvironmentVariable("BENCHMARK_SERVER")!;
45+
var database = Environment.GetEnvironmentVariable("BENCHMARK_DATABASE") ?? "master";
46+
var user = Environment.GetEnvironmentVariable("BENCHMARK_USER")!;
47+
var password = Environment.GetEnvironmentVariable("BENCHMARK_PASSWORD")!;
48+
var serverCert = Environment.GetEnvironmentVariable("BENCHMARK_SERVER_CERTIFICATE");
49+
var hostInCert = Environment.GetEnvironmentVariable("BENCHMARK_HOSTNAME_IN_CERTIFICATE");
50+
51+
var builder = new SqlConnectionStringBuilder
52+
{
53+
DataSource = server,
54+
InitialCatalog = database,
55+
UserID = user,
56+
Password = password,
57+
Encrypt = EncryptMode switch
58+
{
59+
"Strict" => SqlConnectionEncryptOption.Strict,
60+
"Mandatory" => SqlConnectionEncryptOption.Mandatory,
61+
"Optional" => SqlConnectionEncryptOption.Optional,
62+
_ => SqlConnectionEncryptOption.Strict
63+
},
64+
TrustServerCertificate = EncryptMode != "Strict", // Not supported with Strict
65+
Pooling = false, // No pooling to force full TLS handshake each time
66+
ConnectTimeout = 30,
67+
Authentication = SqlAuthenticationMethod.SqlPassword,
68+
};
69+
70+
if (!string.IsNullOrEmpty(serverCert))
71+
{
72+
builder.ServerCertificate = serverCert;
73+
}
74+
75+
if (!string.IsNullOrEmpty(hostInCert))
76+
{
77+
builder.HostNameInCertificate = hostInCert;
78+
}
79+
80+
_connectionString = builder.ConnectionString;
81+
82+
// Validate connectivity
83+
using var conn = new SqlConnection(_connectionString);
84+
conn.Open();
85+
}
86+
87+
[Benchmark(Description = "Batch Open+Query+Close (no pooling)")]
88+
public void BatchOpenQueryClose()
89+
{
90+
for (int i = 0; i < ConnectionCount; i++)
91+
{
92+
using var connection = new SqlConnection(_connectionString);
93+
connection.Open();
94+
using var cmd = connection.CreateCommand();
95+
cmd.CommandText = "SELECT 1";
96+
cmd.ExecuteScalar();
97+
}
98+
}
99+
100+
[Benchmark(Description = "Batch Async Open+Query+Close (no pooling)")]
101+
public async Task BatchOpenQueryCloseAsync()
102+
{
103+
for (int i = 0; i < ConnectionCount; i++)
104+
{
105+
await using var connection = new SqlConnection(_connectionString);
106+
await connection.OpenAsync();
107+
await using var cmd = connection.CreateCommand();
108+
cmd.CommandText = "SELECT 1";
109+
await cmd.ExecuteScalarAsync();
110+
}
111+
}
112+
113+
[GlobalCleanup]
114+
public void Cleanup()
115+
{
116+
SqlConnection.ClearAllPools();
117+
}
118+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using System.Diagnostics;
2+
using BenchmarkDotNet.Analysers;
3+
using BenchmarkDotNet.Columns;
4+
using BenchmarkDotNet.Diagnosers;
5+
using BenchmarkDotNet.Engines;
6+
using BenchmarkDotNet.Exporters;
7+
using BenchmarkDotNet.Loggers;
8+
using BenchmarkDotNet.Reports;
9+
using BenchmarkDotNet.Running;
10+
using BenchmarkDotNet.Validators;
11+
12+
namespace StrictEncryptMemoryBenchmark;
13+
14+
/// <summary>
15+
/// Custom BenchmarkDotNet diagnoser that captures native/private memory metrics.
16+
/// This is critical for detecting the SChannel TLS session ticket cache leak,
17+
/// which manifests in native heap (not managed GC heap).
18+
/// </summary>
19+
public class NativeMemoryDiagnoser : IDiagnoser
20+
{
21+
private long _privateBytesBefore;
22+
private long _privateBytesAfter;
23+
private long _workingSetBefore;
24+
private long _workingSetAfter;
25+
26+
public IEnumerable<string> Ids => ["NativeMemory"];
27+
28+
public IEnumerable<IExporter> Exporters => [];
29+
30+
public IEnumerable<IAnalyser> Analysers => [];
31+
32+
public RunMode GetRunMode(BenchmarkCase benchmarkCase) => RunMode.NoOverhead;
33+
34+
public bool RequiresBlockingAcknowledgments(BenchmarkCase benchmarkCase) => false;
35+
36+
public void Handle(HostSignal signal, DiagnoserActionParameters parameters)
37+
{
38+
var process = Process.GetCurrentProcess();
39+
40+
switch (signal)
41+
{
42+
case HostSignal.BeforeActualRun:
43+
process.Refresh();
44+
_privateBytesBefore = process.PrivateMemorySize64;
45+
_workingSetBefore = process.WorkingSet64;
46+
break;
47+
48+
case HostSignal.AfterActualRun:
49+
process.Refresh();
50+
_privateBytesAfter = process.PrivateMemorySize64;
51+
_workingSetAfter = process.WorkingSet64;
52+
break;
53+
}
54+
}
55+
56+
public IEnumerable<Metric> ProcessResults(DiagnoserResults results)
57+
{
58+
yield return new Metric(
59+
new MetricDescriptor("NativeMemGrowth", "Native Mem Growth", "MB"),
60+
(_privateBytesAfter - _privateBytesBefore) / (1024.0 * 1024.0));
61+
62+
yield return new Metric(
63+
new MetricDescriptor("PrivateBytesAfter", "Private Bytes After", "MB"),
64+
_privateBytesAfter / (1024.0 * 1024.0));
65+
66+
yield return new Metric(
67+
new MetricDescriptor("WorkingSetAfter", "Working Set After", "MB"),
68+
_workingSetAfter / (1024.0 * 1024.0));
69+
}
70+
71+
public IEnumerable<ValidationError> Validate(ValidationParameters validationParameters) => [];
72+
73+
public void DisplayResults(ILogger logger)
74+
{
75+
logger.WriteLineInfo($"Native Memory Growth: {(_privateBytesAfter - _privateBytesBefore) / (1024.0 * 1024.0):F2} MB");
76+
logger.WriteLineInfo($"Private Bytes After: {_privateBytesAfter / (1024.0 * 1024.0):F2} MB");
77+
}
78+
79+
private class MetricDescriptor : IMetricDescriptor
80+
{
81+
public MetricDescriptor(string id, string displayName, string unit)
82+
{
83+
Id = id;
84+
DisplayName = displayName;
85+
Unit = unit;
86+
}
87+
88+
public string Id { get; }
89+
public string DisplayName { get; }
90+
public string Unit { get; }
91+
public string Legend => $"{DisplayName} ({Unit})";
92+
public string NumberFormat => "F2";
93+
public UnitType UnitType => UnitType.Size;
94+
public bool TheGreaterTheBetter => false;
95+
public int PriorityInCategory => 0;
96+
public bool GetIsAvailable(Metric metric) => true;
97+
}
98+
}

0 commit comments

Comments
 (0)