This is an automated email from the ASF dual-hosted git repository.
curth pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-adbc.git
The following commit(s) were added to refs/heads/main by this push:
new 68a2d61b5 feat(csharp/Benchmarks): Add CloudFetch E2E performance
benchmark (#3660)
68a2d61b5 is described below
commit 68a2d61b5e44ed364505f01419893fd2c40b06c6
Author: eric-wang-1990 <[email protected]>
AuthorDate: Mon Nov 3 09:39:51 2025 -0800
feat(csharp/Benchmarks): Add CloudFetch E2E performance benchmark (#3660)
## Summary
Adds comprehensive E2E benchmark for Databricks CloudFetch to measure
real-world performance with actual cluster and configurable queries.
## Changes
- **CloudFetchRealE2EBenchmark**: Real E2E benchmark against actual
Databricks cluster
- Configurable via JSON file (DATABRICKS_TEST_CONFIG_FILE environment
variable)
- Power BI consumption simulation with batch-size proportional delays
(5ms per 10K rows)
- Peak memory tracking using Process.WorkingSet64
- Custom peak memory column in results table with console output
reference
- **CloudFetchBenchmarkRunner**: Standalone runner for CloudFetch
benchmarks
- Simplified to only run real E2E benchmark
- Optimized iteration counts (1 warmup + 3 actual) for faster execution
- Hides confusing Error/StdDev columns from summary table
- **README.md**: Documentation for running and understanding the
benchmarks
## Configuration
Benchmark requires `DATABRICKS_TEST_CONFIG_FILE` environment variable
pointing to JSON config:
```json
{
"uri":
"https://your-workspace.cloud.databricks.com/sql/1.0/warehouses/xxx",
"token": "dapi...",
"query": "select * from main.tpcds_sf1_delta.catalog_sales"
}
```
## Run Command
```bash
export DATABRICKS_TEST_CONFIG_FILE=/path/to/config.json
cd csharp
dotnet run -c Release --project Benchmarks/Benchmarks.csproj --framework
net8.0 CloudFetchBenchmarkRunner -- --filter "*"
```
## Example Output
**Console output during benchmark execution:**
```
Loaded config from: /path/to/databricks-config.json
Hostname: adb-6436897454825492.12.azuredatabricks.net
HTTP Path: /sql/1.0/warehouses/2f03dd43e35e2aa0
Query: select * from main.tpcds_sf1_delta.catalog_sales
Benchmark will test CloudFetch with 5ms per 10K rows read delay
// Warmup
CloudFetch E2E [Delay=5ms/10K rows] - Peak memory: 272.97 MB
WorkloadWarmup 1: 1 op, 11566591709.00 ns, 11.5666 s/op
// Actual iterations
CloudFetch E2E [Delay=5ms/10K rows] - Peak memory: 249.11 MB
WorkloadResult 1: 1 op, 8752445353.00 ns, 8.7524 s/op
CloudFetch E2E [Delay=5ms/10K rows] - Peak memory: 261.95 MB
WorkloadResult 2: 1 op, 9794630771.00 ns, 9.7946 s/op
CloudFetch E2E [Delay=5ms/10K rows] - Peak memory: 258.39 MB
WorkloadResult 3: 1 op, 9017280271.00 ns, 9.0173 s/op
```
**Summary table:**
```
BenchmarkDotNet v0.15.4, macOS Sequoia 15.7.1 (24G231) [Darwin 24.6.0]
Apple M1 Max, 1 CPU, 10 logical and 10 physical cores
.NET SDK 8.0.407
[Host] : .NET 8.0.19 (8.0.19, 8.0.1925.36514), Arm64 RyuJIT armv8.0-a
| Method | ReadDelayMs | Mean | Min | Max | Median |
Peak Memory (MB) | Gen0 | Gen1 | Gen2 | Allocated |
|------------------ |------------
|--------:|--------:|--------:|--------:|--------------------------:|-----------:|-----------:|-----------:|----------:|
| ExecuteLargeQuery | 5 | 9.19 s | 8.75 s | 9.79 s | 9.02 s |
See previous console output | 28000.0000 | 28000.0000 | 28000.0000 | 1.78 GB |
```
**Key Metrics:**
- **E2E Time**: 8.75-9.79 seconds (includes query execution, CloudFetch
downloads, LZ4 decompression, batch consumption)
- **Peak Memory**: 249-262 MB (tracked via Process.WorkingSet64, printed
in console)
- **Total Allocated**: 1.78 GB managed memory
- **GC Collections**: 28K Gen0/Gen1/Gen2 collections
## Test Plan
- [x] Built successfully
- [x] Verified benchmark runs with real Databricks cluster
- [x] Confirmed peak memory tracking works
- [x] Validated Power BI simulation delays are proportional to batch
size
- [x] Checked results table formatting
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Sreekanth Vadigi <[email protected]>
Co-authored-by: Sreekanth Vadigi <[email protected]>
Co-authored-by: Jade Wang <[email protected]>
Co-authored-by: Claude <[email protected]>
---
csharp/Benchmarks/Benchmarks.csproj | 7 +
csharp/Benchmarks/CloudFetchBenchmarkRunner.cs | 44 ++++
.../Databricks/CloudFetchRealE2EBenchmark.cs | 286 +++++++++++++++++++++
csharp/Benchmarks/Databricks/README.md | 142 ++++++++++
4 files changed, 479 insertions(+)
diff --git a/csharp/Benchmarks/Benchmarks.csproj
b/csharp/Benchmarks/Benchmarks.csproj
index e06731aa1..9a5b7d931 100644
--- a/csharp/Benchmarks/Benchmarks.csproj
+++ b/csharp/Benchmarks/Benchmarks.csproj
@@ -6,6 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ProcessArchitecture>$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture.ToString().ToLowerInvariant())</ProcessArchitecture>
+
<StartupObject>Apache.Arrow.Adbc.Benchmarks.CloudFetchBenchmarkRunner</StartupObject>
</PropertyGroup>
<ItemGroup>
@@ -13,9 +14,15 @@
<PackageReference Include="DuckDB.NET.Bindings.Full"
GeneratePathProperty="true" />
</ItemGroup>
+ <ItemGroup>
+ <PackageReference Include="K4os.Compression.LZ4" />
+ <PackageReference Include="K4os.Compression.LZ4.Streams" />
+ </ItemGroup>
+
<ItemGroup>
<ProjectReference
Include="..\src\Apache.Arrow.Adbc\Apache.Arrow.Adbc.csproj" />
<ProjectReference Include="..\src\Client\Apache.Arrow.Adbc.Client.csproj"
/>
+ <ProjectReference
Include="..\src\Drivers\Databricks\Apache.Arrow.Adbc.Drivers.Databricks.csproj"
/>
<ProjectReference
Include="..\test\Apache.Arrow.Adbc.Tests\Apache.Arrow.Adbc.Tests.csproj" />
</ItemGroup>
diff --git a/csharp/Benchmarks/CloudFetchBenchmarkRunner.cs
b/csharp/Benchmarks/CloudFetchBenchmarkRunner.cs
new file mode 100644
index 000000000..13c7ad515
--- /dev/null
+++ b/csharp/Benchmarks/CloudFetchBenchmarkRunner.cs
@@ -0,0 +1,44 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You under the Apache License, Version 2.0
+* (the "License"); you may not use this file except in compliance with
+* the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+using Apache.Arrow.Adbc.Benchmarks.Databricks;
+using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Running;
+
+namespace Apache.Arrow.Adbc.Benchmarks
+{
+ /// <summary>
+ /// Standalone runner for CloudFetch benchmarks only.
+ /// Usage: dotnet run -c Release --framework net8.0
CloudFetchBenchmarkRunner
+ /// </summary>
+ public class CloudFetchBenchmarkRunner
+ {
+ public static void Main(string[] args)
+ {
+ // Configure to include the peak memory column and hide confusing
error column
+ var config = DefaultConfig.Instance
+ .AddColumn(new PeakMemoryColumn())
+ .HideColumns("Error", "StdDev"); // Hide statistical columns
that are confusing with few iterations
+
+ // Run only the real E2E CloudFetch benchmark
+ var summary = BenchmarkSwitcher.FromTypes(new[] {
+ typeof(CloudFetchRealE2EBenchmark) // Real E2E with
Databricks (requires credentials)
+ }).Run(args, config);
+ }
+ }
+}
diff --git a/csharp/Benchmarks/Databricks/CloudFetchRealE2EBenchmark.cs
b/csharp/Benchmarks/Databricks/CloudFetchRealE2EBenchmark.cs
new file mode 100644
index 000000000..5838442b8
--- /dev/null
+++ b/csharp/Benchmarks/Databricks/CloudFetchRealE2EBenchmark.cs
@@ -0,0 +1,286 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You under the Apache License, Version 2.0
+* (the "License"); you may not use this file except in compliance with
+* the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Apache.Arrow.Adbc.Drivers.Apache.Spark;
+using Apache.Arrow.Adbc.Drivers.Databricks;
+using Apache.Arrow.Ipc;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Reports;
+using BenchmarkDotNet.Running;
+
+namespace Apache.Arrow.Adbc.Benchmarks.Databricks
+{
+ /// <summary>
+ /// Custom column to display peak memory usage in the benchmark results
table.
+ /// </summary>
+ public class PeakMemoryColumn : IColumn
+ {
+ public string Id => nameof(PeakMemoryColumn);
+ public string ColumnName => "Peak Memory (MB)";
+ public string Legend => "Peak working set memory during benchmark
execution";
+ public UnitType UnitType => UnitType.Size;
+ public bool AlwaysShow => true;
+ public ColumnCategory Category => ColumnCategory.Custom;
+ public int PriorityInCategory => 0;
+ public bool IsNumeric => true;
+ public bool IsAvailable(Summary summary) => true;
+ public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) =>
false;
+
+ public string GetValue(Summary summary, BenchmarkCase benchmarkCase)
+ {
+ // Try CloudFetchRealE2EBenchmark (includes parameters in key)
+ if (benchmarkCase.Descriptor.Type ==
typeof(CloudFetchRealE2EBenchmark))
+ {
+ // Extract ReadDelayMs parameter
+ var readDelayParam = benchmarkCase.Parameters["ReadDelayMs"];
+ string key = $"ExecuteLargeQuery_{readDelayParam}";
+ if
(CloudFetchRealE2EBenchmark.PeakMemoryResults.TryGetValue(key, out var
peakMemoryMB))
+ {
+ return $"{peakMemoryMB:F2}";
+ }
+ }
+
+ return "See previous console output";
+ }
+
+ public string GetValue(Summary summary, BenchmarkCase benchmarkCase,
SummaryStyle style)
+ {
+ return GetValue(summary, benchmarkCase);
+ }
+
+ public override string ToString() => ColumnName;
+ }
+
+ /// <summary>
+ /// Configuration model for Databricks test configuration JSON file.
+ /// </summary>
+ internal class DatabricksTestConfig
+ {
+ public string? uri { get; set; }
+ public string? token { get; set; }
+ public string? query { get; set; }
+ public string? type { get; set; }
+ public string? catalog { get; set; }
+ public string? schema { get; set; }
+ }
+
+ /// <summary>
+ /// Real E2E performance benchmark for Databricks CloudFetch with actual
cluster.
+ ///
+ /// Prerequisites:
+ /// - Set DATABRICKS_TEST_CONFIG_FILE environment variable
+ /// - Config file should contain cluster connection details
+ ///
+ /// Run with: dotnet run -c Release --project Benchmarks/Benchmarks.csproj
--framework net8.0 -- --filter "*CloudFetchRealE2E*" --job dry
+ ///
+ /// Measures:
+ /// - Peak memory usage
+ /// - Total allocations
+ /// - GC collections
+ /// - Query execution time
+ /// - Row processing throughput
+ ///
+ /// Parameters:
+ /// - ReadDelayMs: Fixed at 5 milliseconds per 10K rows to simulate Power
BI consumption
+ /// </summary>
+ [MemoryDiagnoser]
+ [GcServer(true)]
+ [SimpleJob(warmupCount: 1, iterationCount: 3)]
+ [MinColumn, MaxColumn, MeanColumn, MedianColumn]
+ public class CloudFetchRealE2EBenchmark
+ {
+ // Static dictionary to store peak memory results for the custom column
+ public static readonly Dictionary<string, double> PeakMemoryResults =
new Dictionary<string, double>();
+
+ private AdbcConnection? _connection;
+ private Process _currentProcess = null!;
+ private long _peakMemoryBytes;
+ private DatabricksTestConfig _testConfig = null!;
+ private string _hostname = null!;
+ private string _httpPath = null!;
+
+ [Params(5)] // Read delay in milliseconds per 10K rows (5 = simulate
Power BI)
+ public int ReadDelayMs { get; set; }
+
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ // Check if Databricks config is available
+ string? configFile =
Environment.GetEnvironmentVariable("DATABRICKS_TEST_CONFIG_FILE");
+ if (string.IsNullOrEmpty(configFile))
+ {
+ throw new InvalidOperationException(
+ "DATABRICKS_TEST_CONFIG_FILE environment variable must be
set. " +
+ "Set it to the path of your Databricks test configuration
JSON file.");
+ }
+
+ // Read and parse config file
+ string configJson = File.ReadAllText(configFile);
+ _testConfig =
JsonSerializer.Deserialize<DatabricksTestConfig>(configJson)
+ ?? throw new InvalidOperationException("Failed to parse config
file");
+
+ if (string.IsNullOrEmpty(_testConfig.uri) ||
string.IsNullOrEmpty(_testConfig.token))
+ {
+ throw new InvalidOperationException("Config file must contain
'uri' and 'token' fields");
+ }
+
+ if (string.IsNullOrEmpty(_testConfig.query))
+ {
+ throw new InvalidOperationException("Config file must contain
'query' field");
+ }
+
+ // Parse URI to extract hostname and http_path
+ // Format: https://hostname/sql/1.0/warehouses/xxx
+ var uri = new Uri(_testConfig.uri);
+ _hostname = uri.Host;
+ _httpPath = uri.PathAndQuery;
+
+ _currentProcess = Process.GetCurrentProcess();
+ Console.WriteLine($"Loaded config from: {configFile}");
+ Console.WriteLine($"Hostname: {_hostname}");
+ Console.WriteLine($"HTTP Path: {_httpPath}");
+ Console.WriteLine($"Query: {_testConfig.query}");
+ Console.WriteLine($"Benchmark will test CloudFetch with
{ReadDelayMs}ms per 10K rows read delay");
+ }
+
+ [IterationSetup]
+ public void IterationSetup()
+ {
+ // Create connection for this iteration using config values
+ var parameters = new Dictionary<string, string>
+ {
+ [AdbcOptions.Uri] = _testConfig.uri!,
+ [SparkParameters.Token] = _testConfig.token!,
+ [DatabricksParameters.UseCloudFetch] = "true",
+ [DatabricksParameters.EnableDirectResults] = "true",
+ [DatabricksParameters.CanDecompressLz4] = "true",
+ [DatabricksParameters.MaxBytesPerFile] = "10485760", // 10MB
per file
+ };
+
+ var driver = new DatabricksDriver();
+ var database = driver.Open(parameters);
+ _connection = database.Connect(parameters);
+
+ // Reset peak memory tracking
+ GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting:
false);
+ GC.WaitForPendingFinalizers();
+ GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting:
false);
+ _currentProcess.Refresh();
+ _peakMemoryBytes = _currentProcess.WorkingSet64;
+ }
+
+ [IterationCleanup]
+ public void IterationCleanup()
+ {
+ _connection?.Dispose();
+ _connection = null;
+
+ // Print and store peak memory for this iteration
+ double peakMemoryMB = _peakMemoryBytes / 1024.0 / 1024.0;
+ Console.WriteLine($"CloudFetch E2E [Delay={ReadDelayMs}ms/10K
rows] - Peak memory: {peakMemoryMB:F2} MB");
+
+ // Store in static dictionary for the custom column (key includes
parameter)
+ string key = $"ExecuteLargeQuery_{ReadDelayMs}";
+ PeakMemoryResults[key] = peakMemoryMB;
+ }
+
+ /// <summary>
+ /// Execute a large query against Databricks and consume all result
batches.
+ /// Simulates client behavior like Power BI reading data.
+ /// Uses the query from the config file.
+ /// </summary>
+ [Benchmark]
+ public async Task<long> ExecuteLargeQuery()
+ {
+ if (_connection == null)
+ {
+ throw new InvalidOperationException("Connection not
initialized");
+ }
+
+ // Execute query from config file
+ var statement = _connection.CreateStatement();
+ statement.SqlQuery = _testConfig.query;
+
+ var result = await statement.ExecuteQueryAsync();
+ if (result.Stream == null)
+ {
+ throw new InvalidOperationException("Result stream is null");
+ }
+
+ // Read all batches and track peak memory
+ long totalRows = 0;
+ long totalBatches = 0;
+ RecordBatch? batch;
+
+ while ((batch = await result.Stream.ReadNextRecordBatchAsync()) !=
null)
+ {
+ totalRows += batch.Length;
+ totalBatches++;
+
+ // Track peak memory periodically
+ if (totalBatches % 10 == 0)
+ {
+ TrackPeakMemory();
+ }
+
+ // Simulate Power BI processing delay if configured
+ // Delay is proportional to batch size: ReadDelayMs per 10K
rows
+ if (ReadDelayMs > 0)
+ {
+ int delayForBatch = (int)((batch.Length / 10000.0) *
ReadDelayMs);
+ if (delayForBatch > 0)
+ {
+ Thread.Sleep(delayForBatch);
+ }
+ }
+
+ batch.Dispose();
+ }
+
+ // Final peak memory check
+ TrackPeakMemory();
+
+ statement.Dispose();
+ return totalRows;
+ }
+
+ private void TrackPeakMemory()
+ {
+ _currentProcess.Refresh();
+ long currentMemory = _currentProcess.WorkingSet64;
+ if (currentMemory > _peakMemoryBytes)
+ {
+ _peakMemoryBytes = currentMemory;
+ }
+ }
+
+ [GlobalCleanup]
+ public void GlobalCleanup()
+ {
+ GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting:
true);
+ GC.WaitForPendingFinalizers();
+ }
+ }
+}
diff --git a/csharp/Benchmarks/Databricks/README.md
b/csharp/Benchmarks/Databricks/README.md
new file mode 100644
index 000000000..e117e2be4
--- /dev/null
+++ b/csharp/Benchmarks/Databricks/README.md
@@ -0,0 +1,142 @@
+<!---
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+# Databricks CloudFetch E2E Benchmark
+
+Real end-to-end benchmark for measuring memory usage and performance of the
Databricks CloudFetch implementation against an actual Databricks cluster.
+
+## Overview
+
+This benchmark tests the complete CloudFetch flow with real queries against a
Databricks warehouse:
+- Full end-to-end CloudFetch flow (query execution, downloads, LZ4
decompression, batch consumption)
+- Real data from Databricks tables
+- Memory usage with actual network I/O
+- Power BI consumption simulation with batch-proportional delays
+
+## Benchmark
+
+### CloudFetchRealE2EBenchmark
+
+**Real end-to-end benchmark against actual Databricks cluster:**
+
+**Parameters:**
+- `ReadDelayMs`: Fixed at 5ms per 10K rows to simulate Power BI processing
delays
+
+**Method:**
+- `ExecuteLargeQuery`: Executes the query specified in the config file, reads
all batches with Power BI-like processing delays
+
+**Prerequisites:**
+- Set `DATABRICKS_TEST_CONFIG_FILE` environment variable pointing to your
config JSON
+- Config file must contain:
+ - `uri`: Full Databricks warehouse URI (e.g.,
`https://hostname/sql/1.0/warehouses/xxx`)
+ - `token`: Databricks access token
+ - `query`: SQL query to execute (this will be run by the benchmark)
+
+## Running the Benchmark
+
+### Run the CloudFetch E2E benchmark:
+```bash
+cd csharp
+export DATABRICKS_TEST_CONFIG_FILE=/path/to/databricks-config.json
+dotnet run -c Release --project Benchmarks/Benchmarks.csproj --framework
net8.0 -- --filter "*CloudFetchRealE2E*"
+```
+
+### Real E2E Benchmark Configuration
+
+Create a JSON config file with your Databricks cluster details:
+
+```json
+{
+ "uri": "https://your-workspace.cloud.databricks.com/sql/1.0/warehouses/xxx",
+ "token": "dapi...",
+ "query": "select * from main.tpcds_sf1_delta.catalog_sales",
+ "type": "databricks"
+}
+```
+
+Then set the environment variable:
+```bash
+export DATABRICKS_TEST_CONFIG_FILE=/path/to/databricks-config.json
+```
+
+**Note**: The `query` field specifies the SQL query that will be executed
during the benchmark. Use a query that returns a large result set to properly
test CloudFetch performance.
+
+## Understanding the Results
+
+### Key Metrics:
+
+- **Peak Memory (MB)**: Maximum working set memory during execution
+ - Printed to console output during each benchmark iteration
+ - Shows the real memory footprint during CloudFetch operations
+
+- **Allocated**: Total managed memory allocated during the operation
+ - Lower is better for memory efficiency
+
+- **Gen0/Gen1/Gen2**: Number of garbage collections
+ - Gen0: Frequent, low cost (short-lived objects)
+ - Gen1/Gen2: Less frequent, higher cost (longer-lived objects)
+ - LOH: Part of Gen2, objects >85KB
+
+- **Mean/Median**: Execution time statistics
+ - Shows the end-to-end time including query execution, CloudFetch downloads,
LZ4 decompression, and batch consumption
+
+### Example Output
+
+**Console output during benchmark execution:**
+```
+Loaded config from: /path/to/databricks-config.json
+Hostname: adb-6436897454825492.12.azuredatabricks.net
+HTTP Path: /sql/1.0/warehouses/2f03dd43e35e2aa0
+Query: select * from main.tpcds_sf1_delta.catalog_sales
+Benchmark will test CloudFetch with 5ms per 10K rows read delay
+
+// Warmup
+CloudFetch E2E [Delay=5ms/10K rows] - Peak memory: 272.97 MB
+WorkloadWarmup 1: 1 op, 11566591709.00 ns, 11.5666 s/op
+
+// Actual iterations
+CloudFetch E2E [Delay=5ms/10K rows] - Peak memory: 249.11 MB
+WorkloadResult 1: 1 op, 8752445353.00 ns, 8.7524 s/op
+
+CloudFetch E2E [Delay=5ms/10K rows] - Peak memory: 261.95 MB
+WorkloadResult 2: 1 op, 9794630771.00 ns, 9.7946 s/op
+
+CloudFetch E2E [Delay=5ms/10K rows] - Peak memory: 258.39 MB
+WorkloadResult 3: 1 op, 9017280271.00 ns, 9.0173 s/op
+```
+
+**Summary table:**
+```
+BenchmarkDotNet v0.15.4, macOS Sequoia 15.7.1 (24G231) [Darwin 24.6.0]
+Apple M1 Max, 1 CPU, 10 logical and 10 physical cores
+.NET SDK 8.0.407
+ [Host] : .NET 8.0.19 (8.0.19, 8.0.1925.36514), Arm64 RyuJIT armv8.0-a
+
+| Method | ReadDelayMs | Mean | Min | Max | Median |
Peak Memory (MB) | Gen0 | Gen1 | Gen2 | Allocated |
+|------------------ |------------
|--------:|--------:|--------:|--------:|--------------------------:|-----------:|-----------:|-----------:|----------:|
+| ExecuteLargeQuery | 5 | 9.19 s | 8.75 s | 9.79 s | 9.02 s |
See previous console output | 28000.0000 | 28000.0000 | 28000.0000 | 1.78 GB |
+```
+
+**Key Metrics:**
+- **E2E Time**: 8.75-9.79 seconds (includes query execution, CloudFetch
downloads, LZ4 decompression, batch consumption)
+- **Peak Memory**: 249-262 MB (tracked via Process.WorkingSet64, printed in
console)
+- **Total Allocated**: 1.78 GB managed memory
+- **GC Collections**: 28K Gen0/Gen1/Gen2 collections
+
+**Note**: Peak memory values are printed to console during execution since
BenchmarkDotNet runs each iteration in a separate process.