Encontrar pérdidas de memoria JavaScript con Chrome

163

He creado un caso de prueba muy simple que crea una vista Backbone, adjunta un controlador a un evento e instancia una clase definida por el usuario. Creo que al hacer clic en el botón "Eliminar" en esta muestra, todo se limpiará y no debería haber pérdidas de memoria.

Un jsfiddle para el código está aquí: 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();

Sin embargo, no tengo claro cómo usar el perfilador de Google Chrome para verificar que este sea, de hecho, el caso. Hay miles de millones de cosas que aparecen en la instantánea del generador de perfiles del montón, y no tengo idea de cómo decodificar lo que es bueno / malo. Los tutoriales que he visto hasta ahora solo me dicen que "use el generador de perfiles de instantáneas" o me dan un manifiesto enormemente detallado sobre cómo funciona todo el generador de perfiles. ¿Es posible usar el generador de perfiles como una herramienta, o realmente tengo que entender cómo se diseñó todo?

EDITAR: Tutoriales como estos:

Solución de fugas de memoria de Gmail

Usando DevTools

Son representativos de algunos de los materiales más fuertes que existen, por lo que he visto. Sin embargo, más allá de presentar el concepto de la técnica de 3 instantáneas , encuentro que ofrecen muy poco en términos de conocimiento práctico (para un principiante como yo). El tutorial 'Uso de DevTools' no funciona a través de un ejemplo real, por lo que su descripción conceptual vaga y general de las cosas no es demasiado útil. En cuanto al ejemplo de 'Gmail':

Entonces encontraste una fuga. ¿Ahora que?

  • Examine la ruta de retención de los objetos filtrados en la mitad inferior del panel Perfiles.

  • Si el sitio de asignación no se puede inferir fácilmente (es decir, oyentes de eventos):

  • Instrumente al constructor del objeto de retención a través de la consola JS para guardar el seguimiento de la pila para asignaciones

  • ¿Usando cierre? Habilite el indicador existente apropiado (es decir, goog.events.Listener.ENABLE_MONITORING) para establecer la propiedad creationStack durante la construcción

Me encuentro más confundido después de leer eso, no menos. Y, de nuevo, solo me dice que haga cosas, no cómo hacerlas. Desde mi punto de vista, toda la información que existe es demasiado vaga o solo tendría sentido para alguien que ya entendió el proceso.

Algunos de estos problemas más específicos se han planteado en la respuesta de @Jonathan Naguin a continuación.

Undécimo
fuente
2
No sé nada acerca de probar el uso de memoria en los navegadores, pero en caso de que no lo haya visto, el artículo de Addy Osmani sobre el inspector web de Chrome podría ser útil.
Paul D. Waite
1
Gracias por la sugerencia, Paul. Sin embargo, cuando tomo una instantánea antes de hacer clic en eliminar, y luego otra después de hacer clic, y luego selecciono 'objetos asignados entre las instantáneas 1 y 2' (como se sugiere en su artículo) todavía hay más de 2000 objetos presentes. Hay 4 entradas 'HTMLButtonElement', por ejemplo, lo que no tiene sentido para mí. En verdad, no tengo idea de lo que está pasando.
EleventyOne
3
Doh, eso no suena particularmente útil. Puede ser que con un lenguaje recolectado como basura, JavaScript, en realidad no esté destinado a verificar lo que está haciendo con la memoria en un nivel tan granular como su prueba. Una mejor manera de verificar si hay pérdidas de memoria podría ser llamar main10,000 veces en lugar de una vez, y ver si al final tiene mucha más memoria en uso.
Paul D. Waite
3
@ PaulD.Waite Sí, tal vez. Pero me parece que todavía necesitaría un análisis de nivel granular para determinar exactamente cuál es el problema, en lugar de simplemente poder decir (o no decir): "Está bien, hay un problema de memoria en alguna parte aquí". Y tengo la impresión de que debería poder usar su perfilador a un nivel tan granular ... Simplemente no estoy seguro de cómo :(
EleventyOne
Deberías echar un vistazo a youtube.com/watch?v=L3ugr9BJqIs
maja

Respuestas:

205

Un buen flujo de trabajo para encontrar fugas de memoria es la técnica de tres instantáneas , utilizada por primera vez por Loreena Lee y el equipo de Gmail para resolver algunos de sus problemas de memoria. Los pasos son, en general:

  • Tome una instantánea del montón.
  • Hacer cosas.
  • Tome otra instantánea del montón.
  • Repite lo mismo.
  • Tome otra instantánea del montón.
  • Filtrar objetos asignados entre las instantáneas 1 y 2 en la vista "Resumen" de la instantánea 3.

Para su ejemplo, he adaptado el código para mostrar este proceso (puede encontrarlo aquí ) retrasando la creación de la Vista Backbone hasta el evento de clic del botón Inicio. Ahora:

  • Ejecute el HTML (guardado localmente al usar esta dirección ) y tome una instantánea.
  • Haga clic en Inicio para crear la vista.
  • Toma otra instantánea.
  • Haz clic en eliminar.
  • Toma otra instantánea.
  • Filtrar objetos asignados entre las instantáneas 1 y 2 en la vista "Resumen" de la instantánea 3.

¡Ahora estás listo para encontrar pérdidas de memoria!

Notará nodos de algunos colores diferentes. Los nodos rojos no tienen referencias directas de Javascript a ellos, pero están vivos porque son parte de un árbol DOM separado. Puede haber un nodo en el árbol al que se hace referencia desde Javascript (tal vez como un cierre o variable), pero casualmente impide que se recolecte basura todo el árbol DOM.

ingrese la descripción de la imagen aquí

Sin embargo, los nodos amarillos tienen referencias directas de Javascript. Busque nodos amarillos en el mismo árbol DOM separado para ubicar referencias de su Javascript. Debe haber una cadena de propiedades que conduzca desde la ventana DOM al elemento.

En su particular, puede ver un elemento Div HTML marcado como rojo. Si expande el elemento verá que hace referencia a una función "caché".

ingrese la descripción de la imagen aquí

Seleccione la fila y en su consola escriba $ 0, verá la función y ubicación reales:

>$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

Aquí es donde se hace referencia a su elemento. Desafortunadamente no hay mucho que puedas hacer, es un mecanismo interno de jQuery. Pero, solo para fines de prueba, vaya a la función y cambie el método a:

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

Ahora, si repite el proceso, no verá ningún nodo rojo :)

