Como encontrar vazamentos de memória JavaScript no Chrome

163

Criei um caso de teste muito simples que cria uma visualização Backbone, anexa um manipulador a um evento e instancia uma classe definida pelo usuário. Acredito que, ao clicar no botão "Remover" neste exemplo, tudo será limpo e não haverá vazamento de memória.

Um jsfiddle para o código está aqui: http://jsfiddle.net/4QhR2/

// scope everything to a function
function main() {

    function MyWrapper() {
        this.element = null;
    }
    MyWrapper.prototype.set = function(elem) {
        this.element = elem;
    }
    MyWrapper.prototype.get = function() {
        return this.element;
    }

    var MyView = Backbone.View.extend({
        tagName : "div",
        id : "view",
        events : {
            "click #button" : "onButton",
        },    
        initialize : function(options) {        
            // done for demo purposes only, should be using templates
            this.html_text = "<input type='text' id='textbox' /><button id='button'>Remove</button>";        
            this.listenTo(this,"all",function(){console.log("Event: "+arguments[0]);});
        },
        render : function() {        
            this.$el.html(this.html_text);

            this.wrapper = new MyWrapper();
            this.wrapper.set(this.$("#textbox"));
            this.wrapper.get().val("placeholder");

            return this;
        },
        onButton : function() {
            // assume this gets .remove() called on subviews (if they existed)
            this.trigger("cleanup");
            this.remove();
        }
    });

    var view = new MyView();
    $("#content").append(view.render().el);
}

main();

No entanto, não estou claro como usar o criador de perfil do Google Chrome para verificar se esse é realmente o caso. Há um zilhão de coisas que aparecem no instantâneo do heap profiler, e não tenho idéia de como decodificar o que é bom / ruim. Os tutoriais que eu vi até agora ou apenas me dizem para "usar o snapshot profiler" ou me dão um manifesto detalhado sobre como o profiler inteiro funciona. É possível apenas usar o criador de perfil como uma ferramenta, ou eu realmente tenho que entender como tudo foi projetado?

EDIT: Tutoriais como estes:

Correção de vazamento de memória do Gmail

Usando o DevTools

São representativos de alguns dos materiais mais fortes por aí, pelo que vi. No entanto, além de introduzir o conceito da 3 Snapshot Technique , acho que eles oferecem muito pouco em termos de conhecimento prático (para um iniciante como eu). O tutorial 'Usando o DevTools' não funciona com um exemplo real, portanto, sua descrição conceitual vaga e geral das coisas não é muito útil. Quanto ao exemplo 'Gmail':

Então você encontrou um vazamento. O que agora?

  • Examine o caminho de retenção de objetos vazados na metade inferior do painel Perfis

  • Se o site de alocação não puder ser facilmente deduzido (ou seja, ouvintes de eventos):

  • Instrumentar o construtor do objeto de retenção por meio do console JS para salvar o rastreamento de pilha para alocações

  • Usando Closure? Ative o sinalizador existente apropriado (por exemplo, goog.events.Listener.ENABLE_MONITORING) para definir a propriedade creationStack durante a construção

Eu me sinto mais confusa depois de ler isso, não menos. E, novamente, está apenas me dizendo para fazer as coisas, não como fazê-las. Do meu ponto de vista, todas as informações existentes são muito vagas ou apenas fazem sentido para alguém que já entendeu o processo.

Algumas dessas questões mais específicas foram levantadas na resposta de Jonathan Naguin abaixo.

EleventyOne
fonte
2
Não sei nada sobre como testar o uso da memória nos navegadores, mas, caso você não o tenha visto, o artigo de Addy Osmani sobre o inspetor da web do Chrome pode ser útil.
Paul D. Waite
1
Obrigado pela sugestão, Paul. No entanto, quando eu tiro um instantâneo antes de clicar em remover e outro após clicar, e seleciono 'objetos alocados entre os instantâneos 1 e 2' (como sugerido no artigo), ainda existem mais de 2000 objetos presentes. Existem 4 entradas 'HTMLButtonElement', por exemplo, o que não faz sentido para mim. Na verdade, não tenho ideia do que está acontecendo.
EleventyOne 27/10/2013
3
doh, isso não parece ser particularmente útil. Pode ser que, com uma linguagem coletada como lixo, você não tenha a intenção de verificar o que está fazendo com a memória em um nível tão granular quanto o seu teste. Uma maneira melhor de verificar vazamentos de memória pode ser ligar main10.000 vezes em vez de uma vez e verificar se você acaba com muito mais memória em uso no final.
Paul D. Waite
3
@ PaulD.Waite Sim, talvez. Mas parece-me que eu ainda precisaria de uma análise de nível granular para determinar exatamente qual é o problema, em vez de apenas poder dizer (ou não dizer): "Ok, há um problema de memória em algum lugar aqui". E eu tenho a impressão de que devo poder usar o criador de perfil em um nível tão granular ... Só não sei como :(
EleventyOne
Você deve dar uma olhada no youtube.com/watch?v=L3ugr9BJqIs
maja

Respostas:

205

Um bom fluxo de trabalho para encontrar vazamentos de memória é a técnica de três instantâneos , usada pela primeira vez por Loreena Lee e pela equipe do Gmail para resolver alguns de seus problemas de memória. As etapas são, em geral:

  • Tire um instantâneo da pilha.
  • Fazer coisas.
  • Faça outro instantâneo da pilha.
  • Repita o mesmo material.
  • Faça outro instantâneo da pilha.
  • Filtre os objetos alocados entre os Snapshots 1 e 2 na visualização "Resumo" do Snapshot 3.

Para o seu exemplo, adaptei o código para mostrar esse processo (você pode encontrá-lo aqui ) atrasando a criação da exibição de backbone até o evento de clique do botão Iniciar. Agora:

  • Execute o HTML (salvo localmente usando este endereço ) e tire uma captura instantânea.
  • Clique em Iniciar para criar a exibição.
  • Tire outro instantâneo.
  • Clique em remover.
  • Tire outro instantâneo.
  • Filtre os objetos alocados entre os Snapshots 1 e 2 na visualização "Resumo" do Snapshot 3.

Agora você está pronto para encontrar vazamentos de memória!

Você notará nós de algumas cores diferentes. Nós vermelhos não têm referências diretas do Javascript para eles, mas estão ativos porque fazem parte de uma árvore DOM desanexada. Pode haver um nó na árvore referenciado pelo Javascript (talvez como um fechamento ou variável), mas coincidentemente está impedindo que a árvore DOM inteira seja coletada como lixo.

insira a descrição da imagem aqui

Nós amarelos, no entanto, têm referências diretas do Javascript. Procure nós amarelos na mesma árvore DOM desanexada para localizar referências do seu Javascript. Deve haver uma cadeia de propriedades que leva da janela DOM para o elemento.

No seu particular, você pode ver um elemento HTML Div marcado como vermelho. Se você expandir o elemento, verá que é referenciado por uma função "cache".

insira a descrição da imagem aqui

Selecione a linha e, no console, digite $ 0, você verá a função e o local reais:

>$0
function cache( key, value ) {
        // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
        if ( keys.push( key += " " ) > Expr.cacheLength ) {
            // Only keep the most recent entries
            delete cache[ keys.shift() ];
        }
        return (cache[ key ] = value);
    }                                                     jquery-2.0.2.js:1166

É aqui que seu elemento está sendo referenciado. Infelizmente, não há muito o que você possa fazer, é um mecanismo interno do jQuery. Mas, apenas para fins de teste, acesse a função e altere o método para:

function cache( key, value ) {
    return value;
}

Agora, se você repetir o processo, não verá nenhum nó vermelho :)

Documentação:

Jonathan Naguin
fonte
8
Eu aprecio o seu esforço. De fato, a técnica dos três instantâneos é mencionada regularmente nos tutoriais. Infelizmente, os detalhes são frequentemente deixados de fora. Por exemplo, eu aprecio a introdução da $0função no console, que era nova para mim - é claro, não tenho idéia do que está fazendo ou como você sabia usá-la ( $1parece inútil, enquanto $2parece fazer a mesma coisa). Em segundo lugar, como você sabia destacar a linha #button in function cache()e não nenhuma das outras dezenas de linhas? Finalmente, existem nós vermelhos NodeListe HTMLInputElementtambém, mas não consigo descobrir.
EleventyOne
7
Como você sabia que a cachelinha continha informações enquanto as outras não? Existem inúmeros ramos que têm uma distância menor que a dele cache. E não tenho certeza de como você sabia que isso HTMLInputElementé filho de HTMLDivElement. Eu o vejo referenciado dentro dele ("nativo em HTMLDivElement"), mas também se refere a ele e dois HTMLButtonElements, o que não faz sentido para mim. Certamente, agradeço sua identificação da resposta para este exemplo, mas realmente não tenho idéia de como generalizar isso para outras questões.
EleventyOne
2
Estranho, eu estava usando o seu exemplo e obtive um resultado diferente do que você obteve (na sua captura de tela). No entanto, agradeço imensamente a sua ajuda. Eu acho que tenho o suficiente por enquanto, e quando tiver um exemplo da vida real com o qual preciso de ajuda específica, vou criar uma nova pergunta aqui. Obrigado novamente.
EleventyOne
2
A explicação para $ 0 pode ser encontrada aqui: developer.chrome.com/devtools/docs/commandline-api#0-4
Sukrit Gupta
4
O que Filter objects allocated between Snapshots 1 and 2 in Snapshot 3's "Summary" view.significa isso ?
K - A toxicidade no SO está crescendo.
8

Aqui está uma dica sobre o perfil de memória de um jsfiddle: Use o seguinte URL para isolar o resultado do jsfiddle, ele remove toda a estrutura do jsfiddle e carrega apenas o resultado.

http://jsfiddle.net/4QhR2/show/

Nunca consegui descobrir como usar a Linha do tempo e o Profiler para rastrear vazamentos de memória, até ler a documentação a seguir. Depois de ler a seção intitulada 'Rastreador de alocação de objetos', pude usar a ferramenta 'Alocações de heap de registros' e rastrear alguns nós do DOM desanexado.

Corrigi o problema passando da ligação de evento jQuery para a delegação de eventos Backbone. Entendo que as versões mais recentes do Backbone desvincularão automaticamente os eventos para você, se você ligar View.remove(). Execute você mesmo algumas demos, elas são configuradas com vazamentos de memória para você identificar. Sinta-se à vontade para fazer perguntas aqui, se você ainda não o receber depois de estudar esta documentação.

https://developers.google.com/chrome-developer-tools/docs/javascript-memory-profiling

Rick Suggs
fonte
6

Basicamente, você precisa observar o número de objetos dentro de seu instantâneo de heap. Se o número de objetos aumentar entre dois instantâneos e você tiver descartado objetos, haverá um vazamento de memória. Meu conselho é procurar manipuladores de eventos no seu código que não sejam desanexados.

Konstantin Dinev
fonte
3
Por exemplo, se eu olhar um instantâneo de pilha do jsfiddle, antes de clicar em 'Remover', haverá muito mais de 100.000 objetos presentes. Onde procuraria os objetos que o código do meu jsfiddle realmente criou? Eu pensei que Window/http://jsfiddle.net/4QhR2/showpoderia ser útil, mas são apenas infinitas funções. Não tenho ideia do que está acontecendo lá.
EleventyOne 27/10/2013
@EleventyOne: Eu não usaria o jsFiddle. Por que não criar um arquivo em seu próprio computador para teste?
Blue Skies
1
@BlueSkies Fiz um jsfiddle para que as pessoas aqui pudessem trabalhar na mesma base de código. No entanto, quando eu crio um arquivo no meu próprio computador para teste, ainda há mais de 50.000 objetos presentes no instantâneo da pilha.
EleventyOne 27/10/2013
O instantâneo de pilha do @EleventyOne One não dá uma idéia de se você tem um vazamento de memória ou não. Você precisa de pelo menos dois.
Konstantin Dinev 28/10
2
De fato. Eu estava destacando como é difícil saber o que procurar quando há milhares de objetos presentes.
precisa
3

