Skip to content

Implement performance benchmarks for Always Encrypted scenarios - #4502

Open
edwardneal wants to merge 9 commits into
dotnet:mainfrom
edwardneal:benchmarks/ae
Open

Implement performance benchmarks for Always Encrypted scenarios#4502
edwardneal wants to merge 9 commits into
dotnet:mainfrom
edwardneal:benchmarks/ae

Conversation

@edwardneal

Copy link
Copy Markdown
Contributor

Description

This builds on a couple of PRs:

In this PR, I merge a few benchmarks (DataTypeReaderRunner and DataTypeReaderAsyncRunner) together, then add an Always Encrypted benchmark on top of them. In the process, I also noticed that the benchmark methodology for these was flawed. I've corrected this, and deliberately changed the benchmark name to provide a clean start for any data analytics.

I've also worked with AsyncLargeDataReadRunner. This wasn't truly async, and it missed a few data pathways which read large binary blobs. Although I've changed this benchmark name too, I've done so on the basis that the performance tests pipeline is new enough that we can afford to make the name consistent without losing a critical amount of historical data. I'm happy to change it back if that's not the case.

Underpinning this are two new RAII types: ColumnMasterKey and ColumnEncryptionKey. There's not much to see here, besides an integration point with the existing certificate fixtures.

This isn't the end of the AE benchmarks - SqlBulkCopy is important too. This is very tightly bound to some custom logic to create/drop database objects though. A future PR can remove this custom logic and then start to make the bulk copy benchmarks AE-agnostic.

Issues

No covering issue, but this continues work flagged as a TODO in code.

Testing

All of these benchmarks continue to run.

* Replace X hardcoded test methods with a single ParamsSource.
* Merge DataTypeReaderRunner and its async variant together.
* Implement base class, and two child classes - one for AlwaysEncrypted, one for plaintext.
* Move the connection open and the table setup logic out of the benchmark code.
This was actually running both sync and async tests.
* Implement base class, and two child classes - one for AlwaysEncrypted, one for plaintext.
* Expand test to cover CommandBehavior.Default and SequentialAccess.
* Include GetFieldValue and GetFieldValueAsync.
* Move repeated allocation of the buffer out of the main ReadLargeDataSync_GetBytes benchmark loop.
* File-scoped namespaces.
* Remove references to removed benchmarks.
@edwardneal
edwardneal requested a review from a team as a code owner August 4, 2026 19:41
@github-project-automation github-project-automation Bot moved this to To triage in SqlClient Board Aug 4, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Comment on lines -38 to -41
Table t = Table.Build(nameof(SqlCommandRunner))
.AddColumn(new Column(type))
.CreateTable(sqlConnection)
.InsertBulkRows(s_rowCount, sqlConnection);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the flaw in the DataTypeReader[Async]Runner methodology. RunBenchmarkAsync doesn't just cover the cost of reading a result set of s_rowCount rows - it also covers the cost of using SqlBulkCopy to write these rows to the table.

