Solucionar Problemas del Depurador Lento
Otros idiomas: English 한국어 Português 中文
Por lo general, la sobrecarga del depurador de Java es mínima. Sin embargo, todavía puede presentar costos significativos de tiempo de ejecución en ciertas circunstancias. En un escenario especialmente desafortunado, el depurador incluso puede congelar completamente la VM.
Examinemos las razones detrás de estos problemas y sus posibles soluciones.
Estoy utilizando IntelliJ IDEA. Los detalles específicos resaltados en este artículo pueden variar en otros IDEs, y algunas de las funciones mencionadas pueden no estar disponibles allí. Sin embargo, la estrategia general de solución de problemas aún debería aplicar.
Diagnóstico de la causa
Antes de explorar las soluciones, es inteligente identificar el problema. Las razones más comunes por las que el depurador ralentiza la aplicación incluyen:
- Puntos de interrupción de método
- Evaluación de expresiones con demasiada frecuencia
- Evaluación de expresiones que son computacionalmente demasiado pesadas
- Depuración remota con alta latencia
IntelliJ IDEA elimina las conjeturas en este paso proporcionando estadísticas detalladas en la pestaña Overhead del depurador:
Para acceder a ella, selecciona Overhead desde la pestaña Layout Settings. Mostrará la lista de puntos de interrupción y características del depurador. Junto a cada punto de interrupción o característica, verás cuántas veces se utilizó y la cantidad de tiempo que tomó para ejecutarse.
Si decides apagar temporalmente una característica que consume muchos recursos, puedes hacerlo desmarcando la casilla correspondiente en la pestaña Overhead.
Una vez que hemos identificado la fuente de la lentitud, veamos las causas más comunes y cómo abordarlas.
Puntos de interrupción del método
Al usar puntos de interrupción de método en Java, es posible que experimentes caídas de rendimiento, dependiendo del depurador que estés utilizando. Esto se debe a que la característica correspondiente proporcionada por la Java Debug Interface es notablemente lenta.
Por esta razón, IntelliJ IDEA ofrece puntos de interrupción de método emulados. Funcionan igual que los puntos de interrupción de método reales, pero mucho más rápido. Esta característica implica un truco bajo la interfaz: en lugar de configurar puntos de interrupción de método reales, el IDE los reemplaza con puntos de interrupción de línea regulares dentro todas las implementaciones del método en todo el proyecto.
Por defecto, todos los puntos de interrupción del método en IntelliJ IDEA están emulados:
Si estás utilizando un depurador que no tiene esta función y encuentras problemas de rendimiento con los puntos de interrupción del método, puedes hacer el mismo truco manualmente. Visitar todas las implementaciones del método podría ser tedioso, pero también puede ahorrarte tiempo durante la depuración.
‘Processing classes for emulated method breakpoints’ tardando demasiado
Si un método tiene un gran número de implementaciones, configurar un punto de interrupción del método en él podría llevar algún tiempo. En este caso, IntelliJ IDEA y Android Studio mostrarán un cuadro de diálogo que dice Processing classes for emulated method breakpoints.
Si te lleva demasiado tiempo, considera usar un punto de interrupción de línea en su lugar. Alternativamente, puedes ceder algo de rendimiento en tiempo de ejecución desmarcando la casilla Emulated en la configuración del punto de interrupción.
Puntos de interrupción condicionales en código caliente
Establecer un punto de interrupción condicional en el código caliente podría ralentizar drásticamente una sesión de depuración, dependiendo de cuántas veces se ejecute este código.
Considera la siguiente ilustración:
public class Loop {
public static final int ITERATIONS = 100_000;
public static void main(String[] args) {
var start = System.currentTimeMillis();
var sum = 0;
for (int i = 0; i < ITERATIONS; i++) {
sum += i;
}
var end = System.currentTimeMillis();
System.out.println(sum);
System.out.printf("The loop took: %d ms\n", end - start);
}
}
const val ITERATIONS = 100_000
fun main() = measureTimeMillis {
var sum = 0
for (i in 0 until ITERATIONS) {
sum += i
}
println(sum)
}.let { println("The loop took: $it ms") }
Establezcamos un punto de interrupción en sum += i
y especifiquemos
false
como la condición.
Esto significará efectivamente que el depurador nunca debería detenerse en este punto de interrupción.
Aún así, cada vez que se ejecuta esta línea,
el depurador tendría que evaluar false
.
En mi caso, los resultados de ejecutar este código con y sin el punto de interrupción fueron 39 ms
y 29855 ms
respectivamente.
¡Sorprendentemente, incluso con tan solo 100,000 iteraciones, la diferencia sigue siendo enorme!
Puede parecer sorprendente que evaluar una condición aparentemente trivial como false
tome tanto tiempo.
Esto se debe a que el tiempo transcurrido no solo se debe al cálculo del resultado de la expresión.
También implica manejar eventos de depurador y comunicarse con el front-end del depurador.
La solución es sencilla. Puedes integrar la condición directamente en el código de la aplicación:
public class Loop {
public static final int ITERATIONS = 100_000;
public static void main(String[] args) {
var start = System.currentTimeMillis();
var sum = 0;
for (int i = 0; i < ITERATIONS; i++) {
if (false) { // condition goes here
System.out.println("break") // breakpoint goes here
}
sum += i;
}
var end = System.currentTimeMillis();
System.out.println(sum);
System.out.printf("The loop took: %d ms\n", end - start);
}
}
fun main() = measureTimeMillis {
var sum = 0
for (i in 0 until ITERATIONS) {
if (false) { // condition goes here
println("break") // breakpoint goes here
}
sum += i
}
println(sum)
}.let { println("The loop took: $it ms") }
Con esta configuración, la máquina virtual ejecutará directamente el código de la condición, e incluso podría optimizarlo. El depurador, por el contrario, solo entrará en juego al encontrar el punto de interrupción. Aunque no se requiere en la mayoría de los casos, este cambio puede ahorrarte tiempo cuando necesitas suspender el programa de manera condicional en medio de una ruta crítica.
La técnica descrita funciona perfectamente con clases con código fuente disponible. Sin embargo, con código compilado, como las bibliotecas, el truco podría ser más difícil de realizar. Este es un caso de uso especial, que cubriré en una discusión separada.
Evaluación implícita
Además de las características donde tú especificas las expresiones, como condiciones de punto de interrupción y vigilancias, también hay características que evalúan implícitamente las expresiones por ti.
He aquí un ejemplo:
Cada vez que suspendes un programa, el depurador muestra los valores de las variables que están disponibles en el contexto actual. Algunos tipos pueden tener estructuras complejas que son difíciles de ver y navegar. Para tu conveniencia, el depurador los transforma utilizando expresiones especiales, llamadas renderers.
Los renderers pueden ser triviales como toString()
o más complejos, como aquellos
que transforman el contenido de las colecciones. Pueden ser integrados o personalizados.
El depurador de IntelliJ IDEA es muy flexible en cómo presenta tus datos. Incluso te permite especificar la configuración del renderizador a través de anotaciones para ofrecer representaciones de clases consistentes cuando varios contribuyentes trabajan en el mismo proyecto.
Para obtener más información sobre cómo configurar el formato para mostrar los datos, consulta la documentación de IntelliJ IDEA.
Por lo general, la sobrecarga aportada por los depuradores es insignificante,
pero el impacto finalmente depende del caso de uso particular.
De hecho, si algunas de tus implementaciones de toString()
contienen código para minar criptodivisas,
¡el depurador tendrá dificultades para mostrar el valor de toString()
para esa clase!
Si el renderizado de una cierta clase resulta ser lento, puedes apagar el renderer correspondiente. Como una alternativa más flexible, puedes hacer que el renderizador sea bajo demanda. Los renderizadores bajo demanda solo se ejecutarán cuando solicites explícitamente que muestren su resultado.
Alta latencia en sesiones de depuración remota
Desde el punto de vista técnico, depurar una aplicación remota no es diferente a una sesión de depuración local. De cualquier manera, la conexión se establece a través de un socket, estamos excluyendo el modo de memoria compartida de esta discusión, y y el depurador ni siquiera es consciente de dónde se ejecuta la JVM host.
Sin embargo, un factor que podría ser distintivo para la depuración remota es la latencia de la red. Algunas características del depurador realizan varias rondas de red cada vez que se utilizan. Combinado con alta latencia, esto puede conducir a una degradación considerable del rendimiento.
Si ese es el caso, piensa en ejecutar el proyecto localmente, ya que podría ahorrar tiempo. De lo contrario, podrías beneficiarte de desactivar temporalmente algunas de las características avanzadas.
Conclusión
En este artículo, aprendimos cómo solucionar los problemas más comunes que causan la lentitud del depurador. Si bien a veces el IDE se encargará de eso por ti, creo que es importante entender los mecanismos subyacentes. Esto te hace más flexible, eficiente y creativo en tu depuración diaria.
Espero que hayas encontrado útiles estos consejos y trucos. Como siempre, tu feedback es muy apreciado! ¡No dudes en ponerte en contacto conmigo en X, LinkedIn, o Telegram!
¡Feliz depuración!