sábado, setembro 08, 2012

JS TUNING / JavaScript Tuning


Vou colocar abaixo o que já descobri sobre como aumentar a performance de um javascript, tanto pela economia de memória, quanto pelo processamento.

MEMORY TUNNING 

Intro

Assim como outras linguagens, o javascript também tem GC (garbage collector), isto é, objetos da memória que não possuem mais nenhuma referência são automaticamente limpos a cada X tempo (ou outro critério).

Criei o hello world abaixo para vermos o GC em ação. No código abaixo, a variável item é limpa a cada execução do GC, já que acabou seu escopo. Ex: http://jsfiddle.net/44Jj2/21/
function createItem() {
    var item = new Array(99999);
    return item;
}

var myGlobal;
window.setInterval(function() {
    myGlobal = createItem();
}, 100);
Isso gera o serrilhado abaixo na memória, pelo inspecionador do Chrome:


Vejamos agora algumas dicas para melhor uso de memória.

 

Uso do VAR 

Sempre use var ao declarar uma variável (a menos que queira uma global). Se não fizer isso, ela será global e variáveis globais não podem ser limpas pelo GC pq ainda podem ser úteis para algo. Os prints abaixo mostram o consumo de memória para o script abaixo. No segundo eu troquei var item = bigSize(); por item = bigSize();.

function createItem() {
    var item = bigSize();
    return item;
}

function bigSize() {
    var a = new Array(9999);
    for (var i = 0; i < a.length; i++) {
        a[i] = new Array(999);
    }
    return a;
}

var myGlobal;
window.setTimeout(function(){
    myGlobal = createItem();
    myGlobal = null;
},1000);


Uso do DELETE

As implementações de js atuais (2012) não permitem que chamemos o método gc() diretamente no nosso código, então temos que esperar o gc() rodar. Porém, dependendo do uso de memória que nosso código faz, torna-se útil remover objetos da memória o quanto antes.

Para isso, usa-se o delete.
(eu ia colocar um exemplo disso funcionando, mas nenhum dos testes que fiz conseguir ver diferença no serrilhado.)

Uso do PROTOTYPE

Usar o prototype para adicionar métodos a uma classe pode economizar muita memória, se houverem muitas instancias dessa classe. Pelos meus testes, o uso de memória caiu de 60MB para 21MB, no código abaixo. Observe que o método come() é criado direto na classe, o que gasta memória, já que a função será armazenada em cada uma das 500k instancias criadas. Já usando prototype, que está comentado abaixo, a função só fica armazenada em um local, na classe Pessoa.

var start = new Date().getTime();
var end;
var pessoas = [];
var interval = setInterval(function(){
    for(var i = 0; i< 1000; i++) run();
},0)

function run(){
    pessoas.push(new Pessoa());
    var target = document.getElementById("target");
    target.value = pessoas.length + " pessoas criadas";
    if (pessoas.length > 500000) {
        clearInterval(interval);
        end = new Date().getTime();
        target.value = pessoas.length + " pessoas criadas em " + (end - start) + "ms";
        pessoas[0].come();
    }
}
    
function Pessoa(){
    this.nome = "XXXXXXXXXXXXXXXXXXXX";
    this.come = function(){console.log("comendo...");} // comentar para testar
}

//Pessoa.prototype.come = function(){console.log("comendo...");} // descomentar para testar​​​​​
<body>
    <input type="text" value="" id="target" style="width: 500px;"/>
</body>

CPU TUNNING  

Intro

Nos testes que fiz com o scrinux heat map descobri que é importante ter noção do gargalo antes de sair otimizando a torto e a direita. No início o problema era a parte gráfica. Após o uso do canvas, o gargalo passou a ser o processamento da matrix de pontos. Segue abaixo algumas descobertas.


Ajuste de frames

Em animações/games é importante considerar algumas frequencias: a taxa de atualização do monitor (60Hz), a taxa de atualização do canvas e a taxa de processamento dos dados, que no meu caso era uma matrix de pontos, cada um com uma cor. Nao adianta atualizar o canvas a 100Hz (100 vezes por segundo), se o monitor só exibe a cada 60Hz. Vai ser perda de processamento. Sobre a taxa de atualizacao dos dados, é melhor atualizar os dados varias vezes a cada atualizacao de tela, assim a simulação pode ser acelerada. Ex:

  • Monitor: 60Hz
  • Canvas: 60Hz
  • Dados: 120Hz
Vale lembrar que se os dados forem muito mais rápidos que o canvas, a animacao podera ter varios "pulos", entao o ideal é ir testando e ajustando.

Menos interrupções, maior desempenho

Uma outra dica é acumular vários processamentos a cada interrupção. O código abaixo mostra a diferenca obtida quando se processa 1x, 10x e 100x a cada interrupcao. O tempo cai de 2135ms para 32ms.

var start = new Date().getTime();
var end;
var ciclos = 0;
var interval = setInterval(function(){
    run();
    //run(); // 501 ciclos processados em 2135ms
    //for(var i = 0; i< 10; i++) run(); // 510 ciclos processados em 214ms
    //for(var i = 0; i< 100; i++) run(); // 600 ciclos processados em 32ms    
},0)

function run(){
    ciclos++;
    if (ciclos>500) {
        clearInterval(interval);
        end = new Date().getTime();
        var target = document.getElementById("target");
        target.value = ciclos + " ciclos processados em " + (end - start) + "ms";
    }
}


WebWorkers

Pendente: aguardando tempo para documentar o que já descobri...

Algoritmos Evolutivos para otimizar configuracao

Pendente: aguardando eu implementar a ideia e ver se dá certo...

 

MISC

Ao criar esse post percebi que é mais difícil do que parece fazer os testes e tirar o print do serrilhado. Isso pq:
  1. Várias coisas afetam o debug do estado da memória. Enquanto eu testava pelo jsfiddle ou pela home do Chrome, os resultados ficavam estranhos. Ajudou bastante criar um index.html só pra isso.
  2. O algoritmo de GC do Chrome é complexo. Ele muda conforme a quantidade de memória já usada, por ex. O serrilhado abaixo mostra que o GC aumentou o limite max de memória no decorrer do tempo.
var myGlobal;
window.setInterval(test, 1);
function test() {
    myGlobal = new Array(9999);
}