Comment on lines -52 to -65
using var insertCmd = new SqlCommand(
$@"INSERT INTO {_tableName} (Data)
SELECT SUBSTRING(
CONVERT(
varbinary(max),
REPLICATE(
CONVERT(varchar(max), CRYPT_GEN_RANDOM(8000), 2),
(@dataSizeBytes + 7999) / 8000
),
2
),
1,
@dataSizeBytes
);", conn);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This had to change, since Always Encrypted requires that values come from the client. The memory allocations aren't included in the benchmark though.

using var conn = new SqlConnection(_connectionString);
conn.Open();
using var cmd = new SqlCommand($"SELECT Data FROM {_tableName}", conn);
using var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always Encrypted doesn't support using CommandBehavior.SequentialAccess, or calling GetBytes or GetStream.

Besides this, there are genuine performance variations between GetBytes/GetStream with a CommandBehavior of SequentialAccess vs Default. In plaintext scenarios, we can test both methods in both scenarios. Always Encrypted scenarios will just use GetFieldValue[Async] with CommandBehavior.Default.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair approach.

@cheenamalhotra cheenamalhotra added this to the 7.1.0-preview3 milestone Aug 7, 2026
@cheenamalhotra cheenamalhotra added Performance 📈 Issues that are targeted to performance improvements. Area\Tests Issues that are targeted to tests or test projects labels Aug 7, 2026
cheenamalhotra and others added 2 commits August 7, 2026 14:14
PerformanceTests now references TestCommon. In Package mode the perf
pipeline pins a released MDS baseline via -p:MdsPackageVersion, but
TestCommon ignored that property and resolved the CPM-managed
in-development version instead. That version is not published, so
restore failed with NU1102 and the mismatch produced an NU1605
downgrade error.

Apply the same conditional VersionOverride pattern used by
PerformanceTests, including the 7.1.0-preview1.26124.5 transitive
dependency pins.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 82c3df83-8f5b-487a-b57a-31d5acb40abe
The large data read benchmarks failed with:

  System.InvalidOperationException: Benchmark
  Plaintext.ReadLargeDataSync_GetBytes ... takes too long to run.
  Prefer to use out-of-process toolchains for long-running benchmarks.

BenchmarkDotNet's in-process executor aborts a benchmark case after a
hard-coded 5 minute default. A single case here reads up to 20 MB per
operation across 20 iterations, plus the extra iterations that
MemoryDiagnoser and ThreadingDiagnoser each add, so it exceeds that.

Switching to an out-of-process toolchain (BenchmarkDotNet's own
suggestion) is not viable for this suite. The AppContext switches set in
Program.SetupConfigurations - managed SNI, connection pool V2, optimized
async behaviour - only apply to the process running Main. An
out-of-process toolchain spawns a generated host where they revert to
their defaults, so the benchmarks would silently measure the wrong code
paths.

Instead, add two optional per-benchmark knobs to runnerconfig.jsonc and
apply them to the two large data read runners:

  TimeoutMinutes - overrides the in-process execution timeout (30
    minutes here); omitted elsewhere, so other benchmarks keep the
    BenchmarkDotNet default.
  RunStrategy - selects the BenchmarkDotNet RunStrategy. The large data
    read runners use Monitoring, which skips the harness-overhead
    measurement Throughput performs; that measurement is meaningless
    when a single operation takes seconds.

Their IterationCount also drops from 20 to 5, since network-bound 20 MB
reads gain little statistical value from the extra iterations.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 82c3df83-8f5b-487a-b57a-31d5acb40abe
@cheenamalhotra

Copy link
Copy Markdown
Member

@edwardneal

Please consider changes in edwardneal#1 as they unblock running perf benchmarks with new changes.

@cheenamalhotra cheenamalhotra moved this from To triage to In review in SqlClient Board Aug 8, 2026
Fix perf-pipeline restore and in-process benchmark timeout for AE benchmarks
This was never used by Always Encrypted.
@edwardneal

Copy link
Copy Markdown
Contributor Author

Thanks @cheenamalhotra, I've just checked and merged; all of the new scenarios continue to benchmark. They also confirm the performance issue I'm chasing down with my other PRs: every AE-protected MB we read from SQL Server results in 5 MB of allocations within the driver.

Looking more widely, there seems to be quite a bit of performance pipeline work which is required because the benchmarks are running in-process - is this solely to make sure that the AppContext switches are being set correctly? I've got a local benchmarking harness (gist) which does this while running out-of-process and might simplify the overall work.

@apoorvdeshmukh

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).

@cheenamalhotra

Copy link
Copy Markdown
Member

Looking more widely, there seems to be quite a bit of performance pipeline work which is required because the benchmarks are running in-process - is this solely to make sure that the AppContext switches are being set correctly? I've got a local benchmarking harness (gist) which does this while running out-of-process and might simplify the overall work.

Yes, we are aware of that too and are starting to run benchmarks internally. The goal is to prevent perf regressions and also confirm app context switches do continue to resolve perf issues as promised and not regress other paths.

We wouldn't be running multiple benchmarks at once, as the goal is to run single test in isolation against a fixed baseline. But I'll look into out-of-process approach to see if it can be useful for developer testing. Thanks for sharing :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area\Tests Issues that are targeted to tests or test projects Performance 📈 Issues that are targeted to performance improvements.

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

4 participants