Skip to content

Commit 1e75102

Browse files
committed
refactor: move inflight counter from CTimer to SignalInflight module
Preparation for extending the JFR-teardown drain to all signal handlers that write JFR (jbachorik review). The counter and drain logic were tied to CTimer, but the question 'can JFR be torn down right now?' is a single global question that any JFR-writing signal handler must answer. Tying it to CTimer made it awkward to extend. No behavior change. Pure rename/move: - new signalInflight.h / signalInflight.cpp: SignalInflight class owns the cache-line-aligned counter, enter()/exit()/hasInflight()/drain() static methods, and the InflightGuard RAII type - ctimer.h: drop _inflight, enterHandler, exitHandler, hasInflightHandlers, drainInflight, and the embedded InflightGuard class - ctimer_linux.cpp: drop the counter definition and drainInflight() implementation - profiler.cpp: Profiler::stop() now calls SignalInflight::drain() instead of CTimer::drainInflight() Existing CTimer signal handlers still construct InflightGuard at entry; that type now lives in signalInflight.h and the existing include of ctimer.h is enough for them (ctimer_linux.cpp also includes signalInflight.h explicitly for clarity). Follow-up commit (B2) will add InflightGuard to PerfEvents, ITimer, ITimerJvmti, WallClockASGCT, WallClockJvmti so they participate in the drain too.
1 parent 7c399c9 commit 1e75102

5 files changed

Lines changed: 146 additions & 115 deletions

File tree

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

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,6 @@
2626

2727
class CTimer : public Engine {
2828
private:
29-
// Count of signal handlers currently executing past the _enabled check.
30-
// drainInflight() waits for this to reach zero before JFR teardown, closing
31-
// the TOCTOU window between the _enabled check and JFR buffer access.
32-
// Placed on its own cache line to avoid false sharing with _enabled:
33-
// _enabled is read-only on the hot path; _inflight is read-write.
34-
alignas(64) static int _inflight;
35-
3629
// cppcheck-suppress unusedPrivateFunction
3730
static void signalHandler(int signo, siginfo_t *siginfo, void *ucontext);
3831

@@ -66,50 +59,10 @@ class CTimer : public Engine {
6659
__atomic_store_n(&_enabled, enabled, __ATOMIC_RELEASE);
6760
}
6861

69-
// In-flight handler tracking accessors (used by InflightGuard).
70-
// ACQUIRE on increment / RELEASE on decrement so drainInflight() observes
71-
// all handler-side writes before observing the counter at zero.
72-
static void enterHandler() {
73-
__atomic_fetch_add(&_inflight, 1, __ATOMIC_ACQUIRE);
74-
}
75-
static void exitHandler() {
76-
__atomic_fetch_sub(&_inflight, 1, __ATOMIC_RELEASE);
77-
}
78-
static bool hasInflightHandlers() {
79-
return __atomic_load_n(&_inflight, __ATOMIC_ACQUIRE) > 0;
80-
}
81-
82-
// Spin until all signal handlers that passed the _enabled=true check have
83-
// returned. Must be called with _enabled already false (after disableEngines()),
84-
// before any JFR teardown that handlers could race against.
85-
// Returns true if all handlers drained successfully, false if timeout fired.
86-
// Caller must NOT proceed with JFR teardown if this returns false.
87-
static bool drainInflight();
88-
8962
// Get the signal number used by CTimer (0 if not initialized)
9063
static int getSignal() { return _signal; }
9164
};
9265

93-
// RAII guard for CTimer signal handler in-flight tracking.
94-
// Increments the in-flight counter on construction and decrements on destruction,
95-
// ensuring the counter is always balanced regardless of which exit path the
96-
// handler takes. The counter (CTimer::_inflight) is cache-line-aligned to avoid
97-
// false sharing with _enabled, minimizing cache line bouncing.
98-
//
99-
// Usage (at the start of CTimer signal handlers):
100-
// InflightGuard inflight;
101-
class InflightGuard {
102-
public:
103-
InflightGuard() { CTimer::enterHandler(); }
104-
~InflightGuard() { CTimer::exitHandler(); }
105-
106-
// Non-copyable, non-movable
107-
InflightGuard(const InflightGuard&) = delete;
108-
InflightGuard& operator=(const InflightGuard&) = delete;
109-
InflightGuard(InflightGuard&&) = delete;
110-
InflightGuard& operator=(InflightGuard&&) = delete;
111-
};
112-
11366
// A CPU-time engine that reuses CTimer's per-thread timer_create / SIGPROF
11467
// dispatch, but instead of walking the stack in the signal handler delegates
11568
// the walk to HotSpot's JFR RequestStackTrace JVMTI extension. The sampled
@@ -141,19 +94,6 @@ class CTimer : public Engine {
14194
}
14295

14396
static bool supported() { return false; }
144-
145-
// No-op on non-Linux platforms
146-
static bool drainInflight() { return true; }
147-
static bool hasInflightHandlers() { return false; }
148-
};
149-
150-
// No-op InflightGuard on non-Linux platforms where CTimer is unavailable.
151-
class InflightGuard {
152-
public:
153-
InflightGuard() {}
154-
~InflightGuard() {}
155-
InflightGuard(const InflightGuard&) = delete;
156-
InflightGuard& operator=(const InflightGuard&) = delete;
15797
};
15898