Documentación:

Jonathan Naguin
fuente
8
Le agradezco su esfuerzo. De hecho, la técnica de las tres instantáneas se menciona regularmente en los tutoriales. Desafortunadamente, los detalles a menudo se omiten. Por ejemplo, agradezco la introducción de la $0función en la consola, que era nueva para mí; por supuesto, no tengo idea de qué está haciendo o cómo sabías usarla ( $1parece inútil mientras $2parece hacer lo mismo). En segundo lugar, ¿cómo sabías resaltar la fila #button in function cache()y no ninguna de las otras docenas de filas? Por último, hay nodos rojos en NodeListy HTMLInputElementtambién, pero no pueden descifrarlos.
EleventyOne
77
¿Cómo sabías que la cachefila contenía información mientras que los demás no? Hay numerosas ramas que tienen una distancia menor que la cache. Y no estoy seguro de cómo sabías que HTMLInputElementes hijo de HTMLDivElement. Lo veo referenciado dentro de él ("nativo en HTMLDivElement"), pero también hace referencia a sí mismo y dos HTMLButtonElements, lo que no tiene sentido para mí. Ciertamente aprecio que haya identificado la respuesta para este ejemplo, pero realmente no tendría idea de cómo generalizar esto a otros temas.
EleventyOne
2
Es extraño, estaba usando tu ejemplo y obtuve un resultado diferente al que obtuviste (de tu captura de pantalla). Sin embargo, agradezco mucho toda su ayuda. Creo que tengo suficiente por ahora, y cuando tenga un ejemplo de la vida real con el que necesito ayuda específica, crearé una nueva pregunta aquí. Gracias de nuevo.
EleventyOne
2
La explicación de $ 0 se puede encontrar aquí: developer.chrome.com/devtools/docs/commandline-api#0-4
Sukrit Gupta
44
Que Filter objects allocated between Snapshots 1 and 2 in Snapshot 3's "Summary" view.significa
K - La toxicidad en SO está creciendo.
8

Aquí hay un consejo sobre el perfil de memoria de un jsfiddle: use la siguiente URL para aislar su resultado jsfiddle, elimina todo el marco jsfiddle y carga solo su resultado.

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

Nunca pude descubrir cómo usar la línea de tiempo y el generador de perfiles para rastrear pérdidas de memoria, hasta que leí la siguiente documentación. Después de leer la sección titulada 'Rastreador de asignación de objetos' pude usar la herramienta 'Grabar asignaciones de montón' y rastrear algunos nodos DOM separados.

Solucioné el problema al cambiar del enlace de eventos jQuery a usar la delegación de eventos Backbone. Tengo entendido que las versiones más recientes de Backbone desvincularán automáticamente los eventos si llama View.remove(). Ejecute algunas de las demostraciones usted mismo, están configuradas con pérdidas de memoria para que pueda identificarlas. Siéntase libre de hacer preguntas aquí si aún no lo recibe después de estudiar esta documentación.

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

Rick Suggs
fuente
6

Básicamente, debe mirar la cantidad de objetos dentro de su instantánea de montón. Si el número de objetos aumenta entre dos instantáneas y ha eliminado los objetos, entonces tiene una pérdida de memoria. Mi consejo es buscar controladores de eventos en su código que no se separen.

