Quantcast
Channel: Take it easy
Viewing all articles
Browse latest Browse all 19

De safari con Chrome: cazando leaks en Javascript

$
0
0

Ay, Javascript, Javascript. Que tiempos aquellos en que todo el uso que te daba era “fadeardivs, hacer un poco de AJAX o añadir algún que otro copo de nieve en fechas señaladas. La robustez de nuestra relación radicaba en las bajas expectativas mutuas. Yo no te veía como un lenguaje de programación, más bien como un juguete prescindible, y tú no me dabas ningún feedback útil, para qué molestarte si no iba a exprimirte el jugo. Pero los tiempos han cambiado, las modas se imponen y suceden, y lo de no tomarte en serio ya no va a ser posible. Y es que se acabó lo de ser la comparsa en una página web, esta es la era de la aplicación web, y las aplicaciones web, cada vez más grandes y complejas, son cosa tuya por derecho. Pero que duro está siendo, que duro…

She was a good dancer, but stepped on me all the time

En fin, basta de chorradas, y al tema. Para el frontend de Ducksboard hemos escrito una aplicación Javascript de un tamaño considerable. Digo aplicación, y no página o conjunto de páginas, porque tiene su importancia. En lo tocante al tema de este artículo, el hecho diferencial es que la aplicación se mantiene en memoria durante toda la sesión del usuario. O sea, que nunca se refresca la página actual o se cargan nuevas siguiendo enlaces. Toda la interacción se lleva a cabo sin abandonar nunca la (única) página, con su buen montón de objetos siguiendo sus ciclos de vida (cosas que se crean, se borran, se modifican, …) mientras el usuario tenga la app abierta, que pueden ser días o semanas. Así que, inevitablemente, toca hacer gestión de memoria de la de toda la vida, esto es… ¡evitar leaks!

Ya comenté hace unos meses que nuestra primera aproximación al frontend de Ducksboard fue totalmente desastrosa. Backbone, por suerte, nos ayudó a poner algo de orden en aquel embrollo. De hecho empezamos a usar Backbone relativamente pronto, tanto que durante meses aparecimos listados como ejemplo de uso en su homepage, allá por su versión 0.5. Backbone sumó mucho al proyecto, sí, pero ser un early adopter puede implicar (y suele hacerlo) comerte los problemas derivados de la inmadurez de un producto. En el caso de Backbone, en concreto, era relativamente fácil leakear memoria debido a la ausencia de mecanismos para gestionar el ciclo de vida de los objetos (en concreto de las vistas). Eso está resuelto en versiones recientes, y además hay mucho más know-how, pero cuando nosotros empezamos a usarlo simplemente la cosa estaba verde. Y nos mordió.

La situación es la siguiente. En Ducksboard (casi) todos los elementos que almacenamos en la base de datos tienen su correspondiente modelo de Backbone. Usuarios, dashboards, widgets, cuentas de servicios, etc. Todo elemento gráfico en la aplicación suele ser una vista de uno de esos modelos. En el caso de los widgets, por ejemplo, el widget en sí (los cuadraditos que puedes ver aquí) son una vista del modelo widget. La pantalla de configuración del widget, que es un modal, también es una vista. Y cada una de la subsecciones de ese modal (accesibles como tabs) es una vista a su vez. No es raro que además elementos individuales como inputs u otros sean vistas en sí mismos. En resumen, renderizamos cantidad de vistas de Backbone para construir la interfaz. En una sesión es sencillo que se rendericen cientos de ellas. Así que, si nuestro uso del viejo Backbone es proclive a perder memoria con cada nueva vista, ¿cuánto estamos leakeando en realidad?

Un modal de configuración de un widget. Son decenas de vistas anidadas.

Antes de entrar en harina hay que entender esa tendencia de las vistas de Backbone a leakear. Las vistas generan los elementos de DOM que se incrustan en la página (a partir de alguna librería de templates normalmente), y registran callbacks para eventos de dichos elementos (como clicks, focuses y otros). Además es muy común que las vistas se suscriban a eventos de cambio de los modelos para actuar sobre el DOM y poder reflejarlos visualmente. De modo que las vistas están referenciadas en los callbacks de varios eventos (tanto de DOM como de modelos, y otros) en una glorificación del patrón observer de los buenos MVC. ¿Cómo se inician los dichosos leaks? Pues bien, aunque en las versiones actuales de Backbone ya está resuelto, en las antiguas no había un método que limpiase todas esas referencias a la vista capturadas en callbacks de eventos. De modo que aunque el elemento de DOM se borrase, y la vista dejase de usarse, los callbacks de los eventos seguían registrados, y con ellos la vista capturada, evitando su recolección por parte del garbage collector. And this is how you leak hard. Eso ahora lo tienen resuelto con una elegante inversión de control en el registro de eventos y un método de eliminación de la vista que hace limpieza. Pero eso es ahora, claro.

Batman wouldn’t be super without a toolbelt

Conocemos el problema, y podemos ir poniendo solución aplicando la fórmula de limpieza correcta de vistas para eliminar esas referencias. Pero, ¿cómo compruebo si los cambios están teniendo efecto o no? Para empezar, ¿estamos leakeando realmente o todo esto es una paja mental mía? Hay que medir. Y hay que medir con precisión, para poder confirmar si los pequeños cambios incrementales que vamos introduciendo cumplen su cometido. Las Dev Tools de Chrome incluyen un par de herramientas pensadas para estas ocasiones:

  • La Timeline muestra la evolución temporal del consumo de memoria del tab actual, así como del recuento de objetos y eventos del DOM existentes (también lista eventos del engine, los FPS de renderizado y otras lindezas, pero aquí no interesan). Además tiene un botoncito para forzar la ejecución del garbage collector.
  • El Profiler hace “fotos” del heap de Javascript, ofreciendo un listado extensivo de los objetos en memoria en el momento de tomar el snapshot. Para cada objeto listado en el informe puede seguirse su lista de referencias en forma de árbol (desde la ráiz del garbage collector hasta el objeto), de modo que puedes saber quién está conservando una referencia a ese objeto que querías ver recolectado.

Ambas herramientas son útiles, de hecho necesarias para entender qué diantre está pasando, pero no son sencillas de usar. La timeline no tiene mucha magia, va dibujando gráficas a medida que pasa el tiempo. Los números desconciertan a ratos (¡¿de dónde salen tantos miles de nodos en DOM?!) y el tab va medio raro con algún que otro cuelgue, pero en fin, no es “rocket science” que dicen aquellos. Pero el profiler… ahí sí hay para volverse loco. Los informes son enormes, con miles y miles de cosas ahí flotando, y en no pocas ocasiones los árboles de retención te harán flipar en colores y jurar en arameo. Paciencia, mucha paciencia. Si se mira fijamente durante el tiempo suficiente pues… pues sigue sin tener el menor sentido, pero es lo que hay xD. Aunque al final es de donde se sacan los datos jugosos.

Nuestra primera aproximación al uso de las Dev Tools fue francamente naif. Bendita ignorancia. La idea era cargar la aplicación, grabar el consumo de recursos con la timeline y comprobar con que acciones leakeábamos memoria. La primera en la frente fue averiguar que sin interactuar con la aplicación, simplemente dejándola a su rollo, el uso de memoria aumentaba igualmente. Pues mal vamos. Tratamos de ver por dónde se nos iban las fuerzas tirando del profiler y comparando dos snapshots tomados con varios minutos de diferencia. Imposible entender nada. La cantidad de objetos que deberían haber sido recolectados pero no lo habían sido era abrumadora. Y es que, tratar de perfilar toda la aplicación del tirón era poco menos que una locura. La clave para meter mano en el entuerto ha sido ir por partes: arreglando componentes de la aplicación por separado, no atacar el conjunto, demasiado grande.

As real as it gets

Así que vamos con un ejemplo de cómo he estudiado una funcionalidad concreta de la aplicación usando las herramientas. Vida real, ejemplos reales. Voy a estudiar exclusivamente el modal de configuración de un widget de Ducksboard, y nada más: el resto de aplicación la tengo al mínimo, nada creado, nada corriendo. Me he escrito una funcioncilla que abre el modal y lo cierra a toda velocidad las veces que le digas. Cuando acaba fuerza una recolección de basura (para poder forzar el garbage collector a mano hay que arrancar Chrome con –js-flags=”–expose-gc” y entonces tenemos disponible gc()). Veamos qué nos dice la timeline del consumo de resursos si hacemos esa apertura 500 veces.

