Skip to content

Commit 0bf979e

Browse files
committed
Add timeout to debugger captures
Signed-off-by: Bob Weinand <bob.weinand@datadoghq.com>
1 parent ba2b056 commit 0bf979e

10 files changed

Lines changed: 321 additions & 3 deletions

ext/compatibility.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,8 @@ static inline zend_string *zend_ini_str(const char *name, size_t name_length, bo
686686
return return_value;
687687
}
688688

689+
#define tsrm_is_managed_thread() (tsrm_get_ls_cache() != NULL)
690+
689691
#define zend_zval_value_name zend_zval_type_name
690692

691693
#define Z_PARAM_ZVAL_OR_NULL(dest) Z_PARAM_ZVAL_EX(dest, 1, 0)

ext/crashtracking_frames.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ static ddog_CharSlice dd_validate_zstr(zend_string *str) {
2424
}
2525

2626
static void dd_frames_callback(void (*emit_frame)(const ddog_crasht_RuntimeStackFrame *)) {
27+
#ifdef ZTS
28+
if (!tsrm_is_managed_thread()) {
29+
return;
30+
}
31+
#endif
32+
2733
zend_execute_data *call;
2834
#if PHP_VERSION_ID >= 80400
2935
zend_execute_data *last_call = NULL;

ext/remote_config.c

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
#include "threads.h"
88
#include <tracer/tracer_api.h>
99
#ifndef _WIN32
10-
#include <signal.h>
10+
#include <sys/time.h>
11+
#include <tracer/ddtrace_globals.h>
1112
#endif
1213

1314
#if PHP_VERSION_ID < 70100
@@ -65,6 +66,50 @@ void datadog_check_for_new_config_now(void) {
6566
static void dd_sigvtalarm_handler(int signal, siginfo_t *siginfo, void *ctx) {
6667
UNUSED(signal, siginfo, ctx);
6768
datadog_set_all_thread_vm_interrupt();
69+
70+
#if defined(__linux__) && defined(ZTS)
71+
if (!tsrm_is_managed_thread()) {
72+
return;
73+
}
74+
#endif
75+
76+
uint64_t now_ns = 0;
77+
#if !defined(__linux__) && defined(ZTS)
78+
// On macOS ZTS, setitimer is per-process; the signal may land on any thread - iterate all threads to check for expirations
79+
uint64_t next_deadline = ~0ull;
80+
tsrm_mutex_lock(datadog_threads_mutex);
81+
void *TSRMLS_CACHE;
82+
ZEND_HASH_FOREACH_PTR(&datadog_tls_bases, TSRMLS_CACHE) {
83+
#endif
84+
// On Linux the signal gets delivered to the thread that set the timer, so we don't need to iterate all threads
85+
uint64_t deadline = DDTRACE_G(capture_deadline_ns);
86+
if (deadline) {
87+
if (!now_ns) {
88+
struct timespec now;
89+
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now);
90+
now_ns = (uint64_t)now.tv_sec * 1000000000ULL + (uint64_t)now.tv_nsec;
91+
}
92+
if (now_ns >= deadline) {
93+
DDTRACE_G(debugger_capture_timed_out) = 1;
94+
}
95+
#if !defined(__linux__) && defined(ZTS)
96+
else {
97+
next_deadline = MIN(deadline, next_deadline);
98+
}
99+
#endif
100+
}
101+
#if !defined(__linux__) && defined(ZTS)
102+
} ZEND_HASH_FOREACH_END();
103+
if (next_deadline != ~0ull) { // re-arm the timer, for ZTS concurrency
104+
uint64_t usec = (next_deadline - now_ns) / 1000ull;
105+
struct itimerval it = {
106+
.it_value = { .tv_sec = usec / 10000000, .tv_usec = usec % 1000000 },
107+
.it_interval = { 0, 0 },
108+
};
109+
setitimer(ITIMER_VIRTUAL, &it, NULL);
110+
}
111+
tsrm_mutex_unlock(datadog_threads_mutex);
112+
#endif
68113
}
69114
#endif
70115

metadata/supported-configurations.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,13 @@
333333
"default": "http://localhost:8125"
334334
}
335335
],
336+
"DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS": [
337+
{
338+
"implementation": "A",
339+
"type": "int",
340+
"default": "15"
341+
}
342+
],
336343
"DD_DYNAMIC_INSTRUMENTATION_ENABLED": [
337344
{
338345
"implementation": "A",
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
--TEST--
2+
Live debugger log probe capture timeout with large data structure
3+
--SKIPIF--
4+
<?php include __DIR__ . '/../includes/skipif_no_dev_env.inc'; ?>
5+
--ENV--
6+
DD_AGENT_HOST=request-replayer
7+
DD_TRACE_AGENT_PORT=80
8+
DD_TRACE_GENERATE_ROOT_SPAN=0
9+
DD_DYNAMIC_INSTRUMENTATION_ENABLED=1
10+
DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS=0.1
11+
DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS=1
12+
--INI--
13+
datadog.trace.agent_test_session_token=live-debugger/log_probe_capture_timeout
14+
--FILE--
15+
<?php
16+
17+
require __DIR__ . "/live_debugger.inc";
18+
19+
reset_request_replayer();
20+
21+
function large_capture($huge_array) {}
22+
23+
await_probe_installation(function() {
24+
build_log_probe([
25+
"where" => ["methodName" => "large_capture"],
26+
"captureSnapshot" => true,
27+
"segments" => [["str" => "capture timeout test"]],
28+
]);
29+
\DDTrace\start_span();
30+
});
31+
32+
// 2-level array: 100 outer x 100 inner strings (100-char each)
33+
// ~10,000 capture operations: reliably exceeds the 1ms CPU-time timeout
34+
// Sentinel is at the very last position [99][99]
35+
$data = [];
36+
for ($i = 0; $i < 99; $i++) {
37+
$data[] = array_fill(0, 100, str_repeat('x', 100));
38+
}
39+
$last = array_fill(0, 99, str_repeat('x', 100));
40+
$last[] = 'LAST_SENTINEL';
41+
$data[] = $last;
42+
43+
large_capture($data);
44+
45+
$dlr = new DebuggerLogReplayer;
46+
$log = $dlr->waitForDebuggerDataAndReplay();
47+
$captures = json_decode($log["body"], true)[0]["debugger"]["snapshot"]["captures"];
48+
$captures_json = json_encode($captures);
49+
50+
// Snapshot was delivered with some captured data
51+
var_dump(!empty($captures));
52+
53+
// Timeout reason must appear somewhere in the captured data
54+
var_dump(strpos($captures_json, '"timeout"') !== false);
55+
56+
// The last element must NOT have been captured before the timeout fired
57+
var_dump(strpos($captures_json, 'LAST_SENTINEL') === false);
58+
59+
?>
60+
--CLEAN--
61+
<?php
62+
require __DIR__ . "/live_debugger.inc";
63+
reset_request_replayer();
64+
?>
65+
--EXPECT--
66+
bool(true)
67+
bool(true)
68+
bool(true)

tracer/configuration.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@
151151
CONFIG(BOOL, DD_APM_TRACING_ENABLED, "true") \
152152
CONFIG(SET, DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES, "", .ini_change = zai_config_system_ini_change) \
153153
CONFIG(SET, DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS, "", .ini_change = zai_config_system_ini_change) \
154+
CONFIG(INT, DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS, "15", .ini_change = zai_config_system_ini_change) \
154155
CONFIG(INT, DD_TRACE_BAGGAGE_MAX_ITEMS, "64") \
155156
CONFIG(INT, DD_TRACE_BAGGAGE_MAX_BYTES, "8192") \
156157
CONFIG(BOOL, DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED, "false") \

tracer/ddtrace_globals.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#ifndef DDTRACE_GLOBALS_H
22
#define DDTRACE_GLOBALS_H
3+
#include <signal.h>
34
#ifndef _WIN32
45
#include <dogstatsd_client/client.h>
6+
#include <time.h>
57
#endif
68

79
#include <ext/datadog.h>
@@ -71,6 +73,16 @@ typedef struct {
7173

7274
dd_capture_arena debugger_capture_arena;
7375
ddog_Vec_DebuggerPayload exception_debugger_buffer;
76+
volatile sig_atomic_t debugger_capture_timed_out;
77+
#ifndef _WIN32
78+
volatile uint64_t capture_deadline_ns;
79+
#ifdef __linux__
80+
timer_t capture_timer;
81+
int capture_timer_active;
82+
#endif
83+
#else
84+
HANDLE capture_timer_handle;
85+
#endif
7486
HashTable active_live_debugger_hooks;
7587
HashTable *agent_rate_by_service;
7688

tracer/exception_serialize.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ static void ddtrace_capture_long_value(zend_long num, struct ddog_CaptureValue *
111111
}
112112

113113
void ddtrace_create_capture_value(zval *zv, struct ddog_CaptureValue *value, const ddog_CaptureConfiguration *config, int remaining_nesting) {
114+
if (UNEXPECTED(DDTRACE_G(debugger_capture_timed_out))) {
115+
value->not_captured_reason = DDOG_CHARSLICE_C("timeout");
116+
return;
117+
}
114118
ZVAL_DEREF(zv);
115119
switch (Z_TYPE_P(zv)) {
116120
case IS_FALSE:
@@ -158,6 +162,10 @@ void ddtrace_create_capture_value(zval *zv, struct ddog_CaptureValue *value, con
158162
if (zend_array_is_list(Z_ARR_P(zv))) {
159163
int remaining_fields = config->max_collection_size;
160164
ZEND_HASH_FOREACH_VAL(Z_ARR_P(zv), val) {
165+
if (UNEXPECTED(DDTRACE_G(debugger_capture_timed_out))) {
166+
value->not_captured_reason = DDOG_CHARSLICE_C("timeout");
167+
break;
168+
}
161169
if (remaining_fields-- == 0) {
162170
value->not_captured_reason = DDOG_CHARSLICE_C("collectionSize");
163171
break;
@@ -172,6 +180,10 @@ void ddtrace_create_capture_value(zval *zv, struct ddog_CaptureValue *value, con
172180
zend_string *key;
173181
int remaining_fields = config->max_collection_size;
174182
ZEND_HASH_FOREACH_KEY_VAL(Z_ARR_P(zv), idx, key, val) {
183+
if (UNEXPECTED(DDTRACE_G(debugger_capture_timed_out))) {
184+
value->not_captured_reason = DDOG_CHARSLICE_C("timeout");
185+
break;
186+
}
175187
if (remaining_fields-- == 0) {
176188
value->not_captured_reason = DDOG_CHARSLICE_C("collectionSize");
177189
break;
@@ -224,6 +236,10 @@ void ddtrace_create_capture_value(zval *zv, struct ddog_CaptureValue *value, con
224236
break;
225237
}
226238
ZEND_HASH_REVERSE_FOREACH_STR_KEY_VAL(ht, key, val) {
239+
if (UNEXPECTED(DDTRACE_G(debugger_capture_timed_out))) {
240+
value->not_captured_reason = DDOG_CHARSLICE_C("timeout");
241+
break;
242+
}
227243
if (!key) {
228244
continue;
229245
}
@@ -401,6 +417,8 @@ static void ddtrace_collect_exception_debug_data(zend_object *exception, zend_ob
401417

402418
memset(&DDTRACE_G(exception_debugger_buffer), 0, sizeof(DDTRACE_G(exception_debugger_buffer)));
403419

420+
dd_start_debugger_timeout();
421+
404422
zval *frame;
405423
int frame_num = 0;
406424
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
480498
}
481499
}
482500

501+
dd_stop_debugger_timeout();
502+
483503
// 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.
484504
ddtrace_sidecar_send_debugger_data(DDTRACE_G(exception_debugger_buffer));
485505

0 commit comments

Comments
 (0)