Vazamentos de memória em JavaScript: como se livrar deles (2/2)

Este artigo é a continuação do artigo “os 4 tipos de vazamentos de memória”
c

O comportamento pouco intuitivo dos coletores de lixo

Embora os coletores de lixo sejam muito práticos, eles vêm com sua parcela de compromissos. Um desses compromissos é o seu não-determinismo. Em outras palavras, os coletores de lixo são imprevisíveis. Geralmente não é possível determinar com certeza quando uma coleta de lixo será realizada. Isso implica que, em alguns casos, o programa usa mais memória do que o necessário. Em outros casos, pode-se notar pequenas interrupções em aplicações sensíveis. Embora não determinístico signifique que nunca se pode ter certeza de quando a coleta será realizada, a maioria das implementações de GC compartilham um padrão comum de execução da coleta durante a alocação. Quando nenhuma alocação é feita, a maioria dos GS permanece inativa.
Considere o seguinte cenário:

  1. Um conjunto consequente de alocações é feito
  2. A maioria (ou todos) desses elementos são marcados como inacessíveis (imagine anular uma referência que aponta para um cache que não é mais necessário)
  3. Não há mais concessões são feitas

Nesse cenário, a maioria dos GCs não coletará mais. Ou seja, mesmo que existam itens inacessíveis disponíveis para coleta, eles não são reivindicados pelo colecionador. Esses não são vazamentos em si, mas o resultado é maior do que o uso normal de memória.
O Google fornece um ótimo exemplo desse comportamento em seus documentos de criação de perfil de memória JavaScript, exemplo nº 2.

Apresentando as ferramentas de criação de perfil de memória do Chrome

O Chrome fornece um bom conjunto de ferramentas para diagnosticar o uso de memória do código JavaScript. Existem principalmente duas visualizações relacionadas à memória: a visualização da linha do tempo e a visualização do perfil.

Visualização da linha do tempo

A visualização da linha do tempo é essencial para descobrir padrões de memória incomuns em nosso código. Quando estamos procurando um grande vazamento, os saltos periódicos que não reduzem tanto quanto cresceram após a coleta devem alertá-lo. Nesta captura, vemos como é o crescimento constante de um objeto com vazamento. Mesmo após a grande coleção final, a quantidade total de memória usada é maior no final do que no início. O número de nós também. Tantas dicas de vazamentos de DOM em algum lugar no código.

Vista de perfil

É essa vista que você vai gastar muito tempo olhando. A visualização de perfis permite obter um instantâneo e comparar instantâneos do uso de memória do seu código JavaScript.
Isso permite que você salve alocações ao longo do tempo. Em todas as visualizações de resultados, existem diferentes tipos de listas, mas as mais relevantes para nossa tarefa são a lista de resumo e a lista de comparação.
A visão de resumo nos dá uma visão geral dos diferentes tipos de objetos alocados e seu tamanho agregado: tamanho superficial (a soma de todos os objetos de um tipo específico) e tamanho retido (o tamanho superficial mais o tamanho de outros objetos retidos por causa desse objeto ). Isso também dá uma noção da distância desse objeto de sua raiz GC (a distância).
A lista de comparação nos dá as mesmas informações, mas nos permite comparar os diferentes instantâneos. Isso é especialmente útil para encontrar vazamentos.
Exemplo: encontrar vazamentos usando o Chrome
Existem basicamente dois tipos de vazamentos: vazamentos que causam aumentos periódicos no uso de memória e vazamentos que ocorrem uma vez e não causam aumentos adicionais no uso de memória. Por razões óbvias, é mais fácil encontrar vazamentos quando eles são periódicos. Eles também são os mais problemáticos, se a memória aumentar com o tempo, vazamentos desse tipo podem tornar o navegador lento ou fazer com que o script pare. Vazamentos não periódicos podem ser facilmente encontrados quando são grandes o suficiente para serem notados entre outras alocações. Isso geralmente não é o caso, então eles passam despercebidos. De certa forma, vazamentos que acontecem apenas uma vez podem ser categorizados como problemas de otimização. Dito isso, vazamentos periódicos são bugs e devem ser corrigidos.
Para ilustrar, pegamos um exemplo no documento do Chrome. O código está copiado abaixo:
var x = [];
function createSomeNodes() {
var div,
i = 100,
frag = document.createDocumentFragment();
for (;i > 0; i--) {
div = document.createElement("div");
div.appendChild(document.createTextNode(i + " - "+ new Date().toTimeString()));
frag.appendChild(div);
}
document.getElementById("nodes").appendChild(frag);
}
function grow() {
x.push(new Array(1000000).join('x'));
createSomeNodes();
setTimeout(grow,1000);
}
Quando o crescimento for chamado, ele começará criando nós DIV e anexando-os ao DOM. Ele também alocará um array grande e adicionará a ele um array referenciado por uma variável global. Isso causará um aumento constante na memória que pode ser encontrada usando as ferramentas mencionadas acima.
Geralmente observamos um padrão de uso de memória oscilante em linguagens baseadas em Garbage Collection. Isso é o que se espera se o código for executado em um loop produzindo as alocações, o que geralmente é o caso. Procuraremos reforços de memória periódicos que não voltem aos níveis anteriores após a coleta.
Primeiro, veja se a memória aumenta periodicamente.
A linha do tempo View é ótima para isso. Abra o exemplo no Chrome, abra as Dev Tools, vá até a timeline, selecione a memória e clique no botão de gravação. Em seguida, vá para a página e clique no botão para iniciar o vazamento de memória. Espere um pouco, então pare de gravar e veja o resultado:

