Плагин Profiling Tools для исследования Java-приложения в OpenIDE

Иногда Java-приложение ведёт себя странно: потребляет больше памяти, чем ожидалось, тормозит без видимой причины или будто «ничего не делает», хотя запросы всё равно обрабатываются медленно. В такие моменты хочется быстро посмотреть «внутрь» Java-приложения, например, сколько памяти оно потребляет, какие потоки в нём запущены и что с ними происходит.
Первое, что приходит в голову, это VisualVM или JDK Mission Control. Однако во время разработки приложения удобнее не переключаться между инструментами, а открыть состояние процесса прямо в IDE.
Для этого мы в OpenIDE выпустили Profiling Tools — плагин для мониторинга и исследования Java-приложений. Его разработкой занималась команда Axiom JDK.
С помощью Profiling Tools можно:
Просмотреть список локально запущенных JVM-приложений и краткую информацию о них (PID/версия JVM/аргументы JVM/Системные свойства).
Подключиться к уже запущенному или запускаемому через run-конфигурацию приложению.
Просмотреть график использования CPU, Heap Memory, Non-Heap Memory и GC Load.
Просмотреть список потоков с возможностью фильтрации по имени и состоянию (RUNNABLE/WAITING и другие) и сортировки по загрузке CPU и выделяемой памяти.
Просмотреть стектрейс (stack trace) интересующих потоков с возможностью навигации к месту в коде в редакторе.
Это не замена полноценному профилированию во всех случаях. Скорее, это быстрый способ понять, что происходит с приложением прямо сейчас. Оно действительно что-то считает? Ждёт ответа от внешнего сервиса? Или вообще тормозит из-за соседнего процесса.
Как выбрать Java-процесс
С помощью плагина вы можете посмотреть не только на то, что вы только что запустили в OpenIDE, но и на любой другой JVM-процесс, запущенный на вашем компьютере. Это касается как тех процессов, что вы запустили из OpenIDE, так и тех, что запустили из консоли. Исключение — процессы, запущенные внутри Docker: их плагин сейчас не показывает.
Работать удобнее с приложением, которое запущено именно из OpenIDE. В этом случае плагин может не только показать стектрейс, но и перейти из него к нужной строке исходного кода.
В списке процессов видно имя Java-приложения и PID. Например, на скриншоте ниже видно, что одновременно запущены:
com.example.fib.FibApplication — приложение, стартовавшее из OpenIDE. Это спринговый сервис, который генерирует числа Фибоначчи и отдаёт через REST.
fib-0.0.1-SNAPSHOT.jar — то же приложение, но запущенное из консоли командой
java -jar.

После выбора процесса можно посмотреть базовую информацию о нём, графики и потоки.
Что можно узнать о процессе
На вкладке Overview можно увидеть следующую информацию:
PID — идентификатор процесса в операционной системе.
Main class — главный класс приложения.
Arguments — аргументы, которые были переданы приложению, а не JVM, при запуске. В нашем примере сервису передан аргумент --debug, чтобы сделать логи более подробными.
Java version — версия JDK.
Java runtime — версия Java-рантайма.
JVM info — информация о JVM.
Java Home — директория в которой лежит JDK —
JAVA_HOME.
На соседней вкладке "JVM arguments" вы увидите аргументы для JVM.

-Xmx2g показывает, что приложению отведено 2 ГБ для использования кучи (heap).
-Xms1g показывает, что при запуске приложение зарезервирует для кучи 1 ГБ.
Там же вы увидите аргументы, которые начинаются с -D, например, -Dfile.encoding, -Dsun.stdout.encoding, -Dsun.stderr.encoding.
На вкладке "System properties" вы увидите список значений системных свойств для текущей JVM, например, java.vm.compressedOopsMode, значение которого вы можете задать самостоятельно, или неизменяемое свойство java.vm.vendor, которое встроено при сборке JVM и другие.

Это не самая зрелищная часть инструмента, но она полезна в повседневной разработке. Иногда нужно быстро понять, с какими аргументами стартовал процесс, какой JAVA_HOME он использует и какие системные свойства реально применились. Смотреть это в одном месте удобнее, чем собирать по логам и настройкам запуска.
Как читать графики: CPU, GC, heap и non-heap
О вашем приложении можно узнать гораздо больше, чем просто список свойств и значения настроек. Сделав двойной клик по названию процесса из списка, вы перейдёте на экран "Charts" с графиками:
CPU (JVM/Total) — использования CPU.
GC Load — работы сборщика мусора (GC).
Heap Memory — использования кучи.
Non-Heap Memory — использования памяти вне кучи.

На скриншоте выше показаны данные за последние 60 секунд (Show Last 60 Seconds). Чтобы посмотреть данные за последние пять минут (Show Last 5 minutes) или от начала мониторинга (Show All Data), кликните на иконку глаза 👁️ под названием вкладки и выберите нужный временной диапазон:

Графики для приложения на Spring, которое не реализует никакую бизнес-логику (то есть “ничего не делает”), выглядят следующим образом:

Размер кучи постоянно растёт, потому что мы наблюдаем за приложением. Для этого приложение отправляет нам некоторые данные. Из-за этого вы видите использование кучи на графике.
Также обратите внимание, что на всех графиках выделен один и тот же момент времени: размер кучи достиг 610 МБ, а сборщик мусора только начал работать. Если вы переместите курсор на какую-то точку по оси времени на одном графике, то на остальных графиках отобразится этот же момент времени. Это удобно.
Расскажем про то, зачем нужен каждый график и что конкретно он показывает. Для того, чтобы было понятнее, подадим нагрузку на приложение и покажем, как это выглядит на графиках.
Вот как выглядит общая картина на момент, когда нагрузка какое-то время была, но закончилась:

А теперь пройдём по отдельным пунктам.
График использования процессора (CPU (JVM / Total))
На графике использования процессора (он в левом верхнем углу) можно увидеть две кривых — Total и JVM.
Total показывает общую загрузку процессора в процентах. Например, если вы помимо Java-приложения запустите у себя рендеринг видео, то значение Total вырастет, даже если Java-приложение почти ничего не делает.
JVM показывает какую часть (в процентах) процессора использует именно то приложение, за которым вы сейчас наблюдаете. Поэтому, когда приложение, например, делает запросы в базу данных, то значение показателя JVM растёт, а когда приложение ничего не делает, то стремится к нулю.
Есть нюанс: если один поток постоянно загружает одно ядро на 100%, график для всего процессора не покажет 100%. Он покажет 100%, делённые на количество логических ядер. Например, если у процессора 22 логических ядра, то полная загрузка одного ядра будет выглядеть как примерно 4–5% общей загрузки CPU.
График работы сборщика мусора (GC)
График GC Load (правый верхний угол) показывает, какую долю последней секунды JVM потратила на работу сборщика мусора.
То, что на нём отражено демонстрирует, что в основном сборщик мусора спокойно “спит” и иногда освобождает память, которую приложение выделило куче.
Так и должно быть, это нормально. Для того, чтобы показать как выглядит график для сборщика мусора, который много работает, нужно подать другую, более сложную нагрузку.
График использования кучи (Heap Memory)
Куча — это область памяти, в которой Java-приложение хранит "живые" объекты. Размер кучи ограничен параметром -Xmx. Но есть два параметра, которые могут меняться в процессе работы.
Во-первых, это объём памяти, который занимают объекты, созданные приложением. Сюда входят как объекты, которые могут использоваться приложением, так и объекты, которые приложению уже не доступны, но сборщик мусора их ещё не удалил. Этот показатель может меняться достаточно динамично и именно его мы показываем на графике.
Есть ещё объём памяти, который JVM зарезервировала под кучу. Он стартует с -Xms, может расти до -Xmx, а при определённых условиях сборщик мусора может вернуть часть этой памяти операционной системе. Но в большинстве повседневных случаев интереснее смотреть именно на занятость кучи объектами: этот показатель лучше показывает, как приложение реально выделяет память.
Любой мониторинг немного влияет на приложение: чтобы показать метрики, процессу нужно собрать и передать данные. В нашем случае это видно на графике Heap Memory: память постепенно растёт, а затем освобождается сборщиком мусора.
Это не признак утечки. Если размер хипа растёт, потом после сборки мусора падает это нормальная картина. Если же кривая растёт без заметных спадов, тогда уже стоит подозревать проблему.
График использования памяти вне кучи (Non-Heap Memory)
Non-heap — это память, в которой находятся загруженные классы приложения, пулы констант и кеш, в который JIT кладёт сгенерированный код.
На графике эта память занимает 50 МБ и её объём не меняется. Однако если приложение создаёт новые классы и не может их выгрузить, то график будет неуклонно идти вверх.
Потоки: состояние, нагрузка и стектрейсы
Во вкладке Threads находится список потоков приложения. Cтандартное Spring-приложение создаёт столько потоков, что они с трудом помещаются на экране:

