- PVSM.RU - https://www.pvsm.ru -

Инструментирующий профайлер своими руками

Давно хотел написать простую утилиту, которая позволила бы показать в каких методах код проводит основное время, какие объекты и в каком количестве создает и оперативно включать трассировку посещения метода не модифицируя исходник.

Основные требования — простота, возможность запуска в текстовом режиме и независимость от архитектуры.


В большинстве java машин существует Java Virtual Machine Tool Interface (JVM TI) [1].
Но этот интерфейс предполагает написание native модуля, поэтому не рассматривался.

Остается возможность инструментирования (intstrumentation) байткода.
Стандартный пакет java.lang.instrument позволяет вмешаться в процесс загрузки байткода с возможностью его изменения на лету.

Выглядит это следующим образом:
При старте java машины указывается ключ -javaagent:jar-file-name=parameters,
где jar-file-name — это полный путь к jar файлу агента, parameters — параметры передаваемые агенту.

В манифесте jar файла указывается класс содержащий метод

public static void premain(String args, Instrumentation instrumentation)

который jvm и вызовет после загрузки агента.
Подробно про этот механизм можно прочитать здесь [2].

Задача нашего агента состоит в изменении методов загружаемых классов таким образом,
что бы при входе и выходе из метода вызывались методы нашего класса TracerAgent [3]:

    @SuppressWarnings("unused")
    public static void methodEnter(MethodInfo methodInfo) {
        methodCallProcessor.methodEnter(methodInfo);
    }
    @SuppressWarnings("unused")
    public static void methodExit() {
        methodCallProcessor.methodExit();
    }

Экземпляр класса MethodInfo [4] содержит информацию о методе.
Нужно где то хранить MethodInfo для каждого инструментированного метода.
Для этого для каждого класса создается shadow класс следующего вида:

public class ShadowClassXXX {
	public static  MethodInfo m1 = agent.TracerAgent.getMethodInfo(1L); // MethodInfo первого метода
	/* .. */
	public static  MethodInfo m99 = agent.TracerAgent.getMethodInfo(99L); // MethodInfo последнего метода
}

Поскольку поля shadow класса статические, они инициализируются автоматически при загрузке. В начало каждого изменяемого метода добавляется код:

TracerAgent.methodEnter(ShadowClass.mXXX);

в конец

TracerAgent.methodExit();

Для модификации и создания байткода использована библиотека Javassist [5]. Позволяет не сосредотачиваться на байткоде, а писать код модификаций на java.

Теперь при входе и выходе из метода будет перехватываться управление и
передаваться соответствующим методам класса MethodCallProcessor.

MethodCallProcessor [6] хранит стэк вызовов для каждого потока используя ThreadLocal.
methodEnter сохраняет в стэк MethodCallMarker [7] со временем вызова и увеличивает счетчик вызовов.
Если вызов метода нужно трассировать, то выводится стэк вызовов.
methodExit выбирает из стэка MethodCallMarker, и добавляет время проведенное в методе к его MethodInfo.

В конструкторе TracerAgent [3] создается Thread и регистрируется в shutdownhook. Этот Thread будет запущен при shutdown sequence jvm и распечатает собранную статистику. Там же создается и запускается Thread периодического вывода статистики.

Параметры:
-javaagent:<полный путь к jar-файлу агента>=p:regexp[#t:regexp][#d:number][#i:number]
p - инструментируемые классы (полное совпадение - match)
t - трассируемые методы (частичное совпадение - find)
d - уровень отладки 
i - период вывода статистики в секундах (0 отключает периодический вывод)

Пример запуска для кода из статьи [8]:

java -version
java version "1.7.0_21"
Java(TM) SE Runtime Environment (build 1.7.0_21-b11)
Java HotSpot(TM) 64-Bit Server VM (build 23.21-b01, mixed mode)

java -XX:-UseSplitVerifier -javaagent:D:traceragenttargettraceragent-1.0-SNAPSHOT-complete.jar=p:examples.*#d:1#i:5

javassist еще не всегда справляется с новым форматом байткода классфайлов версии 51.
ключ -XX:-UseSplitVerifier отключает новый верификатор байткода, введенный в JVM7, подробности [9].

результат

[AGENT+] *** performance report ***

Top 5 time consuming methods (microseconds):
          965050  examples.data.CashAccountRow$masks.access$100(examples.data.CashAccountRow$masks)
          779944  examples.data.CashAccountRow.setAge(int)
          766023  examples.data.CashAccountRow.setGender(int)
          764292  examples.data.CashAccountRow.setHeight(int)
          763387  examples.data.CashAccountRow.setAmount(int)

Top 5 called methods:
        22805261 examples.data.CashAccountRow$masks.access$100(examples.data.CashAccountRow$masks)
         9122242 examples.data.CashAccountRow$shifts.access$000(examples.data.CashAccountRow$shifts)
         2280592 examples.data.CashAccountRow.setAge(int)
         2280623 examples.data.CashAccountRow.setBitStorage(long)
         2280653 examples.data.CashAccountRow.setGender(int)

Top 5 objects constructed:
               1 examples.data.CashAccountStore
               1 examples.data.CashAccountRow

[AGENT-] *** end of performance report ***

--- skip ---

Number of records matched:38
Elapsed time:7707ms
Used Memory:91MB

[AGENT+] *** performance report ***

Top 10 time consuming methods (microseconds):
        36081913  examples.App.main(java.lang.String[])
        20606259  examples.data.CashAccountStore()
        15428502  examples.data.CashAccountStore.find2(examples.data.CashAccountStore$CashAccountFinder)
        12439241  examples.data.GenMatcherAMOUNTHEIGHTGENDER.c(examples.data.CashAccountRow)
         9469502  examples.data.CashAccountRow.getAmount()
         5928018  examples.data.CashAccountRow$masks.access$100(examples.data.CashAccountRow$masks)
         3392146  examples.data.CashAccountRow$shifts.access$000(examples.data.CashAccountRow$shifts)
         3279163  examples.data.CashAccountRow.setAge(int)
         3271959  examples.data.CashAccountRow.setHeight(int)
         3267890  examples.data.CashAccountRow.setAmount(int)

Top 10 called methods:
       140079808 examples.data.CashAccountRow$masks.access$100(examples.data.CashAccountRow$masks)
        80079812 examples.data.CashAccountRow$shifts.access$000(examples.data.CashAccountRow$shifts)
        40000000 examples.data.CashAccountRow.getAmount()
        30000000 examples.data.CashAccountRow.setBitStorage(long)
        20000000 examples.data.GenMatcherAMOUNTHEIGHTGENDER.c(examples.data.CashAccountRow)
        10000000 examples.data.CashAccountRow.setGender(int)
        10000000 examples.data.CashAccountRow.setAge(int)
        10000000 examples.data.CashAccountRow.setAmount(int)
        10000000 examples.data.CashAccountRow.setHeight(int)
        10000000 examples.data.CashAccountRow.getBitStorage()

Top 10 objects constructed:
               3 examples.data.CashAccountStore$CashAccountFinder$FieldGetter
               3 examples.data.CashAccountStore$CashAccountFinder$PredicateHolder
               3 examples.data.CashAccountRow
               1 examples.data.GenMatcherAMOUNTHEIGHTGENDER
               1 examples.data.GenMatcherBase
               1 examples.data.CashAccountStore$CashAccountFinder$HeightFieldGetter
               1 examples.data.CashAccountStore$CashAccountFinder
               1 examples.data.CashAccountStore
               1 examples.data.CashAccountStore$CashAccountFinder$AmountFieldGetter
               1 examples.data.CashAccountStore$CashAccountFinder$GenderFieldGetter

[AGENT-] *** end of performance report ***

Естественно вызов методов агента не мог не сказаться на быстродействии.
Как видно время работы увеличилось почти в 500 раз.
Hо это экстремальный случай, где вызываются методы с практически пустым телом, в обычной жизни с оверхедом можно мириться.

Целью является не столько точный профайлинг по времени, сколько точное кол-во вызовов,
созданных объектов и возможность трассировки. Код доступен на GitHub [10].

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

Автор: xlix123

Источник [11]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/37641

Ссылки в тексте:

[1] Java Virtual Machine Tool Interface (JVM TI): http://docs.oracle.com/javase/7/docs/platform/jvmti/jvmti.html

[2] здесь: http://nkoksharov.blogspot.ru/2008/11/javaagent.html

[3] TracerAgent: https://github.com/scanban/traceragent/blob/master/src/main/java/agent/TracerAgent.java

[4] MethodInfo: https://github.com/scanban/traceragent/blob/master/src/main/java/agent/MethodInfo.java

[5] Javassist: http://www.jboss.org/javassist

[6] MethodCallProcessor: https://github.com/scanban/traceragent/blob/master/src/main/java/agent/MethodCallProcessor.java

[7] MethodCallMarker: https://github.com/scanban/traceragent/blob/master/src/main/java/agent/MethodCallMarker.java

[8] статьи: http://habrahabr.ru/post/182788/

[9] подробности: http://chrononsystems.com/blog/java-7-design-flaw-leads-to-huge-backward-step-for-the-jvm.

[10] GitHub: https://github.com/scanban/traceragent

[11] Источник: http://habrahabr.ru/post/185010/