-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimizing Xdebug Performance When Debugging is Inactive #4
Conversation
f6b2e5a
to
1444aa9
Compare
@@ -608,23 +612,30 @@ static void collect_params(function_stack_entry *fse, zend_execute_data *zdata, | |||
#endif | |||
} | |||
|
|||
function_stack_entry *xdebug_add_stack_frame(zend_execute_data *zdata, zend_op_array *op_array, int type) | |||
function_stack_entry *xdebug_add_stack_frame(zend_execute_data *zdata, zend_op_array *op_array, int type, int use_current_execute_data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is reused to reconstruct the stack when jit debugging is started. We will call it successively with the execute data for each stack frame. To be able to do this, we add a new parameter that tells it if it needs to use the current_execute_data
or the passed zdata
to record the data for that frame
src/base/base.c
Outdated
@@ -874,18 +885,57 @@ static bool should_run_user_handler_wrapper(zend_execute_data *execute_data) | |||
#endif | |||
} | |||
|
|||
static zend_extension *xdebug_find_xdebug_extension() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We use this function to find the current extension struct data so that we can set or unset the statement_handler
function
src/base/base.c
Outdated
return NULL; | ||
} | ||
|
||
static void disable_statement_extension_handler() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two functions are used to enable or disable the statement handler. Disable needs to be called before we can call enable because we need to record the existing handler
/* We still need this to do "include", "require", and "eval" */ | ||
static void xdebug_execute_ex(zend_execute_data *execute_data) | ||
{ | ||
bool run_user_handler = should_run_user_handler_wrapper(execute_data); | ||
|
||
if (run_user_handler) { | ||
if (run_user_handler && (XG_DBG(debugger_disabled) == 0 || execute_data->func->type == ZEND_EVAL_CODE)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We only call the xdebug_execute_user_code_begin()
and xdebug_execute_user_code_end()
functions if the debugger has not been disabled. Notice that we cannot just use a single if statement for both functions because the status of the debugger_disabled flag can change during the call to the xdebug_old_execute_ex()
function
We also need to call the begin function if we are entering into some eval code because we want to record this eval code so that it can be used later
@@ -1011,7 +1061,7 @@ static void xdebug_execute_internal(zend_execute_data *current_execute_data, zva | |||
{ | |||
bool run_internal_handler = should_run_internal_handler(current_execute_data); | |||
|
|||
if (run_internal_handler) { | |||
if (run_internal_handler && XG_DBG(debugger_disabled) == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We proceed in a similar fashion in the internal execute handler
@@ -112,4 +112,11 @@ static inline void xdebug_vector_destroy(xdebug_vector *v) | |||
xdfree(v); | |||
} | |||
|
|||
static inline void xdebug_vector_empty(xdebug_vector *v) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New utility function to empty the stack
@@ -8,12 +8,14 @@ xdebug.start_with_request=yes | |||
xdebug.client_discovery_header=I_LIKE_COOKIES | |||
xdebug.discover_client_host=1 | |||
xdebug.client_port=9999 | |||
xdebug.log= | |||
xdebug.log={TMP}/{RUNID}{TEST_PHP_WORKER}bug01656.txt |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that now we are not printing the failed connection attempt in the std output, we need to read it from the log to confirm that it contains the expected data
@@ -30,4 +30,4 @@ unlink (sys_get_temp_dir() . '/' . getenv('UNIQ_RUN_ID') . getenv('TEST_PHP_WORK | |||
[%d] [Step Debug] WARN: Invalid remote address provided containing URI spec 'unix:///tmp/haxx0r.sock'. | |||
[%d] [Step Debug] WARN: Could not discover client host through HTTP headers, connecting to configured address/port: unix:///tmp/xdbg.sock:0. | |||
[%d] [Step Debug] WARN: Creating socket for 'unix:///tmp/xdbg.sock', connect: No such file or directory. | |||
[%d] [Step Debug] ERR: Could not connect to debugging client. Tried: unix:///tmp/xdbg.sock:0 (fallback through xdebug.client_host/xdebug.client_port). | |||
[%d] [Step Debug] WARN: Could not connect to debugging client. Tried: unix:///tmp/xdbg.sock:0 (fallback through xdebug.client_host/xdebug.client_port). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In some other cases where we were already checking the log, we just need to change the severity in the test
@@ -569,7 +569,10 @@ PHP_MINIT_FUNCTION(xdebug) | |||
return SUCCESS; | |||
} | |||
|
|||
xdebug_library_minit(); | |||
if (XDEBUG_MODE_IS(XDEBUG_MODE_TRACING) || XDEBUG_MODE_IS(XDEBUG_MODE_COVERAGE)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This xdebug_library_minit
function overrides a number of opcode handlers but the vast majority of them are only used during tracing or code coverage. The only one that is used during debugging is the INCLUDE_OR_EVAL code. So we only call this function if we are tracing or doing coverage. The INCLUDE_OR_EVAL
opcode will be overridden separately later for debugging
@@ -823,7 +829,7 @@ ZEND_DLEXPORT void xdebug_zend_shutdown(zend_extension *extension) | |||
|
|||
ZEND_DLEXPORT void xdebug_init_oparray(zend_op_array *op_array) | |||
{ | |||
if (XDEBUG_MODE_IS_OFF()) { | |||
if (!XDEBUG_MODE_IS(XDEBUG_MODE_COVERAGE)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code in this handler is only useful if we are doing code coverage, so we return early in other cases
1444aa9
to
800db5b
Compare
add_stack_frame_recursively(EG(current_execute_data)); | ||
} | ||
|
||
void xdebug_enable_debugger_if_disabled() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These public functions are used to enable and disable the debugger and to rebuild the stack as needed
@@ -447,6 +453,11 @@ void xdebug_debugger_error_cb(zend_string *error_filename, int error_lineno, int | |||
xdebug_hash_find(XG_DBG(context).exception_breakpoints, error_type_str, strlen(error_type_str), (void *) &extra_brk_info) || | |||
xdebug_hash_find(XG_DBG(context).exception_breakpoints, "*", 1, (void *) &extra_brk_info) | |||
) { | |||
if (stack_rebuilt) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are stopping on a pseudo exception we need to enable the debugger and rebuild the stack if needed. We use this boolean flag because the stack may have already been rebuilt if we have notified about this error
xdebug_debug_init_if_requested_on_xdebug_break(); | ||
|
||
if (!xdebug_is_debug_connection_active()) { | ||
RETURN_FALSE; | ||
} | ||
|
||
xdebug_enable_debugger_if_disabled(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then if the connection is active, we need to re-enable the debugger
800db5b
to
8438742
Compare
@@ -874,18 +884,36 @@ static bool should_run_user_handler_wrapper(zend_execute_data *execute_data) | |||
#endif | |||
} | |||
|
|||
void xdebug_save_statement_handler(zend_extension *extension, statement_handler_func_t statement_handler) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is used to save a pointer to the xdebug zend_extension and to the xdebug statement handler so that we can use this data to enable or disable this handler
} | ||
|
||
|
||
static void xdebug_disable_statement_extension_handler() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two functions enable or disable xdebug's statement handler
@@ -787,6 +793,7 @@ ZEND_DLEXPORT void xdebug_statement_call(zend_execute_data *frame) | |||
|
|||
ZEND_DLEXPORT int xdebug_zend_startup(zend_extension *extension) | |||
{ | |||
xdebug_save_statement_handler(extension, xdebug_statement_call); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When starting the extension, we save a pointer to it and to the statement handler function
@@ -1067,6 +1101,83 @@ static zend_observer_fcall_handlers xdebug_observer_init(zend_execute_data *exec | |||
return (zend_observer_fcall_handlers){xdebug_execute_begin, xdebug_execute_end}; | |||
} | |||
#endif | |||
|
|||
static void xdebug_enable_debugger_handlers() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is called when we want to enable the debugger after having been disabled. We restore the function handler and the statement handler. Restoring the function handler in PHP 8.0 is not needed because we won't be disabling it as in PHP 8.0 it is called instead of the Observer API functions
xdebug_enable_statement_extension_handler(); | ||
} | ||
|
||
static void xdebug_disable_debugger_handlers() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is called when we want to disable the debugger. We remove the normal function handler if using PHP 8.1+ as all the functionality we need in this mode can be handled by the handlers called by the Observer API. If it is PHP 8.0 we need to leave it in place.
When using Xdebug, enabling debug mode significantly impacts execution speed, often doubling the run time of code compared to when Xdebug is not loaded. This performance penalty occurs even if the debugger is not actively used —i.e., when no client connects or no breakpoints are set. As a result, developers often find themselves frequently enabling and disabling Xdebug to avoid noticeable slowdowns.
Cause of the Slowdown
This slowdown happens because Xdebug injects additional processing at two critical points:
These operations are essential when debugging but become unnecessary overhead when the debugger is inactive.
What This PR Changes
This PR introduces optimizations that dynamically disable unnecessary debugging operations when:
By skipping function call logging and statement checks in these cases, we can greatly reduce execution overhead while maintaining full debugging capabilities when needed.
Handling Just-In-Time (JIT) Debugging
One challenge is JIT debugging, where the debugger is automatically started when:
xdebug_break()
is called.xdebug_connect_to_client()
is used.In these cases, some debugging functionality must remain available to ensure Xdebug can correctly attach to the process and capture execution state. As a result, the optimizations in this PR do not entirely eliminate Xdebug’s overhead but significantly reduce it compared to the current implementation.
Challenges
A key challenge is that JIT debugging sessions will not have a pre-recorded stack trace. However, this is an issue thar can be overcome because:
This is a reasonable compromise to ensure that Xdebug remains performant when debugging is not actively in use.
Performance Improvements
To illustrate the benefits of this approach, we measured execution times for two common real-world use cases: Running RectorPHP and PHPStan on a codebase.
These tests were conducted with Xdebug enabled, a debugger client connected, and no breakpoints set. As the results show, the optimized implementation runs 3–4× faster than the current version when debugging is not actively used.
Goal of This Change
The ultimate goal of this improvement is to make Xdebug’s performance impact minimal when debugging is inactive. This means developers no longer need to constantly toggle Xdebug on and off. Instead, they can leave it enabled at all times with:
This allows seamless debugging when needed, without causing unnecessary slowdowns during regular execution.
Additional improvements
This PR also includes a few optimizations that enhance the execution speed of the debugger, regardless of whether it is actively used or not. These improvements further reduce the overall overhead of Xdebug, making debugging more efficient while maintaining full functionality.
Support This Work
If you or your company use Xdebug, please consider supporting my open-source work by sponsoring me on GitHub. Your support helps fund further improvements, and I have several ideas for enhancing Xdebug even further.