Skip to content

Commit 1aa5656

Browse files
committed
Merge
2 parents 0c0e70d + 6f9b6c9 commit 1aa5656

23 files changed

Lines changed: 1319 additions & 48 deletions

build-logic/conventions/src/main/kotlin/com/datadoghq/profiler/ProfilerTestPlugin.kt

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import org.gradle.api.provider.Property
1515
import org.gradle.api.tasks.Exec
1616
import org.gradle.api.tasks.SourceSetContainer
1717
import org.gradle.api.tasks.testing.Test
18+
import java.io.File
1819
import java.time.Duration
1920
import javax.inject.Inject
2021

@@ -76,6 +77,61 @@ import javax.inject.Inject
7677
* ```
7778
*/
7879
class ProfilerTestPlugin : Plugin<Project> {
80+
81+
/**
82+
* Major version of the *test* JVM, read from its `release` file (`JAVA_VERSION="..."`) rather
83+
* than by executing the launcher.
84+
*
85+
* Executing `$JAVA_TEST_HOME/bin/java -version` (PlatformUtils.testJvmMajorVersion()) is
86+
* unreliable here: in the musl split-JDK matrix it has been observed to report the build JDK
87+
* (21) even when the test JVM is JDK 8, which put a JDK-21-only `--add-exports` onto a JDK-8
88+
* launcher and aborted it. Reading the `release` file is a pure file read of the same
89+
* JAVA_TEST_HOME the executable is resolved from — deterministic, no subprocess, no exec-format
90+
* or PATH hazards. Returns 0 when it cannot be determined (missing/old `release`), so callers
91+
* fail safe: they omit the flag, the profiler degrades to thread-scoped storage, and the
92+
* carrier-scoping tests skip — never an abort.
93+
*/
94+
private fun testJvmMajorVersionFromRelease(): Int = try {
95+
val release = File(PlatformUtils.testJavaHome(), "release")
96+
val version = release.takeIf { it.isFile }
97+
?.readLines()
98+
?.firstOrNull { it.startsWith("JAVA_VERSION=") }
99+
?.substringAfter('=')?.trim()?.trim('"')
100+
// "1.8.0_452" -> 8 ; "21.0.5" -> 21
101+
val parts = version?.split('.').orEmpty()
102+
val majorToken = when {
103+
parts.isEmpty() -> ""
104+
parts[0] == "1" && parts.size > 1 -> parts[1]
105+
else -> parts[0]
106+
}
107+
majorToken.takeWhile { it.isDigit() }.toIntOrNull() ?: 0
108+
} catch (e: Exception) {
109+
0
110+
}
111+
112+
/**
113+
* JVM args required to enable carrier-scoped OTEL context storage
114+
* (`OtelContextStorage.Mode.CARRIER`), or an empty list when the test JVM does not support it.
115+
*
116+
* Carrier scoping resolves `jdk.internal.misc.CarrierThreadLocal`, which lives in a
117+
* non-exported package, so it needs `--add-exports java.base/jdk.internal.misc=ALL-UNNAMED`.
118+
* That type only exists on JDK 21+, and the flag *aborts* a Java 8 JVM ("Unrecognized option"),
119+
* so it is gated on the version of the actual test JVM.
120+
*
121+
* MUST be evaluated at task execution time (inside doFirst), not configuration time: the test
122+
* JVM is selected via JAVA_TEST_HOME, which the CI only makes resolvable at execution time (see
123+
* the `executable` assignments below).
124+
*/
125+
private fun carrierExportJvmArgs(project: Project): List<String> {
126+
val major = testJvmMajorVersionFromRelease()
127+
val enabled = major >= 21
128+
project.logger.info(
129+
"ddprof: carrier --add-exports gate — testJavaHome={}, detected major={}, flag {}",
130+
PlatformUtils.testJavaHome(), major, if (enabled) "ADDED" else "omitted"
131+
)
132+
return if (enabled) listOf("--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED") else emptyList()
133+
}
134+
79135
override fun apply(project: Project) {
80136
val extension = project.extensions.create(
81137
"profilerTest",
@@ -238,6 +294,8 @@ class ProfilerTestPlugin : Plugin<Project> {
238294
testTask.doFirst {
239295
val allArgs = mutableListOf<String>()
240296
allArgs.addAll(testConfig.standardJvmArgs)
297+
// Version-gated at execution time, when the real test JVM is resolvable.
298+
allArgs.addAll(carrierExportJvmArgs(project))
241299

242300
if (extension.nativeLibDir.isPresent) {
243301
allArgs.add("-Djava.library.path=${extension.nativeLibDir.get().asFile.absolutePath}")
@@ -255,12 +313,21 @@ class ProfilerTestPlugin : Plugin<Project> {
255313

256314
// Sanitizer conditions
257315
when (testConfig.configName) {
258-
"asan" -> testTask.onlyIf {
259-
PlatformUtils.locateLibasan() != null &&
260-
// Skip J9+ASAN: OpenJ9 has known GC stack-scanning and defineClass
261-
// race bugs exposed by ASAN timing
262-
// https://github.com/eclipse-openj9/openj9/issues/23514
263-
!PlatformUtils.isTestJvmJ9()
316+
"asan" -> {
317+
testTask.onlyIf {
318+
PlatformUtils.locateLibasan() != null &&
319+
// Skip J9+ASAN: OpenJ9 has known GC stack-scanning and defineClass
320+
// race bugs exposed by ASAN timing
321+
// https://github.com/eclipse-openj9/openj9/issues/23514
322+
!PlatformUtils.isTestJvmJ9()
323+
}
324+
// The ASAN test JVM is pinned to -Xmx512m (see ConfigurationPresets.kt)
325+
// to keep the heap below ASan's shadow-memory region. Without forking,
326+
// Gradle reuses one JVM for the whole suite, and per-test allocations
327+
// (JFR recordings, JMC object models) accumulate until it OOMs late in
328+
// the run even though every individual test passes. Restart periodically
329+
// to bound cumulative heap growth.
330+
testTask.setForkEvery(25)
264331
}
265332
// TSan + JVM integration tests are incompatible: the profiler's signal
266333
// handlers (SIGPROF at 1ms) are TSan-instrumented; when a signal fires
@@ -302,6 +369,8 @@ class ProfilerTestPlugin : Plugin<Project> {
302369

303370
// JVM args
304371
allArgs.addAll(testConfig.standardJvmArgs)
372+
// Version-gated at execution time, when the real test JVM (JAVA_TEST_HOME) is resolvable.
373+
allArgs.addAll(carrierExportJvmArgs(project))
305374
if (extension.nativeLibDir.isPresent) {
306375
allArgs.add("-Djava.library.path=${extension.nativeLibDir.get().asFile.absolutePath}")
307376
}
@@ -661,7 +730,13 @@ abstract class ProfilerTestExtension @Inject constructor(
661730
abstract val applicationMainClass: Property<String>
662731

663732
init {
664-
// Standard JVM arguments for profiler testing
733+
// Standard JVM arguments for profiler testing.
734+
// NOTE: JDK-version-gated flags (e.g. the carrier-scoping --add-exports) must NOT be
735+
// added here. This convention is computed at configuration time, where JAVA_TEST_HOME
736+
// is not yet resolvable and PlatformUtils.testJavaHome() falls back to the *build* JDK
737+
// (JAVA_HOME) — which misdetects in the musl split-JDK CI (build JDK 21, test JDK 8) and
738+
// would emit a JDK-21 flag onto a JDK-8 test JVM. Version-gated flags are added at
739+
// execution time in the task doFirst blocks instead (see ProfilerTestPlugin).
665740
standardJvmArgs.convention(listOf(
666741
"-Djdk.attach.allowAttachSelf", // Allow profiler to attach to self
667742
"-Djol.tryWithSudo=true", // JOL memory layout analysis

ddprof-lib/src/main/cpp/ctimer.h

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,15 @@
2525
#include <signal.h>
2626

2727
class CTimer : public Engine {
28+
private:
29+
// cppcheck-suppress unusedPrivateFunction
30+
static void signalHandler(int signo, siginfo_t *siginfo, void *ucontext);
31+
2832
protected:
29-
// This is accessed from signal handlers, so must be async-signal-safe
33+
// Accessed from signal handlers (including CTimerJvmti subclass), so must
34+
// be async-signal-safe. Mutated via enableEvents().
3035
static bool _enabled;
36+
3137
static long _interval;
3238
static CStack _cstack;
3339
static int _signal;
@@ -38,10 +44,6 @@ class CTimer : public Engine {
3844
int registerThread(int tid);
3945
void unregisterThread(int tid);
4046

41-
private:
42-
// cppcheck-suppress unusedPrivateFunction
43-
static void signalHandler(int signo, siginfo_t *siginfo, void *ucontext);
44-
4547
public:
4648
const char *units() { return "ns"; }
4749

ddprof-lib/src/main/cpp/ctimer_linux.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "counters.h"
2121
#include "guards.h"
2222
#include "ctimer.h"
23+
#include "signalInflight.h"
2324
#include "debugSupport.h"
2425
#include "jvmThread.h"
2526
#include "libraries.h"
@@ -180,6 +181,8 @@ void CTimer::stop() {
180181
for (int i = 0; i < _max_timers; i++) {
181182
unregisterThread(i);
182183
}
184+
// Note: SignalInflight::drain() is called by Profiler::stop() after
185+
// disableEngines(); see signalInflight.h.
183186
}
184187

185188
Error CTimerJvmti::check(Arguments &args) {
@@ -210,6 +213,8 @@ void CTimerJvmti::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
210213
}
211214
Counters::increment(CTIMER_SIGNAL_OWN);
212215

216+
InflightGuard inflight;
217+
213218
CriticalSection cs;
214219
if (!cs.entered()) {
215220
return;
@@ -261,6 +266,8 @@ void CTimer::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
261266
}
262267
Counters::increment(CTIMER_SIGNAL_OWN);
263268

269+
InflightGuard inflight;
270+
264271
// Atomically try to enter critical section - prevents all reentrancy races
265272
CriticalSection cs;
266273
if (!cs.entered()) {
@@ -270,8 +277,9 @@ void CTimer::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
270277
int saved_errno = errno;
271278
// we want to ensure memory order because of the possibility the instance gets
272279
// cleared
273-
if (!__atomic_load_n(&_enabled, __ATOMIC_ACQUIRE))
280+
if (!__atomic_load_n(&_enabled, __ATOMIC_ACQUIRE)) {
274281
return;
282+
}
275283
int tid = 0;
276284
ProfiledThread *current = ProfiledThread::currentSignalSafe();
277285
assert(current == nullptr || !current->isDeepCrashHandler());

ddprof-lib/src/main/cpp/itimer.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@
2020
#include "jvmThread.h"
2121
#include "os.h"
2222
#include "profiler.h"
23+
#include "signalInflight.h"
2324
#include "stackWalker.h"
2425
#include "threadLocalData.h"
2526
#include "threadState.inline.h"
2627
#include "guards.h"
2728
#include <sys/time.h>
2829

29-
volatile bool ITimer::_enabled = false;
30+
bool ITimer::_enabled = false;
3031
long ITimer::_interval;
3132
CStack ITimer::_cstack;
3233

@@ -38,7 +39,8 @@ void ITimer::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
3839
// is therefore vulnerable to the foreign-SIGPROF deadlock scenario this
3940
// feature addresses. Use CTimer (the default) when signal-origin
4041
// validation is required.
41-
if (!_enabled)
42+
InflightGuard inflight;
43+
if (!__atomic_load_n(&_enabled, __ATOMIC_ACQUIRE))
4244
return;
4345

4446
// Atomically try to enter critical section - prevents all reentrancy races
@@ -99,11 +101,12 @@ void ITimer::stop() {
99101
setitimer(ITIMER_PROF, &tv, NULL);
100102
}
101103

102-
volatile bool ITimerJvmti::_enabled = false;
104+
bool ITimerJvmti::_enabled = false;
103105
long ITimerJvmti::_interval = 0;
104106

105107
void ITimerJvmti::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
106108
SIGNAL_HANDLER_GUARD();
109+
InflightGuard inflight;
107110
CriticalSection cs;
108111
if (!cs.entered()) {
109112
return;

ddprof-lib/src/main/cpp/itimer.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
class ITimer : public Engine {
2424
private:
25-
static volatile bool _enabled;
25+
static bool _enabled;
2626
static long _interval;
2727
static CStack _cstack;
2828

@@ -39,7 +39,9 @@ class ITimer : public Engine {
3939
Error start(Arguments &args);
4040
void stop();
4141

42-
inline void enableEvents(bool enabled) { _enabled = enabled; }
42+
inline void enableEvents(bool enabled) {
43+
__atomic_store_n(&_enabled, enabled, __ATOMIC_RELEASE);
44+
}
4345
};
4446

4547
// CPU-time engine identical to ITimer in its timer mechanism (process-wide
@@ -51,7 +53,7 @@ class ITimer : public Engine {
5153
// stack walking rather than relying on the signal-frame PC.
5254
class ITimerJvmti : public Engine {
5355
private:
54-
static volatile bool _enabled;
56+
static bool _enabled;
5557
static long _interval;
5658

5759
static void signalHandler(int signo, siginfo_t *siginfo, void *ucontext);

ddprof-lib/src/main/cpp/perfEvents.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class StackContext;
3030

3131
class PerfEvents : public Engine {
3232
private:
33-
static volatile bool _enabled;
33+
static bool _enabled;
3434
static int _max_events;
3535
static PerfEvent *_events;
3636
static PerfEventType *_event_type;
@@ -62,7 +62,9 @@ class PerfEvents : public Engine {
6262

6363
static const char *getEventName(int event_id);
6464

65-
inline void enableEvents(bool enabled) { _enabled = enabled; }
65+
inline void enableEvents(bool enabled) {
66+
__atomic_store_n(&_enabled, enabled, __ATOMIC_RELEASE);
67+
}
6668
};
6769

6870
#else

ddprof-lib/src/main/cpp/perfEvents_linux.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "os.h"
3030
#include "perfEvents.h"
3131
#include "profiler.h"
32+
#include "signalInflight.h"
3233
#include "spinLock.h"
3334
#include "stackFrame.h"
3435
#include "stackWalker.h"
@@ -559,7 +560,7 @@ class PerfEvent : public SpinLock {
559560
friend class PerfEvents;
560561
};
561562

562-
volatile bool PerfEvents::_enabled = false;
563+
bool PerfEvents::_enabled = false;
563564
int PerfEvents::_max_events = -1;
564565
PerfEvent *PerfEvents::_events = NULL;
565566
PerfEventType *PerfEvents::_event_type = NULL;
@@ -734,6 +735,7 @@ void PerfEvents::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
734735
// Looks like an external signal; don't treat as a profiling event
735736
return;
736737
}
738+
InflightGuard inflight;
737739
// Atomically try to enter critical section - prevents all reentrancy races
738740
CriticalSection cs;
739741
if (!cs.entered()) {
@@ -744,7 +746,7 @@ void PerfEvents::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
744746
current->noteCPUSample(Profiler::instance()->recordingEpoch());
745747
}
746748
int tid = current != NULL ? current->tid() : OS::threadId();
747-
if (_enabled) {
749+
if (__atomic_load_n(&_enabled, __ATOMIC_ACQUIRE)) {
748750
Shims::instance().setSighandlerTid(tid);
749751

750752
u64 counter = readCounter(siginfo, ucontext);

0 commit comments

Comments
 (0)