Skip to content

Excessive allocations in HttpContent.ReadAsByteArrayAsyncΒ #81628

@dotMorten

Description

@dotMorten

Description

When using HttpClient.SendAsync and reading the HttpResponse.Content into a byte-array, it results in allocations in my testing 4 times that of the actual http response. If using HttpClient.GetByteArrayAsync allocations much closer to the actual http response is made. However this API call only allows for basic get requests, and no way to look at response headers etc.

I'm trying to find ways to heavily reduce memory allocations while still having full access to the power SendAsync provides over the simplified Get* methods.

Configuration

.NET 7.0.101, Windows 11 Pro, build 22621, x64

Regression?

Same thing observed with .NET 6, allocations almost identical.

Data

Benchmark:

   [MemoryDiagnoser]
    public class HttpTests
    {
        private readonly HttpClient client;
        private const string url = "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3/query?where=1%3D1&text=&objectIds=&time=&timeRelation=esriTimeRelationOverlaps&geometry=&geometryType=esriGeometryEnvelope&inSR=&spatialRel=esriSpatialRelIntersects&distance=&units=esriSRUnit_Foot&relationParam=&outFields=*&returnGeometry=true&returnTrueCurves=false&maxAllowableOffset=&geometryPrecision=&outSR=&havingClause=&returnIdsOnly=false&returnCountOnly=false&orderByFields=&groupByFieldsForStatistics=&outStatistics=&returnZ=false&returnM=false&gdbVersion=&historicMoment=&returnDistinctValues=false&resultOffset=&resultRecordCount=&returnExtentOnly=false&sqlFormat=none&datumTransformation=&parameterValues=&rangeValues=&quantizationParameters=&featureEncoding=esriDefault&f=pjson";
        public HttpTests()
        {
            client = new HttpClient();
        }

        [Benchmark] 
        public async Task<byte[]> Client_GetByteArrayAsync()
        {
            // Allocates ~1mb, about size of response
            var response = await client.GetByteArrayAsync(url).ConfigureAwait(false);
            return response;
        }
        [Benchmark]
        public async Task<byte[]> Client_Send_GetByteArrayAsync()
        {
            // Allocates ~4mb, about 4 times the response size
            var request = new HttpRequestMessage(HttpMethod.Get, url);
            var response = await client.SendAsync(request);
            var bytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
            return bytes;
        }
   }
Method Gen0 Gen1 Gen2 Allocated
Client_GetByteArrayAsync - - - 1.02 MB
Client_Send_GetByteArrayAsync 1000.0000 1000.0000 1000.0000 4.87 MB

Now I was able to reduce memory by about 1mb, using this approach, but still quite short of the simple GetByteArrayAsync:

        public async Task<int> ReadAsSpanAsync()
        {
            var request = new HttpRequestMessage(HttpMethod.Get, url);
            var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
            using MemoryStream stream = new MemoryStream();
            await response.Content.CopyToAsync(stream);
            ReadOnlyMemory<byte> bytes;
            if (!stream.TryGetBuffer(out ArraySegment<byte> dataSegment))
                throw new Exception("Can't get buffer");
            bytes = dataSegment;
            if(bytes.Span.Length == 0)
                throw new Exception("Buffer empty");
            return bytes.Span.Length;
        }
Method Gen0 Gen1 Gen2 Allocated
ReadAsSpanAsync 750.0000 750.0000 750.0000 3.87 MB

Analysis

Looking at the HttpContent, lots of byte array copying is performed to ensure no access to the internal memorystream that was buffered to. It might be helpful to create some access to this data with ReadOnlyMemory<byte> or similar.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions