Se não for possível medir, não será possível melhorar.
Lord Kelvin
Para que seus jogos HTML5 sejam executados mais rapidamente, primeiro você precisa identificar os gargalos de desempenho, mas isso pode ser difícil. Avaliar os dados de frames por segundo (FPS, na sigla em inglês) é um começo, mas para ter uma visão completa, você precisa entender as nuances nas atividades do Chrome.
A ferramenta about:tracing
oferece insights que ajudam a evitar soluções alternativas apressadas com o objetivo de melhorar o desempenho, mas que são essencialmente suposições bem intencionadas. Você vai economizar muito tempo e energia, ter uma ideia mais clara do que o Chrome está fazendo com cada frame e usar essas informações para otimizar seu jogo.
Olá, about:tracing
A ferramenta about:tracing
do Chrome mostra todas as atividades do Chrome em um período de tempo com tanta granularidade que pode parecer opressor no início. Muitas das funções no Chrome são instrumentadas para rastreamento pronto para uso. Portanto, sem fazer nenhuma instrumentação manual, você ainda pode usar about:tracing
para acompanhar seu desempenho. Consulte uma seção posterior sobre como instrumentar manualmente seu JS.
Para acessar a visualização de rastreamento, basta digitar "about:tracing" na barra de endereço do Chrome.
Na ferramenta de rastreamento, você pode começar a gravar, executar o jogo por alguns segundos e conferir os dados de rastreamento. Confira um exemplo de como os dados podem ficar:
Sim, isso mesmo. Vamos falar sobre como ler.
Cada linha representa um processo que está sendo perfilado. O eixo da esquerda para a direita indica o tempo, e cada caixa colorida é uma chamada de função instrumentada. Há linhas para vários tipos de recursos. As mais interessantes para o perfil de jogo são CrGpuMain, que mostra o que a unidade de processamento gráfico (GPU) está fazendo, e CrRendererMain. Cada trace contém linhas CrRendererMain para cada guia aberta durante o período de rastreamento (incluindo a própria guia about:tracing
).
Ao ler dados de rastreamento, sua primeira tarefa é determinar qual linha CrRendererMain corresponde ao jogo.
Neste exemplo, os dois candidatos são: 2216 e 6516. Atualmente, não há uma maneira refinada de escolher seu aplicativo, exceto procurar a linha que está fazendo muitas atualizações periódicas (ou, se você tiver instrumentado manualmente o código com pontos de rastreamento, procurar a linha que contém os dados de rastreamento). Neste exemplo, parece que 6516 está executando um loop principal com base na frequência de atualizações. Se você fechar todas as outras guias antes de iniciar o rastreamento, será mais fácil encontrar o CrRendererMain correto. No entanto, ainda pode haver linhas CrRendererMain para processos diferentes do jogo.
Como encontrar seu frame
Depois de localizar a linha correta na ferramenta de rastreamento do jogo, a próxima etapa é encontrar o loop principal. O loop principal se parece com um padrão repetido nos dados de rastreamento. Você pode navegar pelos dados de rastreamento usando as teclas W, A, S e D: A e D para mover para a esquerda ou direita (para frente e para trás no tempo) e W e S para aumentar e diminuir o zoom nos dados. O loop principal deve ser um padrão que se repete a cada 16 milissegundos se o jogo estiver sendo executado a 60 Hz.
Depois de localizar o batimento cardíaco do jogo, você pode analisar o que o código está fazendo em cada frame. Use W, A, S, D para aumentar o zoom até que você consiga ler o texto nas caixas de função.
Essa coleção de caixas mostra uma série de chamadas de função, com cada chamada representada por uma caixa colorida. Cada função foi chamada pela caixa acima dela. Nesse caso, você pode ver que MessageLoop::RunTask chamou RenderWidget::OnSwapBuffersComplete, que, por sua vez, chamou RenderWidget::DoDeferredUpdate e assim por diante. Ao ler esses dados, você tem uma visão completa do que chamou o que e quanto tempo cada execução levou.
Mas é aqui que as coisas ficam um pouco complicadas. As informações expostas por about:tracing
são as chamadas de função brutas do código-fonte do Chrome. Você pode fazer suposições educadas sobre o que cada função está fazendo com base no nome, mas as informações não são exatamente amigáveis ao usuário. É útil ver o fluxo geral do frame, mas você precisa de algo um pouco mais legível para humanos para descobrir o que está acontecendo.
Como adicionar tags de rastreamento
Felizmente, há uma maneira fácil de adicionar instrumentação manual ao código para criar dados de rastreamento: console.time
e console.timeEnd
.
console.time("update");
update();
console.timeEnd("update");
console.time("render");
update();
console.timeEnd("render");
O código acima cria novas caixas no nome da visualização de rastreamento com as tags especificadas. Se você executar o app novamente, as caixas "update" e "render" vão mostrar o tempo decorrido entre as chamadas de início e de término de cada tag.
Com isso, você pode criar dados de rastreamento legíveis por humanos para acompanhar os pontos de acesso no código.
GPU ou CPU?
Com gráficos acelerados por hardware, uma das perguntas mais importantes que você pode fazer durante o perfil é: esse código é vinculado à GPU ou à CPU? Com cada frame, você vai fazer renderização na GPU e alguma lógica na CPU. Para entender o que está deixando o jogo lento, você precisa saber como o trabalho é equilibrado entre os dois recursos.
Primeiro, encontre a linha na visualização de rastreamento chamada CrGPUMain, que indica se a GPU está ocupada em um determinado momento.
É possível notar que cada frame do jogo causa trabalho de CPU no CrRendererMain e na GPU. O rastro acima mostra um caso de uso muito simples em que a CPU e a GPU estão ociosas durante a maior parte de cada frame de 16 ms.
A visualização de rastreamento é muito útil quando você tem um jogo que está rodando lentamente e não sabe qual recurso está no limite. Analisar como as linhas da GPU e da CPU se relacionam é a chave para a depuração. Use o mesmo exemplo anterior, mas adicione um pouco mais de trabalho no loop de atualização.
console.time("update");
doExtraWork();
update(Math.min(50, now - time));
console.timeEnd("update");
console.time("render");
render();
console.timeEnd("render");
Agora você vai ver um rastro como este:
O que esse rastro nos diz? Podemos ver que o frame mostrado vai de cerca de 2.270ms a 2.320ms, o que significa que cada frame leva cerca de 50ms (uma taxa de 20Hz). Você pode ver fragmentos de caixas coloridas representando a função de renderização ao lado da caixa de atualização, mas o frame é totalmente dominado pela própria atualização.
Em contraste com o que está acontecendo na CPU, a GPU ainda está ociosa na maior parte de cada frame. Para otimizar esse código, procure operações que podem ser feitas no código do sombreador e mova-as para a GPU para aproveitar melhor os recursos.
E quando o código do sombreador é lento e a GPU está sobrecarregada? E se removermos o trabalho desnecessário da CPU e adicionarmos um pouco de trabalho ao código do sombreador de fragmentos? Confira um exemplo de shader de fragmento caro e desnecessário:
#ifdef GL_ES
precision highp float;
#endif
void main(void) {
for(int i=0; i<9999; i++) {
gl_FragColor = vec4(1.0, 0, 0, 1.0);
}
}
Como é um trace de código usando esse shader?
Mais uma vez, observe a duração de um frame. Aqui, o padrão de repetição vai de cerca de 2.750 ms a 2.950 ms, uma duração de 200 ms (taxa de frames de cerca de 5 Hz). A linha CrRendererMain está quase completamente vazia, o que significa que a CPU fica inativa a maior parte do tempo, enquanto a GPU está sobrecarregada. Isso é um sinal claro de que seus shaders são muito pesados.
Se você não soubesse exatamente o que estava causando a baixa taxa de frames, poderia observar a atualização de 5 Hz e tentar otimizar ou remover a lógica do jogo. Nesse caso, isso não seria útil, porque a lógica no loop do jogo não é o que consome tempo. Na verdade, o que esse rastro indica é que fazer mais trabalho de CPU em cada frame seria essencialmente "sem custo financeiro", já que a CPU está ociosa. Portanto, dar mais trabalho a ela não afeta o tempo que o frame leva.
Exemplos reais
Agora vamos conferir como são os dados de rastreamento de um jogo real. Uma das vantagens dos jogos criados com tecnologias abertas da Web é que você pode conferir o que está acontecendo nos seus produtos favoritos. Se você quiser testar ferramentas de criação de perfil, escolha seu título WebGL favorito na Chrome Web Store e crie um perfil com about:tracing
. Este é um exemplo de rastro do excelente jogo Skid Racer para WebGL.
Parece que cada frame leva cerca de 20 ms, o que significa que a taxa de frames é de cerca de 50 QPS. O trabalho está equilibrado entre a CPU e a GPU, mas a GPU é o recurso mais demandado. Se você quiser saber como criar perfis de exemplos reais de jogos WebGL, experimente alguns dos títulos da Chrome Web Store criados com WebGL, incluindo:
- Skid Racer
- Bouncy Mouse (em inglês)
- Bejeweled
- FieldRunners
- Angry Birds
- Bug Village (em inglês)
- Monster Dash
Conclusão
Se você quiser que o jogo seja executado a 60 Hz, todas as operações terão que caber em 16 ms de CPU e 16 ms de GPU para cada frame. Você tem dois recursos que podem ser usados em paralelo e pode alternar o trabalho entre eles para maximizar o desempenho. A visualização about:tracing
do Chrome é uma ferramenta valiosa para entender o que o código está fazendo e ajudar a maximizar o tempo de desenvolvimento ao resolver os problemas certos.
A seguir
Além da GPU, você também pode rastrear outras partes do ambiente de execução do Chrome. O Chrome Canary, a versão inicial do Chrome, é instrumentado para rastrear IO, IndexedDB e várias outras atividades. Leia este artigo do Chromium para entender melhor o estado atual dos eventos de rastreamento.
Se você desenvolve jogos para a Web, assista o vídeo abaixo. É uma apresentação da equipe de defensores de desenvolvedores de jogos do Google na GDC 2012 sobre otimização de desempenho para jogos do Chrome: