-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
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=¶meterValues=&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.