viernes, 1 de febrero de 2008

SUnit, memoria, velocidad y metaprogramación

Uno de los problemas que hemos tenido últimamente con SUnit es la gran cantidad de memoria que consumía al correr los 15.540 test que tenemos en la actualidad. Al correr todos estos test, el proceso de VisualAge crecía en consumo de memoria de 90 megas a 600 megas!. Al crecer dicho proceso a 600 megas, el uso de la pc se hacía insoportable por el trashing que realizaba constantemente puesto que estamos trabajando con máquinas de 1Gb de memoria (los 600 megas sumados al consumo del Firefox, GemStone, etc, rápidamente sobrepasaban el giga).
Lógicamente el problema se debía a que había objetos que quedaban referenciados durante la ejecución de los tests y esto hacía que creciera tanto el consumo de memoria. ¿Qué objetos prodrían ser?. Al principio pensamos que serían los test resource, pero rápidamente nos dimos cuenta que no.
Luego de investigar un poco más me dí cuenta que dichos objetos eran en realidad los mismos test que se ejecutaban y al poseer estos variables de instancias (que en algunos casos referenciaban a instancias completas del sistema) el consumo de memoria crecía de acuerdo a la ejecución de los mismos. Los test son referenciados desde varios lugares, entre ellos el TestResult que posee el resultado de la ejecución de los test y el TestSuite en caso de utilizarse uno.
La solución consistía entonces en lograr que estos objetos no queden referenciados. Lo primero que intenté fue hacer que no queden referenciados desde el TestResult pero el comportamiento de las herramientas de ejecución como el SUnitRunner o el SUnitBrowser no se comportaban bien ante este cambio. Peor aún, no podía lograr fácilmente hacer que el TestSuite dejase de referenciar a los test. Para hacerlo tenía que modificar bastante SUnit, algo que no me atraía demasiado.
Lo que se me ocurrió hacer fue que luego de terminar la ejecución del test y hacer el #tearDown, se pongan todas las variables de instancia con "nil". Hacer esto fue inmediato gracias al metamodelo y la capacidad reflexiba de Smalltalk, simplemente implemente el mensaje #cleanUpIntanceVariables de la siguiente manera:

TestCase>>cleanUpInstanceVariables

(TestCase allInstVarNames size + 1) to: self class allInstVarNames size do: [ :index | self instVarAt: index put: nil ]

Y luego modifique #runCase así:

TestCase>>runCase

[self setUp.
self performTest] sunitEnsure: [self tearDown;cleanUpInstanceVariables]

Con este simple cambio logré que no se consumiera más memoria, durante la ejecución de los 15.540 test la memoria en uso del proceso no aumenta y la mayor sorpresa fue ver que la ejecución de los test bajó en un 50%!! Si, ahora los test corren en la mitad de tiempo de lo que lo hacían antes puesto que Smalltalk no tiene que estar realizando una recolección de basura constantemente, con el scavenging alcanza.

En fin, un buen ejemplo de como un buen metamodelo puede ayudar a resolver problemas que, de no poseerlo, hubiese sido muy costoso hacerlo. Por ejemplo, otra solución sería obligar a cada test a implementar el mensaje #cleanUpInstanceVariables. En nuestro caso hubiese sido necesario implementar este mensaje 2530 veces puesto que esa es la cantidad de subclasses que posee TestCase, teniendo en cuenta que cada una de estas implementaciones deberían modificar a mano las variables de instancia para que referencien a "nil" y luego no olvidarse de mantener dicha implementación cada vez que se agrege una nueva variable de instancia. ¡Impensable!

2 comentarios:

GallegO dijo...

Muy bueno, jeje lo mejor de todo no es evitar el consumo de memoria, eso es simple... Lo mejor es hacer que corran en la mitad del tiempo sin haber pensado siquiera en eso!! Como se la aguanta el VAST, tiene la memoria tuneada o esta asi de fábrica nomas?
Saludos, GallegO

Hernan Wilkinson dijo...

Que hacés GallegO!, si VAST tiene varias opciones para tunear la memoria. Lo hemos hecho para nuestro sistema en producción y las diferencias son enormes.
Por ejemplo, hemos logrado que un server que soporta 15 clientes promedio por día no haga más de 1 solo GC!(con mark & sweep), eso es buenísimo, solo hace scavenges y en todo un día no supero el par de segundos en scavenges.