Skip to content

Commit 030f6b2

Browse files
committed
fix(profiling): add CPU timer health disable
1 parent 6e0add3 commit 030f6b2

4 files changed

Lines changed: 95 additions & 5 deletions

File tree

ddtrace/internal/datadog/profiling/docs/timer_create.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ Current mitigations:
9999
blocks off-CPU where the per-thread CPU timer does not advance. They pass
100100
(no `EINTR`), which is the recorded result for the current Linux targets.
101101
- Run native-heavy ecosystem workloads before enabling this more broadly.
102+
- Permanently disable CPU-timer mode if health windows show a sustained high
103+
rate of capture failures or ring overflows. This keeps pathological
104+
environments from emitting failure-only timer traffic indefinitely.
102105

103106
Required hardening before considering broader rollout:
104107

@@ -120,7 +123,9 @@ Required hardening before considering broader rollout:
120123
thread CPU clock, it already conserves the CPU consumed during coalesced
121124
expirations, so overruns are not used to weight samples (doing so would
122125
double-count CPU). A rising overrun rate indicates the handler or interval is
123-
dropping sampling resolution and is a cue to widen the interval.
126+
dropping sampling resolution and is a cue to widen the interval. Sustained
127+
high capture-failure or ring-overflow windows disable CPU-timer mode and are
128+
reported with `health_disable_count`.
124129
4. Keep the 2 ms minimum interval until the native syscall and ecosystem tests
125130
show that a lower interval is safe. Do not expose the interval as public API
126131
while this remains experimental.

ddtrace/internal/datadog/profiling/stack/include/cpu_timer.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ struct DebugStats
3131
uint64_t timer_syscall_failures = 0;
3232
uint64_t accepted_signal_oob_tid_count = 0;
3333
uint64_t handler_hijack_disable_count = 0;
34-
uint64_t fast_copy_conflict_count = 0;
34+
uint64_t health_disable_count = 0;
3535
uint64_t dropped_count = 0;
3636
uint64_t dropped_cpu_ns = 0;
3737
uint64_t capture_failed_count = 0;

ddtrace/internal/datadog/profiling/stack/src/cpu_timer.cpp

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ constexpr uint32_t kDefaultRingCapacity = 64;
7575
constexpr size_t kMinAltStackSize = 128 * 1024;
7676
constexpr size_t kAltStackSigstkszMultiplier = 4;
7777
constexpr size_t kMaxSlotBytes = 256 * 1024 * 1024;
78+
constexpr uint64_t kHealthWindowEvents = 128;
79+
constexpr uint64_t kHealthBadRatioPercent = 90;
80+
constexpr uint32_t kHealthBadWindowsLimit = 3;
7881

7982
int g_cookie;
8083

@@ -194,6 +197,7 @@ struct CaptureState
194197
bool timer_deleted = false;
195198
bool retired = false;
196199

200+
std::atomic<uint64_t> published_count{ 0 };
197201
std::atomic<uint64_t> dropped_count{ 0 };
198202
std::atomic<uint64_t> dropped_cpu_ns{ 0 };
199203
std::atomic<uint64_t> capture_failed_count{ 0 };
@@ -202,6 +206,14 @@ struct CaptureState
202206
std::atomic<uint64_t> timer_overrun_total{ 0 };
203207
std::atomic<uint64_t> coalesced_signal_count{ 0 };
204208

209+
uint64_t health_seen_published_count = 0;
210+
uint64_t health_seen_dropped_count = 0;
211+
uint64_t health_seen_capture_failed_count = 0;
212+
uint64_t health_window_published_count = 0;
213+
uint64_t health_window_dropped_count = 0;
214+
uint64_t health_window_capture_failed_count = 0;
215+
uint32_t health_bad_window_count = 0;
216+
205217
CaptureState(uint64_t thread_id, uint64_t tid, const char* thread_name)
206218
: python_thread_id(thread_id)
207219
, native_tid(tid)
@@ -248,7 +260,7 @@ struct EngineState
248260
std::atomic<uint64_t> timer_syscall_failures{ 0 };
249261
std::atomic<uint64_t> accepted_signal_oob_tid_count{ 0 };
250262
std::atomic<uint64_t> handler_hijack_disable_count{ 0 };
251-
std::atomic<uint64_t> fast_copy_conflict_count{ 0 };
263+
std::atomic<uint64_t> health_disable_count{ 0 };
252264
std::atomic<uint64_t> stage2_invalid_frame_count{ 0 };
253265
};
254266

@@ -703,6 +715,49 @@ drain_state(EchionSampler& echion, CaptureState& state)
703715
}
704716
}
705717

