Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,12 @@ class ProfilerTestPlugin : Plugin<Project> {
allArgs.add("-Dtest.filter=$testsFilter")
}

// Profiler options from -Pprofiler.options property
val profilerOptions = project.findProperty("profiler.options") as String?
if (profilerOptions != null) {
allArgs.add("-Dddprof.test.options=$profilerOptions")
}
Comment on lines +318 to +322
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass profiler options to the normal Test tasks

This only wires -Pprofiler.options into the Exec-based paths, but on glibc/macOS the regular test<Config> tasks are created by createTestTask, which never adds -Dddprof.test.options to its JVM args. As a result, the documented/normal workflow such as testDebug -Pprofiler.options=... silently ignores the override unless the user switches to the new testProcess<Config> task or runs on musl.

Useful? React with 👍 / 👎.


// Classpath (includes custom test runner)
allArgs.add("-cp")
allArgs.add(testConfig.testClasspath.asPath)
Expand Down Expand Up @@ -356,6 +362,95 @@ class ProfilerTestPlugin : Plugin<Project> {
}
}

/**
* Create Exec-based test task that always runs in a separate process.
* Available on all platforms. Supports -Pprofiler.options for overriding profiler settings.
* Task name: testProcess<Config> (e.g., testProcessDebug, testProcessRelease)
*/
private fun createProcessTestTask(
project: Project,
extension: ProfilerTestExtension,
testConfig: TestTaskConfiguration,
testCfg: Configuration,
sourceSets: SourceSetContainer
) {
val taskName = "testProcess${testConfig.configName.replaceFirstChar { it.uppercase() }}"
project.tasks.register(taskName, Exec::class.java) {
val execTask = this
execTask.description = "Runs tests in separate process with ${testConfig.configName} library (supports -Pprofiler.options)"
execTask.group = "verification"
execTask.onlyIf { testConfig.isActive && !project.hasProperty("skip-tests") }

// Dependencies
execTask.dependsOn(project.tasks.named("compileTestJava"))
execTask.dependsOn(testCfg)
execTask.dependsOn(sourceSets.getByName("test").output)

// Configure at execution time to capture properties
execTask.doFirst {
execTask.executable = PlatformUtils.testJavaExecutable()

val allArgs = mutableListOf<String>()

// JVM args
allArgs.addAll(testConfig.standardJvmArgs)
if (extension.nativeLibDir.isPresent) {
allArgs.add("-Djava.library.path=${extension.nativeLibDir.get().asFile.absolutePath}")
}
allArgs.addAll(testConfig.extraJvmArgs)

// System properties
testConfig.systemProperties.forEach { (key, value) ->
allArgs.add("-D$key=$value")
}

// Test filter from -Ptests property
val testsFilter = project.findProperty("tests") as String?
if (testsFilter != null) {
allArgs.add("-Dtest.filter=$testsFilter")
}

// Profiler options from -Pprofiler.options property
val profilerOptions = project.findProperty("profiler.options") as String?
if (profilerOptions != null) {
allArgs.add("-Dddprof.test.options=$profilerOptions")
}

// Classpath
allArgs.add("-cp")
allArgs.add(testConfig.testClasspath.asPath)

// Use custom test runner
allArgs.add("com.datadoghq.profiler.test.ProfilerTestRunner")

execTask.args = allArgs
}

// Environment variables
testConfig.environmentVariables.forEach { (key, value) ->
execTask.environment(key, value)
}

// Remove LD_LIBRARY_PATH to let RPATH work correctly
execTask.doFirst {
val currentLdLibPath = (execTask.environment["LD_LIBRARY_PATH"] as? String) ?: System.getenv("LD_LIBRARY_PATH")
if (!currentLdLibPath.isNullOrEmpty()) {
project.logger.info("Removing LD_LIBRARY_PATH to prevent cross-JDK library conflicts (was: $currentLdLibPath)")
execTask.environment.remove("LD_LIBRARY_PATH")
}
}

// Sanitizer conditions
when (testConfig.configName) {
"asan" -> execTask.onlyIf {
PlatformUtils.locateLibasan() != null &&
!PlatformUtils.isTestJvmJ9()
}
"tsan" -> execTask.onlyIf { false }
}
}
}

