Skip to content

Conversation

@danielcrenna
Copy link

Using the buffered stream approach in ClickHouseDataReader allocates 512KB for each usage, regardless of payload size.

This change switches to using pooled RecyclableMemoryStream instances, removing GC pressure for more frequent queries, with good characteristics for small to medium result sets, and comparable performance <50000 rows.

Benchmark results are below.

@SpencerTorres


BenchmarkDotNet v0.15.1, Windows 11 (10.0.26100.4770/24H2/2024Update/HudsonValley)
11th Gen Intel Core i7-11800H 2.30GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.302
  [Host]     : .NET 9.0.7 (9.0.725.31616), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  Job-VQDIOV : .NET 9.0.7 (9.0.725.31616), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI

IterationCount=5  WarmupCount=3  

Method rowCount Mean Error StdDev Ratio RatioSD Gen0 Gen1 Gen2 Allocated Alloc Ratio
MultipleSmallQueries_BufferedStream ? 8,363.45 μs 626.341 μs 96.927 μs 1.00 0.01 8265.6250 7984.3750 7984.3750 54723.52 KB 1.00
MultipleSmallQueries_RecyclableStream ? 4,498.88 μs 205.551 μs 31.809 μs 0.54 0.01 281.2500 - - 3518.76 KB 0.06
ReadAllData_BufferedStream 100 92.50 μs 12.453 μs 3.234 μs 1.00 0.04 88.2568 85.3271 85.3271 547.24 KB 1.00
ReadAllData_RecyclableMemoryStream 100 50.53 μs 5.207 μs 1.352 μs 0.55 0.02 2.8687 - - 35.19 KB 0.06
ReadAllData_BufferedStream 5000 2,601.00 μs 174.953 μs 27.074 μs 1.00 0.01 218.7500 97.6563 97.6563 2040.19 KB 1.00
ReadAllData_RecyclableMemoryStream 5000 2,703.11 μs 782.847 μs 203.303 μs 1.04 0.07 121.0938 - - 1528.31 KB 0.75
ReadAllData_BufferedStream 50000 24,327.43 μs 831.624 μs 128.695 μs 1.00 0.01 1250.0000 31.2500 31.2500 15751.07 KB 1.00
ReadAllData_RecyclableMemoryStream 50000 24,226.19 μs 3,581.985 μs 930.230 μs 1.00 0.04 1218.7500 - - 15240.4 KB 0.97

…ory usage for small to medium result sets by replacing BufferedStream with RecyclableMemoryStream in ClickHouseDataReader�Every query allocated a new 512KB buffer via BufferedStream, regardless of actual response size, creating allocation and GC pressure for typical workloads.
@CLAassistant
Copy link

CLAassistant commented Aug 5, 2025

CLA assistant check
All committers have signed the CLA.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR optimizes memory usage in ClickHouseDataReader by replacing the buffered stream approach with pooled RecyclableMemoryStream instances to reduce GC pressure and improve performance for typical workloads.

  • Switches from BufferedStream with fixed 512KB allocation to RecyclableMemoryStreamManager with configurable pooling
  • Adds comprehensive benchmarking infrastructure with comparison tests between old and new implementations
  • Updates target framework from .NET 8.0 to .NET 9.0 for benchmark and integration test projects

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
ClickHouse.Driver/ADO/Readers/ClickHouseDataReader.cs Core implementation change replacing BufferedStream with RecyclableMemoryStream
ClickHouse.Driver/Types/TypeConverter.cs Added InternalsVisibleTo attribute for benchmark project access
ClickHouse.Driver.Benchmark/RecyclableMemoryStreamBenchmark.cs New benchmark comparing BufferedStream vs RecyclableMemoryStream performance
ClickHouse.Driver.Benchmark/References/BufferedStreamClickHouseDataReader.cs Reference implementation of original BufferedStream approach for benchmarking
ClickHouse.Driver.Benchmark/ClickHouse.Driver.Benchmark.csproj Target framework update to .NET 9.0
ClickHouse.Driver.IntegrationTests/ClickHouse.Driver.IntegrationTests.csproj Target framework update to .NET 9.0
run-benchmarks.ps1 PowerShell script for running memory stream benchmarks
run-tests.ps1 PowerShell script for test execution with Docker ClickHouse setup

private readonly HttpResponseMessage httpResponse; // Used to dispose at the end of reader
private readonly ExtendedBinaryReader reader;
private readonly RecyclableMemoryStream recyclableStream;

Copy link

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

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

The constructor signature has changed by adding a RecyclableMemoryStream parameter. This is a breaking change to the internal API that could affect code that directly instantiates this class, even though it's marked as internal.

Suggested change
// Backward-compatible constructor overload (without RecyclableMemoryStream)
private ClickHouseDataReader(HttpResponseMessage httpResponse, ExtendedBinaryReader reader, string[] names, ClickHouseType[] types)
: this(httpResponse, reader, null, names, types)
{
}

Copilot uses AI. Check for mistakes.
{
var stream = new BufferedStream(httpResponse.Content.ReadAsStreamAsync().GetAwaiter().GetResult(), BufferSize);
reader = new ExtendedBinaryReader(stream); // will dispose of stream
recyclableStream = StreamManager.GetStream("ClickHouseDataReader");
Copy link

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

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

The string literal "ClickHouseDataReader" should be extracted to a constant to avoid magic strings and ensure consistency if used elsewhere.

Suggested change
recyclableStream = StreamManager.GetStream("ClickHouseDataReader");
recyclableStream = StreamManager.GetStream(StreamName);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants