From c3729f7731d5498a4bce4e1d8f6c3dc0645a8390 Mon Sep 17 00:00:00 2001 From: Roman Pronskiy Date: Sat, 14 Mar 2026 22:09:08 +0100 Subject: [PATCH 1/3] Perf: defer heavy debugger RINIT until after connection check Split xdebug_debugger_rinit() into two phases: - rinit_early(): minimal setup (IDE key, no_exec, context init) - rinit(): heavy work (opcache disable, breakable_lines_map alloc) The full rinit() now only runs when an IDE client actually connects. When no IDE is listening (the common case with triggers), we skip: - breakable_lines_map hash allocation (2048 buckets) - xdebug_disable_opcache_optimizer() INI modifications - related string allocations Mid-request connections (xdebug_break, start_upon_error) lazily call xdebug_debugger_rinit() via xdebug_init_debugger(). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/debugger/com.c | 7 +++++++ src/debugger/debugger.c | 21 ++++++++++++++------- src/debugger/debugger.h | 1 + xdebug.c | 23 ++++++++--------------- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/debugger/com.c b/src/debugger/com.c index cc4dcae..36a9eeb 100644 --- a/src/debugger/com.c +++ b/src/debugger/com.c @@ -608,6 +608,13 @@ static void xdebug_init_debugger() { xdebug_str *connection_attempts = xdebug_str_new(); + /* Lazy init: allocate breakable_lines_map if deferred from RINIT + * (happens when debug session is initiated mid-request via xdebug_break() + * or start_upon_error, after the early connection check failed) */ + if (!XG_DBG(breakable_lines_map)) { + xdebug_debugger_rinit(); + } + /* Get handler from mode */ XG_DBG(context).handler = &xdebug_handler_dbgp; diff --git a/src/debugger/debugger.c b/src/debugger/debugger.c index f51480d..4e8d5fa 100644 --- a/src/debugger/debugger.c +++ b/src/debugger/debugger.c @@ -874,13 +874,11 @@ void xdebug_debugger_minfo(void) php_info_print_table_end(); } -void xdebug_debugger_rinit(void) +void xdebug_debugger_rinit_early(void) { char *idekey; - xdebug_disable_opcache_optimizer(); - - /* Get the ide key for this session */ + /* Get the ide key for this session (needed for early connection check) */ XG_DBG(ide_key) = NULL; idekey = xdebug_debugger_get_ide_key(); if (idekey && *idekey) { @@ -919,7 +917,7 @@ void xdebug_debugger_rinit(void) XG_DBG(breakpoints_allowed) = 1; XG_DBG(suppress_return_value_step) = 0; XG_DBG(detached) = 0; - XG_DBG(breakable_lines_map) = xdebug_hash_alloc(2048, (xdebug_hash_dtor_t) xdebug_line_list_dtor); + XG_DBG(breakable_lines_map) = NULL; XG_DBG(function_count) = 0; XG_DBG(class_count) = 0; @@ -940,6 +938,13 @@ void xdebug_debugger_rinit(void) XG_DBG(context).detached_message = NULL; } +void xdebug_debugger_rinit(void) +{ + xdebug_disable_opcache_optimizer(); + + XG_DBG(breakable_lines_map) = xdebug_hash_alloc(2048, (xdebug_hash_dtor_t) xdebug_line_list_dtor); +} + void xdebug_debugger_post_deactivate(void) { if (XG_DBG(remote_connection_enabled)) { @@ -960,8 +965,10 @@ void xdebug_debugger_post_deactivate(void) XG_DBG(context).list.last_filename = NULL; } - xdebug_hash_destroy(XG_DBG(breakable_lines_map)); - XG_DBG(breakable_lines_map) = NULL; + if (XG_DBG(breakable_lines_map)) { + xdebug_hash_destroy(XG_DBG(breakable_lines_map)); + XG_DBG(breakable_lines_map) = NULL; + } if (XG_DBG(context).connected_hostname) { xdfree(XG_DBG(context).connected_hostname); diff --git a/src/debugger/debugger.h b/src/debugger/debugger.h index 7d4df2f..8bf3044 100644 --- a/src/debugger/debugger.h +++ b/src/debugger/debugger.h @@ -90,6 +90,7 @@ void xdebug_debugger_zend_startup(void); void xdebug_debugger_zend_shutdown(void); void xdebug_debugger_minit(void); void xdebug_debugger_minfo(void); +void xdebug_debugger_rinit_early(void); void xdebug_debugger_rinit(void); void xdebug_debugger_post_deactivate(void); diff --git a/xdebug.c b/xdebug.c index b223a98..62ccf96 100644 --- a/xdebug.c +++ b/xdebug.c @@ -566,7 +566,7 @@ PHP_RINIT_FUNCTION(xdebug) xdebug_library_rinit(); if (XDEBUG_MODE_IS(XDEBUG_MODE_STEP_DEBUG)) { - xdebug_debugger_rinit(); + xdebug_debugger_rinit_early(); if (xdebug_debugger_bailout_if_no_exec_requested()) { zend_bailout(); @@ -581,18 +581,10 @@ PHP_RINIT_FUNCTION(xdebug) xdebug_base_rinit(); if (XDEBUG_MODE_IS(XDEBUG_MODE_STEP_DEBUG)) { - /* Check early if debugging could be requested this request. - * For start_with_request=default (trigger mode), check if any - * trigger is present. If not, disable all heavy hooks for - * near-zero overhead. The actual connection happens on first - * function call if triggers are present. */ /* Check if debugging could be requested this request. * For trigger/default mode: check triggers, cookies, env vars. * For yes mode: always expect a connection. - * For no mode: no debugging will happen. - * Note: xdebug_break() can initiate connections without triggers, - * but it handles re-enabling the observer itself. */ - /* Respect start_with_request=no and XDEBUG_IGNORE */ + * For no mode: no debugging will happen. */ int debug_requested = !xdebug_lib_never_start_with_request() && !xdebug_should_ignore() && ( xdebug_lib_start_with_request(XDEBUG_MODE_STEP_DEBUG) || xdebug_lib_start_with_trigger(XDEBUG_MODE_STEP_DEBUG, NULL) || @@ -605,17 +597,18 @@ PHP_RINIT_FUNCTION(xdebug) * before enabling expensive EXT_STMT opcodes. This avoids ~2x * overhead when triggers are present but no IDE is connected. */ if (xdebug_early_connect_to_client()) { + /* Client connected — do full debugger init (opcache disable, + * breakable lines map, etc.) */ + xdebug_debugger_rinit(); CG(compiler_options) = CG(compiler_options) | ZEND_COMPILE_EXTENDED_STMT; } else { - /* Trigger present but no client listening — stay dormant */ + /* Trigger present but no client listening — stay dormant. + * Skip heavy debugger init (breakable_lines_map, opcache disable). */ XG_BASE(observer_active) = 0; XG_BASE(statement_handler_enabled) = false; } } else { - /* No debug trigger: disable all heavy hooks for near-zero overhead. - * Note: xdebug_break() jit mode won't have full stepping support - * without EXT_STMT opcodes. Use start_with_request=yes or a trigger - * for full debugging support. */ + /* No debug trigger: disable all heavy hooks for near-zero overhead. */ XG_BASE(observer_active) = 0; XG_BASE(statement_handler_enabled) = false; } From 56a9d937187504b81f652796ab41957d477d7b10 Mon Sep 17 00:00:00 2001 From: Roman Pronskiy Date: Sat, 14 Mar 2026 23:38:27 +0100 Subject: [PATCH 2/3] Perf: skip heavy RINIT allocations when no debugger connects Restructure RINIT to move the trigger/connection check before heavy allocations. When no IDE is listening (the common case), the extension now uses dormant init paths that skip: - xdebug_library_rinit: diagnosis_buffer, headers list, trait_location_map (256-bucket hash), log file open, path mapping - xdebug_base_rinit: fiber_stacks (64-bucket hash), stack vector, filters linked list, closure serializer, control socket, error/exception handler overrides Added NULL guards to all cleanup paths and the fiber switch observer to handle the dormant state safely. This eliminates all per-request heap allocations when no debugger will connect, reducing overhead for short-lived PHP-FPM requests. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/base/base.c | 40 ++++++++++++++++++++++++++++++++++++---- src/base/base.h | 1 + src/lib/lib.c | 29 ++++++++++++++++++++++++----- src/lib/lib.h | 1 + xdebug.c | 49 +++++++++++++++++++++---------------------------- 5 files changed, 83 insertions(+), 37 deletions(-) diff --git a/src/base/base.c b/src/base/base.c index 311217d..586a167 100644 --- a/src/base/base.c +++ b/src/base/base.c @@ -1056,7 +1056,14 @@ static xdebug_vector *find_stack_for_fiber(zend_string *fiber_key, zend_fiber_co static void xdebug_fiber_switch_observer(zend_fiber_context *from, zend_fiber_context *to) { xdebug_vector *current_stack; - zend_string *to_key = create_key_for_fiber(to); + zend_string *to_key; + + /* Skip fiber tracking when dormant (fiber_stacks not allocated) */ + if (!XG_BASE(fiber_stacks)) { + return; + } + + to_key = create_key_for_fiber(to); if (from->status == ZEND_FIBER_STATUS_DEAD) { zend_string *from_key = create_key_for_fiber(from); @@ -1205,6 +1212,27 @@ void xdebug_base_post_startup() zend_compile_file = xdebug_compile_file; } +void xdebug_base_rinit_dormant() +{ + /* Minimal init when no debug session will happen. + * Skip heavy allocations (fiber_stacks, stack, filters, control socket). + * NULL pointers are handled by cleanup code. */ + XG_BASE(fiber_stacks) = NULL; + XG_BASE(stack) = NULL; + XG_BASE(in_debug_info) = 0; + XG_BASE(prev_memory) = 0; + XG_BASE(function_count) = -1; + XG_BASE(last_eval_statement) = NULL; + XG_BASE(last_exception_trace) = NULL; + XG_BASE(statement_handler_enabled) = false; + XG_BASE(in_var_serialisation) = 0; + XG_BASE(in_execution) = 1; + XG_BASE(observer_active) = 0; + XG_BASE(needs_debug_init) = 0; + XG_BASE(filter_type_stack) = XDEBUG_FILTER_NONE; + XG_BASE(filters_stack) = NULL; +} + void xdebug_base_rinit() { /* Hack: We check for a soap header here, if that's existing, we don't use @@ -1287,8 +1315,10 @@ void xdebug_base_rinit() void xdebug_base_post_deactivate() { - xdebug_hash_destroy(XG_BASE(fiber_stacks)); - XG_BASE(fiber_stacks) = NULL; + if (XG_BASE(fiber_stacks)) { + xdebug_hash_destroy(XG_BASE(fiber_stacks)); + XG_BASE(fiber_stacks) = NULL; + } XG_BASE(stack) = NULL; XG_BASE(in_debug_info) = 0; @@ -1303,7 +1333,9 @@ void xdebug_base_post_deactivate() } /* filters */ - xdebug_llist_destroy(XG_BASE(filters_stack), NULL); + if (XG_BASE(filters_stack)) { + xdebug_llist_destroy(XG_BASE(filters_stack), NULL); + } #if HAVE_XDEBUG_CONTROL_SOCKET_SUPPORT /* Close Down Control Socket */ diff --git a/src/base/base.h b/src/base/base.h index 597f34c..4cb4243 100644 --- a/src/base/base.h +++ b/src/base/base.h @@ -22,6 +22,7 @@ void xdebug_base_mshutdown(); void xdebug_base_post_startup(); +void xdebug_base_rinit_dormant(); void xdebug_base_rinit(); void xdebug_base_post_deactivate(); void xdebug_base_rshutdown(); diff --git a/src/lib/lib.c b/src/lib/lib.c index a9b3508..ed783dc 100644 --- a/src/lib/lib.c +++ b/src/lib/lib.c @@ -80,6 +80,19 @@ void xdebug_library_mshutdown(void) xdebug_set_free(XG_LIB(opcode_handlers_set)); } +void xdebug_library_rinit_dormant(void) +{ + /* Minimal init when no debug session will happen. + * Skip heavy allocations (headers, trait_location_map, path_mapping). + * NULL pointers are handled by cleanup code. */ + XG_LIB(diagnosis_buffer) = NULL; + XG_LIB(headers) = NULL; + XG_LIB(dumped) = 0; + XG_LIB(do_collect_errors) = 0; + XG_LIB(trait_location_map) = NULL; + XG_LIB(path_mapping_information) = NULL; +} + void xdebug_library_rinit(void) { XG_LIB(diagnosis_buffer) = xdebug_str_new(); @@ -118,14 +131,20 @@ void xdebug_library_rinit(void) void xdebug_library_post_deactivate(void) { /* Clean up collected headers */ - xdebug_llist_destroy(XG_LIB(headers), NULL); - XG_LIB(headers) = NULL; + if (XG_LIB(headers)) { + xdebug_llist_destroy(XG_LIB(headers), NULL); + XG_LIB(headers) = NULL; + } - xdebug_hash_destroy(XG_LIB(trait_location_map)); + if (XG_LIB(trait_location_map)) { + xdebug_hash_destroy(XG_LIB(trait_location_map)); + } xdebug_close_log(); - xdebug_str_free(XG_LIB(diagnosis_buffer)); - XG_LIB(diagnosis_buffer) = NULL; + if (XG_LIB(diagnosis_buffer)) { + xdebug_str_free(XG_LIB(diagnosis_buffer)); + XG_LIB(diagnosis_buffer) = NULL; + } if (XG_LIB(path_mapping_information)) { xdebug_path_maps_dtor(XG_LIB(path_mapping_information)); diff --git a/src/lib/lib.h b/src/lib/lib.h index 962a70a..a30f0a8 100644 --- a/src/lib/lib.h +++ b/src/lib/lib.h @@ -230,6 +230,7 @@ void xdebug_library_zend_startup(void); void xdebug_library_zend_shutdown(void); void xdebug_library_minit(void); void xdebug_library_mshutdown(void); +void xdebug_library_rinit_dormant(void); void xdebug_library_rinit(void); void xdebug_library_post_deactivate(void); diff --git a/xdebug.c b/xdebug.c index 62ccf96..ff6d080 100644 --- a/xdebug.c +++ b/xdebug.c @@ -563,24 +563,19 @@ PHP_RINIT_FUNCTION(xdebug) this can override the idekey if one is set */ xdebug_env_config(); - xdebug_library_rinit(); - if (XDEBUG_MODE_IS(XDEBUG_MODE_STEP_DEBUG)) { + /* Minimal debugger setup: IDE key, no_exec check, context init. + * Heavy allocations are deferred until we know a debugger will connect. */ xdebug_debugger_rinit_early(); if (xdebug_debugger_bailout_if_no_exec_requested()) { zend_bailout(); } - } - - xdebug_init_auto_globals(); - /* Early debug init: attempt connection at RINIT so observer_active is set - * before any user code runs. This allows xdebug_observer_init to return - * {NULL, NULL} for functions first-called when no debugger is connected. */ - xdebug_base_rinit(); + /* Initialize auto globals early — needed for trigger check + * (xdebug_lib_find_in_globals accesses _GET, _POST, _COOKIE, _ENV) */ + xdebug_init_auto_globals(); - if (XDEBUG_MODE_IS(XDEBUG_MODE_STEP_DEBUG)) { /* Check if debugging could be requested this request. * For trigger/default mode: check triggers, cookies, env vars. * For yes mode: always expect a connection. @@ -592,26 +587,24 @@ PHP_RINIT_FUNCTION(xdebug) getenv("XDEBUG_SESSION_START") != NULL ); - if (debug_requested) { - /* Debug session requested: check if a client is actually listening - * before enabling expensive EXT_STMT opcodes. This avoids ~2x - * overhead when triggers are present but no IDE is connected. */ - if (xdebug_early_connect_to_client()) { - /* Client connected — do full debugger init (opcache disable, - * breakable lines map, etc.) */ - xdebug_debugger_rinit(); - CG(compiler_options) = CG(compiler_options) | ZEND_COMPILE_EXTENDED_STMT; - } else { - /* Trigger present but no client listening — stay dormant. - * Skip heavy debugger init (breakable_lines_map, opcache disable). */ - XG_BASE(observer_active) = 0; - XG_BASE(statement_handler_enabled) = false; - } + if (debug_requested && xdebug_early_connect_to_client()) { + /* Client connected — do full init */ + xdebug_library_rinit(); + xdebug_debugger_rinit(); + xdebug_base_rinit(); + CG(compiler_options) = CG(compiler_options) | ZEND_COMPILE_EXTENDED_STMT; } else { - /* No debug trigger: disable all heavy hooks for near-zero overhead. */ - XG_BASE(observer_active) = 0; - XG_BASE(statement_handler_enabled) = false; + /* Dormant: no debugger will connect this request. + * Skip heavy allocations (library, debugger, base init). + * Set minimal flags for correctness. */ + xdebug_library_rinit_dormant(); + xdebug_base_rinit_dormant(); } + } else { + /* Non-debug mode (shouldn't happen since profiler/coverage are stripped, + * but handle gracefully) */ + xdebug_library_rinit(); + xdebug_base_rinit(); } return SUCCESS; From 2d7b7cf8babd7316978c1bdce5e23b52a60e5f5e Mon Sep 17 00:00:00 2001 From: Roman Pronskiy Date: Sun, 15 Mar 2026 14:38:03 +0100 Subject: [PATCH 3/3] Fix: open log before trigger check so dormant path still logs Extract xdebug_library_rinit_log() to open diagnosis_buffer and log file early in RINIT, before the trigger/connection check. This ensures log messages from trigger evaluation (XDEBUG_IGNORE, trigger mismatch) and connection attempts are captured even on the dormant path. Fixes tests: bug01978, bug02348, start_ignore_yes_env, start_with_request_trigger_match-009, trigger_match-010. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/lib.c | 17 ++++++++++++----- src/lib/lib.h | 1 + xdebug.c | 4 ++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/lib/lib.c b/src/lib/lib.c index ed783dc..c960254 100644 --- a/src/lib/lib.c +++ b/src/lib/lib.c @@ -80,12 +80,20 @@ void xdebug_library_mshutdown(void) xdebug_set_free(XG_LIB(opcode_handlers_set)); } +void xdebug_library_rinit_log(void) +{ + /* Open log and diagnosis buffer early, before trigger/connection check. + * This ensures all log messages during trigger evaluation and + * connection attempts are captured. */ + XG_LIB(diagnosis_buffer) = xdebug_str_new(); + xdebug_open_log(); +} + void xdebug_library_rinit_dormant(void) { /* Minimal init when no debug session will happen. - * Skip heavy allocations (headers, trait_location_map, path_mapping). - * NULL pointers are handled by cleanup code. */ - XG_LIB(diagnosis_buffer) = NULL; + * Log and diagnosis_buffer already opened by rinit_log(). + * Skip heavy allocations (headers, trait_location_map, path_mapping). */ XG_LIB(headers) = NULL; XG_LIB(dumped) = 0; XG_LIB(do_collect_errors) = 0; @@ -95,8 +103,7 @@ void xdebug_library_rinit_dormant(void) void xdebug_library_rinit(void) { - XG_LIB(diagnosis_buffer) = xdebug_str_new(); - xdebug_open_log(); + /* Log and diagnosis_buffer already opened by rinit_log(). */ XG_LIB(headers) = xdebug_llist_alloc(xdebug_llist_string_dtor); diff --git a/src/lib/lib.h b/src/lib/lib.h index a30f0a8..87dff56 100644 --- a/src/lib/lib.h +++ b/src/lib/lib.h @@ -230,6 +230,7 @@ void xdebug_library_zend_startup(void); void xdebug_library_zend_shutdown(void); void xdebug_library_minit(void); void xdebug_library_mshutdown(void); +void xdebug_library_rinit_log(void); void xdebug_library_rinit_dormant(void); void xdebug_library_rinit(void); void xdebug_library_post_deactivate(void); diff --git a/xdebug.c b/xdebug.c index ff6d080..f2702c5 100644 --- a/xdebug.c +++ b/xdebug.c @@ -563,6 +563,10 @@ PHP_RINIT_FUNCTION(xdebug) this can override the idekey if one is set */ xdebug_env_config(); + /* Open log and diagnosis buffer early — needed for trigger check + * and connection logging. Remaining library init is deferred. */ + xdebug_library_rinit_log(); + if (XDEBUG_MODE_IS(XDEBUG_MODE_STEP_DEBUG)) { /* Minimal debugger setup: IDE key, no_exec check, context init. * Heavy allocations are deferred until we know a debugger will connect. */