У каждого потока можно посмотреть несколько параметров.
Название потока (Thread Name)
В колонке Thread Name находится название потока. Если вы создаёте потоки самостоятельно, лучше давать им понятные имена. Потом это сильно упрощает диагностику.
Например, потоки с именами вида http-nio-8081-exec-* создаёт Embedded Tomcat. Именно в них обычно выполняются обработчики HTTP-запросов, то есть в нашем случае — методы контроллеров.
Состояние потока (Thread State)
В колонке Thread State отображается состояние потока. В Java поток может находиться в одном из состояний:
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED.
В плагине вы обычно не увидите NEW и TERMINATED: первое состояние бывает до запуска потока, второе — после завершения. Такие потоки в рабочий список не попадают.
Нагрузка на процессор (CPU Load)
Колонка CPU Load показывает, насколько активно поток использует процессор.
Один поток не может выполнять вычисления сразу на нескольких ядрах, поэтому проценты в этой колонке относятся к одному ядру. Если в течение секунды поток полсекунды выполнял вычисления, а полсекунды ждал ответа от базы данных, его CPU Load будет около 50%.
Из-за этого может возникнуть на первый взгляд странная ситуация: в списке потоков один поток показывает несколько процентов CPU Load, а на общем графике JVM почти не нагружает процессор. Это нормально. Хотя в списке потоков видно, что RMI TCP Connection(1)-192.168.1.39 в колонке CPU Load показывает почти 3%. На 3% загружено одно ядро, а весь процессор загружен меньше, чем на один процент.
Выделенная память (Allocated Bytes)
В колонке "Allocated Bytes" отображается количество памяти, которое выделил поток.
Это не то же самое, что количество объектов, с которыми он работает сейчас. Если сборщик мусора соберёт все объекты, которые когда-то выделил поток, то показатель в колонке Allocated Bytes не изменится. После того, как объект создан, его связь с породившим его потоком исчезает и отследить сколько объектов, созданных конкретным потоком, уже удалены из памяти, не получится.
Уже упомянутый RMI TCP Connection(1)-192.168.1.39, например, выделил 20 МБ, но позже это значение будет только расти, потому что этот поток передаёт данные плагину в течение всего времени работы приложения. Как эти объекты видны на графике использования кучи, вы можете посмотреть выше.
Фильтрация по имени потока
Если потоков много, их можно фильтровать.
Чтобы отфильтровать потоки по вхождению подстроки в имя потока, наведите курсор на увеличительное стекло:

Кликните по увеличительному стеклу, и заголовок колонки "Thread Name" исчезнет, а на его месте появится поле ввода, где можно напечатать строку, по которой вы хотите отфильтровать потоки. На скриншоте вы видите потоки, в названии которых есть подстрока http. Это потоки, запущенные Embedded Tomcat.

В VisualVM, другом широко известном средстве профилирования, например, нет возможности сделать фильтрацию потоков по имени (по крайней мере из коробки), только найти поток по имени.
Фильтрация по статусу
Вы можете отфильтровать потоки по статусу. Для этого надо кликнуть по иконке с фильтром (справа от иконки с увеличительным стеклом). На скриншоте ниже показаны работающие потоки.

Просмотр стектрейса потока
Чтобы посмотреть стектрейс потока:
1. Кликните на иконку чекбокса (check box) справа от иконки фильтра. Рядом с каждым потоком появится чекбокс.

2. Поставьте галочку напротив того потока, у которого хотите посмотреть стектрейс.

В стектрейсе потока, обрабатывающего HTTP-запрос, ожидаешь встретить метод контроллера, но конкретно в этом стектрейсе его нет. Эндпойнт, который обрабатывает этот поток, вычисляет первые 100 тыс. чисел Фибоначчи, а потом возвращает их в формате JSON-массива.
Первые 100 тыс. чисел Фибоначчи генерируются быстро. Основное время занимает преобразование полученного списка BigInteger в формат JSON. Именно этот процесс загружает ядро почти на 100%.
Переход из стектрейса в код
Profiling Tools позволяет перейти из стектрейса прямо к конкретному месту в исходном коде.
Для демонстрации удобно сделать так, чтобы выполнение надолго задержалось в методе контроллера. Самый простой способ — вызвать Thread.sleep(). Поток перейдёт в состояние TIMED_WAITING, и его будет легко найти в списке.
После этого можно кликнуть по соответствующей строке в стектрейсе, и OpenIDE откроет место, где сейчас находится выполнение.

Переходить можно не только по своему коду, но и по коду платформы, потому что он открыт.
Просмотр стектрейсов нескольких потоков
Ещё одна полезная возможность — смотреть стектрейсы нескольких выбранных потоков, а не весь thread dump целиком. В уже упомянутом VisualVM, обычно приходится смотреть полный thread dump. В ежедневной диагностике возможность выбрать только интересующие потоки экономит время.

Быстрые проверки: аргументы запуска, свойства и рабочая директория
Иногда нужно ответить на простой вопрос: из какой рабочей директории запущено приложение.
На первый взгляд всё очевидно: из какой директории запустили процесс, такая директория и будет рабочей. На сервере в проде это обычно определяется настройками окружения и процессами деплоя. Но локально, особенно при запуске из IDE, всё не всегда так очевидно.
В одномодульном Maven-проекте рабочей директорией обычно будет директория, где находится pom.xml.
В многомодульном проекте интереснее.
Запускаем тесты и смотрим на список процессов, который запустит maven.

Оказывается, Maven запускает не один процесс, а целых два. Первый — это общий maven-процесс. Он запускается в той директории, в которой находится родительский модуль.
А второй процесс — это как раз то, что запускает unit-тесты в конкретном модуле. И он уже запускается в той директории, в которой находится файл pom.xml конкретного модуля.

Это можно выяснять через документацию и логи, а можно просто открыть свойства процесса в Profiling Tools и посмотреть фактическое значение.
Когда процессор нагружен не Java-приложением
Бывает, что сервис работает медленно, хотя видимых причин у него для этого нет. И по исходному коду бывает сложно понять, что не так. Однако если посмотреть в график загруженности процессора, то всё становится яснее. Напомню, что в этом графике две кривые: кривая, которая показывает насколько вообще загружен процессор и кривая, которая показывает насколько он загружен Java-процессом. Посмотрим на этот график:

Тут видно, что большая часть нагрузки на процессор приходится не на JVM, а на какое-то другое приложение. Это приложение — Python-процесс для распознавания речи, который использует нативные библиотеки.
Казалось бы, зачем нам понадобилось смотреть на график, когда понятно, что такое приложение, будучи запущенным на одной машине с Java-процессом, приведёт к тому, что Java будет работать медленно. Но до этого всё работало на GPU, проблема возникла недавно. Само распознавание выполнялось на GPU, но отдельный этап — определение, кто из участников диалога произносит конкретные реплики, — стал выполняться на CPU. Этот этап обычно называют диаризацией, то есть разделением реплик по говорящим.
Сначала это не выглядело критичным. Но после распараллеливания диаризации процесс начал стабильно забирать значительную часть процессорного времени. На графике это видно сразу: общая загрузка CPU поднялась почти до 40%, а линия JVM осталась около нуля.
Получается, что Java-сервис тормозил не потому, что активно работал сам, а потому что конкурировал за ресурсы с соседним процессом.
Если смотреть только на JVM, можно решить, что приложение «ничего не делает». Если смотреть только на общий CPU, можно ошибочно обвинить Java-процесс. А вместе эти две метрики быстро показывают, где искать проблему.
Приложение работает или просто ждёт?
Может быть так, что сервис работает медленно, хотя ничего такого в фоне не запущено.
Пример из жизни: сгенерированный ИИ нагрузочный скрипт должен был делать 30 запросов в минуту, то есть примерно по одному запросу каждые две секунды.
Через минуту после запуска прошло только четыре запроса. Выглядело так, будто сервис обрабатывает запросы слишком долго, а значит, проблема где-то в коде.
Но график CPU показал, что процессор почти не загружен. Можно было предположить, что ядер много и нагрузка на одно ядро просто теряется на общем графике. Тогда стоит посмотреть на потоки Tomcat: если сервис действительно постоянно что-то делает, среди http-nio-*-exec-* должен быть поток в состоянии RUNNABLE.
Но все exec-потоки были в состоянии WAITING.
Значит, приложение не было нагружено и не могло «обрабатывать запрос так долго».

Для сравнения: вот как выглядит список потоков в котором один активно работает.

Когда запрос попробовали сделать вручную, оказалось, что он выполняется за миллисекунды.
Оказалось, что ИИ для того, чтобы отправлять запросы каждые две секунды, сделал bash-скрипт. Этот скрипт должен был отправлять запросы каждые две секунды, но ненадёжно измерял время. В результате между запросами могло пройти не две секунды, а двадцать или даже две минуты.
Если приложение «не справляется с нагрузкой», сначала стоит убедиться, что нагрузка действительно до него доходит.
Если поток ждёт внешний сервис?
На этом месте можно возразить: если поток в состоянии WAITING, это не всегда значит, что приложение ничего не делает.
HTTP-обработчик может ждать ответ от соседнего сервиса, базы данных или другого внешнего ресурса. С точки зрения бизнес-логики он находится внутри обработки запроса, но с точки зрения JVM поток действительно ждёт.
Например, метод контроллера может в середине работы сделать HTTP-запрос к сервису, который специально отвечает с задержкой.

