JEP 435: Asynchronous Stack Trace VM API
API виртуальной машины для асинхронного получения трассировок стека
| Ответственный | Johannes Bechberger |
| Тип | Feature |
| Область | JDK |
| Статус | Closed / Withdrawn |
| Компонент | hotspot / svc |
| Обсуждение | serviceability dash dev at openjdk dot org |
| Трудоёмкость | S |
| Длительность | S |
| Рецензенты | Andrei Pangin, Christoph Langer, Jaroslav Bachorík |
| Создан | 2022/04/04 11:02 |
| Обновлён | 2026/07/30 19:05 |
| Задача | 8284289 |
Аннотация
Определить эффективный и надёжный API для асинхронного сбора трассировок стека, включающих информацию как о Java-фреймах, так и о нативных фреймах стека.
Цели
-
Предоставить профилировщикам хорошо протестированный API для получения информации о Java-фреймах и нативных фреймах.
-
Поддерживать асинхронное использование (например, вызов из обработчиков сигналов) и синхронное использование
-
Не влиять на производительность, когда API не используется.
-
Не увеличивать существенно требования к памяти по сравнению с существующим API
AsyncGetCallTrace.
Мотивация
API AsyncGetCallTrace используют почти все доступные профилировщики, как с открытым исходным кодом, так и коммерческие, в том числе, например, async-profiler. Однако у него есть три крупных недостатка:
- Это внутренний API, который не экспортируется ни в одном заголовочном файле, и
- Он возвращает информацию только о Java-фреймах, а именно их метод и индексы байт-кода.
- Его нельзя использовать для обхода собирать трассировки стека в отдельном потоке, вне обработчика сигналов, чтобы реализовать выборку в стиле JFR.
Из-за этих проблем реализовывать профилировщики и связанные с ними инструменты сложнее. Некоторую дополнительную информацию можно извлечь из HotSpot VM с помощью сложного кода, но другая полезная информация скрыта, и получить её невозможно:
- Встроен ли скомпилированный Java-фрейм (сейчас это можно узнать только для самых верхних скомпилированных фреймов),
- Уровень компиляции Java-фрейма (т. е. скомпилирован он C1 или C2), и
- Информация о фреймах C/C++, которые находятся не на вершине стека.
Такие данные могут быть полезны при профилировании и настройке VM для конкретного приложения, а также при профилировании кода, который активно использует JNI.
Описание
Мы предлагаем новый API AsyncGetStackTrace, построенный по образцу API AsyncGetCallTrace:
void AsyncGetStackTrace(ASGST_CallTrace *trace, jint depth, void* ucontext, uint32_t options);
Профилировщики могут вызывать этот API, чтобы получить трассировку стека потока, но он не гарантирует получение всех фреймов и работает по принципу «насколько возможно». Его реализация будет как минимум так же стабильна, как AsyncGetCallTrace или код обхода стека JFR, благодаря фаззинг-тестам и тестам стабильности в JDK и обширным проверкам безопасности в самой реализации. VM заполняет информацию о фреймах, количество фреймов и вид трассировки. API можно безопасно использовать из отдельного потока, и это рекомендуемый способ использования, но его можно использовать и в обработчике сигналов. Вы явно сообщить API обойти тот же поток с помощью опции ASGST_WALK_SAME_THREAD, при этом предполагается, что переданный ucontext всегда приходит из того же потока. Вызывающий API код должен выделить для массива CallTrace достаточно памяти для запрошенной глубины стека. Обходимые потоки во время обхода стека должны быть остановлены.
Параметры:
trace— буфер для структурированных данных, которые заполняет VMdepth— максимальная глубина трассировки стека вызововucontext—ucontext_tпотока, с которого должен начинаться обход стекаoptions— набор битов для опций
Сейчас учитываются только два младших бита options, все остальные биты считаются равными 0:
enum ASGST_Options {
ASGST_INCLUDE_NON_JAVA_FRAMES = 1,
ASGST_WALK_SAME_THREAD = 2
};
ASGST_INCLUDE_NON_JAVA_FRAMES включает в трассировку не-Java-фреймы, которые иначе пропускаются. ASGST_WALK_SAME_THREAD позволяет пользователю профилировщика обходить стек того же потока, т. е. непосредственно в обработчике сигналов), при этом отключаются защитные механизмы, которые включены только в режиме отдельного потока.
Существуют разные виды трассировок в зависимости от назначения кода, выполняющегося в данный момент в обходимом потоке:
enum ASGST_TRACE_KIND {
ASGST_JAVA_TRACE = 1
};
- ASGST_JAVA_TRACE: вид для полностью функционирующего Java-потока (который выполняет Java-код)
Все остальные виды (всего до 8, значения должны быть степенями двойки) зависят от реализации и не должны представлять трассировки, содержащие Java-фреймы.
Структура trace
typedef struct {
JNIEnv *env_id; // Env where trace was recorded
jint num_frames; // number of frames in this trace,
// (< 0 indicates the frame is not walkable).
uint8_t kind; // kind of the trace, if non zero initialized, it is a bit mask for accepted kinds
jint state; // thread state (jvmti->GetThreadState), if non zero initialized,
// it is a bit mask for accepted states, non Java kind traces are always accepted
// and get state -1
ASGST_CallFrame *frames; // frames that make up this trace. Callee followed by callers.
void* frame_info; // more information on frames
} ASGST_CallTrace;
заполняется VM. Её поле num_frames содержит фактическое количество фреймов в массиве frames или код ошибки. Поле frame_info этой структуры в дальнейшем может использоваться для хранения дополнительной информации, но сейчас оно равно nullptr.
Поля kind и state служат двойной цели: если они ненулевые, это битовые маски допустимых видов и состояний (так же, как в JVMTI GetThreadState), и с их помощью профилировщики могут ограничивать виды получаемых трассировок и состояния обходимых потоков. Если обход прерывается из-за несовпадения вида или состояния, устанавливаются код ошибки ASGST_WRONG_KIND и ASGST_WRONG_STATE. Поле kind содержит корректную информацию, только если не произошло никаких ошибок, кроме ASGST_WRONG_KIND. Поле kind содержит корректную информацию, только если не произошло никаких ошибок, кроме ASGST_WRONG_STATE.
Коды ошибок от 0 до -5 определены следующим образом:
enum ASGST_Error {
ASGST_NO_JAVA_FRAME = 0,
ASGST_THREAD_EXIT = -1, // dying thread
ASGST_NO_THREAD = -2, // related to walking the separate in a separate thread
ASGST_WRONG_STATE = -3, // trace not obtained because of wrong state (is not included in the passed allowed states)
ASGST_WRONG_KIND = -4, // same but with kind
};
Все остальные коды ошибок (< -5) зависят от реализации и должны быть задокументированы каждым поставщиком.
Каждый CallFrame является элементом объединения (union), поскольку информация, которая хранится для Java-фреймов и не-Java-фреймов, различается:
typedef union {
uint8_t type; // to distinguish between JavaFrame and NonJavaFrame
ASGST_JavaFrame java_frame;
ASGST_NonJavaFrame non_java_frame;
} ASGST_CallFrame;
Различаются несколько типов фреймов:
enum ASGST_FrameTypeId {
ASGST_FRAME_JAVA = 1, // JIT compiled and interpreted
ASGST_FRAME_JAVA_INLINED = 2, // inlined JIT compiled
ASGST_FRAME_JAVA_NATIVE = 3, // barrier frames between Java and C/C++
ASGST_FRAME_NON_JAVA = 4 // C/C++/... frames
};
Первые два типа относятся к Java-фреймам, для которых мы храним следующую информацию в структуре типа JavaFrame:
typedef struct {
uint8_t type; // frame type
int8_t comp_level; // compilation level, 0 is interpreted, -1 is undefined, > 1 is JIT compiled
uint16_t bci; // 0 <= bci < 65536, 65535 (= -1) if the bci is >= 65535 or not available (like in native frames)
ASGST_Method method;
} ASGST_JavaFrame; // used for FRAME_JAVA, FRAME_JAVA_INLINED and FRAME_JAVA_NATIVE
comp_level обозначает уровень компиляции метода, связанного с фреймом; смысл этого числа зависит от реализации.
ASGST_Method — зависящий от реализации идентификатор метода, отличный от jmethodID. Для работы с идентификатором метода есть несколько методов, безопасных для вызова из обработчиков сигналов:
struct ASGST_MethodInfo {
char* class_name;
jint class_name_len;
char* generic_class_name;
jint generic_class_name_len;
char* method_name;
jint method_name_len;
char* signature;
jint signature_len;
char* generic_signature;
jint generic_signature_len;
jint modifiers;
};
void ASGST_GetMethodInfo(ASGST_Method method, ASGST_MethodInfo* info);
Получает информацию о методе для заданного ASGST_Method и сохраняет её в заранее выделенной структуре info. Фактическая длина сохраняется в полях _len, а в строковых полях — строка, завершённая нулевым символом. Метод безопасно вызывать из обработчиков сигналов. Поле устанавливается в \0, если информация недоступна.
Преобразование из ASGST_Method в jmethodID доступно через jmethodID ASGST_MethodToJMethodID(ASGST_Method method); и ASGST_Method jMethodIDToASGST_Method(jmethodID method);, но использование этих методов небезопасно в обработчиках сигналов.
Получить jclass для заданного метода можно через jclass ASGST_GetClass(ASGST_Method method);, но нужно учитывать, что этот метод небезопасен в обработчиках сигналов и что время жизни полученного указателя jclass ограничено.
Информация обо всех остальных фреймах хранится в структурах NonJavaFrame:
typedef struct {
uint8_t type; // frame type
void *pc; // current program counter inside this frame, might be a nullptr for JVM internal frames like stub frames, …
} ASGST_NonJavaFrame; // used for FRAME_NON_JAVA
Хотя API предоставляет больше информации, объём памяти на один фрейм (например, 16 байт на x86) такой же, как у существующего API AsyncGetCallTrace.
Мы предлагаем поместить приведённые выше объявления в новый заголовочный файл profile.h, который будет размещён в каталоге include образа JDK. Лицензия заголовочного файла должна включать Classpath Exception, чтобы его могли использовать сторонние инструменты профилирования.
Реализацию можно найти в репозитории jdk-sandbox, а демонстрацию её работы с модифицированным async-profiler — здесь.
Риски и допущения
Возврат информации о фреймах C/C++ раскрывает детали реализации, но это верно и для Java-фреймов AsyncGetCallTrace, поскольку они раскрывают детали реализации файлов стандартной библиотеки и включают фреймы нативных обёрток.
Тестирование
Реализация содержит несколько стресс-тестов и фаззинг-тестов для выявления проблем стабильности на всех поддерживаемых платформах: набор бенчмарков renaissance многократно профилируется методом выборки с малыми интервалами (<= 0,1 мс). Фаззинг-тесты проверяют, что AsyncGetStackTrace можно вызывать с изменёнными указателями стека и фрейма без аварийного завершения VM. Мы также добавили несколько тестов, которые покрывают базовое использование API.
Альтернативы
Предоставить API на основе итератора, который поддерживает обход в безопасных точках (safepoints) и инкрементальную трассировку.