private fun generateMultiConfigTasks(project: Project, extension: ProfilerTestExtension) {
val nativeBuildExt = project.rootProject.extensions.findByType(NativeBuildExtension::class.java)
?: return // No native build extension, nothing to generate
Expand Down Expand Up @@ -423,6 +518,10 @@ class ProfilerTestPlugin : Plugin<Project> {
createTestTask(project, extension, testConfig, testCfg, sourceSets)
}

// Create process-based test task (always uses Exec, available on all platforms)
// Supports -Pprofiler.options for overriding profiler settings
createProcessTestTask(project, extension, testConfig, testCfg, sourceSets)

// Create application tasks for specified configs
if (configName in applicationConfigs && appMainClass.isNotEmpty()) {
// Create main configuration
Expand Down
5 changes: 5 additions & 0 deletions ddprof-lib/src/main/cpp/arguments.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,11 @@ Error Arguments::parse(const char *args) {
msg = "jstackdepth must be > 0";
}

CASE("fjmethodid")
if (value != nullptr && strcmp(value, "false") == 0) {
_force_jmethodID = false;
}

CASE("safemode")
_safe_mode = value == NULL ? INT_MAX : (int)strtol(value, NULL, 0);

Expand Down
4 changes: 3 additions & 1 deletion ddprof-lib/src/main/cpp/arguments.h
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ class Arguments {
bool _enable_method_cleanup;
bool _remote_symbolication; // Enable remote symbolication for native frames
bool _jvmtistacks; // Delegate CPU/wall stack walks to HotSpot JFR RequestStackTrace extension
bool _force_jmethodID; // Load all jmethodIDs, true by default

Arguments(bool persistent = false)
: _buf(NULL),
Expand Down Expand Up @@ -229,7 +230,8 @@ class Arguments {
_lightweight(false),
_enable_method_cleanup(true),
_remote_symbolication(false),
_jvmtistacks(false) {}
_jvmtistacks(false),
_force_jmethodID(false) {}

~Arguments();

Expand Down
28 changes: 19 additions & 9 deletions ddprof-lib/src/main/cpp/flightRecorder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
#include "context_api.h"
#include "counters.h"
#include "dictionary.h"
#include "flightRecorder.h"
#include "flightRecorder.inline.h"
#include "incbin.h"
#include "jfrMetadata.h"
#include "jniHelper.h"
#include "jvmSupport.inline.h"
#include "os.h"
#include "profiler.h"
#include "signalSafety.h"
Expand Down Expand Up @@ -485,8 +486,17 @@ MethodInfo *Lookup::resolveMethod(ASGCT_CallFrame &frame) {
static const char* UNKNOWN = "unknown";
unsigned long key;
jint bci = frame.bci;
jmethodID method_id = frame.method_id;

jmethodID method = frame.method_id;
// Resolve native method
if (FrameType::isRawPointer(bci)) {
method_id = JVMSupport::resolve(frame.method);
}

// Maps JMETHODID_NOT_WAKEABLE back to nullptr
if (method_id == JMETHODID_NOT_WALKABLE) {
method_id = nullptr;
}

// BCI_VTABLE_RECEIVER: method holds a VMSymbol* (see vmEntry.h). Resolve
// to a class_id via the per-dump cache once, then key MethodMap by the
Expand All @@ -495,10 +505,10 @@ MethodInfo *Lookup::resolveMethod(ASGCT_CallFrame &frame) {
// row.
u32 vtable_class_id = 0;
if (bci == BCI_VTABLE_RECEIVER) {
vtable_class_id = resolveVTableReceiverCached((void *)method);
vtable_class_id = resolveVTableReceiverCached((void *)method_id);
}

if (method == nullptr) {
if (method_id == nullptr) {
key = MethodMap::makeKey(UNKNOWN);
} else if (bci == BCI_ERROR || bci == BCI_NATIVE_FRAME) {
key = MethodMap::makeKey(frame.native_function_name);
Expand All @@ -511,7 +521,7 @@ MethodInfo *Lookup::resolveMethod(ASGCT_CallFrame &frame) {
assert(frame_type == FRAME_INTERPRETED || frame_type == FRAME_JIT_COMPILED ||
frame_type == FRAME_INLINED || frame_type == FRAME_C1_COMPILED ||
VM::isOpenJ9()); // OpenJ9 may have bugs that produce invalid frame types
key = MethodMap::makeKey(method);
key = MethodMap::makeKey(method_id);
}

MethodInfo *mi = &(*_method_map)[key];
Expand All @@ -522,12 +532,12 @@ MethodInfo *Lookup::resolveMethod(ASGCT_CallFrame &frame) {
if (first_time) {
mi->_key = _method_map->size() + 1; // avoid zero key
}
if (method == nullptr) {
if (method_id == nullptr) {
fillNativeMethodInfo(mi, UNKNOWN, nullptr);
} else if (bci == BCI_ERROR) {
fillNativeMethodInfo(mi, (const char *)method, nullptr);
fillNativeMethodInfo(mi, (const char *)method_id, nullptr);
} else if (bci == BCI_NATIVE_FRAME) {
const char *name = (const char *)method;
const char *name = (const char *)method_id;
fillNativeMethodInfo(mi, name,
Profiler::instance()->getLibraryName(name));
} else if (bci == BCI_NATIVE_FRAME_REMOTE) {
Expand Down Expand Up @@ -575,7 +585,7 @@ MethodInfo *Lookup::resolveMethod(ASGCT_CallFrame &frame) {
mi->_type = FRAME_NATIVE;
mi->_is_entry = false;
} else {
fillJavaMethodInfo(mi, method, first_time);
fillJavaMethodInfo(mi, method_id, first_time);
}
}

Expand Down
23 changes: 2 additions & 21 deletions ddprof-lib/src/main/cpp/flightRecorder.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,27 +90,8 @@ class MethodInfo {
std::shared_ptr<SharedLineNumberTable> _line_number_table;
FrameTypeId _type;

jint getLineNumber(jint bci) {
// if the shared pointer is not pointing to the line number table, consider
// size 0
if (!_line_number_table || _line_number_table->_size == 0) {
return 0;
}

int i = 1;
while (i < _line_number_table->_size &&
bci >= ((jvmtiLineNumberEntry *)_line_number_table->_ptr)[i]
.start_location) {
i++;
}
return ((jvmtiLineNumberEntry *)_line_number_table->_ptr)[i - 1]
.line_number;
}

bool isHidden() {
// 0x1400 = ACC_SYNTHETIC(0x1000) | ACC_BRIDGE(0x0040)
return _modifiers == 0 || (_modifiers & 0x1040);
}
jint getLineNumber(jint bci);
bool isHidden();
};

// MethodMap's key can be derived from 3 sources:
Expand Down
31 changes: 31 additions & 0 deletions ddprof-lib/src/main/cpp/flightRecorder.inline.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright The async-profiler authors
* Copyright 2026, Datadog, Inc.
* SPDX-License-Identifier: Apache-2.0
*/

#include "flightRecorder.h"

#include "jvmSupport.inline.h"


jint MethodInfo::getLineNumber(jint bci) {
// if the shared pointer is not pointing to the line number table, consider
// size 0
if (!_line_number_table || _line_number_table->_size == 0) {
return 0;
}

int i = 1;
while (i < _line_number_table->_size &&
bci >= ((jvmtiLineNumberEntry *)_line_number_table->_ptr)[i]
.start_location) {
i++;
}
return ((jvmtiLineNumberEntry *)_line_number_table->_ptr)[i - 1]
.line_number;
}

bool MethodInfo::isHidden() {
return JVMSupport::isHidden(_modifiers);
}
15 changes: 12 additions & 3 deletions ddprof-lib/src/main/cpp/frame.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#ifndef _FRAME_H
#define _FRAME_H

#include <cassert>
#include "vmEntry.h"

enum FrameTypeId {
FRAME_INTERPRETED = 0,
FRAME_JIT_COMPILED = 1,
Expand All @@ -14,9 +17,11 @@ enum FrameTypeId {
};

class FrameType {
static constexpr int RAW_POINTER_MASK = 1 << 30;
public:
static inline int encode(int type, int bci) {
return (1 << 24) | (type << 25) | (bci & 0xffffff);
static inline int encode(int type, int bci, bool rawPointer = false) {
assert((!rawPointer || VM::isHotspot()) && "Raw pointer is only valid for hotspot");
return (1 << 24) | (type << 25) | (bci & 0xffffff) | (rawPointer ? RAW_POINTER_MASK : 0);
}

static inline FrameTypeId decode(int bci) {
Expand All @@ -25,9 +30,13 @@ class FrameType {
return FRAME_JIT_COMPILED;
}
// Clamp to valid FrameTypeId range to defend against corrupted values
int raw_type = bci >> 25;
int raw_type = (bci & ~ RAW_POINTER_MASK) >> 25;
return (FrameTypeId)(raw_type <= FRAME_TYPE_MAX ? raw_type : FRAME_TYPE_MAX);
}

static inline bool isRawPointer(int bci) {
return bci > 0 && (bci & RAW_POINTER_MASK) != 0;
}
};

#endif // _FRAME_H
Loading
Loading