Você também pode consultar a guia Linha do tempo nas ferramentas do desenvolvedor. Registre o uso do seu aplicativo e fique de olho na contagem de ouvintes de nós e eventos do DOM.

Se o gráfico de memória realmente indicar um vazamento de memória, você poderá usar o criador de perfil para descobrir o que está vazando.

Robert Falkén
fonte
2

Segui o conselho de tirar um instantâneo de pilha, eles são excelentes para detectar vazamentos de memória, o chrome faz um excelente trabalho de captura de instantâneos.

No meu projeto de pesquisa para minha graduação, eu estava criando um aplicativo da web interativo que precisava gerar muitos dados construídos em 'camadas', muitas dessas camadas seriam 'excluídas' na interface do usuário, mas, por algum motivo, a memória não era sendo desalocado, usando a ferramenta de captura instantânea, pude determinar que o JQuery mantinha uma referência no objeto (a fonte era quando eu estava tentando acionar um .load() evento que mantinha a referência apesar de sair do escopo). Ter essas informações em mãos salvou sozinho meu projeto, é uma ferramenta muito útil quando você está usando as bibliotecas de outras pessoas e você tem esse problema de referências persistentes que impedem o GC de fazer seu trabalho.

EDIT: Também é útil planejar com antecedência quais ações você executará para minimizar o tempo gasto em snapshots, colocar a hipótese do que poderia estar causando o problema e testar cada cenário, fazendo snapshots antes e depois.

ProgrammerInProgress
fonte
0

Algumas observações importantes sobre a identificação de vazamentos de memória usando as ferramentas do desenvolvedor do Chrome:

1) O próprio Chrome possui vazamentos de memória para certos elementos, como campos de senha e número. https://bugs.chromium.org/p/chromium/issues/detail?id=967438 . Evite usá-los durante a depuração, pois eles poluem seu instantâneo de heap ao procurar elementos desanexados.

2) Evite registrar qualquer coisa no console do navegador. O Chrome não coleta objetos gravados no console, afetando o resultado. Você pode suprimir a saída colocando o seguinte código no início do seu script / página:

console.log = function() {};
console.warn = console.log;
console.error = console.log;

3) Use instantâneos de heap e procure por "desanexar" para identificar elementos DOM desanexados. Ao passar o mouse sobre os objetos, você obtém acesso a todas as propriedades, incluindo id e outerHTML, que podem ajudar a identificar cada elemento. Captura de tela do instantâneo JS Heap com detalhes sobre o elemento DOM desanexado Se os elementos desanexados ainda forem muito genéricos para reconhecer, atribua a eles IDs exclusivos usando o console do navegador antes de executar seu teste, por exemplo:

var divs = document.querySelectorAll("div");
for (var i = 0 ; i < divs.length ; i++)
{
    divs[i].id = divs[i].id || "AutoId_" + i;
}
divs = null; // Free memory

Agora, quando você identifica um elemento desanexado com, digamos id = "AutoId_49", recarregue sua página, execute o trecho acima novamente e encontre o elemento com id = "AutoId_49" usando o inspetor DOM ou document.querySelector (..) . Naturalmente, isso só funciona se o conteúdo da sua página for previsível.

Como executo meus testes para identificar vazamentos de memória

1) Carregar página (com a saída do console suprimida!)

2) Faça coisas na página que possam resultar em vazamento de memória

3) Use as Ferramentas do desenvolvedor para tirar um instantâneo da pilha e procurar "desanexar"

4) Passe o mouse sobre os elementos para identificá-los a partir de suas propriedades id ou outerHTML

Jimmy Thomsen
fonte
Além disso, é sempre uma boa idéia desabilitar a redução / aumento da largura de banda, pois isso dificulta a depuração no navegador.
Jimmy Thomsen