Aumentado a performance do HttpClient
Cristiano Rodrigues
Posted on March 9, 2023
Muitas vezes escrevemos códigos de forma automática, sem considerar possíveis variações que poderiam melhorar a performance.
Em um mundo cada vez mais dependente de APIs, em que os sistemas se conectam principalmente por meio do protocolo HTTP, saber usar o HttpClient pode ser uma grande vantagem e contribuir significativamente para um sistema mais performático.
O HttpClient é uma classe fundamental para a comunicação HTTP em aplicativos .NET. Ele fornece uma maneira fácil de enviar solicitações HTTP e receber respostas HTTP. No entanto, o HttpClient tem um comportamento padrão de carregar todo o conteúdo da resposta HTTP em memória antes de retornar a resposta como um objeto HttpResponseMessage, ou seja, a resposta é lida para um MemoryStream e só depois é consumida pelos métodos "padrões" que utilizamos (GetAsync e SendAsync).
Isso pode ser problemático quando a resposta HTTP contém uma grande quantidade de dados.
Felizmente, o HttpClient também permite que você trabalhe com fluxos (streams) em vez de carregar todo o conteúdo da resposta na memória. Isso é feito passando um valor HttpCompletionOption para os métodos HttpClient.GetAsync, HttpClient.SendAsync e HttpClient.Send.
O HttpCompletionOption é um enum com duas opções:
ResponseHeadersRead: Esta opção indica que a chamada HTTP é considerada concluída quando apenas os cabeçalhos da resposta foram lidos. O conteúdo da resposta ainda não foi lido e não está disponível para uso.
ResponseContentRead: Esta opção indica que a chamada HTTP é considerada concluída quando a resposta foi lida completamente. Isso significa que todo o conteúdo da resposta foi carregado em memória e está disponível para uso.
O uso dessas opções pode ter implicações importantes no desempenho e no uso da memória do sistema. A opção ResponseContentRead pode ser a mais conveniente em muitos casos, pois permite que todo o conteúdo da resposta seja lido e manipulado em um único lugar. No entanto, para respostas muito grandes, pode ser preferível usar a opção ResponseHeadersRead para evitar carregar todo o conteúdo da resposta em memória de uma só vez.
Código do HttpClient no GitHub:
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
// Called outside of async state machine to propagate certain exception even without awaiting the returned task.
CheckRequestBeforeSend(request);
(CancellationTokenSource cts, bool disposeCts, CancellationTokenSource pendingRequestsCts) = PrepareCancellationTokenSource(cancellationToken);
return Core(request, completionOption, cts, disposeCts, pendingRequestsCts, cancellationToken);
async Task<HttpResponseMessage> Core(
HttpRequestMessage request, HttpCompletionOption completionOption,
CancellationTokenSource cts, bool disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
{
bool telemetryStarted = StartSend(request);
bool responseContentTelemetryStarted = false;
HttpResponseMessage? response = null;
try
{
// Wait for the send request to complete, getting back the response.
response = await base.SendAsync(request, cts.Token).ConfigureAwait(false);
ThrowForNullResponse(response);
// Buffer the response content if we've been asked to.
if (ShouldBufferResponse(completionOption, request))
{
if (HttpTelemetry.Log.IsEnabled() && telemetryStarted)
{
HttpTelemetry.Log.ResponseContentStart();
responseContentTelemetryStarted = true;
}
await response.Content.LoadIntoBufferAsync(_maxResponseContentBufferSize, cts.Token).ConfigureAwait(false);
}
return response;
}
catch (Exception e)
{
HandleFailure(e, telemetryStarted, response, cts, originalCancellationToken, pendingRequestsCts);
throw;
}
finally
{
FinishSend(cts, disposeCts, telemetryStarted, responseContentTelemetryStarted);
}
}
}
Analisando o método SendAsync, observamos uma decisão para armazenar a resposta em um buffer (ShouldBufferResponse):
if (ShouldBufferResponse(completionOption, request))
{
if (HttpTelemetry.Log.IsEnabled() && telemetryStarted)
{
HttpTelemetry.Log.ResponseContentStart();
responseContentTelemetryStarted = true;
}
await response.Content.LoadIntoBufferAsync(_maxResponseContentBufferSize, cts.Token).ConfigureAwait(false);
}
csharp
private static bool ShouldBufferResponse(HttpCompletionOption completionOption, HttpRequestMessage request) =>
completionOption == HttpCompletionOption.ResponseContentRead &&
!string.Equals(request.Method.Method, "HEAD", StringComparison.OrdinalIgnoreCase);
Testando a performance:
Usando uma API que retorna um array de inteiros com base na quantidade solicitada, vamos executar dois testes. O primeiro teste solicitará uma quantidade de 10 inteiros e o segundo teste solicitará uma quantidade de 100.000 inteiros.
Primeiro teste com quantidade 10:
Segundo teste com quantidade 100.000:
Nos dois primeiros testes desserializamos o resultado em uma lista utilizando System.Text.Json.JsonSerializer.
Vamos realizar mais dois testes com as mesmas quantidades, porém sem desserializar o resultado, apenas utilizando a leitura do buffer.
Primeiro teste com quantidade 10:
Segundo teste com o limite de 100.000:
Após analisarmos os resultados dos testes, podemos observar que a utilização da opção HttpCompletionOption.ResponseHeadersRead pode ser um grande aliado na otimização de chamadas a APIs que retornam um grande volume de dados. Isso pode ajudar a aliviar o consumo de memória e reduzir a pressão do GC na aplicação, contribuindo significativamente para um sistema mais performático.
Portanto, ao trabalhar com HttpClient em aplicativos .NET, é importante considerar o uso da opção HttpCompletionOption.ResponseHeadersRead para evitar carregar todo o conteúdo da resposta em memória de uma só vez, especialmente quando lidando com respostas muito grandes.
Até a próxima!
Referências:
Posted on March 9, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.