Utilizando chamadas assíncronas e paralelas para melhorar o desempenho de uma aplicação ASP.NET MVC

No post anterior, mostrei que o desempenho percebido pelo usuário pode ser melhorado através da renderização das views de forma assíncrona. Esse é um recurso de cliente muito útil e possui diversas aplicações, mas ele melhora muito pouco o tempo de resposta total da aplicação. Para conseguirmos um ganho real de desempenho, precisamos melhorar o tempo de resposta no lado do servidor, que é o principal ponto de lentidão da aplicação proposta. Para entender melhor o contexto desse artigo, sugiro a leitura do post anterior.

Um dos recursos que o .Net Framework oferece desde a versão 4.5 é a opção de trabalhar com métodos assíncronos, utilizando as palavras reservadas async e await. Esse tipo de função não bloqueia a thread principal do servidor web, que é rapidamente liberada para atender a outras requisições.  Quando termina de executar, esse tipo de método retorna um objeto do tipo Task<TResult>, que vai executar em uma thread que esteja disponível, podendo ser a thread que iniciou a execução ou não. Para maiores detalhes, sugiro a leitura desse artigo: http://www.asp.net/mvc/overview/performance/using-asynchronous-methods-in-aspnet-mvc-4.

Esse tipo de chamada permite melhorar a escalabilidade de uma aplicação, tornando-a capaz de receber mais requisições de usuário, o que melhora o tempo de resposta da aplicação de forma geral.

Vamos alterar a aplicação apresentada no post anterior, transformado os métodos lentos do repositório em assíncronos.

O método  RecuperarTodasContasAPagar fica assim:

O método RecuperarTodosLogs fica assim:

Para chamar os novos métodos assíncronos, precisamos tornar a action do controller assíncrona. Observe que é necessário utilizar a palavra reservada await nas chamadas:

Testando o desempenho da versão assíncrona, percebemos que o tempo de resposta é praticamente o mesmo da versão síncrona:

Versão síncrona executou em 3010 milissegundos
Versão síncrona executou em 3010 milissegundos
Versão assíncrona executou em 3001 milissegundos
Versão assíncrona executou em 3001 milissegundos
ActionsTempo Execução em Milissegundos
Síncrona3010
Assíncrona3001
Ganho porcentual de performance0,30%

O ganho de performance obtido ao mudar os métodos lentos para assíncrono foi de menos de 1%. Podemos perceber, dessa forma, que chamadas assíncronas permitem um ganho de tempo de resposta para aplicações com muitos usuários simultâneos concorrendo por recursos do servidor. O nosso cenário é um pouco diferente, estamos focados em melhorar o tempo de resposta para cada usuário. A aplicação tem poucos acessos concorrentes e o uso de async/await não melhorou muito nesse cenário. Para conseguirmos um ganho mais expressivo de desempenho, precisamos utilizar um outro recurso oferecido pelo .Net Framework, que é a execução em paralelo de métodos síncronos. Essa funcionalidade é fornecida através da classe Parallel, com o seu método Invoke. Dessa forma, continuaremos utilizando os métodos originais, síncronos. O que muda é que agora eles executarão em paralelo na action da controller: Um ponto interessante é que o método Invoke da classe Parallel é um helper, que utiliza o método WaitAll da classe Task. Abaixo, um exemplo de como a chamada acima poderia ser escrita utilizando WaitAll:

O Elemar Jr escreveu uma série de posts sobre programação assíncrona e paralela utilizando C#, vale a pena conferir: http://elemarjr.net/2011/09/07/paralelismo-em-mtodos-sncronos/http://elemarjr.net/2011/08/13/classe-task-alguns-exemplos/.

Executando a aplicação com a action paralela, percebemos um ganho efetivo de desempenho em relação à versão original:

Utilizando chamadas paralelas, obtemos ganho real de desempenho.
Utilizando chamadas paralelas, obtemos ganho real de desempenho.
ActionsTempo Execução em Milissegundos
Síncrona3010
Assíncrona3001
Paralela2013
Ganho porcentual de performance33,12%

Utilizando métodos síncronos executados de forma paralela, conseguimos um ganho de desempenho de 33% no servidor.

A partir desses exemplos, podemos concluir que o .Net Framework apresenta diversas opções de melhoria de desempenho e escalabilidade de uma aplicação. Porém, é necessário conhecer os detalhes de cada uma delas para poder avaliar e decidir qual é a melhor opção para atingir os objetivos específicos que você procura para o seu caso de uso.

Demonstração funcional da aplicação:

Action com chamadas assíncronas: http://viewassincronasaspnetmvc.azurewebsites.net/Home/IndexAsync

Action com execução paralela: http://viewassincronasaspnetmvc.azurewebsites.net/Home/IndexParallel

Código da aplicação: https://github.com/marcellalves/viewsparciaisassincronasASPNETMVC

Melhorando a performance percebida com views parciais assíncronas