Большую часть времени поток, связанный с HTTP-обработчиком, будет находиться в WAITING, хотя запрос пользователя ещё не завершён.
Как отличить поток, который просто ждёт новую задачу, от потока, который ждёт ответ внешнего сервиса? По стектрейсу.
Поток, который просто ждёт новый HTTP-запрос, выглядит коротко и обычно упирается в очередь задач Tomcat:
http-nio-8080-exec-7
Unsafe.park (jdk.internal.misc)
LockSupport.park:370 (java.util.concurrent.locks)
AbstractQueuedSynchronizer$ConditionNode.block:521 (java.util.concurrent.locks)
ForkJoinPool.unmanagedBlock:4365 (java.util.concurrent)
ForkJoinPool.managedBlock:4311 (java.util.concurrent)
AbstractQueuedSynchronizer$ConditionObject.await:1753 (java.util.concurrent.locks)
LinkedBlockingQueue.take:436 (java.util.concurrent)
TaskQueue.take:109 (org.apache.tomcat.util.threads)
TaskQueue.take:33 (org.apache.tomcat.util.threads)
ThreadPoolExecutor.getTask:887 (org.apache.tomcat.util.threads)
ThreadPoolExecutor.runWorker:934 (org.apache.tomcat.util.threads)
ThreadPoolExecutor$Worker.run:481 (org.apache.tomcat.util.threads)
TaskThread$WrappingRunnable.run:58 (org.apache.tomcat.util.threads)
Thread.runWith:1488 (java.lang)
Thread.run:1475 (java.lang)
А поток, который уже обрабатывает запрос и ждёт HTTP-ответ, выглядит иначе. В стектрейсе появляется код Spring-клиента и метод контроллера:
http-nio-8080-exec-6
Unsafe.park (jdk.internal.misc)
LockSupport.park:224 (java.util.concurrent.locks)
CompletableFuture$Signaller.block:1886 (java.util.concurrent)
ForkJoinPool.unmanagedBlock:4365 (java.util.concurrent)
ForkJoinPool.managedBlock:4311 (java.util.concurrent)
CompletableFuture.waitingGet:1920 (java.util.concurrent)
CompletableFuture.get:2094 (java.util.concurrent)
JdkClientHttpRequest.executeInternal:127 (org.springframework.http.client)
AbstractStreamingClientHttpRequest.executeInternal:88 (org.springframework.http.client)
AbstractClientHttpRequest.execute:81 (org.springframework.http.client)
DefaultRestClient$DefaultRequestBodyUriSpec.exchangeInternal:615 (org.springframework.web.client)
DefaultRestClient$DefaultRequestBodyUriSpec.exchange:573 (org.springframework.web.client)
RestClient$RequestHeadersSpec.exchange:748 (org.springframework.web.client)
DefaultRestClient$DefaultResponseSpec.executeAndExtract:925 (org.springframework.web.client)
DefaultRestClient$DefaultResponseSpec.toBodilessEntity:889 (org.springframework.web.client)
FibonacciController.fibonacci:46 (com.example.fib)
LambdaForm$DMH/0x0000000009418000.invokeVirtual (java.lang.invoke)
LambdaForm$MH/0x0000000009419400.invoke (java.lang.invoke)
LambdaForm$MH/0x0000000009042400.invokeExact_MT (java.lang.invoke)
DirectMethodHandleAccessor.invokeImpl:157 (jdk.internal.reflect)
DirectMethodHandleAccessor.invoke:105 (jdk.internal.reflect)
Method.invoke:566 (java.lang.reflect)
InvocableHandlerMethod.doInvoke:253 (org.springframework.web.method.support)
InvocableHandlerMethod.invokeForRequest:185 (org.springframework.web.method.support)
ServletInvocableHandlerMethod.invokeAndHandle:118 (org.springframework.web.servlet.mvc.method.annotation)
RequestMappingHandlerAdapter.invokeHandlerMethod:935 (org.springframework.web.servlet.mvc.method.annotation)
RequestMappingHandlerAdapter.handleInternal:854 (org.springframework.web.servlet.mvc.method.annotation)
AbstractHandlerMethodAdapter.handle:87 (org.springframework.web.servlet.mvc.method)
DispatcherServlet.doDispatch:964 (org.springframework.web.servlet)
DispatcherServlet.doService:867 (org.springframework.web.servlet)
FrameworkServlet.processRequest:1001 (org.springframework.web.servlet)
FrameworkServlet.doGet:893 (org.springframework.web.servlet)
HttpServlet.service:623 (jakarta.servlet.http)
FrameworkServlet.service:875 (org.springframework.web.servlet)
HttpServlet.service:711 (jakarta.servlet.http)
ApplicationFilterChain.doFilter:129 (org.apache.catalina.core)
WsFilter.doFilter:54 (org.apache.tomcat.websocket.server)
ApplicationFilterChain.doFilter:108 (org.apache.catalina.core)
RequestContextFilter.doFilterInternal:101 (org.springframework.web.filter)
OncePerRequestFilter.doFilter:117 (org.springframework.web.filter)
ApplicationFilterChain.doFilter:108 (org.apache.catalina.core)
FormContentFilter.doFilterInternal:94 (org.springframework.web.filter)
OncePerRequestFilter.doFilter:117 (org.springframework.web.filter)
ApplicationFilterChain.doFilter:108 (org.apache.catalina.core)
CharacterEncodingFilter.doFilterInternal:200 (org.springframework.web.filter)
OncePerRequestFilter.doFilter:117 (org.springframework.web.filter)
ApplicationFilterChain.doFilter:108 (org.apache.catalina.core)
StandardWrapperValve.invoke:166 (org.apache.catalina.core)
StandardContextValve.invoke:78 (org.apache.catalina.core)
AuthenticatorBase.invoke:493 (org.apache.catalina.authenticator)
StandardHostValve.invoke:114 (org.apache.catalina.core)
ErrorReportValve.invoke:84 (org.apache.catalina.valves)
StandardEngineValve.invoke:73 (org.apache.catalina.core)
CoyoteAdapter.service:342 (org.apache.catalina.connector)
Http11Processor.service:398 (org.apache.coyote.http11)
AbstractProcessorLight.process:64 (org.apache.coyote)
AbstractProtocol$ConnectionHandler.process:904 (org.apache.coyote)
NioEndpoint$SocketProcessor.doRun:1802 (org.apache.tomcat.util.net)
SocketProcessorBase.run:53 (org.apache.tomcat.util.net)
ThreadPoolExecutor.runWorker:947 (org.apache.tomcat.util.threads)
ThreadPoolExecutor$Worker.run:481 (org.apache.tomcat.util.threads)
TaskThread$WrappingRunnable.run:58 (org.apache.tomcat.util.threads)
Thread.runWith:1488 (java.lang)
Thread.run:1475 (java.lang)
Определить, что мы ждём чего-то, связанного с HTTP-взаимодействием, можно по этой строке
JdkClientHttpRequest.executeInternal:127 (org.springframework.http.client)
Обратите внимание, что явного ожидания I/O в стектрейсе нет, есть только блокировка на ожидании ответа от какого-то Future. Кажется, можно определить, что в потоке происходит какое-то I/O просто увидев в стектрейсе InputStream::read или что-то в этом духе, но оказывается нет. Надо делать эвристики и искать в стектрейсе например, как в нашем случае, конкретно JdkClientHttpRequest.executeInternal.
В будущем мы хотели бы показывать это прямо в интерфейсе: не просто «поток ждёт», а «поток ждёт ответ от HTTP-сервиса» или «ждёт базу данных».
Что есть в OpenIDE Pro
То, что я перечислил в статье это, скорее, не про профилирование, а про исследование приложения. Лёгкий мониторинг (если можно так выразиться) приложения: посмотреть графики, потоки, стектрейсы, аргументы запуска и состояние процесса. В OpenIDE Pro возможностей больше. Профайлер умеет:
Показывать процент времени, который код занимается выполнением определённой строки.
Читать JFR-файлы.
Строить дерево JFR-событий.
Показывать дерево вызовов.
Показывать список горячих методов.
Рисовать flame graph.
Фичи в будущих релизах OpenIDE
В ближайшем будущем мы хотим добавитьнесколько возможностей, которых сейчас не хватает в ежедневной работе:
Показывать какой GC сейчас используется.
Копировать стектрейсы для всех выделенных галочкой потоков сразу, а не по одному.
Раскрывать стектрейсы выделенных потоков для просмотра.
Ждите новых релизов. Пока же можно запустить свои приложения и посмотреть что происходит внутри. Вполне возможно, что вы увидите что-то неожиданное, и вам захочется понять как сложилась такая ситуация. В наше время, когда LLM может дать все ответы, вопросы сильно выросли в цене. Хорошо, что есть инструмент, который даёт повод их сформулировать.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.