From 4d792719669d8aee1cd04549c7b9f14c3c7db290 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Mon, 3 Aug 2026 17:30:06 +0200 Subject: [PATCH 1/3] Add disabled regression test for unprimed native threads --- .../native/tasks/NativeCompileTask.kt | 10 ++- .../native/tasks/NativeLinkExecutableTask.kt | 8 ++ .../datadoghq/native/tasks/NativeLinkTask.kt | 8 ++ .../datadoghq/native/util/PlatformUtils.kt | 33 ++++++++ ddprof-lib/src/main/cpp/safeAccess.cpp | 3 +- .../profiler/cpu/NativeThreadPrimingTest.java | 76 +++++++++++++++++++ 6 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 ddprof-test/src/test/java/com/datadoghq/profiler/cpu/NativeThreadPrimingTest.java diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeCompileTask.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeCompileTask.kt index 4a9ec0e75d..56b7a665e9 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeCompileTask.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeCompileTask.kt @@ -4,6 +4,7 @@ package com.datadoghq.native.tasks import com.datadoghq.native.model.ErrorHandlingMode import com.datadoghq.native.model.LogLevel import com.datadoghq.native.model.SourceSet +import com.datadoghq.native.util.PlatformUtils import org.gradle.api.DefaultTask import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.file.ConfigurableFileCollection @@ -42,6 +43,13 @@ abstract class NativeCompileTask @Inject constructor( @get:Input abstract val compilerArgs: ListProperty + /** + * Target architecture derived from the JVM running the build. Declared on every platform so + * changing architecture invalidates up-to-date checks instead of reusing stale objects. + */ + @get:Input + val targetArchitecture: String = PlatformUtils.targetArchitecture() + /** * The C++ source files to compile. */ @@ -250,7 +258,7 @@ abstract class NativeCompileTask @Inject constructor( objDir.mkdirs() // Build base compiler arguments with convenience properties - val baseArgs = compilerArgs.get().toMutableList() + val baseArgs = (PlatformUtils.macosArchitectureArgs() + compilerArgs.get()).toMutableList() // Add C++ standard if specified if (standardVersion.isPresent) { diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeLinkExecutableTask.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeLinkExecutableTask.kt index d2d8d5e081..4e6ae45ad5 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeLinkExecutableTask.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeLinkExecutableTask.kt @@ -34,6 +34,13 @@ abstract class NativeLinkExecutableTask @Inject constructor( @get:Input abstract val linkerArgs: ListProperty + /** + * Target architecture derived from the JVM running the build. Declared on every platform so + * changing architecture invalidates up-to-date checks instead of reusing stale link outputs. + */ + @get:Input + val targetArchitecture: String = PlatformUtils.targetArchitecture() + /** * The object files to link. */ @@ -135,6 +142,7 @@ abstract class NativeLinkExecutableTask @Inject constructor( // Build command line val cmdLine = mutableListOf().apply { add(linker.get()) + addAll(PlatformUtils.macosArchitectureArgs()) addAll(objectPaths) addAll(linkerArgs.get()) diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeLinkTask.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeLinkTask.kt index 59185c6787..61082bd8ff 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeLinkTask.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeLinkTask.kt @@ -40,6 +40,13 @@ abstract class NativeLinkTask @Inject constructor( @get:Input abstract val linkerArgs: ListProperty + /** + * Target architecture derived from the JVM running the build. Declared on every platform so + * changing architecture invalidates up-to-date checks instead of reusing stale link outputs. + */ + @get:Input + val targetArchitecture: String = PlatformUtils.targetArchitecture() + /** * The object files to link. */ @@ -269,6 +276,7 @@ abstract class NativeLinkTask @Inject constructor( val cmdLine = mutableListOf().apply { add(linker.get()) add(sharedFlag) + addAll(PlatformUtils.macosArchitectureArgs()) addAll(objectPaths) addAll(linkerArgs.get()) diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/util/PlatformUtils.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/util/PlatformUtils.kt index 52719c3766..26f410f648 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/util/PlatformUtils.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/util/PlatformUtils.kt @@ -216,6 +216,39 @@ object PlatformUtils { Platform.MACOS -> "dylib" } + /** + * Architecture targeted by the JVM running the build. + * + * This is used as a Gradle task input on every platform. Native compiler options remain + * platform-specific: macOS accepts a portable {@code -arch} flag, whereas Linux cross + * compilation requires a configured cross compiler (and usually a sysroot). + */ + fun targetArchitecture(): String = currentArchitecture.toString() + + /** + * Forces Apple Clang to produce objects for the JVM's architecture. + * + * The compiler process may run through Rosetta even when Gradle and the JVM run natively on + * Apple Silicon. Without an explicit target, Apple Clang follows the compiler process + * architecture instead, producing objects that cannot be linked with native dependencies for + * the JVM architecture. This is intentionally macOS-only: {@code -arch} is an Apple Clang + * option; Linux cross compilation must be configured with its own target compiler and sysroot. + */ + fun macosArchitectureArgs(): List { + if (currentPlatform != Platform.MACOS) { + return emptyList() + } + + val architecture = when (currentArchitecture) { + Architecture.X64 -> "x86_64" + Architecture.ARM64 -> "arm64" + else -> throw GradleException( + "Unsupported macOS native build architecture: $currentArchitecture" + ) + } + return listOf("-arch", architecture) + } + /** * Find Homebrew LLVM installation on macOS. * Returns the LLVM installation path or null if not found. diff --git a/ddprof-lib/src/main/cpp/safeAccess.cpp b/ddprof-lib/src/main/cpp/safeAccess.cpp index bd637d051a..5738c0f4a2 100644 --- a/ddprof-lib/src/main/cpp/safeAccess.cpp +++ b/ddprof-lib/src/main/cpp/safeAccess.cpp @@ -61,7 +61,8 @@ static void verify_safecopy_range() { #ifdef __APPLE__ #if defined(__x86_64__) - #define current_pc context_rip + #define DU3_PREFIX(s, m) __ ## s.__ ## m + #define current_pc uc_mcontext->DU3_PREFIX(ss,rip) #elif defined(__aarch64__) #define DU3_PREFIX(s, m) __ ## s.__ ## m #define current_pc uc_mcontext->DU3_PREFIX(ss,pc) diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/NativeThreadPrimingTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/NativeThreadPrimingTest.java new file mode 100644 index 0000000000..c3abc2b8bf --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/NativeThreadPrimingTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.datadoghq.profiler.cpu; + +import com.datadoghq.profiler.CStackAwareAbstractProfilerTest; +import com.datadoghq.profiler.junit.CStack; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.params.provider.ValueSource; +import org.openjdk.jmc.common.item.IItem; +import org.openjdk.jmc.common.item.IItemCollection; +import org.openjdk.jmc.common.item.IItemIterable; +import org.openjdk.jmc.common.item.IMemberAccessor; +import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; + +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Regression coverage for JVM threads that existed before the profiler initialized. Thread + * priming must create a {@code ProfiledThread} for those compiler and GC threads before this test + * can be enabled again. + */ +@Disabled("Re-enable when thread priming is reintroduced") +public class NativeThreadPrimingTest extends CStackAwareAbstractProfilerTest { + private static final String UNKNOWN_NATIVE_THREAD_FRAME = + "UNKNOWN_PACKAGE.Unknown Native Thread"; + private static final Pattern NO_JAVA_FRAME_ONLY = + Pattern.compile("^\\s*\\.?no_Java_frame\\(\\)(?:\\s+line:\\s+0)?\\s*$"); + private static final int MAX_SYNTHETIC_NATIVE_THREAD_SAMPLES = 10; + + public NativeThreadPrimingTest(@CStack String cstack) { + super(cstack); + } + + @TestTemplate + @ValueSource(strings = {"vm"}) + public void testPreExistingNativeThreadsHaveUsableFrames(@CStack String cstack) throws Exception { + try (ProfiledCode profiledCode = new ProfiledCode(profiler)) { + for (int i = 0, id = 1; i < 100; i++, id += 3) { + profiledCode.method1(id); + } + stopProfiler(); + + IItemCollection events = verifyEvents("datadog.ExecutionSample"); + int syntheticSamples = 0; + int totalSamples = 0; + for (IItemIterable cpuSamples : events) { + IMemberAccessor frameAccessor = + JdkAttributes.STACK_TRACE_STRING.getAccessor(cpuSamples.getType()); + for (IItem sample : cpuSamples) { + totalSamples++; + String stackTrace = frameAccessor.getMember(sample); + if (stackTrace.contains(UNKNOWN_NATIVE_THREAD_FRAME) + || NO_JAVA_FRAME_ONLY.matcher(stackTrace).matches()) { + syntheticSamples++; + } + } + } + + assertTrue(syntheticSamples <= MAX_SYNTHETIC_NATIVE_THREAD_SAMPLES, + "Expected at most " + MAX_SYNTHETIC_NATIVE_THREAD_SAMPLES + + " samples with a synthetic native-thread frame, got " + + syntheticSamples + " of " + totalSamples); + } + } + + @Override + protected String getProfilerCommand() { + return "cpu=1ms"; + } +} From c5830770f74f048652c7abf90ca93fb8d796e640 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Tue, 4 Aug 2026 15:56:56 +0200 Subject: [PATCH 2/3] Gate macOS -arch flag on clang-family drivers; drop unused test param Fixes PR review feedback: macosArchitectureArgs() unconditionally added Apple Clang's -arch flag, breaking g++ builds via -Pnative.forceCompiler. Also removes an unused @CStack parameter from a test template method. Co-Authored-By: Claude Sonnet 5 --- build-logic/conventions/build.gradle.kts | 8 +++ .../native/tasks/NativeCompileTask.kt | 2 +- .../native/tasks/NativeLinkExecutableTask.kt | 2 +- .../datadoghq/native/tasks/NativeLinkTask.kt | 2 +- .../datadoghq/native/util/PlatformUtils.kt | 34 +++++++++-- .../native/util/PlatformUtilsTest.kt | 57 +++++++++++++++++++ .../profiler/cpu/NativeThreadPrimingTest.java | 2 +- 7 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 build-logic/conventions/src/test/kotlin/com/datadoghq/native/util/PlatformUtilsTest.kt diff --git a/build-logic/conventions/build.gradle.kts b/build-logic/conventions/build.gradle.kts index d7a1e5255d..e64fb3796c 100644 --- a/build-logic/conventions/build.gradle.kts +++ b/build-logic/conventions/build.gradle.kts @@ -14,6 +14,14 @@ repositories { dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib") implementation("com.diffplug.spotless:spotless-plugin-gradle:8.9.0") + + testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.3") + testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.9.3") +} + +tasks.test { + useJUnitPlatform() } gradlePlugin { diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeCompileTask.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeCompileTask.kt index 56b7a665e9..580275f794 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeCompileTask.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeCompileTask.kt @@ -258,7 +258,7 @@ abstract class NativeCompileTask @Inject constructor( objDir.mkdirs() // Build base compiler arguments with convenience properties - val baseArgs = (PlatformUtils.macosArchitectureArgs() + compilerArgs.get()).toMutableList() + val baseArgs = (PlatformUtils.macosArchitectureArgs(compiler.get()) + compilerArgs.get()).toMutableList() // Add C++ standard if specified if (standardVersion.isPresent) { diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeLinkExecutableTask.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeLinkExecutableTask.kt index 4e6ae45ad5..a0d9c1be00 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeLinkExecutableTask.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeLinkExecutableTask.kt @@ -142,7 +142,7 @@ abstract class NativeLinkExecutableTask @Inject constructor( // Build command line val cmdLine = mutableListOf().apply { add(linker.get()) - addAll(PlatformUtils.macosArchitectureArgs()) + addAll(PlatformUtils.macosArchitectureArgs(linker.get())) addAll(objectPaths) addAll(linkerArgs.get()) diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeLinkTask.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeLinkTask.kt index 61082bd8ff..492a89eb74 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeLinkTask.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/tasks/NativeLinkTask.kt @@ -276,7 +276,7 @@ abstract class NativeLinkTask @Inject constructor( val cmdLine = mutableListOf().apply { add(linker.get()) add(sharedFlag) - addAll(PlatformUtils.macosArchitectureArgs()) + addAll(PlatformUtils.macosArchitectureArgs(linker.get())) addAll(objectPaths) addAll(linkerArgs.get()) diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/util/PlatformUtils.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/util/PlatformUtils.kt index 26f410f648..33c63e3f19 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/util/PlatformUtils.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/util/PlatformUtils.kt @@ -225,6 +225,16 @@ object PlatformUtils { */ fun targetArchitecture(): String = currentArchitecture.toString() + /** + * Returns true unless the given compiler/linker driver is GCC-family (`gcc`, `g++`, or a + * versioned variant like `g++-13`), which does not accept Apple Clang's {@code -arch} flag. + * Anything else (`clang++`, `c++`, `cc`, or a custom path) is treated as clang-like. + */ + private fun isClangLikeDriver(driver: String): Boolean { + val name = File(driver).name + return !Regex("""^(gcc|g\+\+)(-\d+(\.\d+)*)?$""").matches(name) + } + /** * Forces Apple Clang to produce objects for the JVM's architecture. * @@ -233,20 +243,34 @@ object PlatformUtils { * architecture instead, producing objects that cannot be linked with native dependencies for * the JVM architecture. This is intentionally macOS-only: {@code -arch} is an Apple Clang * option; Linux cross compilation must be configured with its own target compiler and sysroot. + * GCC (via {@code -Pnative.forceCompiler}) doesn't accept {@code -arch}, so the flag is + * skipped when {@code driver} resolves to a GCC-family executable. */ - fun macosArchitectureArgs(): List { - if (currentPlatform != Platform.MACOS) { + fun macosArchitectureArgs(driver: String): List { + return macosArchitectureArgsFor(currentPlatform, currentArchitecture, driver) + } + + /** + * Platform/architecture-parameterized implementation of [macosArchitectureArgs], split out so + * it can be unit tested independently of the host OS running the build. + */ + internal fun macosArchitectureArgsFor( + platform: Platform, + architecture: Architecture, + driver: String + ): List { + if (platform != Platform.MACOS || !isClangLikeDriver(driver)) { return emptyList() } - val architecture = when (currentArchitecture) { + val archFlag = when (architecture) { Architecture.X64 -> "x86_64" Architecture.ARM64 -> "arm64" else -> throw GradleException( - "Unsupported macOS native build architecture: $currentArchitecture" + "Unsupported macOS native build architecture: $architecture" ) } - return listOf("-arch", architecture) + return listOf("-arch", archFlag) } /** diff --git a/build-logic/conventions/src/test/kotlin/com/datadoghq/native/util/PlatformUtilsTest.kt b/build-logic/conventions/src/test/kotlin/com/datadoghq/native/util/PlatformUtilsTest.kt new file mode 100644 index 0000000000..ace5d86c12 --- /dev/null +++ b/build-logic/conventions/src/test/kotlin/com/datadoghq/native/util/PlatformUtilsTest.kt @@ -0,0 +1,57 @@ +package com.datadoghq.native.util + +import com.datadoghq.native.model.Architecture +import com.datadoghq.native.model.Platform +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class PlatformUtilsTest { + + @Test + fun `gcc driver on macOS gets no arch flag`() { + assertEquals( + emptyList(), + PlatformUtils.macosArchitectureArgsFor(Platform.MACOS, Architecture.X64, "gcc") + ) + } + + @Test + fun `versioned g++ driver on macOS gets no arch flag`() { + assertEquals( + emptyList(), + PlatformUtils.macosArchitectureArgsFor(Platform.MACOS, Architecture.ARM64, "g++-13") + ) + } + + @Test + fun `clang++ driver on macOS gets x86_64 arch flag`() { + assertEquals( + listOf("-arch", "x86_64"), + PlatformUtils.macosArchitectureArgsFor(Platform.MACOS, Architecture.X64, "clang++") + ) + } + + @Test + fun `cc driver on macOS gets arm64 arch flag`() { + assertEquals( + listOf("-arch", "arm64"), + PlatformUtils.macosArchitectureArgsFor(Platform.MACOS, Architecture.ARM64, "cc") + ) + } + + @Test + fun `clang-like driver on linux gets no arch flag`() { + assertEquals( + emptyList(), + PlatformUtils.macosArchitectureArgsFor(Platform.LINUX, Architecture.X64, "clang++") + ) + } + + @Test + fun `gcc driver on linux gets no arch flag`() { + assertEquals( + emptyList(), + PlatformUtils.macosArchitectureArgsFor(Platform.LINUX, Architecture.X64, "gcc") + ) + } +} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/NativeThreadPrimingTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/NativeThreadPrimingTest.java index c3abc2b8bf..a231ee4dbb 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/NativeThreadPrimingTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/NativeThreadPrimingTest.java @@ -39,7 +39,7 @@ public NativeThreadPrimingTest(@CStack String cstack) { @TestTemplate @ValueSource(strings = {"vm"}) - public void testPreExistingNativeThreadsHaveUsableFrames(@CStack String cstack) throws Exception { + public void testPreExistingNativeThreadsHaveUsableFrames() throws Exception { try (ProfiledCode profiledCode = new ProfiledCode(profiler)) { for (int i = 0, id = 1; i < 100; i++, id += 3) { profiledCode.method1(id); From b677367bf35d5d077719a6f30551ea509f8c2e65 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Wed, 5 Aug 2026 12:21:09 +0200 Subject: [PATCH 3/3] Disable MonitorDeflationThreadSafetyTest on J9/OpenJ9 AsyncGetCallTrace can self-deadlock on jitArtifactMonitor under this test's monitor churn workload. Upstream bug: eclipse-openj9/openj9#24472 Co-Authored-By: Claude Sonnet 5 --- .../cpu/MonitorDeflationThreadSafetyTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/MonitorDeflationThreadSafetyTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/MonitorDeflationThreadSafetyTest.java index fa109f5619..8fcbc41be4 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/MonitorDeflationThreadSafetyTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/MonitorDeflationThreadSafetyTest.java @@ -6,6 +6,8 @@ package com.datadoghq.profiler.cpu; import com.datadoghq.profiler.AbstractProfilerTest; +import com.datadoghq.profiler.Platform; +import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; /** @@ -38,6 +40,15 @@ public class MonitorDeflationThreadSafetyTest extends AbstractProfilerTest { @RetryingTest(3) public void monitorDeflationDoesNotCrashProfiler() throws Exception { + // Disabled on J9/OpenJ9: this test's monitor-inflate/deflate churn + // combined with signal-based sampling reliably self-deadlocks the + // JVM inside AsyncGetCallTrace, which can re-enter the + // non-reentrant jitArtifactMonitor lock on the same thread that + // already holds it via the J9VM_JIT_FULL_SPEED_DEBUG fallback path + // (jitGetExceptionTable -> jitGetExceptionTableFromPCSync). Upstream + // bug: https://github.com/eclipse-openj9/openj9/issues/24472 + Assumptions.assumeFalse(Platform.isJ9(), "known OpenJ9 hang, see eclipse-openj9/openj9#24472"); + // The profiler is already started by AbstractProfilerTest.setupProfiler(). // Run monitor churn on the test thread so the CPU profiler definitely // delivers signals during the deflation window.