15999
class CTimerJvmti : public Engine {

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

Lines changed: 3 additions & 48 deletions
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"
@@ -49,7 +50,6 @@ int CTimer::_max_timers = 0;
4950
int *CTimer::_timers = NULL;
5051
CStack CTimer::_cstack;
5152
bool CTimer::_enabled = false;
52-
alignas(64) int CTimer::_inflight = 0;
5353
int CTimer::_signal;
5454

5555
int CTimer::registerThread(int tid) {
@@ -177,57 +177,12 @@ Error CTimer::start(Arguments &args) {
177177
return Error::OK;
178178
}
179179

180-
// 200 ms: long enough for any legitimate recordSample() call to finish,
181-
// short enough to avoid a perceptible hang if a handler is somehow stuck.
182-
static const long DRAIN_TIMEOUT_NS = 200000000L;
183-
184-
bool CTimer::drainInflight() {
185-
if (__atomic_load_n(&_inflight, __ATOMIC_ACQUIRE) == 0) {
186-
return true; // fast path: nothing in flight
187-
}
188-
189-
struct timespec deadline;
190-
if (clock_gettime(CLOCK_MONOTONIC, &deadline) != 0) {
191-
// Cannot establish a deadline; refuse to spin unbounded and conservatively
192-
// report timeout so the caller skips JFR teardown.
193-
Log::error("CTimer::drainInflight: clock_gettime(CLOCK_MONOTONIC) failed (errno=%d). "
194-
"Skipping JFR teardown to prevent use-after-free.", errno);
195-
return false;
196-
}
197-
deadline.tv_nsec += DRAIN_TIMEOUT_NS;
198-
if (deadline.tv_nsec >= 1000000000L) {
199-
deadline.tv_sec += 1;
200-
deadline.tv_nsec -= 1000000000L;
201-
}
202-
203-
while (__atomic_load_n(&_inflight, __ATOMIC_ACQUIRE) > 0) {
204-
struct timespec now;
205-
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) {
206-
Log::error("CTimer::drainInflight: clock_gettime(CLOCK_MONOTONIC) failed (errno=%d). "
207-
"Skipping JFR teardown to prevent use-after-free.", errno);
208-
return false;
209-
}
210-
if (now.tv_sec > deadline.tv_sec ||
211-
(now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) {
212-
int remaining = __atomic_load_n(&_inflight, __ATOMIC_ACQUIRE);
213-
Log::error("CTimer::drainInflight: timed out after %ldms waiting for "
214-
"%d in-flight SIGPROF handler(s). Skipping JFR teardown to "
215-
"prevent use-after-free. This indicates a stuck signal handler.",
216-
DRAIN_TIMEOUT_NS / 1000000L, remaining);
217-
return false; // timeout: handlers still in-flight
218-
}
219-
sched_yield();
220-
}
221-
return true; // success: all handlers drained
222-
}
223-
224180
void CTimer::stop() {
225181
for (int i = 0; i < _max_timers; i++) {
226182
unregisterThread(i);
227183
}
228-
// Note: drainInflight() is called by Profiler::stop() after all engines
229-
// have stopped, not here. Profiler needs the return value to decide whether
230-
// to proceed with JFR teardown.
184+
// Note: SignalInflight::drain() is called by Profiler::stop() after
185+
// disableEngines(); see signalInflight.h.
231186
}
232187

233188
Error CTimerJvmti::check(Arguments &args) {

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "common.h"
1515
#include "counters.h"
1616
#include "ctimer.h"
17+
#include "signalInflight.h"
1718
#include "dwarf.h"
1819
#include "flightRecorder.h"
1920
#include "itimer.h"
@@ -1504,19 +1505,19 @@ Error Profiler::stop() {
15041505
}
15051506

15061507
// Order matters: disable engines first so the _enabled check inside signal
1507-
// handlers will fail for any new signal delivered from now on. drainInflight()
1508-
// then waits for handlers that already passed the check to leave recordSample().
1509-
// Only once those are gone is it safe to run the rest of the teardown
1508+
// handlers will fail for any new signal delivered from now on. drain() then
1509+
// waits for handlers that already passed the check to leave their JFR write
1510+
// path. Only once those are gone is it safe to run the rest of the teardown
15101511
// (engine stops, JFR teardown) without risking use-after-free.
15111512
disableEngines();
15121513

1513-
if (!CTimer::drainInflight()) {
1514-
// SIGPROF handlers stuck past the 200 ms timeout. Leave state == RUNNING so
1515-
// the caller cannot start() against half-torn-down JFR and so engine stops
1514+
if (!SignalInflight::drain()) {
1515+
// Signal handlers stuck past the timeout. Leave state == RUNNING so the
1516+
// caller cannot start() against half-torn-down JFR and so engine stops
15161517
// (notably BaseWallClock::stop()'s pthread_join) are not double-invoked.
15171518
// The operation is idempotent on retry: disableEngines() above is an atomic
15181519
// store, and no other engine stop has run yet.
1519-
return Error("SIGPROF handlers did not drain; teardown skipped, retry stop()");
1520+
return Error("signal handlers did not drain; teardown skipped, retry stop()");
15201521
}
15211522

15221523
if (_event_mask & EM_ALLOC)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2026, Datadog, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "signalInflight.h"
18+
#include "log.h"
19+
20+
#include <errno.h>
21+
#include <sched.h>
22+
#include <time.h>
23+
24+
alignas(64) int SignalInflight::_counter = 0;
25+
26+
// 200 ms: long enough for any legitimate signal handler to finish,
27+
// short enough to avoid a perceptible hang if a handler is somehow stuck.
28+
static const long DRAIN_TIMEOUT_NS = 200000000L;
29+
30+
bool SignalInflight::drain() {
31+
if (__atomic_load_n(&_counter, __ATOMIC_ACQUIRE) == 0) {
32+
return true; // fast path: nothing in flight
33+
}
34+
35+
struct timespec deadline;
36+
if (clock_gettime(CLOCK_MONOTONIC, &deadline) != 0) {
37+
Log::error("SignalInflight::drain: clock_gettime(CLOCK_MONOTONIC) failed (errno=%d). "
38+
"Skipping JFR teardown to prevent use-after-free.", errno);
39+
return false;
40+
}
41+
deadline.tv_nsec += DRAIN_TIMEOUT_NS;
42+
if (deadline.tv_nsec >= 1000000000L) {
43+
deadline.tv_sec += 1;
44+
deadline.tv_nsec -= 1000000000L;
45+
}
46+
47+
while (__atomic_load_n(&_counter, __ATOMIC_ACQUIRE) > 0) {
48+
struct timespec now;
49+
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) {
50+
Log::error("SignalInflight::drain: clock_gettime(CLOCK_MONOTONIC) failed (errno=%d). "
51+
"Skipping JFR teardown to prevent use-after-free.", errno);
52+
return false;
53+
}
54+
if (now.tv_sec > deadline.tv_sec ||
55+
(now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) {
56+
int remaining = __atomic_load_n(&_counter, __ATOMIC_ACQUIRE);
57+
Log::error("SignalInflight::drain: timed out after %ldms waiting for "
58+
"%d in-flight signal handler(s). Skipping JFR teardown to "
59+
"prevent use-after-free. This indicates a stuck signal handler.",
60+
DRAIN_TIMEOUT_NS / 1000000L, remaining);
61+
return false;
62+
}
63+
sched_yield();
64+
}
65+
return true;
66+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2026, Datadog, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#ifndef _SIGNAL_INFLIGHT_H
18+
#define _SIGNAL_INFLIGHT_H
19+
20+
// Global tally of signal handlers currently executing past their _enabled
21+
// check and capable of writing JFR. Profiler::stop() waits (drain()) for the
22+
// counter to reach zero before calling _jfr.stop(), closing the TOCTOU
23+
// window between a handler's _enabled check and its JFR buffer access.
24+
//
25+
// All CPU and wall-clock signal handlers that can write JFR should construct
26+
// an InflightGuard at handler entry so they participate in the drain.
27+
//
28+
// The counter is on its own cache line (alignas(64)) so that handler-side
29+
// writes do not invalidate the cache line backing each engine's _enabled
30+
// flag, which is read on every signal.
31+
class SignalInflight {
32+
public:
33+
// ACQUIRE on increment / RELEASE on decrement: drain() observes all
34+
// handler-side writes before observing the counter at zero.
35+
static void enter() {
36+
__atomic_fetch_add(&_counter, 1, __ATOMIC_ACQUIRE);
37+
}
38+
static void exit() {
39+
__atomic_fetch_sub(&_counter, 1, __ATOMIC_RELEASE);
40+
}
41+
static bool hasInflight() {
42+
return __atomic_load_n(&_counter, __ATOMIC_ACQUIRE) > 0;
43+
}
44+
45+
// Spin until the counter reaches zero or DRAIN_TIMEOUT_NS elapses.
46+
// Returns true on clean drain, false on timeout. Callers MUST NOT proceed
47+
// with JFR teardown when this returns false.
48+
static bool drain();
49+
50+
private:
51+
alignas(64) static int _counter;
52+
};
53+
54+
// RAII guard for signal-handler in-flight tracking. Construct as the first
55+
// statement (after SIGNAL_HANDLER_GUARD) of any signal handler that can
56+
// write JFR; the counter is incremented on construction and decremented on
57+
// destruction, balancing automatically across every return path.
58+
class InflightGuard {
59+
public:
60+
InflightGuard() { SignalInflight::enter(); }
61+
~InflightGuard() { SignalInflight::exit(); }
62+
63+
InflightGuard(const InflightGuard&) = delete;
64+
InflightGuard& operator=(const InflightGuard&) = delete;
65+
InflightGuard(InflightGuard&&) = delete;
66+
InflightGuard& operator=(InflightGuard&&) = delete;
67+
};
68+
69+
#endif // _SIGNAL_INFLIGHT_H

0 commit comments

Comments
 (0)