Почему VisualVM Sampler не предоставляет полную информацию о загрузке процессора (время выполнения метода)?

Проблема заключается в следующем: сэмплер VisualVM показывает дерево вызовов по времени. Для некоторых методов сэмплер показывает только "Self time", поэтому я не вижу, что делает этот метод медленным. Вот пример.
Как увеличить глубину профилирования?

Ответы

Ответ 1

К сожалению, пробоотборники выборки довольно ограничены, когда дело доходит до углубленного профилирования по ряду причин:

  • Пробоотборники ограничены периодом выборки: например, VisualVM в настоящее время имеет минимальный период выборки 20 мс. Современные процессоры могут выполнять несколько миллионов команд за это время - конечно, более чем достаточно, чтобы называть несколько коротких методов и возвращаться от них.

    Хотя очевидным решением было бы сократить период выборки, это также увеличило бы влияние профилировщика на ваше приложение, представляя приятный пример неопределенности принцип.

  • Пробоотборники легко путаются встроенным кодом: как JVM, так и любой достойный компилятор будут выстраивать тривиальные и/или часто называемые методы, тем самым включая их код в код своего вызывающего. Профилировщики пробоотбора не имеют возможности сказать, какие части каждого метода действительно принадлежат ему и которые принадлежат к встроенным вызовам.

    В случае VisualVM Self time фактически включает время выполнения как метода, так и любого встроенного кода.

  • Сэмплеры могут запутаться с помощью продвинутой виртуальной машины: например, в современных реализациях JVM методы не имеют стабильного представления. Представьте себе, например, следующий метод:

    void A() {
        ...
        B();
        ...
    }
    

    Когда запуск JVM B() интерпретируется прямо из байт-кода, тем самым занимает довольно много времени, что делает его видимым для сэмплера. Затем через некоторое время JVM решает, что B() является хорошим кандидатом для оптимизации и компилирует его в собственный код, что значительно ускоряет его. И еще через некоторое время JVM может решить включить вызов B(), включив его код в A().

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

    В худшем случае эта стоимость может быть назначена на вызов сестры, а не на вызывающего. Например, я в настоящее время профилирую приложение с помощью VisualVM, где точка доступа кажется методом ArrayList.size(). В моей реализации Java этот метод является простым полевым getter, что любая JVM должна быстро встраиваться. Тем не менее, профайлер показывает это как основного потребителя времени, полностью игнорируя кучу соседних вызовов HashMap, которые, очевидно, намного дороже.

Единственный способ избежать этих недостатков - использовать инструментальный профилировщик, а не выборку. Инструментальные профилировщики, например, такие, которые предусмотрены на вкладке Profiler в VisualVM, по существу записывают каждый вход и выход метода в выбранном коде. К сожалению, инструментальные профилировщики довольно сильно влияют на профилированный код:

  • Они вставляют свой контрольный код вокруг каждого метода, который полностью изменяет способ обработки метода JVM. Даже простые методы getter/setter field не могут быть больше привязаны из-за дополнительного кода, что искажает любые результаты. Профайлер обычно пытается объяснить эти изменения, но это не всегда успешно.

  • Они вызывают массовые замедления для профилированного кода, что делает их совершенно непригодными для мониторинга полных приложений.

По этим причинам инструментальные профилирующие устройства в основном подходят для анализа горячих точек, которые уже были обнаружены с использованием другого метода, такого как профилировщик выборки. Используя только выбранный набор классов и/или методов, можно ограничить побочные эффекты профилирования конкретными частями приложения.

Ответ 2

В этом примере нет ничего плохого. Похоже, что updateInfoInDirection() вызывает new SequenceInfo() и SequenceInfo.next(). "Самостоятельное время" означает, что время затрачено на код самих методов (метод updateInfoInDirection() находится в нижней части стека в момент, когда был выбран образец потока).