diff --git a/.github/scripts/test_alpine_aarch64.sh b/.github/scripts/test_alpine_aarch64.sh index 4d107ca5a..af30d6208 100755 --- a/.github/scripts/test_alpine_aarch64.sh +++ b/.github/scripts/test_alpine_aarch64.sh @@ -29,6 +29,6 @@ JAVA_VERSION=$("${JAVA_TEST_HOME}/bin/java" -version 2>&1 | awk -F '"' '/version }') export JAVA_VERSION -apk update && apk add curl moreutils wget hexdump linux-headers bash make g++ clang git cppcheck jq cmake gtest-dev gmock tar >/dev/null +apk update && apk add curl moreutils wget hexdump linux-headers bash make g++ clang git cppcheck jq cmake gtest-dev gmock tar binutils >/dev/null ./gradlew -PCI -PkeepJFRs :ddprof-test:test${CONFIG} --no-daemon --parallel --build-cache --no-watch-fs \ No newline at end of file diff --git a/.github/workflows/test_workflow.yml b/.github/workflows/test_workflow.yml index e96121b7c..c9738218d 100644 --- a/.github/workflows/test_workflow.yml +++ b/.github/workflows/test_workflow.yml @@ -72,7 +72,7 @@ jobs: if: steps.set_enabled.outputs.enabled == 'true' run: | sudo apt-get update - sudo apt-get install -y curl zip unzip libgtest-dev libgmock-dev + sudo apt-get install -y curl zip unzip libgtest-dev libgmock-dev binutils if [[ ${{ matrix.java_version }} =~ "-zing" ]]; then sudo apt-get install -y g++-9 gcc-9 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 100 --slave /usr/bin/g++ g++ /usr/bin/g++-9 @@ -135,7 +135,7 @@ jobs: steps: - name: Setup OS run: | - apk update && apk add curl moreutils wget hexdump linux-headers bash make g++ clang git cppcheck jq cmake gtest-dev gmock tar >/dev/null + apk update && apk add curl moreutils wget hexdump linux-headers bash make g++ clang git cppcheck jq cmake gtest-dev gmock tar binutils >/dev/null - uses: actions/checkout@v3 - name: Cache Gradle Wrapper Binaries uses: actions/cache@v4 @@ -286,7 +286,7 @@ jobs: sudo apt update -y sudo apt remove -y g++ sudo apt autoremove -y - sudo apt install -y curl zip unzip clang make build-essential + sudo apt install -y curl zip unzip clang make build-essential binutils if [[ ${{ matrix.java_version }} =~ "-zing" ]]; then sudo apt -y install g++-9 gcc-9 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 100 --slave /usr/bin/g++ g++ /usr/bin/g++-9 diff --git a/.github/workflows/update_assets.yml b/.github/workflows/update_assets.yml index c76512ce1..fe2e1112a 100644 --- a/.github/workflows/update_assets.yml +++ b/.github/workflows/update_assets.yml @@ -35,7 +35,7 @@ jobs: TAG="$GITHUB_REF_NAME" fi VERSION=$(echo "${TAG}" | sed -e 's/v_//g') - ASSET_URL="https://oss.sonatype.org/service/local/repositories/releases/content/com/datadoghq/ddprof/${VERSION}/ddprof-${VERSION}.jar" + ASSET_URL="https://repo1.maven.org/maven2/com/datadoghq/ddprof/${VERSION}/ddprof-${VERSION}.jar" RESULT=1 while [ $RESULT -ne 0 ]; do wget -q $ASSET_URL diff --git a/README.md b/README.md index b8e24356b..93fa275dc 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,39 @@ The project includes both Java and C++ unit tests. You can run them using: ### Cross-JDK Testing `JAVA_TEST_HOME= ./gradlew testDebug` +## Release Builds and Debug Information + +### Split Debug Information +Release builds automatically generate split debug information to optimize deployment size while preserving debugging capabilities: + +- **Stripped libraries** (~1.2MB): Production-ready binaries with symbols removed for deployment +- **Debug symbol files** (~6.1MB): Separate `.debug` files containing full debugging information +- **Debug links**: Stripped libraries include `.gnu_debuglink` sections pointing to debug files + +### Build Artifacts Structure +``` +ddprof-lib/build/ +├── lib/main/release/linux/x64/ +│ ├── libjavaProfiler.so # Original library with debug symbols +│ ├── stripped/ +│ │ └── libjavaProfiler.so # Stripped library (83% smaller) +│ └── debug/ +│ └── libjavaProfiler.so.debug # Debug symbols only +├── native/release/ +│ └── META-INF/native-libs/linux-x64/ +│ └── libjavaProfiler.so # Final stripped library (deployed) +└── native/release-debug/ + └── META-INF/native-libs/linux-x64/ + └── libjavaProfiler.so.debug # Debug symbols package +``` + +### Build Options +- **Skip debug extraction**: `./gradlew buildRelease -Pskip-debug-extraction=true` +- **Debug extraction requires**: `objcopy` (Linux) or `dsymutil` (macOS) + - Ubuntu/Debian: `sudo apt-get install binutils` + - Alpine: `apk add binutils` + - macOS: Included with Xcode command line tools + ## Development ### Code Quality diff --git a/build.gradle b/build.gradle index 89d72cf96..68cf63159 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ plugins { id "com.diffplug.spotless" version "6.11.0" } -version = "1.28.0" +version = "1.29.0" apply plugin: "com.dipien.semantic-version" version = project.findProperty("ddprof_version") ?: version @@ -39,7 +39,8 @@ repositories { mavenContent { snapshotsOnly() } - url 'https://oss.sonatype.org/content/repositories/snapshots/' + // see https://central.sonatype.org/publish/publish-portal-snapshots/#consuming-via-gradle + url 'https://central.sonatype.com/repository/maven-snapshots/' } } @@ -75,7 +76,14 @@ nexusPublishing { password = "admin123" } } else { + // see https://github.com/gradle-nexus/publish-plugin#publishing-to-maven-central-via-sonatype-central + // For official documentation: + // staging repo publishing https://central.sonatype.org/publish/publish-portal-ossrh-staging-api/#configuration + // snapshot publishing https://central.sonatype.org/publish/publish-portal-snapshots/#publishing-via-other-methods sonatype { + nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) + snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) + username = System.getenv("SONATYPE_USERNAME") password = System.getenv("SONATYPE_PASSWORD") } diff --git a/ddprof-lib/build.gradle b/ddprof-lib/build.gradle index 9ceb5c4c2..93d664fae 100644 --- a/ddprof-lib/build.gradle +++ b/ddprof-lib/build.gradle @@ -8,6 +8,166 @@ plugins { id 'de.undercouch.download' version '4.1.1' } +// Helper function to check if objcopy is available +def checkObjcopyAvailable() { + try { + def process = ['objcopy', '--version'].execute() + process.waitFor() + return process.exitValue() == 0 + } catch (Exception e) { + return false + } +} + +// Helper function to check if dsymutil is available (for macOS) +def checkDsymutilAvailable() { + try { + def process = ['dsymutil', '--version'].execute() + process.waitFor() + return process.exitValue() == 0 + } catch (Exception e) { + return false + } +} + +// Helper function to check if debug extraction should be skipped +def shouldSkipDebugExtraction() { + // Skip if explicitly disabled + if (project.hasProperty('skip-debug-extraction')) { + return true + } + + // Skip if required tools are not available + if (os().isLinux() && !checkObjcopyAvailable()) { + return true + } + + if (os().isMacOsX() && !checkDsymutilAvailable()) { + return true + } + + return false +} + +// Helper function to get debug file path for a given config +def getDebugFilePath(config) { + def extension = os().isLinux() ? 'so' : 'dylib' + return file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/debug/libjavaProfiler.${extension}.debug") +} + +// Helper function to get stripped file path for a given config +def getStrippedFilePath(config) { + def extension = os().isLinux() ? 'so' : 'dylib' + return file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/stripped/libjavaProfiler.${extension}") +} + +// Helper function to create error message for missing tools +def getMissingToolErrorMessage(toolName, installInstructions) { + return """ + |${toolName} is not available but is required for split debug information. + | + |To fix this issue: + |${installInstructions} + | + |If you want to build without split debug info, set -Pskip-debug-extraction=true + """.stripMargin() +} + +// Helper function to create debug extraction task +def createDebugExtractionTask(config, linkTask) { + return tasks.register('extractDebugLibRelease', Exec) { + onlyIf { + !shouldSkipDebugExtraction() + } + dependsOn linkTask + description = 'Extract debug symbols from release library' + workingDir project.buildDir + + doFirst { + def sourceFile = linkTask.get().linkedFile.get().asFile + def debugFile = getDebugFilePath(config) + + // Ensure debug directory exists + debugFile.parentFile.mkdirs() + + // Set the command line based on platform + if (os().isLinux()) { + commandLine = ['objcopy', '--only-keep-debug', sourceFile.absolutePath, debugFile.absolutePath] + } else { + // For macOS, we'll use dsymutil instead + commandLine = ['dsymutil', sourceFile.absolutePath, '-o', debugFile.absolutePath.replace('.debug', '.dSYM')] + } + } + } +} + +// Helper function to create debug link task (Linux only) +def createDebugLinkTask(config, linkTask, extractDebugTask) { + return tasks.register('addDebugLinkLibRelease', Exec) { + onlyIf { + os().isLinux() && !shouldSkipDebugExtraction() + } + dependsOn extractDebugTask + description = 'Add debug link to the original library' + + doFirst { + def sourceFile = linkTask.get().linkedFile.get().asFile + def debugFile = getDebugFilePath(config) + + commandLine = ['objcopy', '--add-gnu-debuglink=' + debugFile.absolutePath, sourceFile.absolutePath] + } + } +} + +// Helper function to create debug file copy task +def createDebugCopyTask(config, extractDebugTask) { + return tasks.register('copyReleaseDebugFiles', Copy) { + onlyIf { + !shouldSkipDebugExtraction() + } + dependsOn extractDebugTask + from file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/debug") + into file(libraryTargetPath(config.name + '-debug')) + include '**/*.debug' + include '**/*.dSYM/**' + } +} + +// Main function to setup debug extraction for release builds +def setupDebugExtraction(config, linkTask) { + if (config.name == 'release' && config.active && !project.hasProperty('skip-native')) { + // Create all debug-related tasks + def extractDebugTask = createDebugExtractionTask(config, linkTask) + def addDebugLinkTask = createDebugLinkTask(config, linkTask, extractDebugTask) + + // Create the strip task and configure it properly + def stripTask = tasks.register('stripLibRelease', StripSymbols) { + // No onlyIf needed here - setupDebugExtraction already handles the main conditions + dependsOn addDebugLinkTask + } + + // Configure the strip task after registration + stripTask.configure { + targetPlatform = linkTask.get().targetPlatform + toolChain = linkTask.get().toolChain + binaryFile = linkTask.get().linkedFile.get().asFile + outputFile = getStrippedFilePath(config) + } + + def copyDebugTask = createDebugCopyTask(config, extractDebugTask) + + // Wire up the copy task to use stripped binaries + def copyTask = tasks.findByName("copyReleaseLibs") + if (copyTask != null) { + copyTask.dependsOn stripTask + copyTask.inputs.files stripTask.get().outputs.files + + // Create an extra folder for the debug symbols + copyTask.dependsOn copyDebugTask + } + } +} + def libraryName = "ddprof" description = "Datadog Java Profiler Library" @@ -366,23 +526,7 @@ tasks.whenTaskAdded { task -> outputs.file linkedFile } if (config.name == 'release') { - def stripTask = tasks.register('stripLibRelease', StripSymbols) { - onlyIf { - config.active - } - dependsOn linkTask - targetPlatform = tasks.linkLibRelease.targetPlatform - toolChain = tasks.linkLibRelease.toolChain - binaryFile = tasks.linkLibRelease.linkedFile.get() - outputFile = file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/stripped/libjavaProfiler.${os().isLinux() ? 'so' : 'dylib'}") - inputs.file binaryFile - outputs.file outputFile - } - def copyTask = tasks.findByName("copyReleaseLibs") - if (copyTask != null) { - copyTask.dependsOn stripTask - copyTask.inputs.files stripTask.get().outputs.files - } + setupDebugExtraction(config, linkTask) } } } diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 4a76de0c3..efdae02a3 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -972,6 +972,7 @@ void Profiler::updateJavaThreadNames() { JNIEnv *jni = VM::jni(); for (int i = 0; i < thread_count; i++) { updateThreadName(jvmti, jni, thread_objects[i]); + jni->DeleteLocalRef(thread_objects[i]); } jvmti->Deallocate((unsigned char *)thread_objects); diff --git a/ddprof-lib/src/main/cpp/safeAccess.cpp b/ddprof-lib/src/main/cpp/safeAccess.cpp new file mode 100644 index 000000000..82aceeeb8 --- /dev/null +++ b/ddprof-lib/src/main/cpp/safeAccess.cpp @@ -0,0 +1,38 @@ +/* + * Copyright 2025 Datadog, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#include "safeAccess.h" + +SafeAccess::SafeFetch32 SafeAccess::_safeFetch32Func = nullptr; + +void SafeAccess::initSafeFetch(CodeCache* libjvm) { + // Hotspot JVM's safefetch implementation appears better, e.g. it actually returns errorValue, + // when the address is invalid. let's use it whenever possible. + // When the methods are not available, fallback to SafeAccess::load32() implementation. + _safeFetch32Func = (SafeFetch32)libjvm->findSymbol("SafeFetch32_impl"); + if (_safeFetch32Func == nullptr && !WX_MEMORY) { + // jdk11 stub implementation other than Macosx/aarch64 + void** entry = (void**)libjvm->findSymbol("_ZN12StubRoutines18_safefetch32_entryE"); + if (entry != nullptr && *entry != nullptr) { + _safeFetch32Func = (SafeFetch32)*entry; + } + } + // Fallback + if (_safeFetch32Func == nullptr) { + _safeFetch32Func = (SafeFetch32)load32; + } +} diff --git a/ddprof-lib/src/main/cpp/safeAccess.h b/ddprof-lib/src/main/cpp/safeAccess.h index 0e39f0b2a..993c359c6 100644 --- a/ddprof-lib/src/main/cpp/safeAccess.h +++ b/ddprof-lib/src/main/cpp/safeAccess.h @@ -18,6 +18,8 @@ #define _SAFEACCESS_H #include "arch_dd.h" +#include "codeCache.h" +#include #include #ifdef __clang__ @@ -28,7 +30,18 @@ #define NOADDRSANITIZE __attribute__((no_sanitize("address"))) class SafeAccess { +private: + typedef int (*SafeFetch32)(int* ptr, int errorValue); + static SafeFetch32 _safeFetch32Func; + public: + static void initSafeFetch(CodeCache* libjvm); + + static inline int safeFetch32(int* ptr, int errorValue) { + assert(_safeFetch32Func != nullptr); + return _safeFetch32Func(ptr, errorValue); + } + NOINLINE NOADDRSANITIZE __attribute__((aligned(16))) static void *load(void **ptr) { return *ptr; } diff --git a/ddprof-lib/src/main/cpp/threadFilter.cpp b/ddprof-lib/src/main/cpp/threadFilter.cpp index 31713ae5d..13e6c2ae0 100644 --- a/ddprof-lib/src/main/cpp/threadFilter.cpp +++ b/ddprof-lib/src/main/cpp/threadFilter.cpp @@ -135,7 +135,7 @@ void ThreadFilter::add(int thread_id) { thread_id = mapThreadId(thread_id); assert(b == bitmap(thread_id)); u64 bit = 1ULL << (thread_id & 0x3f); - if (!(__sync_fetch_and_or(&word(b, thread_id), bit) & bit)) { + if (!(__atomic_fetch_or(&word(b, thread_id), bit, __ATOMIC_RELAXED) & bit)) { atomicInc(_size); } } @@ -148,7 +148,7 @@ void ThreadFilter::remove(int thread_id) { } u64 bit = 1ULL << (thread_id & 0x3f); - if (__sync_fetch_and_and(&word(b, thread_id), ~bit) & bit) { + if (__atomic_fetch_and(&word(b, thread_id), ~bit, __ATOMIC_RELAXED) & bit) { atomicInc(_size, -1); } } diff --git a/ddprof-lib/src/main/cpp/threadFilter.h b/ddprof-lib/src/main/cpp/threadFilter.h index 8db82f9fb..7454be576 100644 --- a/ddprof-lib/src/main/cpp/threadFilter.h +++ b/ddprof-lib/src/main/cpp/threadFilter.h @@ -70,6 +70,10 @@ class ThreadFilter { void init(const char *filter); void clear(); + inline bool isValid(int thread_id) { + return thread_id >= 0 && thread_id < _max_thread_id; + } + bool accept(int thread_id); void add(int thread_id); void remove(int thread_id); diff --git a/ddprof-lib/src/main/cpp/vmStructs_dd.cpp b/ddprof-lib/src/main/cpp/vmStructs_dd.cpp index d8efce3b8..4dc043a54 100644 --- a/ddprof-lib/src/main/cpp/vmStructs_dd.cpp +++ b/ddprof-lib/src/main/cpp/vmStructs_dd.cpp @@ -44,6 +44,7 @@ namespace ddprof { initOffsets(); initJvmFunctions(); initUnsafeFunctions(); + initSafeFetch(libjvm); } void VMStructs_::initOffsets() { @@ -98,6 +99,10 @@ namespace ddprof { } } + void VMStructs_::initSafeFetch(CodeCache* libjvm) { + SafeAccess::initSafeFetch(libjvm); + } + const void *VMStructs_::findHeapUsageFunc() { if (VM::hotspot_version() < 17) { // For JDK 11 it is really unreliable to find the memory_usage function - diff --git a/ddprof-lib/src/main/cpp/vmStructs_dd.h b/ddprof-lib/src/main/cpp/vmStructs_dd.h index 1139669b8..431aa4329 100644 --- a/ddprof-lib/src/main/cpp/vmStructs_dd.h +++ b/ddprof-lib/src/main/cpp/vmStructs_dd.h @@ -20,6 +20,7 @@ #include "common.h" #include "jniHelper.h" #include "jvmHeap.h" +#include "safeAccess.h" #include "threadState.h" #include "vmEntry.h" #include "vmStructs.h" @@ -51,6 +52,8 @@ namespace ddprof { static void initOffsets(); static void initJvmFunctions(); static void initUnsafeFunctions(); + // We need safe access for all jdk versions + static void initSafeFetch(CodeCache* libjvm); static void checkNativeBinding(jvmtiEnv *jvmti, JNIEnv *jni, jmethodID method, void *address); @@ -66,9 +69,19 @@ namespace ddprof { inline static int thread_osthread_offset() { return _thread_osthread_offset; } + inline static int osthread_state_offset() { return _osthread_state_offset; } + + inline static int osthread_id_offset() { + return _osthread_id_offset; + } + + inline static int thread_state_offset() { + return _thread_state_offset; + } + inline static int flag_type_offset() { return _flag_type_offset; } @@ -131,13 +144,49 @@ namespace ddprof { OSThreadState osThreadState() { if (ddprof::VMStructs::thread_osthread_offset() >= 0 && ddprof::VMStructs::osthread_state_offset() >= 0) { - const char *osthread = *(const char **)at(ddprof::VMStructs::thread_osthread_offset()); + const char *osthread = *(char **)at(ddprof::VMStructs::thread_osthread_offset()); if (osthread != nullptr) { - return static_cast( - *(int *)(osthread + ddprof::VMStructs::osthread_state_offset())); + // If the location is not accessible, the thread must have been terminated + int value = SafeAccess::safeFetch32((int*)(osthread + ddprof::VMStructs::osthread_state_offset()), + static_cast(OSThreadState::TERMINATED)); + // Checking for bad data + if (value > static_cast(OSThreadState::SYSCALL)) { + return OSThreadState::TERMINATED; + } + return static_cast(value); } + } + return OSThreadState::UNKNOWN; + } + + int osThreadId() { + if (ddprof::VMStructs::thread_osthread_offset() >= 0 && ddprof::VMStructs::osthread_id_offset() >=0) { + const char* osthread = *(const char**) at(ddprof::VMStructs::thread_osthread_offset()); + if (osthread == nullptr) { + return -1; + } else { + return SafeAccess::safeFetch32((int*)(osthread + ddprof::VMStructs::osthread_id_offset()), -1); + } + } + return -1; + } + + int state() { + int offset = ddprof::VMStructs::thread_state_offset(); + if (offset >= 0) { + int* state = (int*)at(offset); + if (state == nullptr) { + return 0; + } else { + int value = SafeAccess::safeFetch32(state, 0); + // Checking for bad data + if (value > _thread_max_state) { + value = 0; + } + return value; + } } - return OSThreadState::UNKNOWN; + return 0; } }; diff --git a/ddprof-lib/src/main/cpp/wallClock.cpp b/ddprof-lib/src/main/cpp/wallClock.cpp index d703b81dd..d261cdcec 100644 --- a/ddprof-lib/src/main/cpp/wallClock.cpp +++ b/ddprof-lib/src/main/cpp/wallClock.cpp @@ -153,41 +153,66 @@ void WallClockASGCT::initialize(Arguments& args) { OS::installSignalHandler(SIGVTALRM, sharedSignalHandler); } +/* This method is extremely racy! + * Thread references, that are returned from JVMTI::GetAllThreads(), only guarantee thread objects + * are not collected by GCs, they don't prevent threads from exiting. + * We have to be extremely careful when accessing thread's data, so it may not be valid. + */ void WallClockJVMTI::timerLoop() { // Check for enablement before attaching/dettaching the current thread if (!isEnabled()) { return; } + + jvmtiEnv* jvmti = VM::jvmti(); + if (jvmti == nullptr) { + return; + } + + // Notice: + // We want to cache threads that are captured by collectThread(), so that we can + // clean them up in cleanThreadRefs(). + // The approach is not ideal, but it is cleaner than cleaning individual thread + // during filtering phases. + jint threads_count = 0; + jthread* threads_ptr = nullptr; + // Attach to JVM as the first step - VM::attachThread("Datadog Profiler Wallclock Sampler"); - auto collectThreads = [&](std::vector& threads) { - jvmtiEnv* jvmti = VM::jvmti(); - if (jvmti == nullptr) { - return; - } - JNIEnv* jni = VM::jni(); - - jint threads_count = 0; - jthread* threads_ptr = nullptr; - jvmti->GetAllThreads(&threads_count, &threads_ptr); - - bool do_filter = Profiler::instance()->threadFilter()->enabled(); - int self = OS::threadId(); - - for (int i = 0; i < threads_count; i++) { - jthread thread = threads_ptr[i]; - if (thread != nullptr) { - ddprof::VMThread* nThread = static_cast(VMThread::fromJavaThread(jni, thread)); - if (nThread == nullptr) { - continue; - } - int tid = nThread->osThreadId(); - if (tid != self && (!do_filter || Profiler::instance()->threadFilter()->accept(tid))) { - threads.push_back({nThread, thread}); - } + VM::attachThread("Datadog Profiler Wallclock Sampler"); + auto collectThreads = [&](std::vector& threads) { + jvmtiEnv* jvmti = VM::jvmti(); + if (jvmti == nullptr) { + return; + } + + if (jvmti->GetAllThreads(&threads_count, &threads_ptr) != JVMTI_ERROR_NONE || + threads_count == 0) { + return; + } + + JNIEnv* jni = VM::jni(); + + ThreadFilter* threadFilter = Profiler::instance()->threadFilter(); + bool do_filter = threadFilter->enabled(); + int self = OS::threadId(); + + for (int i = 0; i < threads_count; i++) { + jthread thread = threads_ptr[i]; + if (thread != nullptr) { + ddprof::VMThread* nThread = static_cast(VMThread::fromJavaThread(jni, thread)); + if (nThread == nullptr) { + continue; + } + int tid = nThread->osThreadId(); + if (!threadFilter->isValid(tid)) { + continue; + } + + if (tid != self && (!do_filter || threadFilter->accept(tid))) { + threads.push_back({nThread, thread, tid}); } } - jvmti->Deallocate((unsigned char*)threads_ptr); + } }; auto sampleThreads = [&](ThreadEntry& thread_entry, int& num_failures, int& threads_already_exited, int& permission_denied) { @@ -200,25 +225,40 @@ void WallClockJVMTI::timerLoop() { raw_thread_state < ddprof::JVMJavaThreadState::_thread_max_state; OSThreadState state = OSThreadState::UNKNOWN; ExecutionMode mode = ExecutionMode::UNKNOWN; - if (vm_thread && is_initialized) { - OSThreadState os_state = vm_thread->osThreadState(); - if (os_state != OSThreadState::UNKNOWN) { - state = os_state; - } - mode = convertJvmExecutionState(raw_thread_state); + if (vm_thread == nullptr || !is_initialized) { + return false; } - if (state == OSThreadState::UNKNOWN) { + OSThreadState os_state = vm_thread->osThreadState(); + if (state == OSThreadState::TERMINATED) { + return false; + } else if (state == OSThreadState::UNKNOWN) { state = OSThreadState::RUNNABLE; + } else { + state = os_state; } + mode = convertJvmExecutionState(raw_thread_state); + event._thread_state = state; event._execution_mode = mode; event._weight = 1; - Profiler::instance()->recordJVMTISample(1, thread_entry.native->osThreadId(), thread_entry.java, BCI_WALL, &event, false); + Profiler::instance()->recordJVMTISample(1, thread_entry.tid, thread_entry.java, BCI_WALL, &event, false); return true; }; - timerLoopCommon(collectThreads, sampleThreads, _reservoir_size, _interval); + auto cleanThreadRefs = [&]() { + JNIEnv* jni = VM::jni(); + for (jint index = 0; index < threads_count; index++) { + jni->DeleteLocalRef(threads_ptr[index]); + } + jvmti->Deallocate((unsigned char*)threads_ptr); + threads_ptr = nullptr; + threads_count = 0; + }; + + timerLoopCommon(collectThreads, sampleThreads, cleanThreadRefs, _reservoir_size, _interval); + + // Don't forget to detach the thread VM::detachThread(); } @@ -257,5 +297,8 @@ void WallClockASGCT::timerLoop() { return true; }; - timerLoopCommon(collectThreads, sampleThreads, _reservoir_size, _interval); + auto doNothing = []() { + }; + + timerLoopCommon(collectThreads, sampleThreads, doNothing, _reservoir_size, _interval); } diff --git a/ddprof-lib/src/main/cpp/wallClock.h b/ddprof-lib/src/main/cpp/wallClock.h index b938fb918..0ead37fac 100644 --- a/ddprof-lib/src/main/cpp/wallClock.h +++ b/ddprof-lib/src/main/cpp/wallClock.h @@ -50,8 +50,8 @@ class BaseWallClock : public Engine { bool isEnabled() const; - template - void timerLoopCommon(CollectThreadsFunc collectThreads, SampleThreadsFunc sampleThreads, int reservoirSize, u64 interval) { + template + void timerLoopCommon(CollectThreadsFunc collectThreads, SampleThreadsFunc sampleThreads, CleanThreadFunc cleanThreads, int reservoirSize, u64 interval) { if (!_enabled.load(std::memory_order_acquire)) { return; } @@ -104,6 +104,8 @@ class BaseWallClock : public Engine { } threads.clear(); + cleanThreads(); + // Get a random sleep duration // clamp the random interval to <1,2N-1> // the probability of clamping is extremely small, close to zero @@ -161,6 +163,7 @@ class WallClockJVMTI : public BaseWallClock { struct ThreadEntry { ddprof::VMThread* native; jthread java; + int tid; }; WallClockJVMTI() : BaseWallClock() {} const char* name() override { diff --git a/ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java b/ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java index cae2c53a6..a7cda6bc4 100644 --- a/ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java +++ b/ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java @@ -37,12 +37,15 @@ public final class JavaProfiler { static final Unsafe UNSAFE; static { Unsafe unsafe = null; - String version = System.getProperty("java.version"); - try { - Field f = Unsafe.class.getDeclaredField("theUnsafe"); - f.setAccessible(true); - unsafe = (Unsafe) f.get(null); - } catch (Exception ignore) { } + // a safety and testing valve to disable unsafe access + if (!Boolean.getBoolean("ddprof.disable_unsafe")) { + try { + Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + unsafe = (Unsafe) f.get(null); + } catch (Exception ignore) { + } + } UNSAFE = unsafe; } diff --git a/ddprof-lib/src/test/cpp/ddprof_ut.cpp b/ddprof-lib/src/test/cpp/ddprof_ut.cpp index 6c5c8b93e..a754c9b02 100644 --- a/ddprof-lib/src/test/cpp/ddprof_ut.cpp +++ b/ddprof-lib/src/test/cpp/ddprof_ut.cpp @@ -129,7 +129,7 @@ // increase step gradually to create different bit densities int step = 1; int size = 0; - for (int tid = 0; tid < maxTid - step - 1; tid += step, size++) { + for (int tid = 1; tid < maxTid - step - 1; tid += step, size++) { EXPECT_FALSE(filter.accept(tid)); filter.add(tid); EXPECT_TRUE(filter.accept(tid)); diff --git a/ddprof-stresstest/build.gradle b/ddprof-stresstest/build.gradle index c108d7352..2934e4aca 100644 --- a/ddprof-stresstest/build.gradle +++ b/ddprof-stresstest/build.gradle @@ -39,7 +39,7 @@ task runStressTests(type: Exec) { } group = 'Execution' description = 'Run JMH stresstests' - commandLine "${javaHome}/bin/java", '-jar', 'build/libs/stresstests.jar' + commandLine "${javaHome}/bin/java", '-jar', 'build/libs/stresstests.jar', 'counters.*' } tasks.withType(JavaCompile).configureEach { diff --git a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/Main.java b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/Main.java index 710d2f065..d71346a8a 100644 --- a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/Main.java +++ b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/Main.java @@ -16,11 +16,18 @@ public class Main { public static final String SCENARIOS_PACKAGE = "com.datadoghq.profiler.stresstest.scenarios."; public static void main(String... args) throws Exception { + String filter = "*"; + if (args.length == 1) { + filter = args[0]; + } else if (args.length > 1) { + System.err.println("Usage: java -jar ddprof-stresstest.jar [scenario filter]"); + System.exit(1); + } CommandLineOptions commandLineOptions = new CommandLineOptions(args); Mode mode = Mode.AverageTime; Options options = new OptionsBuilder() .parent(new CommandLineOptions(args)) - .include(SCENARIOS_PACKAGE + "*") + .include(SCENARIOS_PACKAGE + filter) .addProfiler(WhiteboxProfiler.class) .forks(commandLineOptions.getForkCount().orElse(1)) .warmupIterations(commandLineOptions.getWarmupIterations().orElse(0)) diff --git a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/WhiteboxProfiler.java b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/WhiteboxProfiler.java index 5d8d54ecb..99899eb45 100644 --- a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/WhiteboxProfiler.java +++ b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/WhiteboxProfiler.java @@ -46,12 +46,16 @@ public Collection afterIteration(BenchmarkParams benchmarkPara JavaProfiler.getInstance().stop(); long fileSize = Files.size(jfr); Files.deleteIfExists(jfr); - List results = new ArrayList<>(); - results.add(new ScalarResult("jfr_filesize_bytes", fileSize, "", AggregationPolicy.MAX)); - for (Map.Entry counter : JavaProfiler.getInstance().getDebugCounters().entrySet()) { - results.add(new ScalarResult(counter.getKey(), counter.getValue(), "", AggregationPolicy.MAX)); + if (!Boolean.parseBoolean(benchmarkParams.getParam("skipResults"))) { + List results = new ArrayList<>(); + results.add(new ScalarResult("jfr_filesize_bytes", fileSize, "", AggregationPolicy.MAX)); + for (Map.Entry counter : JavaProfiler.getInstance().getDebugCounters().entrySet()) { + results.add(new ScalarResult(counter.getKey(), counter.getValue(), "", AggregationPolicy.MAX)); + } + return results; + } else { + return Collections.emptyList(); } - return results; } catch (IOException e) { e.printStackTrace(); return Collections.emptyList(); diff --git a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/CapturingLambdas.java b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/CapturingLambdas.java similarity index 93% rename from ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/CapturingLambdas.java rename to ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/CapturingLambdas.java index a1d008915..a6d2e06e3 100644 --- a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/CapturingLambdas.java +++ b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/CapturingLambdas.java @@ -1,4 +1,4 @@ -package com.datadoghq.profiler.stresstest.scenarios; +package com.datadoghq.profiler.stresstest.scenarios.counters; import com.datadoghq.profiler.stresstest.Configuration; import org.openjdk.jmh.annotations.Benchmark; diff --git a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/DumpRecording.java b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/DumpRecording.java similarity index 95% rename from ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/DumpRecording.java rename to ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/DumpRecording.java index a76cf30f8..83c4cc29c 100644 --- a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/DumpRecording.java +++ b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/DumpRecording.java @@ -1,4 +1,4 @@ -package com.datadoghq.profiler.stresstest.scenarios; +package com.datadoghq.profiler.stresstest.scenarios.counters; import com.datadoghq.profiler.JavaProfiler; import com.datadoghq.profiler.stresstest.Configuration; diff --git a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/GraphMutation.java b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/GraphMutation.java similarity index 92% rename from ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/GraphMutation.java rename to ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/GraphMutation.java index bbc91009b..58a2e6cd4 100644 --- a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/GraphMutation.java +++ b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/GraphMutation.java @@ -1,4 +1,4 @@ -package com.datadoghq.profiler.stresstest.scenarios; +package com.datadoghq.profiler.stresstest.scenarios.counters; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Threads; diff --git a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/GraphState.java b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/GraphState.java similarity index 88% rename from ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/GraphState.java rename to ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/GraphState.java index a614507e7..cd612832b 100644 --- a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/GraphState.java +++ b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/GraphState.java @@ -1,4 +1,4 @@ -package com.datadoghq.profiler.stresstest.scenarios; +package com.datadoghq.profiler.stresstest.scenarios.counters; import com.datadoghq.profiler.stresstest.Configuration; import org.openjdk.jmh.annotations.*; diff --git a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/NanoTime.java b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/NanoTime.java similarity index 79% rename from ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/NanoTime.java rename to ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/NanoTime.java index 0c592083e..8e4b7dd12 100644 --- a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/NanoTime.java +++ b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/NanoTime.java @@ -1,4 +1,4 @@ -package com.datadoghq.profiler.stresstest.scenarios; +package com.datadoghq.profiler.stresstest.scenarios.counters; import com.datadoghq.profiler.stresstest.Configuration; import org.openjdk.jmh.annotations.Benchmark; diff --git a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/ThreadFilterBenchmark.java b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/ThreadFilterBenchmark.java similarity index 99% rename from ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/ThreadFilterBenchmark.java rename to ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/ThreadFilterBenchmark.java index b0ec1daed..246cbd1dd 100644 --- a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/ThreadFilterBenchmark.java +++ b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/ThreadFilterBenchmark.java @@ -1,9 +1,8 @@ -package com.datadoghq.profiler.stresstest.scenarios; +package com.datadoghq.profiler.stresstest.scenarios.counters; import com.datadoghq.profiler.JavaProfiler; import com.datadoghq.profiler.stresstest.Configuration; import org.openjdk.jmh.annotations.*; -import org.openjdk.jmh.infra.Blackhole; import java.io.FileWriter; import java.io.IOException; diff --git a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/TracedParallelWork.java b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/TracedParallelWork.java similarity index 97% rename from ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/TracedParallelWork.java rename to ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/TracedParallelWork.java index e8e081349..2ed4227bd 100644 --- a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/TracedParallelWork.java +++ b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/counters/TracedParallelWork.java @@ -1,4 +1,4 @@ -package com.datadoghq.profiler.stresstest.scenarios; +package com.datadoghq.profiler.stresstest.scenarios.counters; import com.datadoghq.profiler.ContextSetter; import com.datadoghq.profiler.JavaProfiler; diff --git a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/throughput/ThreadFilterBenchmark.java b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/throughput/ThreadFilterBenchmark.java new file mode 100644 index 000000000..bc03e0ca7 --- /dev/null +++ b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/throughput/ThreadFilterBenchmark.java @@ -0,0 +1,113 @@ +package com.datadoghq.profiler.stresstest.scenarios.throughput; + +import com.datadoghq.profiler.JavaProfiler; +import com.datadoghq.profiler.stresstest.Configuration; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +@State(Scope.Benchmark) +public class ThreadFilterBenchmark extends Configuration { + private JavaProfiler profiler; + + @Param(BASE_COMMAND + ",filter=1") + public String command; + + @Param("true") + public String skipResults; + + @Param({"0", "7", "70000"}) + public String workload; + + private long workloadNum = 0; + + @Setup(Level.Trial) + public void setup() throws IOException { + profiler = JavaProfiler.getInstance(); + workloadNum = Long.parseLong(workload); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Fork(value = 3, warmups = 3) + @Warmup(iterations = 5) + @Measurement(iterations = 8) + @Threads(1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void threadFilterStress01() throws InterruptedException { + profiler.addThread(); + // Simulate per-thread work + Blackhole.consumeCPU(workloadNum); + profiler.removeThread(); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Fork(value = 3, warmups = 3) + @Warmup(iterations = 5) + @Measurement(iterations = 8) + @Threads(2) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void threadFilterStress02() throws InterruptedException { + profiler.addThread(); + // Simulate per-thread work + Blackhole.consumeCPU(workloadNum); + profiler.removeThread(); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Fork(value = 3, warmups = 3) + @Warmup(iterations = 5) + @Measurement(iterations = 8) + @Threads(4) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void threadFilterStress04() throws InterruptedException { + profiler.addThread(); + // Simulate per-thread work + Blackhole.consumeCPU(workloadNum); + profiler.removeThread(); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Fork(value = 3, warmups = 3) + @Warmup(iterations = 5) + @Measurement(iterations = 8) + @Threads(8) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void threadFilterStress08() throws InterruptedException { + profiler.addThread(); + // Simulate per-thread work + Blackhole.consumeCPU(workloadNum); + profiler.removeThread(); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Fork(value = 3, warmups = 3) + @Warmup(iterations = 5) + @Measurement(iterations = 8) + @Threads(16) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void threadFilterStress16() throws InterruptedException { + profiler.addThread(); + // Simulate per-thread work + Blackhole.consumeCPU(workloadNum); + profiler.removeThread(); + } +} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/alloc/AllocationProfilerTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/alloc/AllocationProfilerTest.java index 09d24e163..d995d065f 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/alloc/AllocationProfilerTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/alloc/AllocationProfilerTest.java @@ -22,6 +22,14 @@ protected boolean isPlatformSupported() { @RetryingTest(5) public void shouldGetObjectAllocationSamples() throws InterruptedException { + + // We seem to hit issues on j9: + // OSR (On stack replacement) creates crashes with the profiler. + // ----------- Stack Backtrace ----------- + // prepareForOSR+0xbf (0x00007F51062A4DDF [libj9jit29.so+0x4a4ddf]) + if (Platform.isJ9() && !Platform.isJavaVersionAtLeast(8)) { + return; + } Assumptions.assumeFalse(isAsan() || isTsan()); AllocatingTarget target1 = new AllocatingTarget(); diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/jfr/ObjectSampleDumpSmokeTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/jfr/ObjectSampleDumpSmokeTest.java index 8582783ac..c7196e4a7 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/jfr/ObjectSampleDumpSmokeTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/jfr/ObjectSampleDumpSmokeTest.java @@ -15,7 +15,7 @@ protected boolean isPlatformSupported() { @Override protected String getProfilerCommand() { - return "memory=128:a"; + return "memory=32:a"; } @RetryingTest(5) diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/ContendedWallclockSamplesTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/ContendedWallclockSamplesTest.java index e680633b7..0adbfd812 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/ContendedWallclockSamplesTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/ContendedWallclockSamplesTest.java @@ -51,13 +51,18 @@ public void after() { @TestTemplate @ValueSource(strings = {"vm", "vmx", "fp", "dwarf"}) public void test(@CStack String cstack) { - String config = System.getProperty("ddprof_test.config"); - boolean isSanitizer = config.endsWith("san"); - boolean isJvmci = System.getProperty("java.vm.version", "").contains("jvmci"); - assumeFalse(Platform.isZing() || Platform.isJ9()); + // Skip test entirely on unsupported JVMs (don't use assumeFalse which gets retried) + if (Platform.isZing() || Platform.isJ9()) { + return; + } // Running vm stackwalker tests on JVMCI (Graal), JDK 24, aarch64 and with a sanitizer is crashing in a weird place // This looks like the sanitizer instrumentation is breaking the longjump based crash recovery :( - assumeFalse(Platform.isJavaVersionAtLeast(24) && isJvmci && Platform.isAarch64() && cstack.startsWith("vm") && isSanitizer); + String config = System.getProperty("ddprof_test.config"); + boolean isJvmci = System.getProperty("java.vm.version", "").contains("jvmci"); + boolean isSanitizer = config.endsWith("san"); + if (Platform.isJavaVersionAtLeast(24) && isJvmci && Platform.isAarch64() && cstack.startsWith("vm") && isSanitizer) { + return; + } long result = 0; for (int i = 0; i < 10; i++) {