A velocidade é um dos principais fatores de sucesso de uma aplicação web. Os usuários buscam uma experiência semelhante à que eles encontram em aplicativos nativos para smartphone ou serviços que utilizam tecnologia de ponta, como Facebook, Google e Twitter. Atender às expectativas de um público cada vez mais exigente é um desafio constante para os desenvolvedores web.

O desempenho de uma aplicação é um fator tão relevante que o Google estima que cada 0,5 segundo extra na renderização de uma página de resultado de pesquisa causa uma queda de 20% na audiência. A Amazon calcula que cada 100 milissegundos de latência custam 1% de prejuízo nas vendas.

Fonte: http://highscalability.com/blog/2009/7/25/latency-is-everywhere-and-it-costs-you-sales-how-to-crush-it.html (em inglês)

Nesse post, apresentarei uma técnica interessante para melhorar a performance percebida pelo usuário em uma aplicação ASP.NET MVC. Com ela, fazemos uso de views parciais para renderizar um layout de maneira assíncrona. Importante notar que essa técnica melhora a performance percebida pelo usuário, mas não substitui otimizações do lado do servidor. No cenário apresentado, a página responde mais rápido para o usuário, mas o tempo total de carregamento da página é praticamente o mesmo.

O cenário que vou expor é de uma página no estilo painel, com diversas áreas que trazem informações diferentes, como a do iGoogle Portal. Essa aplicação de exemplo é a página inicial de um sistema financeiro, que mostra as últimas 5 transações de contas a pagar e a receber e uma lista com os últimos 10 logs de evento gerados pelo sistema:

Tela inicial
Clique para ampliar

Cada um dos painéis possui a sua própria origem de dados. Ocorre que por motivos de processamento interno, as fontes de dados de contas a pagar e logs demoram para serem recuperadas, fazendo com que o carregamento inicial da página fique lento. Dessa forma, a página inicial e o painel de contas a pagar, que são rápidos, demoram a aparecer devido à lentidão dos outros dois painéis.

Ao executarmos a aplicação com renderização da página inicial de forma síncrona, obtemos o seguinte resultado:

Desempenho síncrono
Clique para ampliar

O tempo total de carregamento da página é de 3,97 segundos, suficiente para tirar a paciência de qualquer usuário. Observe que o gargalo maior se encontra no tempo que o servidor demora para enviar os dados para a aplicação: 3,21 segundos, 98% do tempo total.

Agora, é hora de aplicar a técnica proposta. A estratégia é simples: vamos carregar primeiro a página inicial e o painel de contas a receber, que são rápidos, e delegar o carregamento dos painéis lentos de contas a pagar e log para chamadas AJAX, que são naturalmente assíncronas. Além disso, vamos obter um benefício adicional ao executar as duas requisições de forma paralela.

Para atingir esse objetivo, precisamos alterar a página inicial. Antes, ela renderizava as views parciais de forma síncrona:

Agora, vamos criar divs que funcionarão como marcadores para o carregamento das informações. Elas foram marcadas com a classe css “conteudoParcial”, para serem referenciadas pelo código jQuery. Além disso, fazemos uso do atributo customizado “data-url”, para armazenar o endereço da fonte de dados da view parcial. Esse é um recurso muito útil introduzido na especificação do HTML 5, para armazenamento de informações customizadas em elementos de marcação. Mais detalhes nesse link (em inglês). Por fim, adicionamos um gif de load para dar um retorno para o usuário quanto ao carregamento dos painéis:

jQuery necessário para executar o carregamento assíncrono das views parciais:

Também foi necessário alterar o controller da página, criando actions separadas para a página inicial / painel de contas a receber e para os outros dois painéis (contas a pagar e logs):

Depois de aplicadas as alterações, percebemos um ganho substancial no tempo de resposta da página:

Desempenho assíncrono
Clique para ampliar

Agora, o tempo total de carregamento inicial é de 1,29 segundo, um ganho de 68% em relação ao anterior. Esse é o novo desempenho percebido pelo usuário no tempo de resposta da aplicação, mesmo que as requisições dos outros painéis continuem demorando um pouco mais para aparecer na tela.

Concluindo: é possível aplicar técnicas no cliente para melhorar a performance percebida pelo usuário em uma aplicação ASP.NET MVC. Esse exemplo poderia ficar ainda melhor se trabalhássemos o lado do servidor, tornando a recuperação das informações necessárias para a página mais ágil. Isso poderia ser conseguido através de chamadas assíncronas dos métodos de recuperação dos dados e será o tema do próximo post. Fique ligado!

Aplicação de exemplo:

Síncrono: http://viewassincronasaspnetmvc.azurewebsites.net/

Assíncrono: http://viewassincronasaspnetmvc.azurewebsites.net/Assincrono

Código da aplicação: https://github.com/marcellalves/viewsparciaisassincronasASPNETMVC.