DEV Community

Cristiano Rodrigues for Unhacked

Posted on

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

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay