lunes, 31 de octubre de 2011

Investigando un memory leak en cocos usando objgraph


Issue 169 en cocos reportaba un memory leak usando tilemaps. El reporte inicial era muy bueno (gracias davexunit !), en tanto que incluía un bugdemo simple; lo simplifiqué un poco más eliminando dos clases de la situación.

En esencia, si se definía una escena usando ciertas clases, cuando se hacia un loop creando una nueva instancia de la escena que reemplazaba la anterior como la activa, la memoria crecía sin limite.
El script no retenía referencias directas a las escenas reemplazadas, así que quedaba alguna en cocos o en pyglet, o bien había ciclos uncolectables.

Ahora bien, una de las clases es relativamente compleja, así que no quería empezar a mirar el código todavía; lo que hacia falta era algo que apuntara mas concretamente a las partes responsables.

Buscando un poco encontré objgraph, una pequeña herramienta que informa acerca de los objetos que python tiene en memoria.

Lo primero que hice es para ciertos valores del contador del loop, imprimirlo y llamar a
    objgraph.show_growth(limit=15)
Esta llamada calcula el numero de instancias de cada clase que vé el interprete, hace un delta con la misma info pero en la llamada anterior a show_growth, imprime las 15 que aumentaron mas.

En la iteración 66 la salida fue
    creations: 66
    dict               16823     +8720
    RectCell            4752     +2880
    list                3214     +1320
    ImageDataRegion      858      +520
    Texture              858      +520
    Tile                 858      +520
    Point3               204      +120
    Matrix3              136       +80
    instancemethod       189       +40
    Camera                68       +40
    Batch                 66       +40
    weakref             1425       +40
    RectMapLayer          66       +40

Los objetos mas pequeños tienden a estar incluidos dentro de otros mas grandes, así que tiene sentido mirar las clases mas gordas primero.
La mas gorda aquí es RectMapLayer, y los numeros dicen que a la iteracion 66 tenemos 66 instancias, 40 más que en la llamada anterior a show_growth.
Pero hay una sola instancia de RectMapLayer por escena, así que en alguna parte tenemos referencias que retienen estas instancias.

Basándome en los ejemplos incluidos en los docs de objgraph, hice una pequeña función
Lo que hace es elegir al azar una instancia de la clase indicada, por default 'RectMapLayer', calcula las cadenas de referencias que terminan en esa instancia y muestra el grafo correspondiente.
Hice visible esa función al InterpreterLayer que provee cocos con   Y modifiqué el loop para que después de k iteraciones dejara de cambiar de escena, dejando la ultima escena como activa.
Entonces, cuando terminan los cambios de escena levanto la consola python que nos provee cocos con un ctrl + I, le doy 'sw()' y como no le pasé filename levanta el visor-editor de grafos xdot, pero viendo que es chico y se vé bien le pido una fotito png para mostrar acá, con 'sw(fname="leak.png")':

Lo que nos dice el grafo es que:
  • al nivel del script tenemos un objeto director, de clase Director
  • que tiene un miembro _event_stack
  • que contiene una lista de 66 valores,
  • uno de los cuales es un dict de un elemento
  • que contiene algo llamado on_cocos_resize
  • que es un instance method asociado a una instancia
  • y esa instancia es la del RectMapLayer que elegimos al azar
Listo!, conociendo la arquitectura de cocos, sabemos que on_cocos_resize es un listener de eventos, y los listener de eventos es usual registrarlos en on_enter y deregistrarlos en on_exit, que son métodos llamados cuando un nodo entra y sale de la escena activa, respectivamente.
Así que el asunto debe estar en esos métodos de RectMapLayer. Es muy poco código, y en seguida se vé cual es el problema: RectMapLayer no está desregistrando el listener.

Aquí solo necesité un par de funciones de objgraph, pero tiene mas, ténganlo presente.

No hay comentarios:

Publicar un comentario