From dbeb0046bbaf9fac280e3c58482e34acd8063a58 Mon Sep 17 00:00:00 2001 From: Bob Weinand Date: Mon, 22 Jun 2026 13:14:01 +0200 Subject: [PATCH 1/4] Add timeout to debugger captures Signed-off-by: Bob Weinand --- ext/compatibility.h | 2 + ext/crashtracking_frames.c | 6 + ext/remote_config.c | 47 +++++- metadata/supported-configurations.json | 7 + .../debugger_log_probe_capture_timeout.phpt | 68 ++++++++ tracer/configuration.h | 1 + tracer/ddtrace_globals.h | 12 ++ tracer/exception_serialize.c | 20 +++ tracer/live_debugger.c | 158 +++++++++++++++++- tracer/live_debugger.h | 3 + 10 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 tests/ext/live-debugger/debugger_log_probe_capture_timeout.phpt diff --git a/ext/compatibility.h b/ext/compatibility.h index 03810b5475f..17ddd59c7b8 100644 --- a/ext/compatibility.h +++ b/ext/compatibility.h @@ -686,6 +686,8 @@ static inline zend_string *zend_ini_str(const char *name, size_t name_length, bo return return_value; } +#define tsrm_is_managed_thread() (tsrm_get_ls_cache() != NULL) + #define zend_zval_value_name zend_zval_type_name #define Z_PARAM_ZVAL_OR_NULL(dest) Z_PARAM_ZVAL_EX(dest, 1, 0) diff --git a/ext/crashtracking_frames.c b/ext/crashtracking_frames.c index 3a0df6eda5f..335b8991a4e 100644 --- a/ext/crashtracking_frames.c +++ b/ext/crashtracking_frames.c @@ -24,6 +24,12 @@ static ddog_CharSlice dd_validate_zstr(zend_string *str) { } static void dd_frames_callback(void (*emit_frame)(const ddog_crasht_RuntimeStackFrame *)) { +#ifdef ZTS + if (!tsrm_is_managed_thread()) { + return; + } +#endif + zend_execute_data *call; #if PHP_VERSION_ID >= 80400 zend_execute_data *last_call = NULL; diff --git a/ext/remote_config.c b/ext/remote_config.c index d0c2bd6fa4c..69cdaefd8b5 100644 --- a/ext/remote_config.c +++ b/ext/remote_config.c @@ -7,7 +7,8 @@ #include "threads.h" #include #ifndef _WIN32 -#include +#include +#include #endif #if PHP_VERSION_ID < 70100 @@ -65,6 +66,50 @@ void datadog_check_for_new_config_now(void) { static void dd_sigvtalarm_handler(int signal, siginfo_t *siginfo, void *ctx) { UNUSED(signal, siginfo, ctx); datadog_set_all_thread_vm_interrupt(); + +#if defined(__linux__) && defined(ZTS) + if (!tsrm_is_managed_thread()) { + return; + } +#endif + + uint64_t now_ns = 0; +#if !defined(__linux__) && defined(ZTS) + // On macOS ZTS, setitimer is per-process; the signal may land on any thread - iterate all threads to check for expirations + uint64_t next_deadline = ~0ull; + tsrm_mutex_lock(datadog_threads_mutex); + void *TSRMLS_CACHE; + ZEND_HASH_FOREACH_PTR(&datadog_tls_bases, TSRMLS_CACHE) { +#endif + // On Linux the signal gets delivered to the thread that set the timer, so we don't need to iterate all threads + uint64_t deadline = DDTRACE_G(capture_deadline_ns); + if (deadline) { + if (!now_ns) { + struct timespec now; + clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now); + now_ns = (uint64_t)now.tv_sec * 1000000000ULL + (uint64_t)now.tv_nsec; + } + if (now_ns >= deadline) { + DDTRACE_G(debugger_capture_timed_out) = 1; + } +#if !defined(__linux__) && defined(ZTS) + else { + next_deadline = MIN(deadline, next_deadline); + } +#endif + } +#if !defined(__linux__) && defined(ZTS) + } ZEND_HASH_FOREACH_END(); + if (next_deadline != ~0ull) { // re-arm the timer, for ZTS concurrency + uint64_t usec = (next_deadline - now_ns) / 1000ull; + struct itimerval it = { + .it_value = { .tv_sec = usec / 10000000, .tv_usec = usec % 1000000 }, + .it_interval = { 0, 0 }, + }; + setitimer(ITIMER_VIRTUAL, &it, NULL); + } + tsrm_mutex_unlock(datadog_threads_mutex); +#endif } #endif diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 2aa33ae425d..22d70ce8fda 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -333,6 +333,13 @@ "default": "http://localhost:8125" } ], + "DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS": [ + { + "implementation": "A", + "type": "int", + "default": "15" + } + ], "DD_DYNAMIC_INSTRUMENTATION_ENABLED": [ { "implementation": "A", diff --git a/tests/ext/live-debugger/debugger_log_probe_capture_timeout.phpt b/tests/ext/live-debugger/debugger_log_probe_capture_timeout.phpt new file mode 100644 index 00000000000..d5b408ccd0d --- /dev/null +++ b/tests/ext/live-debugger/debugger_log_probe_capture_timeout.phpt @@ -0,0 +1,68 @@ +--TEST-- +Live debugger log probe capture timeout with large data structure +--SKIPIF-- + +--ENV-- +DD_AGENT_HOST=request-replayer +DD_TRACE_AGENT_PORT=80 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_DYNAMIC_INSTRUMENTATION_ENABLED=1 +DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS=0.1 +DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS=1 +--INI-- +datadog.trace.agent_test_session_token=live-debugger/log_probe_capture_timeout +--FILE-- + ["methodName" => "large_capture"], + "captureSnapshot" => true, + "segments" => [["str" => "capture timeout test"]], + ]); + \DDTrace\start_span(); +}); + +// 2-level array: 100 outer x 100 inner strings (100-char each) +// ~10,000 capture operations: reliably exceeds the 1ms CPU-time timeout +// Sentinel is at the very last position [99][99] +$data = []; +for ($i = 0; $i < 99; $i++) { + $data[] = array_fill(0, 100, str_repeat('x', 100)); +} +$last = array_fill(0, 99, str_repeat('x', 100)); +$last[] = 'LAST_SENTINEL'; +$data[] = $last; + +large_capture($data); + +$dlr = new DebuggerLogReplayer; +$log = $dlr->waitForDebuggerDataAndReplay(); +$captures = json_decode($log["body"], true)[0]["debugger"]["snapshot"]["captures"]; +$captures_json = json_encode($captures); + +// Snapshot was delivered with some captured data +var_dump(!empty($captures)); + +// Timeout reason must appear somewhere in the captured data +var_dump(strpos($captures_json, '"timeout"') !== false); + +// The last element must NOT have been captured before the timeout fired +var_dump(strpos($captures_json, 'LAST_SENTINEL') === false); + +?> +--CLEAN-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) diff --git a/tracer/configuration.h b/tracer/configuration.h index 45790f6a649..6e1b6880449 100644 --- a/tracer/configuration.h +++ b/tracer/configuration.h @@ -151,6 +151,7 @@ CONFIG(BOOL, DD_APM_TRACING_ENABLED, "true") \ CONFIG(SET, DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES, "", .ini_change = zai_config_system_ini_change) \ CONFIG(SET, DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS, "", .ini_change = zai_config_system_ini_change) \ + CONFIG(INT, DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS, "15", .ini_change = zai_config_system_ini_change) \ CONFIG(INT, DD_TRACE_BAGGAGE_MAX_ITEMS, "64") \ CONFIG(INT, DD_TRACE_BAGGAGE_MAX_BYTES, "8192") \ CONFIG(BOOL, DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED, "false") \ diff --git a/tracer/ddtrace_globals.h b/tracer/ddtrace_globals.h index 3865b2e48f9..26738f4d3f1 100644 --- a/tracer/ddtrace_globals.h +++ b/tracer/ddtrace_globals.h @@ -1,7 +1,9 @@ #ifndef DDTRACE_GLOBALS_H #define DDTRACE_GLOBALS_H +#include #ifndef _WIN32 #include +#include #endif #include @@ -71,6 +73,16 @@ typedef struct { dd_capture_arena debugger_capture_arena; ddog_Vec_DebuggerPayload exception_debugger_buffer; + volatile sig_atomic_t debugger_capture_timed_out; +#ifndef _WIN32 + volatile uint64_t capture_deadline_ns; +#ifdef __linux__ + timer_t capture_timer; + int capture_timer_active; +#endif +#else + HANDLE capture_timer_handle; +#endif HashTable active_live_debugger_hooks; HashTable *agent_rate_by_service; diff --git a/tracer/exception_serialize.c b/tracer/exception_serialize.c index e4b8324eecb..d4c7982fdd9 100644 --- a/tracer/exception_serialize.c +++ b/tracer/exception_serialize.c @@ -111,6 +111,10 @@ static void ddtrace_capture_long_value(zend_long num, struct ddog_CaptureValue * } void ddtrace_create_capture_value(zval *zv, struct ddog_CaptureValue *value, const ddog_CaptureConfiguration *config, int remaining_nesting) { + if (UNEXPECTED(DDTRACE_G(debugger_capture_timed_out))) { + value->not_captured_reason = DDOG_CHARSLICE_C("timeout"); + return; + } ZVAL_DEREF(zv); switch (Z_TYPE_P(zv)) { case IS_FALSE: @@ -158,6 +162,10 @@ void ddtrace_create_capture_value(zval *zv, struct ddog_CaptureValue *value, con if (zend_array_is_list(Z_ARR_P(zv))) { int remaining_fields = config->max_collection_size; ZEND_HASH_FOREACH_VAL(Z_ARR_P(zv), val) { + if (UNEXPECTED(DDTRACE_G(debugger_capture_timed_out))) { + value->not_captured_reason = DDOG_CHARSLICE_C("timeout"); + break; + } if (remaining_fields-- == 0) { value->not_captured_reason = DDOG_CHARSLICE_C("collectionSize"); break; @@ -172,6 +180,10 @@ void ddtrace_create_capture_value(zval *zv, struct ddog_CaptureValue *value, con zend_string *key; int remaining_fields = config->max_collection_size; ZEND_HASH_FOREACH_KEY_VAL(Z_ARR_P(zv), idx, key, val) { + if (UNEXPECTED(DDTRACE_G(debugger_capture_timed_out))) { + value->not_captured_reason = DDOG_CHARSLICE_C("timeout"); + break; + } if (remaining_fields-- == 0) { value->not_captured_reason = DDOG_CHARSLICE_C("collectionSize"); break; @@ -224,6 +236,10 @@ void ddtrace_create_capture_value(zval *zv, struct ddog_CaptureValue *value, con break; } ZEND_HASH_REVERSE_FOREACH_STR_KEY_VAL(ht, key, val) { + if (UNEXPECTED(DDTRACE_G(debugger_capture_timed_out))) { + value->not_captured_reason = DDOG_CHARSLICE_C("timeout"); + break; + } if (!key) { continue; } @@ -401,6 +417,8 @@ static void ddtrace_collect_exception_debug_data(zend_object *exception, zend_ob memset(&DDTRACE_G(exception_debugger_buffer), 0, sizeof(DDTRACE_G(exception_debugger_buffer))); + dd_start_debugger_timeout(); + zval *frame; int frame_num = 0; ZEND_HASH_FOREACH_NUM_KEY_VAL(Z_ARR_P(trace), frame_num, frame) { @@ -480,6 +498,8 @@ static void ddtrace_collect_exception_debug_data(zend_object *exception, zend_ob } } + dd_stop_debugger_timeout(); + // Note: We MUST immediately send this, and not defer, as stuff may be freed during span processing. Including stuff potentially contained within the exception debugger payload. ddtrace_sidecar_send_debugger_data(DDTRACE_G(exception_debugger_buffer)); diff --git a/tracer/live_debugger.c b/tracer/live_debugger.c index 81a5109e4a1..fe1756684e9 100644 --- a/tracer/live_debugger.c +++ b/tracer/live_debugger.c @@ -16,9 +16,130 @@ #include #include "zend_generators.h" #include +#ifndef _WIN32 +#include +#include +#endif ZEND_EXTERN_MODULE_GLOBALS(datadog); +#ifndef _WIN32 +#ifdef __linux__ +#include +#elif defined(ZTS) +uint64_t dd_find_lowest_dealine_timer(void) { + uint64_t usec = 0; + uint64_t next_deadline = ~0ull; + tsrm_mutex_lock(datadog_threads_mutex); + void *TSRMLS_CACHE; + ZEND_HASH_FOREACH_PTR(&datadog_tls_bases, TSRMLS_CACHE) { + uint64_t deadline = DDTRACE_G(capture_deadline_ns); + if (deadline) { + next_deadline = MIN(deadline, next_deadline); + } + } ZEND_HASH_FOREACH_END(); + tsrm_mutex_unlock(datadog_threads_mutex); + if (next_deadline != ~0ull) { + struct timespec now; + clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now); + uint64_t now_ns = (uint64_t)now.tv_sec * 1000000000ULL + (uint64_t)now.tv_nsec; + usec = (next_deadline - now_ns) / 1000ull; + } + return usec; +} +#endif + +// SIGEV_THREAD_ID delivers SIGVTALRM to exactly this thread, not a random one (critical for ZTS). +void dd_start_debugger_timeout(void) { + zend_long ms = get_global_DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS(); + if (ms <= 0) { + return; + } + struct timespec now; + clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now); + uint64_t now_ns = (uint64_t)now.tv_sec * 1000000000ULL + (uint64_t)now.tv_nsec; + // Set deadline BEFORE arming the timer so a racing SIGVTALRM sees a valid deadline. + DDTRACE_G(capture_deadline_ns) = now_ns + (uint64_t)ms * 1000000ULL; + DDTRACE_G(debugger_capture_timed_out) = 0; +#ifdef __linux__ + struct sigevent sev = {0}; + sev.sigev_notify = SIGEV_THREAD_ID; + sev.sigev_signo = SIGVTALRM; + sev._sigev_un._tid = (pid_t)syscall(SYS_gettid); // gettid() is glibc 2.30 only + if (timer_create(CLOCK_THREAD_CPUTIME_ID, &sev, &DDTRACE_G(capture_timer)) == 0) { + struct itimerspec it = { + .it_value = { .tv_sec = ms / 1000, .tv_nsec = (ms % 1000) * 1000000LL }, + }; + timer_settime(DDTRACE_G(capture_timer), 0, &it, NULL); + DDTRACE_G(capture_timer_active) = 1; + } +#else + uint64_t usec = (uint64_t)ms * 1000ULL; +#ifdef ZTS + usec = dd_find_lowest_dealine_timer(); +#endif + struct itimerval it = { + .it_value = { .tv_sec = usec / 10000000, .tv_usec = usec % 1000000 }, + .it_interval = { 0, 0 }, + }; + setitimer(ITIMER_VIRTUAL, &it, NULL); +#endif +} + +void dd_stop_debugger_timeout(void) { + // Clear deadline BEFORE deleting the timer so a racing signal sees "not active". + DDTRACE_G(capture_deadline_ns) = 0; +#ifdef __linux__ + if (DDTRACE_G(capture_timer_active)) { + timer_delete(DDTRACE_G(capture_timer)); + DDTRACE_G(capture_timer_active) = 0; + } +#else + // Reset timer to zero - on ZTS check other threads for timeouts first + uint64_t usec = 0; +#ifdef ZTS + usec = dd_find_lowest_dealine_timer(); +#endif + struct itimerval it = { + .it_value = { .tv_sec = usec / 10000000, .tv_usec = usec % 1000000 }, + .it_interval = { 0, 0 }, + }; + setitimer(ITIMER_VIRTUAL, &it, NULL); +#endif + DDTRACE_G(debugger_capture_timed_out) = 0; +} +#else +#include + +static void CALLBACK dd_timeout_callback(PVOID param, BOOLEAN fired) { + UNUSED(fired); + *((volatile sig_atomic_t *)param) = 1; +} + +void dd_start_debugger_timeout(void) { + zend_long ms = get_global_DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS(); + if (ms <= 0) { + return; + } + DDTRACE_G(debugger_capture_timed_out) = 0; + HANDLE timer = NULL; + // Pass a stable pointer to this thread's flag; the callback writes it from the timer-pool thread. + if (CreateTimerQueueTimer(&timer, NULL, dd_timeout_callback, + (PVOID)&DDTRACE_G(debugger_capture_timed_out), + (DWORD)ms, 0, WT_EXECUTEONLYONCE)) { + DDTRACE_G(capture_timer_handle) = timer; + } +} + +void dd_stop_debugger_timeout(void) { + if (DDTRACE_G(capture_timer_handle)) { + DeleteTimerQueueTimer(NULL, DDTRACE_G(capture_timer_handle), NULL); + DDTRACE_G(capture_timer_handle) = NULL; + } + DDTRACE_G(debugger_capture_timed_out) = 0; +} +#endif + struct eval_ctx { zend_execute_data *frame; zend_arena *arena; @@ -297,6 +418,7 @@ static void dd_span_decoration_end(zend_ulong invocation, zend_execute_data *exe } dd_probe_mark_active(def); + dd_start_debugger_timeout(); if (def->probe.probe.span_decoration.target == DDOG_SPAN_PROBE_TARGET_ROOT) { span = &span->stack->root_span->span; @@ -306,6 +428,9 @@ static void dd_span_decoration_end(zend_ulong invocation, zend_execute_data *exe bool condition_result = true; const ddog_ProbeCondition *const *condition = def->probe.probe.span_decoration.conditions; for (uintptr_t i = 0; i < def->probe.probe.span_decoration.span_tags_num; ++i) { + if (UNEXPECTED(DDTRACE_G(debugger_capture_timed_out))) { + break; + } const ddog_SpanProbeTag *spanTag = def->probe.probe.span_decoration.span_tags + i; if (spanTag->next_condition) { ddog_ConditionEvaluationResult result = dd_eval_condition(*(condition++), retval); @@ -340,6 +465,7 @@ static void dd_span_decoration_end(zend_ulong invocation, zend_execute_data *exe } } } + dd_stop_debugger_timeout(); } static bool dd_span_decoration_begin(zend_ulong invocation, zend_execute_data *execute_data, void *auxiliary, void *dynamic) { @@ -419,6 +545,9 @@ static void dd_log_probe_capture_snapshot(ddog_DebuggerCapture *capture, dd_log_ zend_string *symbol; zval *variable; ZEND_HASH_FOREACH_STR_KEY_VAL_IND(symbol_table, symbol, variable) { + if (UNEXPECTED(DDTRACE_G(debugger_capture_timed_out))) { + break; + } if (symbol) { struct ddog_CaptureValue capture_value = {0}; ddog_CharSlice name_slice = dd_zend_string_to_CharSlice(symbol); @@ -432,6 +561,9 @@ static void dd_log_probe_capture_snapshot(ddog_DebuggerCapture *capture, dd_log_ } else if (EX(func)->internal_function.arg_info) { uint32_t num_args = EX(func)->internal_function.num_args; for (uintptr_t i = 0; i < num_args; ++i) { + if (UNEXPECTED(DDTRACE_G(debugger_capture_timed_out))) { + break; + } const char *name = EX(func)->internal_function.arg_info[i].name; ddog_CharSlice name_slice = { .len = strlen(name), .ptr = name }; struct ddog_CaptureValue capture_value = {0}; @@ -454,7 +586,7 @@ static void dd_probe_capture_stack(ddog_DebuggerPayload *payload, zend_execute_d #if PHP_VERSION_ID >= 80400 zend_execute_data *last_call = NULL; #endif - while (call && --remaining_depth > 0) { + while (call && --remaining_depth > 0 && EXPECTED(!DDTRACE_G(debugger_capture_timed_out))) { if (UNEXPECTED(!call->func)) { /* This is the fake frame inserted for nested generators. Normally, * this frame is preceded by the actual generator frame and then @@ -568,6 +700,9 @@ static void dd_log_probe_add_capture_fields(ddog_DebuggerCapture *capture, dd_lo uintptr_t num = def->parent.probe.probe.log.capture_expressions_num; const ddog_CaptureExpression *exprs = def->parent.probe.probe.log.capture_expressions; for (uintptr_t i = 0; i < num; i++) { + if (UNEXPECTED(DDTRACE_G(debugger_capture_timed_out))) { + break; + } ctx.config = exprs[i].capture; ddog_ValueEvaluationResult result = ddog_evaluate_value(exprs[i].expr, &ctx); @@ -634,7 +769,10 @@ static void dd_log_probe_end(zend_ulong invocation, zend_execute_data *execute_d return; } + dd_start_debugger_timeout(); + if (def->parent.probe.evaluate_at == DDOG_EVALUATE_AT_EXIT && !dd_log_probe_eval_condition(def, execute_data, retval)) { + dd_stop_debugger_timeout(); return; } @@ -704,6 +842,7 @@ static void dd_log_probe_end(zend_ulong invocation, zend_execute_data *execute_d dd_log_probe_add_capture_fields(capture, def, execute_data, retval); } } + dd_stop_debugger_timeout(); ddtrace_sidecar_send_debugger_datum(dyn->payload); if (DDTRACE_G(debugger_capture_arena).arena) { dd_free_capture_ephemerals(DDTRACE_G(debugger_capture_arena).ephemerals); @@ -724,6 +863,8 @@ static bool dd_log_probe_begin(zend_ulong invocation, zend_execute_data *execute zval retval; ZVAL_NULL(&retval); + dd_start_debugger_timeout(); + dyn->payload = NULL; dyn->rejected = def->parent.probe.evaluate_at == DDOG_EVALUATE_AT_ENTRY && !dd_log_probe_eval_condition(def, execute_data, &retval); dyn->capture_arena = (dd_capture_arena){0}; @@ -747,6 +888,7 @@ static bool dd_log_probe_begin(zend_ulong invocation, zend_execute_data *execute } } + dd_stop_debugger_timeout(); return true; } @@ -1297,7 +1439,7 @@ static ddog_VoidCollection dd_eval_try_enumerate(void *ctx, const void *zvp) { if (iter->funcs->rewind) { iter->funcs->rewind(iter); } - while (!EG(exception) && idx < max_items && iter->funcs->valid(iter) == SUCCESS) { + while (!EG(exception) && idx < max_items && !DDTRACE_G(debugger_capture_timed_out) && iter->funcs->valid(iter) == SUCCESS) { zval key_zv, *data = iter->funcs->get_current_data(iter); if (iter->funcs->get_current_key) { @@ -1348,6 +1490,9 @@ static ddog_VoidCollection dd_eval_try_enumerate(void *ctx, const void *zvp) { ddog_VoidCollection collection = dd_alloc_kv_collection(count); int idx = 0; ZEND_HASH_FOREACH_KEY_VAL_IND(values, num_key, str_key, val) { + if (UNEXPECTED(DDTRACE_G(debugger_capture_timed_out))) { + break; + } zval key_zv; if (str_key) { ZVAL_STR(&key_zv, str_key); @@ -1406,6 +1551,9 @@ static void dd_stringify_zval(const zval *zv, smart_str *str, const ddog_Capture if (zend_array_is_list(Z_ARR_P(zv))) { int remaining_fields = config->max_collection_size; ZEND_HASH_FOREACH_VAL(Z_ARR_P(zv), val) { + if (UNEXPECTED(DDTRACE_G(debugger_capture_timed_out))) { + break; + } if (!first) { smart_str_appends(str, ", "); } @@ -1422,6 +1570,9 @@ static void dd_stringify_zval(const zval *zv, smart_str *str, const ddog_Capture zend_string *key; int remaining_fields = config->max_collection_size; ZEND_HASH_FOREACH_KEY_VAL(Z_ARR_P(zv), idx, key, val) { + if (UNEXPECTED(DDTRACE_G(debugger_capture_timed_out))) { + break; + } if (!first) { smart_str_appends(str, ", "); } @@ -1478,6 +1629,9 @@ static void dd_stringify_zval(const zval *zv, smart_str *str, const ddog_Capture : Z_OBJPROP_P(zv); bool first = true; ZEND_HASH_REVERSE_FOREACH_STR_KEY_VAL(ht, key, val) { + if (UNEXPECTED(DDTRACE_G(debugger_capture_timed_out))) { + break; + } if (!key) { continue; } diff --git a/tracer/live_debugger.h b/tracer/live_debugger.h index a57777b4640..cc9421d425f 100644 --- a/tracer/live_debugger.h +++ b/tracer/live_debugger.h @@ -26,4 +26,7 @@ void dd_free_capture_ephemerals(struct dd_refcounted_linked *ephemerals); void ddtrace_sidecar_send_debugger_data(ddog_Vec_DebuggerPayload payloads); void ddtrace_sidecar_send_debugger_datum(ddog_DebuggerPayload *payload); +void dd_start_debugger_timeout(void); +void dd_stop_debugger_timeout(void); + #endif // DD_LIVE_DEBUGGER_H From 92fe6c4bcb3459d0c614f84fb962b0854d7c3987 Mon Sep 17 00:00:00 2001 From: Bob Weinand Date: Thu, 25 Jun 2026 16:15:02 +0200 Subject: [PATCH 2/4] Fix alpine build Signed-off-by: Bob Weinand --- tracer/live_debugger.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tracer/live_debugger.c b/tracer/live_debugger.c index fe1756684e9..c6109dad6df 100644 --- a/tracer/live_debugger.c +++ b/tracer/live_debugger.c @@ -62,10 +62,14 @@ void dd_start_debugger_timeout(void) { DDTRACE_G(capture_deadline_ns) = now_ns + (uint64_t)ms * 1000000ULL; DDTRACE_G(debugger_capture_timed_out) = 0; #ifdef __linux__ + // musl exposes it, but glibc doesn't always. Define the glibc variant here. +#ifndef sigev_notify_thread_id +#define sigev_notify_thread_id _sigev_un._tid +#endif struct sigevent sev = {0}; sev.sigev_notify = SIGEV_THREAD_ID; sev.sigev_signo = SIGVTALRM; - sev._sigev_un._tid = (pid_t)syscall(SYS_gettid); // gettid() is glibc 2.30 only + sev.sigev_notify_thread_id = (pid_t)syscall(SYS_gettid); // gettid() is glibc 2.30 only if (timer_create(CLOCK_THREAD_CPUTIME_ID, &sev, &DDTRACE_G(capture_timer)) == 0) { struct itimerspec it = { .it_value = { .tv_sec = ms / 1000, .tv_nsec = (ms % 1000) * 1000000LL }, From c6da6b1b25d281f4d0c00e57c14701e69f1cfa66 Mon Sep 17 00:00:00 2001 From: Bob Weinand Date: Thu, 25 Jun 2026 19:55:33 +0200 Subject: [PATCH 3/4] Windows timers have less resolution, increase amount Signed-off-by: Bob Weinand --- tests/ext/live-debugger/debugger_log_probe.phpt | 1 + .../live-debugger/debugger_log_probe_capture_timeout.phpt | 7 +++---- .../ext/live-debugger/debugger_span_decoration_probe.phpt | 1 + tests/ext/live-debugger/exception-replay_001.phpt | 1 + tests/ext/live-debugger/exception-replay_002.phpt | 1 + .../exception-replay_non_regression_2989_mysqli.phpt | 1 + 6 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/ext/live-debugger/debugger_log_probe.phpt b/tests/ext/live-debugger/debugger_log_probe.phpt index 72d9e076a27..1904e2271db 100644 --- a/tests/ext/live-debugger/debugger_log_probe.phpt +++ b/tests/ext/live-debugger/debugger_log_probe.phpt @@ -8,6 +8,7 @@ DD_TRACE_AGENT_PORT=80 DD_TRACE_GENERATE_ROOT_SPAN=0 DD_DYNAMIC_INSTRUMENTATION_ENABLED=1 DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS=0.1 +DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS=5000 --INI-- datadog.trace.agent_test_session_token=live-debugger/log_probe --FILE-- diff --git a/tests/ext/live-debugger/debugger_log_probe_capture_timeout.phpt b/tests/ext/live-debugger/debugger_log_probe_capture_timeout.phpt index d5b408ccd0d..71f10e6aa91 100644 --- a/tests/ext/live-debugger/debugger_log_probe_capture_timeout.phpt +++ b/tests/ext/live-debugger/debugger_log_probe_capture_timeout.phpt @@ -29,12 +29,11 @@ await_probe_installation(function() { \DDTrace\start_span(); }); -// 2-level array: 100 outer x 100 inner strings (100-char each) -// ~10,000 capture operations: reliably exceeds the 1ms CPU-time timeout -// Sentinel is at the very last position [99][99] +// 3-level array: 10 outer x 100 mid x 100 inner strings (100-char each) +// ~100,000 capture operations: reliably exceeds the 1ms CPU-time timeout $data = []; for ($i = 0; $i < 99; $i++) { - $data[] = array_fill(0, 100, str_repeat('x', 100)); + $data[] = array_fill(0, 10, array_fill(0, 100, str_repeat('x', 100))); } $last = array_fill(0, 99, str_repeat('x', 100)); $last[] = 'LAST_SENTINEL'; diff --git a/tests/ext/live-debugger/debugger_span_decoration_probe.phpt b/tests/ext/live-debugger/debugger_span_decoration_probe.phpt index 4ce2aaebc1a..aef2957b869 100644 --- a/tests/ext/live-debugger/debugger_span_decoration_probe.phpt +++ b/tests/ext/live-debugger/debugger_span_decoration_probe.phpt @@ -8,6 +8,7 @@ DD_TRACE_AGENT_PORT=80 DD_TRACE_GENERATE_ROOT_SPAN=0 DD_DYNAMIC_INSTRUMENTATION_ENABLED=1 DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS=0.1 +DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS=5000 --INI-- datadog.trace.agent_test_session_token=live-debugger/span_decoration_probe --FILE-- diff --git a/tests/ext/live-debugger/exception-replay_001.phpt b/tests/ext/live-debugger/exception-replay_001.phpt index fd27388f95c..df3fdbec869 100644 --- a/tests/ext/live-debugger/exception-replay_001.phpt +++ b/tests/ext/live-debugger/exception-replay_001.phpt @@ -9,6 +9,7 @@ DD_TRACE_AGENT_PORT=80 DD_TRACE_GENERATE_ROOT_SPAN=0 DD_EXCEPTION_REPLAY_ENABLED=1 DD_EXCEPTION_REPLAY_CAPTURE_INTERVAL_SECONDS=1 +DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS=5000 --INI-- datadog.trace.agent_test_session_token=live-debugger/exception-replay_001 --FILE-- diff --git a/tests/ext/live-debugger/exception-replay_002.phpt b/tests/ext/live-debugger/exception-replay_002.phpt index 1fb473b3818..a40df420709 100644 --- a/tests/ext/live-debugger/exception-replay_002.phpt +++ b/tests/ext/live-debugger/exception-replay_002.phpt @@ -8,6 +8,7 @@ DD_TRACE_AGENT_PORT=80 DD_TRACE_GENERATE_ROOT_SPAN=0 DD_EXCEPTION_REPLAY_ENABLED=1 DD_EXCEPTION_REPLAY_CAPTURE_INTERVAL_SECONDS=1 +DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS=5000 --INI-- datadog.trace.agent_test_session_token=live-debugger/exception-replay_002 --FILE-- diff --git a/tests/ext/live-debugger/exception-replay_non_regression_2989_mysqli.phpt b/tests/ext/live-debugger/exception-replay_non_regression_2989_mysqli.phpt index d739c6e22a1..a7e373ebd63 100644 --- a/tests/ext/live-debugger/exception-replay_non_regression_2989_mysqli.phpt +++ b/tests/ext/live-debugger/exception-replay_non_regression_2989_mysqli.phpt @@ -10,6 +10,7 @@ DD_TRACE_AGENT_PORT=80 DD_TRACE_GENERATE_ROOT_SPAN=0 DD_EXCEPTION_REPLAY_ENABLED=1 DD_EXCEPTION_REPLAY_CAPTURE_INTERVAL_SECONDS=1 +DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS=5000 --INI-- datadog.trace.agent_test_session_token=live-debugger/non_regression_2989_mysqli --FILE-- From 89a1dcde8c74d18f3b455aed973b1e6f543bd559 Mon Sep 17 00:00:00 2001 From: Bob Weinand Date: Mon, 29 Jun 2026 17:08:21 +0200 Subject: [PATCH 4/4] Address code review Signed-off-by: Bob Weinand --- ext/remote_config.c | 2 +- tracer/live_debugger.c | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/ext/remote_config.c b/ext/remote_config.c index 69cdaefd8b5..85e0549b927 100644 --- a/ext/remote_config.c +++ b/ext/remote_config.c @@ -103,7 +103,7 @@ static void dd_sigvtalarm_handler(int signal, siginfo_t *siginfo, void *ctx) { if (next_deadline != ~0ull) { // re-arm the timer, for ZTS concurrency uint64_t usec = (next_deadline - now_ns) / 1000ull; struct itimerval it = { - .it_value = { .tv_sec = usec / 10000000, .tv_usec = usec % 1000000 }, + .it_value = { .tv_sec = usec / 1000000, .tv_usec = usec % 1000000 }, .it_interval = { 0, 0 }, }; setitimer(ITIMER_VIRTUAL, &it, NULL); diff --git a/tracer/live_debugger.c b/tracer/live_debugger.c index c6109dad6df..3b3c7059259 100644 --- a/tracer/live_debugger.c +++ b/tracer/live_debugger.c @@ -27,7 +27,7 @@ ZEND_EXTERN_MODULE_GLOBALS(datadog); #ifdef __linux__ #include #elif defined(ZTS) -uint64_t dd_find_lowest_dealine_timer(void) { +uint64_t dd_find_lowest_deadline_timer(void) { uint64_t usec = 0; uint64_t next_deadline = ~0ull; tsrm_mutex_lock(datadog_threads_mutex); @@ -55,6 +55,9 @@ void dd_start_debugger_timeout(void) { if (ms <= 0) { return; } + if (DDTRACE_G(capture_deadline_ns)) { + LOG(WARN, "Starting debugger timeout when it was already active. Last timeout was not stopped properly?"); + } struct timespec now; clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now); uint64_t now_ns = (uint64_t)now.tv_sec * 1000000000ULL + (uint64_t)now.tv_nsec; @@ -80,10 +83,10 @@ void dd_start_debugger_timeout(void) { #else uint64_t usec = (uint64_t)ms * 1000ULL; #ifdef ZTS - usec = dd_find_lowest_dealine_timer(); + usec = dd_find_lowest_deadline_timer(); #endif struct itimerval it = { - .it_value = { .tv_sec = usec / 10000000, .tv_usec = usec % 1000000 }, + .it_value = { .tv_sec = usec / 1000000, .tv_usec = usec % 1000000 }, .it_interval = { 0, 0 }, }; setitimer(ITIMER_VIRTUAL, &it, NULL); @@ -102,10 +105,10 @@ void dd_stop_debugger_timeout(void) { // Reset timer to zero - on ZTS check other threads for timeouts first uint64_t usec = 0; #ifdef ZTS - usec = dd_find_lowest_dealine_timer(); + usec = dd_find_lowest_deadline_timer(); #endif struct itimerval it = { - .it_value = { .tv_sec = usec / 10000000, .tv_usec = usec % 1000000 }, + .it_value = { .tv_sec = usec / 1000000, .tv_usec = usec % 1000000 }, .it_interval = { 0, 0 }, }; setitimer(ITIMER_VIRTUAL, &it, NULL); @@ -125,6 +128,9 @@ void dd_start_debugger_timeout(void) { if (ms <= 0) { return; } + if (DDTRACE_G(capture_timer_handle)) { + LOG(WARN, "Starting debugger timeout when it was already active. Last timeout was not stopped properly?"); + } DDTRACE_G(debugger_capture_timed_out) = 0; HANDLE timer = NULL; // Pass a stable pointer to this thread's flag; the callback writes it from the timer-pool thread. @@ -137,7 +143,7 @@ void dd_start_debugger_timeout(void) { void dd_stop_debugger_timeout(void) { if (DDTRACE_G(capture_timer_handle)) { - DeleteTimerQueueTimer(NULL, DDTRACE_G(capture_timer_handle), NULL); + DeleteTimerQueueTimer(NULL, DDTRACE_G(capture_timer_handle), INVALID_HANDLE_VALUE); DDTRACE_G(capture_timer_handle) = NULL; } DDTRACE_G(debugger_capture_timed_out) = 0;