lunes, 3 de marzo de 2014

Análisis de memoria 101

Este artículo es básicamente una traducción de dos documentos publicados por Google:

Para entenderla mejor, recomendamos leer antes, algo sobre Montículos y Teoría de Grafos.

Dicha traducción pretende servir de antesala para una serie de traducciones posteriores sobre depuración de memoria en Javascript. Agradezco cualquier comentario sobre errores o mejoras que hayáis podido encontrar en dicha traducción.

Análisis de memoria 101

Esta es una introducción al análisis de memoria. Los conceptos e ideas descritas aquí, son usadas en el Heap Profiler UI y en la correspondiente documentación. Debes conocerlos previamente, para poder usar la herramienta de un modo más efectivo.

Términos generales

Esta sección describe conceptos comunes usados en el análisis de memoria, y es aplicable a una variedad de herramientas de depuración de memoria, de diferentes lenguajes. Si ya tienes experiencia con, por ejemplo, depuradores para Java o .Net, es muy probable que te sientas familiarizado con ellas.

El montículo, es una red de objetos interconectados. En el mundo matemático, esta estructura es llamada grafo. Un grafo, es una red de nodos, unidos por vértices. Ambos, nodos y vértices, tienen nombres. Los nombres de los nodos usan el nombre de la función constructora que fue usada para crearlos, y los vértices usan los nombres de sus propiedades.

La secuencia de vértices que se necesita atravesar para alcanzar un objeto a partir de otro, es denominada "ruta". Normalmente, sólo estaremos interesados en rutas simples, por ejemplo, aquellas que no pasan por el mismo nodo dos veces.

Tamaño de objetos: shallow size y retained size

Piensa en la memoria como un grafo con tipos primitivos (números y cadenas), y objetos (arrays asociativos). Esto puede ser respresentado en una gráfica con un determinado número de nodos, que serían los objetos, interconectados por vértices, como hemos visto anteriormente.

La memoria puede ser utilizada de dos modos:

  • directamente por un objeto, 
  • o implícitamente almacenando referencias a otros objetos, y así, evitando que sean eliminados automáticamente con el recolector de basura (en adelante GC, por sus siglas en inglés de Garbage Collector).

El tamaño de la memoria que es utilizado por un objeto, es llamado "shallow size" (tamaño trivial o efímero). Los objetos propios de JavaScript tienen cierta memoria reservada para sus descripciones y para almacenar sus valores inmediatos.

Normalmente, sólo los arrays y strings, pueden necesitar un mayor tamaño: un mayor shallow size. De cualquier forma, los strings tienen sus propio lugar de almacenamiento en la memoria de renderizado, con lo cual, sólo exponen un pequeño wrapper object en el montículo JavaScript.

De cualquier forma, incluso un objeto pequeño puede requerir una gran cantidad de memoria de manera indirecta, al evitar que otros objetos sean eliminados por el GC. El tamaño de memoria que sería liberado si un objeto, y los que dependen de él, es liberado al borrarse, se denomita "retained size".

Cuando trabajamos con el Heap Profiler (depurador de montículos) en las DevTools de Google Chrome, verás varias columnas de información. Dos de ellas representan el shallow size y el retained size respectivamente.

Rutas retenidas de objetos (retaining paths)

Llamaremos "ruta retenida" (retaining path), a cualquier ruta desde la raíz del recolector de basura (GC) hasta un objeto en particular. Si no existe ruta alguna para poder llegar a un objeto, entonces ese objeto es llamado "inalcanzable", y se pone a disposición del GC para que sea eliminado.

Nuestras rutas empezarán siempre desde la raíz del GC. Existen muchas raíces internas del GC, las cuales no nos interesan demasiado. Como punto de partida para las aplicaciones, tenemos:

  • El objeto global Window, uno para cada iframe. Hay un campo "distancia" en los heap snapshots, que representan el número de propiedades referenciadas desde la raíz de window, yendo por el retaining path más corto.
  • El árbol del DOM, que consiste en todos los nodos alcanzables desde la raíz. No todos pueden tener wrapper objects asociados, pero en caso de que los tengan, dichos nodos permanecerán en memoria mientras permanezca el documento.
  • Objetos retenidos por el depurador. Algunas veces, los objetos pueden permanecer retenidos por el depurador, tras una evaluación en la consola. Por ello es recomendable realizar heap snapshots con la consola limpia y sin breakpoints en el depurador.

