Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 46 additions & 14 deletions ddprof-lib/src/main/cpp/flightRecorder.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Copyright The async-profiler authors
* Copyright 2025, Datadog, Inc.
* Copyright 2025, 2026 Datadog, Inc.
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down Expand Up @@ -43,10 +43,22 @@
static const char *const SETTING_RING[] = {NULL, "kernel", "user", "any"};
static const char *const SETTING_CSTACK[] = {NULL, "no", "fp", "dwarf", "lbr"};

static void deallocateLineNumberTable(void *ptr) {}

SharedLineNumberTable::~SharedLineNumberTable() {
VM::jvmti()->Deallocate((unsigned char *)_ptr);
// Always attempt to deallocate if we have a valid pointer
// JVMTI spec requires that memory allocated by GetLineNumberTable
// must be freed with Deallocate
if (_ptr != nullptr) {
jvmtiEnv *jvmti = VM::jvmti();
if (jvmti != nullptr) {
jvmtiError err = jvmti->Deallocate((unsigned char *)_ptr);
// If Deallocate fails, log it for debugging (this could indicate a JVM bug)
// JVMTI_ERROR_ILLEGAL_ARGUMENT means the memory wasn't allocated by JVMTI
// which would be a serious bug in GetLineNumberTable
if (err != JVMTI_ERROR_NONE) {
TEST_LOG("Unexpected error while deallocating linenumber table: %d", err);
}
}
}
}

void Lookup::fillNativeMethodInfo(MethodInfo *mi, const char *name,
Expand Down Expand Up @@ -151,24 +163,44 @@ void Lookup::fillJavaMethodInfo(MethodInfo *mi, jmethodID method,
jvmti->GetMethodName(method, &method_name, &method_sig, NULL) == 0) {

if (first_time) {
jvmti->GetLineNumberTable(method, &line_number_table_size,
jvmtiError line_table_error = jvmti->GetLineNumberTable(method, &line_number_table_size,
&line_number_table);
// Defensive: if GetLineNumberTable failed, clean up any potentially allocated memory
// Some buggy JVMTI implementations might allocate despite returning an error
if (line_table_error != JVMTI_ERROR_NONE) {
if (line_number_table != nullptr) {
// Try to deallocate to prevent leak from buggy JVM
jvmti->Deallocate((unsigned char *)line_number_table);
}
line_number_table = nullptr;
line_number_table_size = 0;
}
}

// Check if the frame is Thread.run or inherits from it
if (strncmp(method_name, "run", 4) == 0 &&
strncmp(method_sig, "()V", 3) == 0) {
jclass Thread_class = jni->FindClass("java/lang/Thread");
jmethodID equals = jni->GetMethodID(jni->FindClass("java/lang/Class"),
"equals", "(Ljava/lang/Object;)Z");
jclass klass = method_class;
do {
entry = jni->CallBooleanMethod(Thread_class, equals, klass);
jniExceptionCheck(jni);
if (entry) {
break;
jclass Class_class = jni->FindClass("java/lang/Class");
if (Thread_class != nullptr && Class_class != nullptr) {
jmethodID equals = jni->GetMethodID(Class_class,
"equals", "(Ljava/lang/Object;)Z");
if (equals != nullptr) {
jclass klass = method_class;
do {
entry = jni->CallBooleanMethod(Thread_class, equals, klass);
if (jniExceptionCheck(jni)) {
entry = false;
break;
}
if (entry) {
break;
}
} while ((klass = jni->GetSuperclass(klass)) != NULL);
}
} while ((klass = jni->GetSuperclass(klass)) != NULL);
}
// Clear any exceptions from the reflection calls above
jniExceptionCheck(jni);
} else if (strncmp(method_name, "main", 5) == 0 &&
strncmp(method_sig, "(Ljava/lang/String;)V", 21)) {
// public static void main(String[] args) - 'public static' translates
Expand Down
11 changes: 11 additions & 0 deletions ddprof-lib/src/main/cpp/vmEntry.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
* Copyright The async-profiler authors
* Copyright 2026, Datadog, Inc.
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down Expand Up @@ -514,6 +515,16 @@ void VM::loadMethodIDs(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass) {
}
}

// CRITICAL: GetClassMethods must be called to preallocate jmethodIDs for AsyncGetCallTrace.
// AGCT operates in signal handlers where lock acquisition is forbidden, so jmethodIDs must
// exist before profiling encounters them. Without preallocation, AGCT cannot identify methods
// in stack traces, breaking profiling functionality.
//
// JVM-internal allocation: This triggers JVM to allocate jmethodIDs internally, which persist
// until class unload. High class churn causes significant memory growth, but this is inherent
// to AGCT architecture and necessary for signal-safe profiling.
//
// See: https://mostlynerdless.de/blog/2023/07/17/jmethodids-in-profiling-a-tale-of-nightmares/
jint method_count;
jmethodID *methods;
if (jvmti->GetClassMethods(klass, &method_count, &methods) == 0) {
Expand Down
25 changes: 22 additions & 3 deletions ddprof-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def addCommonTestDependencies(Configuration configuration) {
configuration.dependencies.add(project.dependencies.create('org.lz4:lz4-java:1.8.0'))
configuration.dependencies.add(project.dependencies.create('org.xerial.snappy:snappy-java:1.1.10.1'))
configuration.dependencies.add(project.dependencies.create('com.github.luben:zstd-jni:1.5.5-4'))
configuration.dependencies.add(project.dependencies.create('org.ow2.asm:asm:9.6'))
configuration.dependencies.add(project.dependencies.project(path: ":ddprof-test-tracer"))
}

Expand Down Expand Up @@ -277,9 +278,27 @@ tasks.withType(Test).configureEach {
def keepRecordings = project.hasProperty("keepJFRs") || Boolean.parseBoolean(System.getenv("KEEP_JFRS"))
environment("CI", project.hasProperty("CI") || Boolean.parseBoolean(System.getenv("CI")))

jvmArgs "-Dddprof_test.keep_jfrs=${keepRecordings}", '-Djdk.attach.allowAttachSelf', '-Djol.tryWithSudo=true',
"-Dddprof_test.config=${config}", "-Dddprof_test.ci=${project.hasProperty('CI')}", "-Dddprof.disable_unsafe=true", '-XX:ErrorFile=build/hs_err_pid%p.log', '-XX:+ResizeTLAB',
'-Xmx512m', '-XX:OnError=/tmp/do_stuff.sh', "-Djava.library.path=${outputLibDir.absolutePath}"
// Base JVM arguments
def jvmArgsList = [
"-Dddprof_test.keep_jfrs=${keepRecordings}",
'-Djdk.attach.allowAttachSelf',
'-Djol.tryWithSudo=true',
"-Dddprof_test.config=${config}",
"-Dddprof_test.ci=${project.hasProperty('CI')}",
"-Dddprof.disable_unsafe=true",
'-XX:ErrorFile=build/hs_err_pid%p.log',
'-XX:+ResizeTLAB',
'-Xmx512m',
'-XX:OnError=/tmp/do_stuff.sh',
"-Djava.library.path=${outputLibDir.absolutePath}"
]

// Enable Native Memory Tracking for leak detection tests if requested
if (project.hasProperty('enableNMT') || Boolean.parseBoolean(System.getenv("ENABLE_NMT"))) {
jvmArgsList.add('-XX:NativeMemoryTracking=detail')
}

jvmArgs jvmArgsList

def javaHome = System.getenv("JAVA_TEST_HOME")
if (javaHome == null) {
Expand Down
Loading
Loading