Vazamento de memória na visualização da linha do tempo

Este exemplo continuará vazando memória a cada segundo. Após interromper a gravação, adicione um ponto de interrupção na função de crescimento para evitar que o script force o Chrome a fechar a página. Existem 2 grandes pistas nesta imagem que mostram que temos vazamento de memória. Os gráficos de Nodes (linha verde) e JS heap (linha azul). Os nós aumentam constantemente e nunca caem. Esta é uma grande bandeira vermelha.
O JS Heap também mostra um aumento constante no uso de memória. É mais difícil de ver devido aos efeitos do Coletor de Lixo. Você pode observar um padrão de crescimento de memória inicial, seguido por uma grande redução, seguida de crescimento, depois um pico, seguido por outra redução. A chave nesse caso é que, após cada redução no uso de memória, o tamanho do heap permanece maior do que na vez anterior. Isso significa que, embora o coletor de lixo recupere com êxito muita memória, algumas são perdidas regularmente.
Agora temos certeza de que há um vazamento. Vamos encontrá-lo.
d

Tire duas fotos

Para encontrar o vazamento, agora iremos para a seção de perfil do Chrome Dev Tools. para manter o uso de memória em um nível gerenciável, recarregue a página antes desta etapa. Vamos usar a função Take Heap Snapshot.
Recarregue a página e tire um instantâneo de pilha logo após terminar de carregar. Usaremos este instantâneo como referência. Agora clique no botão novamente, espere alguns segundos e tire um novo instantâneo. Depois de tirar o instantâneo, é uma boa ideia colocar um ponto de interrupção no script para evitar que o vazamento consuma mais memória.

Instantâneos de pilha

Há duas maneiras de examinar as alocações entre dois instantâneos. Clique em Resumo e, em seguida, vá para a direita e selecione Objetos alocados entre Snapshot 1 e Snapshot 2, ou clique em Comparação em vez de Resumo. Em ambos os casos, veremos uma lista de objetos que foram alocados entre os dois snapshots.
Nesse caso, é bastante fácil encontrar os vazamentos: eles são grandes. Veja o Size Delta do construtor (string). 8MBs para 58 novos itens. Isso parece suspeito: novos objetos são alocados, mas não liberados e 8 MBs são consumidos.
Se abrirmos a lista de alocações para o construtor (string), perceberemos que existem algumas alocações grandes entre muitas pequenas. Os grandes imediatamente chamam nossa atenção. Se selecionarmos algum deles, encontraremos algo interessante com ele na seção Retentores logo abaixo.

Retentores para o objeto selecionado

Vemos que a alocação selecionada faz parte de um array. Por sua vez, o array é referenciado pela variável x no objeto global window. Isso nos dá o caminho completo do nosso objeto grande até sua raiz não colecionável (janela). Encontramos nosso vazamento potencial e onde ele é referenciado.
Até agora tudo bem. Mas nosso exemplo foi fácil: grandes alocações como no exemplo não são a norma. Felizmente, nosso exemplo também vaza nós DOM, que são menores. É fácil encontrar esses nós usando o instantâneo acima, mas em sites maiores as coisas ficam mais complicadas. Versões recentes do Chrome fornecem uma ferramenta adicional bem adequada ao nosso trabalho: a função Record Heap Allocations.