718+
uint64_t
719+
counter_delta(uint64_t current, uint64_t& seen)
720+
{
721+
const uint64_t delta = current >= seen ? current - seen : 0;
722+
seen = current;
723+
return delta;
724+
}
725+
726+
bool
727+
capture_state_health_should_disable(CaptureState& state)
728+
{
729+
// AIDEV-NOTE: Health is evaluated on the sampler thread, never in the SIGPROF handler.
730+
// The handler only increments lock-free counters. Timer deletion and state retirement stay
731+
// on the normal control path so pathological capture failures cannot turn into signal-time
732+
// locking or allocation.
733+
const uint64_t published = state.published_count.load(std::memory_order_relaxed);
734+
const uint64_t dropped = state.dropped_count.load(std::memory_order_relaxed);
735+
const uint64_t failed = state.capture_failed_count.load(std::memory_order_relaxed);
736+
737+
state.health_window_published_count += counter_delta(published, state.health_seen_published_count);
738+
state.health_window_dropped_count += counter_delta(dropped, state.health_seen_dropped_count);
739+
state.health_window_capture_failed_count += counter_delta(failed, state.health_seen_capture_failed_count);
740+
741+
const uint64_t bad_events = state.health_window_dropped_count + state.health_window_capture_failed_count;
742+
const uint64_t total_events = bad_events + state.health_window_published_count;
743+
if (total_events < kHealthWindowEvents) {
744+
return false;
745+
}
746+
747+
const bool bad_window = bad_events * 100 >= total_events * kHealthBadRatioPercent;
748+
state.health_window_published_count = 0;
749+
state.health_window_dropped_count = 0;
750+
state.health_window_capture_failed_count = 0;
751+
752+
if (!bad_window) {
753+
state.health_bad_window_count = 0;
754+
return false;
755+
}
756+
757+
state.health_bad_window_count++;
758+
return state.health_bad_window_count >= kHealthBadWindowsLimit;
759+
}
760+
706761
void
707762
disable_all_timers_locked()
708763
{
@@ -729,6 +784,19 @@ disable_all_timers_for_hijack()
729784
disable_all_timers_locked();
730785
}
731786

787+
void
788+
disable_all_timers_for_health_locked()
789+
{
790+
if (!g_state.active.load(std::memory_order_acquire)) {
791+
return;
792+
}
793+
g_state.health_disable_count.fetch_add(1, std::memory_order_relaxed);
794+
g_state.active.store(false, std::memory_order_release);
795+
g_state.replacing_wall_cpu.store(false, std::memory_order_release);
796+
g_state.permanently_disabled.store(true, std::memory_order_release);
797+
disable_all_timers_locked();
798+
}
799+
732800
void
733801
disable_all_timers_for_blocked_signal()
734802
{
@@ -915,6 +983,7 @@ cpu_timer_signal_handler(int signo, siginfo_t* si, void* ucontext)
915983
}
916984

917985
state->ring.publish_for_producer();
986+
state->published_count.fetch_add(1, std::memory_order_relaxed);
918987
g_state.handler_active[tid].store(false, std::memory_order_seq_cst);
919988
errno = saved_errno;
920989
}
@@ -1258,6 +1327,22 @@ Engine::drain(EchionSampler& echion)
12581327
for (auto& state : ready_to_free) {
12591328
drain_state(echion, *state);
12601329
}
1330+
1331+
{
1332+
std::lock_guard<std::mutex> lock(g_state.registry_lock);
1333+
bool should_disable = false;
1334+
if (g_state.active.load(std::memory_order_acquire)) {
1335+
for (auto& item : g_state.live_by_thread_id) {
1336+
if (capture_state_health_should_disable(*item.second)) {
1337+
should_disable = true;
1338+
break;
1339+
}
1340+
}
1341+
}
1342+
if (should_disable) {
1343+
disable_all_timers_for_health_locked();
1344+
}
1345+
}
12611346
#else
12621347
(void)echion;
12631348
#endif
@@ -1313,7 +1398,7 @@ Engine::debug_stats() const
13131398
stats.timer_syscall_failures = g_state.timer_syscall_failures.load(std::memory_order_relaxed);
13141399
stats.accepted_signal_oob_tid_count = g_state.accepted_signal_oob_tid_count.load(std::memory_order_relaxed);
13151400
stats.handler_hijack_disable_count = g_state.handler_hijack_disable_count.load(std::memory_order_relaxed);
1316-
stats.fast_copy_conflict_count = g_state.fast_copy_conflict_count.load(std::memory_order_relaxed);
1401+
stats.health_disable_count = g_state.health_disable_count.load(std::memory_order_relaxed);
13171402
stats.stage2_invalid_frame_count = g_state.stage2_invalid_frame_count.load(std::memory_order_relaxed);
13181403

13191404
std::lock_guard<std::mutex> lock(g_state.registry_lock);

ddtrace/internal/datadog/profiling/stack/src/stack.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ stack_cpu_timer_debug_stats(PyObject* Py_UNUSED(self), PyObject* Py_UNUSED(args)
390390
set_debug_stat(dict, "timer_syscall_failures", stats.timer_syscall_failures) != 0 ||
391391
set_debug_stat(dict, "accepted_signal_oob_tid_count", stats.accepted_signal_oob_tid_count) != 0 ||
392392
set_debug_stat(dict, "handler_hijack_disable_count", stats.handler_hijack_disable_count) != 0 ||
393-
set_debug_stat(dict, "fast_copy_conflict_count", stats.fast_copy_conflict_count) != 0 ||
393+
set_debug_stat(dict, "health_disable_count", stats.health_disable_count) != 0 ||
394394
set_debug_stat(dict, "dropped_count", stats.dropped_count) != 0 ||
395395
set_debug_stat(dict, "dropped_cpu_ns", stats.dropped_cpu_ns) != 0 ||
396396
set_debug_stat(dict, "capture_failed_count", stats.capture_failed_count) != 0 ||

0 commit comments

Comments
 (0)