Una de las cosas a tener en cuenta en el depurador, es la distancia desde la raíz del GC. Si casi todos los objetos del mismo tipo, se encuentran a la misma distancia, y sólo unos pocos están a mayor distancia, es algo que susceptible de ser investigado.

Dominadores (Dominators)

El dominador de un objeto A, es otro objeto que existe en cada ruta simple desde la raíz del GC a dicho objeto A. Esto significa, que si eliminamos del montículo el dominador de un objeto, (junto a todas las demás referencias), el objeto A será inalcanzable, y por tanto, eliminado.

Los objetos dominadores tienen una estructura de árbol, ya que todo objeto tiene exactamente un dominador. El dominador de un objeto puede carecer de referencias directas al objeto que domina, es decir, el árbol de dominación no es una estructura que se expande.

Objetos a modo de colecciones, pueden retener una gran cantidad de memoria, cuando dominan otros objetos. Cada nodo del árbol es llamado "punto de acumulación" (accumulation point).

Particularidades del V8

En esta sección describiremos algunos temas relacionados con la memoria, que sólo corresponden a la máquina virtual V8 de Javascript. Su lectura debe ayudar a comprender por qué las capturas de montículos (heap snapshots), tienen dicha apariencia.

Representación de objetos en Javascript

Los números pueden ser presentados tanto como valores inmediatos enteros de 31 bits (small integers), o como objetos de un montículo (heap objects). Éstos últimos, son ulitizados por valores que no encajan dentro de los small integers, como por ejemplo: doubles, o para los casos en los que un valor necesita ser encapsulado, por ejemplo, al establecer propiedades en él.

El contenido de los strings, puede ser almacenado tanto en el montículo de la máquina virtual V8, como externamente, en la memoria de renderizado. El contenido recibido desde la Web (por ejemplo, desde un script), no es copiado dentro del montículo de la máquina virtual, y en su lugar, se crea un wrapper object, para acceder al contenido exterior.

Cuando dos cadenas son concatenadas, sus contenidos son inicialmente almacenados de forma separada, y sólo son unidas de manera lógica, usando un objeto llamado cons string. La unión de los contenidos de los cons strings, es ejecutado sólo cuando se precisa, por ejemplo, cuando necesitamos construir una subcadena resultado de la unión.

Los arrays son usados muy frecuentemente en la V8 para almacenar gran cantidad de datos. Los diccionarios (conjunto de pares llave-valor), son guardados en arrays. Por tanto, los arrays son el bloque de construcción básico de los objetos Javascript. Un objeto en Javascript posee dos arrays: uno para almacenar los nombres de las propiedades, y el otro para almecenar el elementos numéricos. Si el número de propiedades de un objeto es muy pequeño, entonces pueden ser almacenadas internamente en el propio objeto Javascript.

Un map object, describe el tipo de objeto y su estructura. Por ejemplo, dichos objetos se usan para describir las jerarquías implícitas de un objeto, como se describe aquí (enlace externo en inglés).

Grupos de objetos

Cada grupo de objeto nativo se crea a partir de objetos que mantienen una referencia mutua, el uno al otro. Si consideramos por ejemplo un subárbol del DOM: cada uno de sus nodos tiene un enlace a su padre, y enlaces al siguiente hijo y siguiente hermano, lo cual conforma un grafo. Aclarar que los objetos nativos (String, Number, Boolean, Array, Date, Math y RegExp) no son representados en el montículo Javascript - esta es la razón por la que tienen un tamaño igual a 0. En lugar de eso, son creados varios wrapper objects. Cada wrapper object, guarda una referencia de su correspondiente objeto nativo, para órdenes de redirección sobre él. Por lo tanto, un grupo de objetos almacena varios wrapper objects. De cualquier forma, esto no crea un ciclo no recolectable, ya que el GC es lo suficientemente inteligente como para liberar grupos de objetos cuyos wrappers no son referenciados por nadie. Sin embargo, si durante el proceso olvida liberar un simple wrapper, mantendrá a todo el grupo de objetos y sus respectivos wrappers.

No hay comentarios :

Publicar un comentario