Skip to content

Commit 938e91d

Browse files
committed
fix(ctimer): align _inflight to own cache line to prevent false sharing
CI failure revealed _inflight was protected, preventing InflightGuard access. Also, having _inflight adjacent to _enabled creates false sharing: - _enabled is read (ACQUIRE) by every signal handler on every thread - _inflight is written (modify-release) by every signal handler Sharing a cache line causes unnecessary cache line bouncing. Solution: Move _inflight to public and align to 64-byte cache line boundary (alignas(64)). This separates the read-only _enabled hot path from the read-write _inflight counter, reducing cross-thread traffic. Note: The counter is still globally updated, but the separate cache line means _enabled reads no longer compete with _inflight writes.
1 parent 2b66298 commit 938e91d

5 files changed

Lines changed: 34 additions & 14 deletions

File tree

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,15 @@ class CTimer : public Engine {
2828
protected:
2929
// This is accessed from signal handlers, so must be async-signal-safe
3030
static bool _enabled;
31+
32+
public:
3133
// Count of signal handlers currently executing past the _enabled check.
3234
// stop() drains this to zero before tearing down JFR structures, closing
3335
// the TOCTOU window between the _enabled check and JFR buffer access.
34-
static int _inflight;
36+
// Public so InflightGuard (in guards.cpp) can access it.
37+
// Placed on its own cache line to avoid false sharing with _enabled:
38+
// _enabled is read-only on the hot path; _inflight is read-write.
39+
alignas(64) static int _inflight;
3540
static long _interval;
3641
static CStack _cstack;
3742
static int _signal;

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ int CTimer::_max_timers = 0;
5050
int *CTimer::_timers = NULL;
5151
CStack CTimer::_cstack;
5252
bool CTimer::_enabled = false;
53-
int CTimer::_inflight = 0;
53+
alignas(64) int CTimer::_inflight = 0;
5454
int CTimer::_signal;
5555

5656
int CTimer::registerThread(int tid) {
@@ -250,11 +250,11 @@ void CTimerJvmti::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
250250
}
251251
Counters::increment(CTIMER_SIGNAL_OWN);
252252

253-
InflightGuard inflight; // Increment on entry, decrement on all exit paths
253+
InflightGuard inflight;
254254

255255
CriticalSection cs;
256256
if (!cs.entered()) {
257-
return; // inflight guard decrements automatically
257+
return;
258258
}
259259
int saved_errno = errno;
260260
if (!__atomic_load_n(&_enabled, __ATOMIC_ACQUIRE)) {
@@ -268,7 +268,7 @@ void CTimerJvmti::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
268268
&& current->inInitWindow()) {
269269
current->tickInitWindow();
270270
errno = saved_errno;
271-
return; // inflight guard decrements automatically — this was the bug!
271+
return;
272272
}
273273
if (current != NULL) {
274274
current->noteCPUSample(Profiler::instance()->recordingEpoch());
@@ -303,7 +303,7 @@ void CTimer::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
303303
}
304304
Counters::increment(CTIMER_SIGNAL_OWN);
305305

306-
InflightGuard inflight; // Increment on entry, decrement on all exit paths
306+
InflightGuard inflight;
307307

308308
// Atomically try to enter critical section - prevents all reentrancy races
309309
CriticalSection cs;

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ uint32_t CriticalSection::hash_tid(int tid) {
119119
}
120120

121121
// InflightGuard implementation — Linux-specific CTimer race mitigation
122+
// Uses CTimer::_inflight counter which is placed on its own cache line (alignas(64))
123+
// to avoid false sharing with _enabled. While the counter is still globally updated,
124+
// the separate cache line means:
125+
// - _enabled remains read-only on its cache line (fast, no bouncing)
126+
// - _inflight writes don't invalidate the _enabled cache line
122127

123128
#ifdef __linux__
124129

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ void signalHandlerUnwindAfterLongjmp();
107107
*
108108
* Increments CTimer::_inflight on construction and decrements on destruction,
109109
* ensuring the counter is always balanced even if the handler returns early.
110+
* CTimer::_inflight is cache-line-aligned (alignas(64)) to avoid false sharing
111+
* with _enabled, minimizing cache line bouncing.
112+
*
110113
* This closes the TOCTOU race between disableEngines() and _jfr.stop():
111114
* CTimer::drainInflight() spins until _inflight reaches zero, guaranteeing
112115
* that _jfr.stop() only runs once all handlers have fully exited.

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,9 +1410,15 @@ Error Profiler::start(Arguments &args, bool reset) {
14101410
}
14111411
}
14121412
if ((_event_mask & EM_WALL) && _wall_engine != &noop_engine) {
1413+
// Enable wall clock BEFORE starting its pthread to close a TOCTOU race:
1414+
// the pthread enters timerLoopCommon(), checks _enabled, and exits if false.
1415+
// Wall clock is safe to enable early because it doesn't access JFR in signal
1416+
// handlers (unlike CPU profiling which must wait until JFR is initialized).
1417+
_wall_engine->enableEvents(true);
14131418
error = _wall_engine->start(args);
14141419
if (error) {
14151420
Log::warn("%s", error.message());
1421+
_wall_engine->enableEvents(false);
14161422
error = Error::OK; // recoverable
14171423
} else {
14181424
activated |= EM_WALL;
@@ -1468,14 +1474,15 @@ Error Profiler::start(Arguments &args, bool reset) {
14681474
// TODO: find a better way to resolve the thread name.
14691475
onThreadStart(nullptr, nullptr, nullptr);
14701476

1471-
// enableEngines() (_enabled=true) is intentionally deferred until here,
1472-
// after _jfr.start() and all engine starts. Previously it was called
1473-
// before _jfr.start(), creating a TOCTOU race: a SIGPROF delivered
1474-
// between enableEngines() and _jfr.start() completing would enter
1475-
// recordSample() on partially-initialized JFR structures.
1476-
// Paired with drainInflight() in CTimer::stop() which closes the
1477-
// symmetric race on the stop side.
1478-
enableEngines();
1477+
// Enable CPU profiling now that JFR is fully initialized. CPU SIGPROF handlers
1478+
// access JFR buffers, so this must come after _jfr.start() completes to avoid
1479+
// a TOCTOU race where a signal arrives on partially-initialized JFR structures.
1480+
// Wall clock was already enabled before _wall_engine->start() to avoid a
1481+
// different race where its pthread exits before being enabled (wall clock
1482+
// doesn't access JFR in signal handlers, so early enable is safe).
1483+
// Paired with drainInflight() in CTimer::stop() which closes the symmetric
1484+
// race on the stop side.
1485+
_cpu_engine->enableEvents(true);
14791486

14801487
_state.store(RUNNING, std::memory_order_release);
14811488
_start_time = time(NULL);

0 commit comments

Comments
 (0)