Perfilar IntelliJ IDEA consigo Mismo

Otros idiomas: English 한국어 Português 中文

Al igual que la entrada anterior, esta va a ser un poco meta. Obviamente, puedes usar IntelliJ IDEA para perfilar otro proceso, pero ¿sabías que IntelliJ IDEA puede perfilarse a sí mismo?

Esto puede ser útil si estás escribiendo un plugin de IntelliJ IDEA y necesitas resolver problemas relacionados con el rendimiento del plugin. Además, independientemente de si eres un autor de plugins, este escenario puede ser interesante para ti porque la estrategia de perfilado que abordaré no es exclusiva de IntelliJ IDEA - puedes usarla para solucionar cuellos de botella similares en otros tipos de proyectos y usando otras herramientas.

El problema

En esta publicación, veremos un cuello de botella de rendimiento bastante interesante con el que me topé hace un par de años. Mientras trabajaba en un proyecto secundario en IntelliJ IDEA, noté que encontrar pruebas (Navigate | Test) para clases con ciertos nombres cortos, como A , era sorprendentemente lento, a menudo tardaba 2 minutos o más.

Un diálogo que dice 'Searching for tests for class...' Un diálogo que dice 'Searching for tests for class...'

La presencia del cuello de botella no parecía depender del tamaño del proyecto - incluso en proyectos que consistían en una única clase llamada A , la navegación aún solía tomar mucho tiempo. Nunca había experimentado retrasos relacionados con esta característica incluso en el enorme monorrepositorio de IntelliJ IDEA, por lo que la desaceleración en un proyecto casi vacío parecía especialmente curiosa.

¿Por qué estaba ocurriendo esto? Y, más importante aún, ¿cómo abordar problemas similares, si te encuentras con ellos en tu proyecto?

Replicar el entorno

Originalmente, escribí este artículo para uso interno en JetBrains, sin embargo, la idea de hacerlo público me vino recientemente. Afortunadamente, con el paso del tiempo, el artículo no ha envejecido bien, y el problema ya no parece ser reproducible en las versiones actuales de IntelliJ IDEA y con hardware más reciente.

Como no pude reproducir la ralentización en mi configuración de trabajo, me encontré sacando el polvo de mi viejo portátil e instalando una versión anterior de IntelliJ IDEA en él. Si quieres seguir la investigación en tu IDE, asegúrate de clonar el repositorio de IntelliJ IDEA Community, ya que esto facilitará la navegación y la depuración para ti.

Asegurémonos también de tener un proyecto vacío con la siguiente clase en él:

public class A {
    public static void main(String[] args) {
        System.out.println("Me gustan las pruebas");
    }
}

IntelliJ Profiler

Como ya sabes, IntelliJ IDEA tiene un perfilador JVM integrado. Puedes ejecutar aplicaciones con el perfilador adjunto. Alternativamente, puedes adjuntar el perfilador a un proceso ya en ejecución, que es lo que vamos a hacer.

Para ello, ve a la ventana de herramientas Profiler y encuentra el proceso correspondiente allí. Si no ves tu IDE en la lista, asegúrate de marcar Show Development Tools en el menú cerca Process. Cuando haces clic en un proceso, IntelliJ IDEA sugiere las herramientas integradas de análisis de rendimiento, que te permiten:

Todas estas herramientas están cubiertas en la documentación, y en esta entrada nos centraremos específicamente en el perfilador.

Hacer clic en el proceso en la ventana de herramientas 'Profiler' revela un menú con la opción 'Adjuntar IntelliJ Profiler' Hacer clic en el proceso en la ventana de herramientas 'Profiler' revela un menú con la opción 'Adjuntar IntelliJ Profiler'

Necesitamos adjuntarlo antes de que ocurra el problema. Por ejemplo, si el problema surge como resultado de la llamada a alguna API, adjunta primero el perfilador al proceso y luego reproduce los eventos que causan el problema.

Tip icon

Idealmente, deberíamos adjuntar el perfilador justo antes de reproducir el problema. Si tu aplicación está ocupada haciendo algo más que simplemente esperar a la entrada, este enfoque se te ayudará a minimizar las muestras irrelevantes.

Dependiendo de cuánto tiempo tarda en ejecutarse el código problemático, también puede tener sentido reproducir el problema varias veces, para que el perfilador pueda recoger más muestras para el análisis. Esto hará que el problema destaque más en el informe resultante.

Cuando desvinculas el perfilador o terminas el proceso, IntelliJ IDEA abre automáticamente el instantánea resultante.

Analizando el informe

Para analizar las instantáneas, tienes varias vistas a tu disposición. Puedes elegir examinar árboles de llamadas, estadísticas de métodos específicos, carga de CPU por hilo, actividad GC, y más.

Para el problema en cuestión, comencemos con la vista Timeline para ver si podemos detectar algo inusual:

La pestaña 'Timeline' en la ventana de herramientas 'Profiler' tiene muchas barras verdes en uno de los hilos La pestaña 'Timeline' en la ventana de herramientas 'Profiler' tiene muchas barras verdes en uno de los hilos

Efectivamente, la línea de tiempo indica que uno de los hilos estuvo extraordinariamente ocupado. Las barras verdes corresponden a las muestras recogidas para un hilo en particular. Al hacer clic en cualquiera de estas barras, podemos ver el seguimiento de pila correspondiente a la muestra.

Hacer clic en una barra de color muestra el seguimiento de pila en el lado derecho de la ventana de herramientas Hacer clic en una barra de color muestra el seguimiento de pila en el lado derecho de la ventana de herramientas

Los seguimientos de pila de muestras individuales sugieren que la actividad del hilo está asociada con la búsqueda de pruebas. Sin embargo, todavía no vemos el panorama general. Vamos a navegar al hilo ocupado en el gráfico de llama:

Gráfico de llama con dos métodos destacados que ocupan casi todo el ancho del gráfico Gráfico de llama con dos métodos destacados que ocupan casi todo el ancho del gráfico

Los métodos que podrían ser de nuestro interés, JavaTestFinder.findTestsForClass() y KotlinTestFinder.findTestsForClass() , están justo en la parte inferior del gráfico. No tenemos en cuenta los métodos plegados debajo de ellos, ya que no tienen un tiempo propio significativo o ramificación. Ellos controlan el flujo en lugar de realizar cálculos intensos.

Para verificar si estos métodos están realmente relacionados con la ralentización, podemos perfilar un caso no problemático: busque pruebas para una clase con un nombre más realista, por ejemplo, ClassWithALongerName . Luego, veremos qué les sucede a estos métodos usando la vista de diferencias.

Pestaña de la lista de métodos con la consulta 'findTestsForClass' muestra los métodos correspondientes con un 93-95% de diferencia Pestaña de la lista de métodos con la consulta 'findTestsForClass' muestra los métodos correspondientes con un 93-95% de diferencia

La instantánea más reciente contiene entre un 93 y un 95% menos de muestras con JavaTestFinder.findTestsForClass() y KotlinTestFinder.findTestsForClass() . El tiempo de ejecución de los otros métodos no difiere tanto. Parece que vamos en la dirección correcta.

La siguiente pregunta es por qué ocurre esto. Intentemos averiguarlo con el depurador.

¿Por qué tal gran diferencia?

Establecer un punto de interrupción en findTestsForClass() y un poco de hacer un seguimiento a través del código nos lleva al siguiente punto:

MinusculeMatcher matcher = NameUtil.buildMatcher("*" + klassName, NameUtil.MatchingCaseSensitivity.NONE);
    for (String eachName : ContainerUtil.newHashSet(cache.getAllClassNames())) {
        if (matcher.matches(eachName)) {
            for (PsiClass eachClass : cache.getClassesByName(eachName, scope)) {
                if (isTestClass(eachClass, klass) && !processor.process(Pair.create(eachClass, TestFinderHelper.calcTestNameProximity(klassName, eachName)))) {
                    return;
                }
            }
        }
    }
}

El código está filtrando los nombres cortos que están actualmente en la caché usando una expresión regular. Para cada una de las cadenas resultantes, busca las clases correspondientes.

Al hacer un registro de los nombres de las clases después de la condición, obtenemos todas las clases que la pasan.

Diálogo de puntos de interrupción con la siguiente condición: "Buscando la clase:" + eachName y la casilla Suspend limpiada Diálogo de puntos de interrupción con la siguiente condición: "Buscando la clase:" + eachName y la casilla Suspend limpiada

Cuando ejecuté el programa, registró cerca de 25000 clases, ¡un número sorprendentemente grande para un proyecto vacío!

La consola muestra muchas líneas que dicen Buscando la clase: seguido de un nombre de clase La consola muestra muchas líneas que dicen Buscando la clase: seguido de un nombre de clase

Los nombres de las clases registradas claramente provienen de algún otro lugar, no de mi proyecto ‘Hello World’. El misterio se resuelve: IntelliJ IDEA tarda tanto en encontrar pruebas para la clase A , porque verifica todas las clases en caché, incluyendo dependencias, JDKs e incluso clases de otros proyectos. Muchas de ellas pasan el filtro porque todas tienen la letra A en sus nombres. Con nombres de clase más largos y realistas, esta ineficiencia habría pasado desapercibida, simplemente porque la mayoría de estos nombres habrían sido filtrados por la regex.

¿La solución?

Desafortunadamente, no pude encontrar una solución simple y fiable para este problema. Una estrategia potencial sería excluir las dependencias del ámbito de búsqueda. Esto parece viable a primera vista, pero existe la posibilidad de que las dependencias puedan contener pruebas. Esto no sucede muy a menudo, pero aún así, este enfoque rompería la funcionalidad para dichas dependencias.

Un enfoque alternativo es introducir una máscara de archivo *.java, que filtraría las clases compiladas. Si bien funciona bien con Java, se vuelve problemático para las pruebas escritas en otros idiomas, como Kotlin. Incluso si agregamos todos los posibles idiomas, esta característica simplemente fallará silenciosamente para los recién soportados, resultando en una sobrecarga añadida para el mantenimiento y la depuración.

Independientemente del enfoque, la solución justifica una entrada por sí misma, por lo que no la estamos implementando ahora. Lo que sí hicimos, sin embargo, es descubrir la causa raíz de una desaceleración, lo cual es exactamente para lo que se usaría un perfilador.

Compartir la instantánea

Antes de concluir, hay una cosa más que vale la pena discutir. ¿Notaste que utilicé una instantánea tomada en una computadora diferente? Además, la instantánea no solo fue de una máquina diferente. El sistema operativo y la versión de IntelliJ IDEA también eran diferentes.

Una cosa hermosa que a menudo se pasa por alto sobre el perfilador es la facilidad para compartir los datos. La instantánea se escribe en un archivo, que puedes enviar a otra persona (o recibir de alguien). A diferencia de otras herramientas, como el depurador, no necesitas un reproductor completo para comenzar con el análisis. De hecho, ni siquiera necesitas un proyecto compilable para eso.

No te quedes con mi palabra; pruébalo tú mismo. Aquí está la instantánea: idea64_exe_2024_07_22_113311.jfr

all posts ->