Buf, esto es malo. Es malo porque aunque abra y cierre el modal, hay cantidad de nodos y de eventos que no se están borrando correctamente. El área azul claro de arriba es el consumo de memoria. La gráfica de líneas de abajo muestra nodos del DOM (en verde) y eventos registrados (en azul). ¿Veis los pequeños bajones de nodos de DOM, que corresponden con pequeños bajones del consumo de memoria? Eso es el garbage collector haciendo (o más bien tratanto de hacer) su trabajo. ¿Por qué es tan malo? Pues porque el consumo de memoria no deja de subir, hasta el infinito y más allá. Y no deja de hacerlo porque estamos leakeando nodos del DOM y eventos registrados. El garbage collector no es capaz de recolectar esos objetos, y se quedan ahí chupando memoria para siempre. ¿Para siempre de verdad? Probemos a ver qué pasa si en vez de abrir el modal 500 veces lo abro 3000, quizás Chrome tenga un tope de consumo de memoria y haga algo que no le he forzado a hacer todavía.

No, no caerá esa breva. El consumo de memoria sube y sube y sigue subiendo. Ya llevo 300 MB zampados y esto no baja ni a patadas. Mal, muy mal. Si tengo a un usuario usando la aplicación durante dos semanas en una pantalla de su oficina voy a dejar su PC sin memoria y la cosa acabará petando de un modo u otro, no sin antes degradar visiblemente su rendimiento. Manos a la obra pues. Vamos arreglando el tema de las vistas, recordemos:

  • usar la inversión de control de suscripción a eventos de Backbone (listenTo y stopListening, en vez de on y off)
  • llamar al método remove de cada vista cuando dejemos de usarlas (hace el stopListening correspondiente)
  • si una vista tiene sub-vistas, guardar un listado de todas ellas para poder llamar a sus correspondientes removes
  • evitar guardar referencias a vistas en los modelos, mejor crearlas, mostrarlas, borrarlas y olvidarlas cada vez

Con eso la cosa mejora de manera evidente, pero seguimos leakeando. Siguen existiendo referencias a nuestras vistas que las mantienen vivas en memoria. Pero, ¿quién las mantiene? Y ahí entra el profiler a escena, echando algo de luz sobre el asunto. Algo que me ha ayudado bastante a entender el output del profiler es marcar la opción Show objects’ hidden properties en la sección General de la ruedecita de settings (abajo a la derecha) del inspector de Chrome. Con eso los paths de retención acaban en objectos que de otro modo no era capaz de reconocer. Un buen artículo sobre cómo usar el profiler es este, y un buen consejo es tener desactivadas extensiones y no usar la consola mientras se hacen snapshots, porque también se van a listar en el informe. Mi manera de usar el profiler (y puede que no sea la correcta por lo laboriosa) consiste en:

  • abrir y cerrar mi famoso modal
  • solicitar un snapshot del heap
  • fijarme en los Detached DOM tree (elementos de DOM que ya no están incustrados en el DOM, pero siguen en memoria, esto es… ¡leakeados!)
  • clickando en los elementos contenidos en esas categorías, la ventana inferior nos muestra los famosos árboles de retención
  • y a tratar de entender qué pasa ahí…

Un par de ejemplos tras abrir y cerrar mi modal:

En el primero, una vista capturada por tenerla referenciada desde un modelo (en algún momento tener this.view = new View() en el modelo me pareció una buena idea, ahora ya no…). Esto se lee como “el objeto params que cuelga de window tiene una referencia a un array, que tiene una referencia a un objeto llamado slot, que a su vez tiene una a uno llamada widget, que a su vez tiene una a settings_view…” . Como resultado, hay un elemento de DOM (ese HTMLElement) que no puede ser recolectado, aunque ya no se esté mostrando en el DOM (está en un árbol de Detached DOM). Si mi modelo widget no tuviese esa referencia a settings_view, se hubiese liberado correctamente, así que ya sé cómo arreglarlo (nada de this.view).

El segundo es un ejemplo de los que más tiempo me han llevado arreglar, porque no los causa código mío, sino código de terceros, y eso ya es un show. En este caso Chosen (una librería para tener selects tuneados) está manteniendo una cadena de referencias que no permiten liberar un div. Con cada Chosen que creo y luego borro del DOM, se están leakeando elementos que él mismo ha creado, lo que es bastante desastroso. Os sorprendería saber la cantidad de librerías de las que usamos que he tenido que parchear de algún modo para que liberen correctamente los recursos. La mentalidad dominante es “¿por qué iba a preocuparme de liberar nada si al recargar la página se limpia todo?”, y ejemplos como el de Chosen los tengo con muchas y muchas otras. Es un auténtico coñazo, porque parchear código que no es tuyo es más complicado, pero es lo que toca si queremos que las cosas funcionen bien.

¿Y cómo de bien? Pues así de bien:

Esto es el mismo modal, abierto y cerrado las mismas 500 veces, pero tras los (muchos) fixes que he esbozado antes. Lo interesante es que ahora las cosas se comportan como queremos. El consumo de memoria (área horizontal y azul superior) tiene la clásica forma de sierra típica de un runtime con garbage collection. Cada vez que el collector entra en acción, se limpian los objetos ya “muertos” y la memoria usada baja (en este ejemplo el collector ataca cada 10 segundos). Como resultado el consumo máximo de memoria es fijo (no superamos los 47 MB). En cuanto a nodos del DOM, las cosas han mejorado drásticamente: todos los eventos creados con la apertura del modal se eliminan al cerrarlo (los picos azules), y los nodos van subiendo mientras el garbage collector descansa, pero vuelven a su número pre-modal en cuanto el collector reclama sus almas. Son esos triángulos verdes que llegan siempre a un mismo tope y luego vuelven a la base de golpe.

¡Todo pinta perfecto! ¿Que tal si volvemos a hacer la prueba de las 3000 aperturas? Si todo funciona bien, debería repetirse el mismo patrón que con 500:

Que belleza… Ha llevado su buen curro (de hecho ha sido un trabajo de chinos, desesperante no pocas veces), pero ahora sé que si un buen cristiano se lía a abrir 500, o 3000, o 30000 modals el consumo de memoria va a ser fijo y estable. Que sí, la memoria no es tan cara, pero tampoco es cuestión de zamparme toda la disponible para dibujar cuatro widgets…

What doesn’t kill you makes you smarter

A modo de resumen (que el post es largo y difuso, soy un maldito agente del caos), con la experiencia he aprendido un par de cosillas:

  • Los eventos de Javascript son geniales por su naturaleza asíncrona, pero multiplican las probabilidades de leaker objetos capturados en sus callbacks.
  • Al escoger una librería o  framework para ser el fundamento de tu aplicación, nunca está de más tener un conocimiento sólidos de sus más y sus menos, sobretodo de sus menos.
  • Con Backbone, en concreto, hay que asegurarse muy mucho de eliminar vistas como mandan los cánones, esto es, usando su método remove sí o sí.
  • Las Chrome Dev Tools son potentes, pero hay que dedicarles rato. Pueden llegar a ser frustrantes, pero la información necesaria está ahí. No conozco herramientas mejores para esta tarea en Javascript de browser hoy por hoy (agradeceré 1000 que alguien me ilumine en los comentarios :)
  • Tratar de perfilar una aplicación grande del tirón puede convertir la tarea en titánica y desanimar antes de empezar. Centrarse en pequeñas porciones de código lo hace más asequible, aunque sin duda consume más tiempo.
  • En no pocos casos la culpable será una librería de terceros que no tenga mecanismo de limpieza. Tocará mojarse y añadir lo que toque (y si se contribuye de vuelta al proyecto original, mejor que mejor).

Y hasta ahí lo que tenía que decir de Javascript y leaks por hoy. Para mis quejas constantes e infantiles sobre el lenguaje en sí, no dejes de seguirme en @aitorciki.



Viewing all articles
Browse latest Browse all 19

Trending Articles