diff --git a/ext/compatibility.h b/ext/compatibility.h index 03810b5475..17ddd59c7b 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 3a0df6eda5..335b8991a4 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 d0c2bd6fa4..85e0549b92 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 / 1000000, .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 2aa33ae425..22d70ce8fd 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.phpt b/tests/ext/live-debugger/debugger_log_probe.phpt index 72d9e076a2..1904e2271d 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 new file mode 100644 index 0000000000..71f10e6aa9 --- /dev/null +++ b/tests/ext/live-debugger/debugger_log_probe_capture_timeout.phpt @@ -0,0 +1,67 @@ +--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(); +}); + +// 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, 10, 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/tests/ext/live-debugger/debugger_span_decoration_probe.phpt b/tests/ext/live-debugger/debugger_span_decoration_probe.phpt index 4ce2aaebc1..aef2957b86 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 fd27388f95..df3fdbec86 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 1fb473b381..a40df42070 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 d739c6e22a..a7e373ebd6 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-- diff --git a/tracer/configuration.h b/tracer/configuration.h index 45790f6a64..6e1b688044 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 3865b2e48f..26738f4d3f 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 e4b8324eec..d4c7982fdd 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 81a5109e4a..3b3c705925 100644 --- a/tracer/live_debugger.c +++ b/tracer/live_debugger.c @@ -16,9 +16,140 @@ #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_deadline_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; + } + 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; + // 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__ + // 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_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 }, + }; + 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_deadline_timer(); +#endif + struct itimerval it = { + .it_value = { .tv_sec = usec / 1000000, .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_deadline_timer(); +#endif + struct itimerval it = { + .it_value = { .tv_sec = usec / 1000000, .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; + } + 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. + 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), INVALID_HANDLE_VALUE); + 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 +428,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 +438,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 +475,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 +555,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 +571,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 +596,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 +710,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 +779,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 +852,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 +873,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 +898,7 @@ static bool dd_log_probe_begin(zend_ulong invocation, zend_execute_data *execute } } + dd_stop_debugger_timeout(); return true; } @@ -1297,7 +1449,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 +1500,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 +1561,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 +1580,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 +1639,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 a57777b464..cc9421d425 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