Salve a alocação de heap para encontrar vazamentos

Desative o ponto de interrupção que você definiu antes, deixe o script ser executado e retorne à seção Perfil do Chrome Dev Tools. Agora toque em Record Heap Allocations. À medida que a ferramenta é executada, você notará picos azuis no gráfico superior. representa subsídios. A cada segundo, uma grande alocação é produzida pelo código. Deixe-o funcionar por alguns segundos e depois pare (não se esqueça de adicionar um ponto de interrupção para evitar que o Chrome consuma mais memória).
Pilha de subsídios economizados.
Nesta imagem, você pode ver o recurso matador dessa ferramenta: selecione uma parte da linha do tempo para ver quais alocações foram feitas durante esse período. Definimos a seleção mais próxima dos grandes picos. Apenas três construtores aparecem na lista. Um deles é o relacionado ao nosso grande vazamento ((string)), o próximo está relacionado às alocações do DOM e o último é o construtor Text (o construtor para nós folha DOM contendo texto).
Selecione um dos construtores HTMLDivElement da lista e clique em pick Allocation stack.
Elemento selecionado nos resultados de alocação de heap
BAM! Agora sabemos onde esse elemento foi alocado (grow -> createSomeNodes). Se observarmos cuidadosamente cada pico no gráfico, perceberemos que o construtor HTMLDivElement é chamado muito. Se retornarmos à visualização de comparação Snapshot, notamos que esse construtor revela muita alocação, mas nenhuma exclusão. Em outras palavras, ele aloca memória regularmente sem permitir que o GC recupere nenhuma. Todos esses são sintomas de um vazamento e, além disso, sabemos exatamente onde esses objetos estão alocados (a função createSomeNodes). Agora é hora de voltar ao código, estudá-lo e corrigir os vazamentos.

  • Outro recurso útil:

Na visualização do resultado das alocações de heap, podemos selecionar a visualização Alocação em vez de Resumo.
Essa visão nos dá uma lista de funções e as alocações de memória relacionadas. Podemos ver imediatamente crescer e createSomeNodes se destacar. Quando selecionamos crescer, damos uma olhada nos construtores dos objetos associados que são chamados por ele. Notamos (string), HTMLDivElement e Text que já sabemos que são os construtores dos objetos vazando.
A combinação dessas ferramentas pode ajudar bastante a encontrar vazamentos. Brinque com ele, execute diferentes perfis em seus sites de produção (idealmente código, não minimizado ou ofuscado). Veja se você consegue encontrar algum vazamento ou itens que são mantidos por mais tempo do que deveriam (dica: eles são mais difíceis de encontrar).
Para usar esses recursos, vá para Dev Tools -> Configurações e ative “registrar rastreamentos de pilha de alocação de heap”. É necessário fazer isso antes de gravar.

  • Para ir mais longe:

Gerenciamento de memória – Mozilla Developer Network
Vazamentos de memória JScript – Douglas Crockford (antigo, em relação aos vazamentos do Internet Explorer 6)
Perfil de memória JavaScript – Documentos do desenvolvedor do Chrome
Diagnóstico de memória – Google Developers
Um tipo interessante de vazamento de memória JavaScript – Blog do Meteor
Fechamentos Grokking V8
Conclusão
Vazamentos de memória podem acontecer e acontecem em linguagens coletadas de lixo como JavaScript. Eles podem viver escondidos por um tempo e, eventualmente, causar estragos. Por esse motivo, as ferramentas de criação de perfil de memória são essenciais para localizar vazamentos de memória. A criação de perfil deve fazer parte dos ciclos de desenvolvimento, especialmente para aplicativos de médio a grande porte. Comece agora para oferecer aos usuários a melhor experiência possível.
Boa depuração!
Encontre o 1º artigo sobre este assunto aqui !
[separator type=”” size=”” icon=”star”] [actionbox color=”default” title=”” description=”JS-REPUBLIC é uma empresa de serviços especializada em desenvolvimento JavaScript. Somos um centro de treinamento aprovado. Encontre todos os nossos treinamentos técnicos em nosso site de parceiros dedicado ao Treinamento” btn_label=”Nosso treinamento” btn_link=”http://training.ux-republic.com” btn_color=”primary” btn_size=”big” btn_icon=”star” btn_external ="1″]