Fullmenu null

 

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

Primer intento

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!!.

Segundo intento

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.

Base de datos de DSLs

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:

memoria classes

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


Script
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
  }
}