(0..100000).each{idx->
new GroovyShell().parse("println 0")
if( idx%100 == 0) { System.gc(); sleep 100}
}
26 August 2018
Hace unos días surgió en un conversación si había comprobado el consumo de recursos en una aplicación que hiciera un uso intensivo de DSL (Domain Specific Language) porque mi interlocutor estaba seguro que había perdidas de memoria incluso reportadas y sin solución. Así que me puse manos a la obra para comprobarlo
El primer intento de comprobarlo fue mediante este simple script
(0..100000).each{idx->
new GroovyShell().parse("println 0")
if( idx%100 == 0) { System.gc(); sleep 100}
}
y utilizando jconsole
comprobé alarmado que era verdad: El consumo de memoria crecía sin parar.
Entonces me fijé en un pequeño detalle: el número de clases cargadas también crecía!!
Efectivamente: cada vez que invocamos a parse
Groovy compila el texto y genera una clase nueva que es cargada y no
se libera porque … no es una instancia de un objeto, es código!!.
El segundo intento fue entonces parsear una sóla vez el script y mantener su referencia:
dsl = new GroovyShell().parse("println 0")
void executeScript(){
dsl.run()
}
(0..100000).each{idx->
executeScript()
if( idx%100 == 0) { System.gc(); sleep 100}
}
Sin embargo, aunque en menor medida, seguía teniendo el mismo problema de no liberar recursos … hasta que aumenté
a 1 segundo el sleep
y entonces empecé a comprobar que el consumo de recursos fluctuaba pero en un rango estable.
Si nuestra aplicación va a tener que ejecutar miles de veces diferentes scripts/dsls y no tenemos en cuenta esta situación nos encontraremos con que al cabo del tiempo nuestra aplicación habrá consumido todos los recursos y tendremos problemas. Así pues una posible solución es mantener un repositorio de scripts donde nuestra responsabilidad sea buscar si el código fuente ya ha sido compilado y utilizar el Script asociado
En este pequeño ejemplo implementamos esta idea:
Creamos al inicio una lista de posibles Scripts a ejecutar y en un Map asociamos cada String con su Script de tal forma que cuando queremos ejecutar uno de ellos, lo buscamos en este Map.
dsls = [
"println new Date()",
"println 1",
"""
println new Random().with {(1..9).collect {(('a'..'z')).join()[ nextInt((('a'..'z')).join().length())]}.join()}
"""
]
database = [:]
dsls.each{
database[it] = new GroovyShell().parse(it)
}
void executeDSL( int idx ){
database[ dsls[idx] ].run()
}
// wait to jconsole
sleep 1000*10
// run a lot of times
(0..100000).each{
executeDSL( (it % dsls.size()) )
if( (it % 1000) == 0) {
sleep 2000
println "liberando "
System.gc()
sleep 2000
}
}
Utilizando jconsole
podemos comprobar que el consumo de recursos se mantiene estable:
Lógicamente estos scripts son muy simples y no son parametrizables por lo que queda como ejercicio para el lector implementar una posible solución más completa
dsls = [
"println new Date()",
"println 1",
"""
println new Random().with {(1..9).collect {(('a'..'z')).join()[ nextInt((('a'..'z')).join().length())]}.join()}
"""
]
database = [:]
dsls.each{
database[it] = new GroovyShell().parse(it)
}
void executeDSL( int idx ){
database[ dsls[idx] ].run()
}
// wait to jconsole
sleep 1000*10
// run a lot of times
(0..100000).each{
executeDSL( (it % dsls.size()) )
if( (it % 1000) == 0) {
sleep 2000
println "liberando "
System.gc()
sleep 2000
}
}