Skip to content
Merged
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
8 changes: 8 additions & 0 deletions build-logic/conventions/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -42,6 +43,13 @@ abstract class NativeCompileTask @Inject constructor(
@get:Input
abstract val compilerArgs: ListProperty<String>

/**
* 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.
*/
Expand Down Expand Up @@ -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(compiler.get()) + compilerArgs.get()).toMutableList()

// Add C++ standard if specified
if (standardVersion.isPresent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ abstract class NativeLinkExecutableTask @Inject constructor(
@get:Input
abstract val linkerArgs: ListProperty<String>

/**
* 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.
*/
Expand Down Expand Up @@ -135,6 +142,7 @@ abstract class NativeLinkExecutableTask @Inject constructor(
// Build command line
val cmdLine = mutableListOf<String>().apply {
add(linker.get())
addAll(PlatformUtils.macosArchitectureArgs(linker.get()))
addAll(objectPaths)
addAll(linkerArgs.get())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ abstract class NativeLinkTask @Inject constructor(
@get:Input
abstract val linkerArgs: ListProperty<String>

/**
* 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.
*/
Expand Down Expand Up @@ -269,6 +276,7 @@ abstract class NativeLinkTask @Inject constructor(
val cmdLine = mutableListOf<String>().apply {
add(linker.get())
add(sharedFlag)
addAll(PlatformUtils.macosArchitectureArgs(linker.get()))
addAll(objectPaths)
addAll(linkerArgs.get())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,63 @@ 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()

/**
* 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.
*
* 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.
* 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(driver: String): List<String> {
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<String> {
if (platform != Platform.MACOS || !isClangLikeDriver(driver)) {
return emptyList()
}

val archFlag = when (architecture) {
Architecture.X64 -> "x86_64"
Architecture.ARM64 -> "arm64"
else -> throw GradleException(
"Unsupported macOS native build architecture: $architecture"
)
}
return listOf("-arch", archFlag)
}

/**
* Find Homebrew LLVM installation on macOS.
* Returns the LLVM installation path or null if not found.
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>(),
PlatformUtils.macosArchitectureArgsFor(Platform.MACOS, Architecture.X64, "gcc")
)
}

@Test
fun `versioned g++ driver on macOS gets no arch flag`() {
assertEquals(
emptyList<String>(),
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<String>(),
PlatformUtils.macosArchitectureArgsFor(Platform.LINUX, Architecture.X64, "clang++")
)
}

@Test
fun `gcc driver on linux gets no arch flag`() {
assertEquals(
emptyList<String>(),
PlatformUtils.macosArchitectureArgsFor(Platform.LINUX, Architecture.X64, "gcc")
)
}
}
3 changes: 2 additions & 1 deletion ddprof-lib/src/main/cpp/safeAccess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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() 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<String, IItem> 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";
}
}
Loading