Aumentado a performance do HttpClient

cnr_br

Cristiano Rodrigues

Posted on March 9, 2023

Aumentado a performance do HttpClient

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);
                }
            }
}


Enter fullscreen mode Exit fullscreen mode

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);
}


Enter fullscreen mode Exit fullscreen mode

 csharp
private static bool ShouldBufferResponse(HttpCompletionOption completionOption, HttpRequestMessage request) =>
            completionOption == HttpCompletionOption.ResponseContentRead &&
            !string.Equals(request.Method.Method, "HEAD", StringComparison.OrdinalIgnoreCase);


Enter fullscreen mode Exit fullscreen mode

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:

Quantidade de 10

Segundo teste com quantidade 100.000:

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:

Sem desserialização com 10

Segundo teste com o limite de 100.000:

Sem desserialização 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:

HttpCompletionOption
HttpClient.SendAsync
HttpClient

💖 💪 🙅 🚩
cnr_br
Cristiano Rodrigues

Posted on March 9, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related