Konstantin Dinev
fuente
3
Por ejemplo, si miro una instantánea del montón de jsfiddle, antes de hacer clic en 'Eliminar', hay muchos más de 100,000 objetos presentes. ¿Dónde buscaría los objetos que realmente creó mi código jsfiddle? Pensé que Window/http://jsfiddle.net/4QhR2/showpodría ser útil, pero son funciones interminables. No tengo idea de lo que está pasando allí.
EleventyOne
@EleventyOne: No usaría jsFiddle. ¿Por qué no simplemente crear un archivo en su propia computadora para probar?
Blue Skies
1
@BlueSkies Hice un jsfiddle para que la gente aquí pudiera trabajar desde la misma base de código. Sin embargo, cuando creo un archivo en mi propia computadora para probar, todavía hay más de 50,000 objetos presentes en la instantánea del montón.
EleventyOne
@EleventyOne Una instantánea del montón no le da una idea de si tiene una pérdida de memoria o no. Necesitas al menos dos.
Konstantin Dinev
2
En efecto. Estaba destacando lo difícil que es saber qué buscar cuando hay miles de objetos presentes.
EleventyOne
3

También puede mirar la pestaña Línea de tiempo en las herramientas para desarrolladores. Registre el uso de su aplicación y vigile el recuento de oyentes del nodo DOM y el evento.

Si el gráfico de memoria indicara una pérdida de memoria, entonces puede usar el generador de perfiles para averiguar qué está perdiendo.

Robert Falkén
fuente
2

Respaldo el consejo de tomar una instantánea del montón, son excelentes para detectar fugas de memoria, Chrome hace un excelente trabajo de instantáneas.

En mi proyecto de investigación para mi título, estaba creando una aplicación web interactiva que tenía que generar una gran cantidad de datos acumulados en 'capas', muchas de estas capas serían 'eliminadas' en la interfaz de usuario, pero por alguna razón la memoria no era al ser desasignado, usando la herramienta de instantáneas pude determinar que JQuery había estado manteniendo una referencia en el objeto (la fuente fue cuando estaba tratando de desencadenar un .load()evento que mantuvo la referencia a pesar de estar fuera de alcance). Tener esta información a la mano guardó mi proyecto sin ayuda, es una herramienta muy útil cuando está utilizando las bibliotecas de otras personas y tiene este problema de referencias persistentes que impiden que el GC haga su trabajo.

EDITAR: También es útil planificar con anticipación qué acciones va a realizar para minimizar el tiempo dedicado a las instantáneas, plantear la hipótesis de lo que podría estar causando el problema y probar cada escenario, haciendo instantáneas antes y después.

ProgrammerInProgress
fuente
0

Un par de notas importantes con respecto a la identificación de pérdidas de memoria con las herramientas de desarrollador de Chrome:

1) Chrome tiene pérdidas de memoria para ciertos elementos, como los campos de contraseña y número. https://bugs.chromium.org/p/chromium/issues/detail?id=967438 . Evite usarlos durante la depuración, ya que contaminan su instantánea del montón al buscar elementos separados.

2) Evite registrar cualquier cosa en la consola del navegador. Chrome no recolectará basura los objetos escritos en la consola, lo que afectará su resultado. Puede suprimir la salida colocando el siguiente código al comienzo de su script / página:

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

3) Use instantáneas de montón y busque "separar" para identificar elementos DOM separados. Al pasar el cursor sobre los objetos, obtienes acceso a todas las propiedades, incluidos id y externalHTML, que pueden ayudar a identificar cada elemento. Captura de pantalla de JS Heap Snapshot con detalles sobre el elemento DOM separado Si los elementos separados siguen siendo demasiado genéricos para reconocerlos, asígneles ID únicos utilizando la consola del navegador antes de ejecutar su prueba, por ejemplo:

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

Ahora, cuando identifique un elemento separado con, digamos id = "AutoId_49", vuelva a cargar su página, ejecute el fragmento de arriba y encuentre el elemento con id = "AutoId_49" utilizando el inspector DOM o document.querySelector (..) . Naturalmente, esto solo funciona si el contenido de su página es predecible.

Cómo ejecuto mis pruebas para identificar pérdidas de memoria

1) Cargar página (con salida de consola suprimida)

2) Haga cosas en la página que podrían provocar pérdidas de memoria

3) Use las herramientas de desarrollador para tomar una instantánea del montón y busque "separar"

4) Desplace los elementos para identificarlos desde sus propiedades id o externalHTML

Jimmy Thomsen
fuente
Además, siempre es una buena idea deshabilitar la minificación / uglificación, ya que dificulta la depuración en el navegador.
Jimmy Thomsen