Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/benchmark-files/generate_summary.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
sort($phpVersions);

// Sort PHP Debugger modes according to the defined order, leaving only those which actually exist in the data
$phpDebuggerModeOrder = ["no", "off", "debug"];
$phpDebuggerModeOrder = ["no", "off", "debug", "debug-on-demand"];
$phpDebuggerModes = array_values(array_filter($phpDebuggerModeOrder, function($mode) use ($phpDebuggerModes) {
return in_array($mode, $phpDebuggerModes);
}));
Expand Down Expand Up @@ -138,6 +138,10 @@
// Calculate performance change compared to previous results
$performanceChange = '--';
$key = $command . '-' . $php . '-' . $phpDebugger;
$comparisonBranch = getenv('COMPARISON_BRANCH');
if ($phpDebugger == 'debug-on-demand' && $comparisonBranch !== false && $comparisonBranch !== '') {
$key = $command . '-' . $php . '-debug';
}
if (isset($previousResults[$key])) {
$previousOverhead = $previousResults[$key];
$performanceChange = '0%';
Expand Down Expand Up @@ -191,6 +195,10 @@

// Calculate performance change if previous data exists
$key = $command . '-' . $php . '-' . $phpDebugger;
$comparisonBranch = getenv('COMPARISON_BRANCH');
if ($phpDebugger == 'debug-on-demand' && $comparisonBranch !== false && $comparisonBranch !== '') {
$key = $command . '-' . $php . '-debug';
}
if (isset($previousResults[$key])) {
$previousOverhead = $previousResults[$key];
$changePercent = 0;
Expand Down
4 changes: 2 additions & 2 deletions .github/benchmark-files/php-debugger-benchmark.ini
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
zend_extension=php_debugger.so
php_debugger.start_with_request=no
xdebug.start_with_request=no
; We need this because some of the functions in bench.php do deep recursion and the default nesting level is not enough
php_debugger.max_nesting_level=2048
xdebug.max_nesting_level=2048
5 changes: 5 additions & 0 deletions .github/benchmark-files/php-debugger-on-demand-benchmark.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
zend_extension=php_debugger.so
xdebug.start_with_request=no
; We need this because some of the functions in bench.php do deep recursion and the default nesting level is not enough
xdebug.max_nesting_level=2048
xdebug.on_demand_debugging_enabled=1
17 changes: 14 additions & 3 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
matrix:
command: ["bench", "symfony", "rector"]
php: ["8.2", "8.3", "8.4", "8.5"]
php_debugger_mode: ["no", "off", "debug"]
php_debugger_mode: ["no", "off", "debug", "debug-on-demand"]
steps:
- uses: actions/checkout@v6

Expand Down Expand Up @@ -90,14 +90,25 @@ jobs:
sudo cp ./.github/benchmark-files/benchmark.ini /etc/php/${{ matrix.php }}/cgi/conf.d

- name: Copy php_debugger ini file
if: matrix.php_debugger_mode != 'no'
if: matrix.php_debugger_mode != 'no' && matrix.php_debugger_mode != 'debug-on-demand'
run: |
sudo cp ./.github/benchmark-files/php-debugger-benchmark.ini /etc/php/${{ matrix.php }}/cli/conf.d
sudo cp ./.github/benchmark-files/php-debugger-benchmark.ini /etc/php/${{ matrix.php }}/cgi/conf.d


- name: Copy on-demand php_debugger ini file
if: matrix.php_debugger_mode == 'debug-on-demand'
run: |
sudo cp ./.github/benchmark-files/php-debugger-on-demand-benchmark.ini /etc/php/${{ matrix.php }}/cli/conf.d
sudo cp ./.github/benchmark-files/php-debugger-on-demand-benchmark.ini /etc/php/${{ matrix.php }}/cgi/conf.d

- name: Set PHP Debugger mode
if: matrix.php_debugger_mode != 'debug-on-demand'
run: echo "PHP_DEBUGGER_MODE=${{ matrix.php_debugger_mode }}" >> $GITHUB_ENV

- name: Set PHP on-demand Debugger mode
if: matrix.php_debugger_mode == 'debug-on-demand'
run: echo "PHP_DEBUGGER_MODE=debug" >> $GITHUB_ENV

- name: Confirm PHP setup
run: |
php -v
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ phpt.*
!run-xdebug-tests.php
!make-release.php
!bench.php
!test-ini.php
!rector.php
!generate_summary.php
!tests/debugger/dbgp/dbgpclient.php
Expand Down
42 changes: 31 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ We measured three different scenarios which we believe represent a good mix of t
| Xdebug | **+35.3%** |
| PHP Debugger | **+1.3%** |

### On-Demand Debugging

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also update the Xdebug compatibility table?


To improve the performance of code running with the PHP Debugger enabled, several features required for on-demand
debugging are disabled if the debugger does not connect to a client at startup.

On-demand debugging allows the debugger to connect later during execution—for example, via `xdebug_connect_to_client()`,
`xdebug_break()`, or when an error or exception occurs.

We consider on-demand debugging to be a relatively uncommon use case, and we want to avoid degrading performance for
the majority of users. However, since some users rely on this functionality, we provide an INI setting to enable
it when needed.

INI setting: `php_debugger.on_demand_debugging_enabled` (default: false)

When this setting is enabled, on-demand debugging features remain active even if no client is connected at startup.
Note that this has a significant performance impact: instead of achieving up to a 97% performance improvement, the average improvement drops to around 60%.

For this reason, we recommend enabling this setting only if you specifically require on-demand debugging.

## Installation

### Manual download
Expand Down Expand Up @@ -104,17 +123,18 @@ Works as-is. No changes needed.

PHP Debugger maintains compatibility with Xdebug's debug mode:

| Feature | PHP Debugger | Xdebug |
|------------------------------------|--------------|---------|
| `extension_loaded("xdebug")` | ✅ true | ✅ true |
| `extension_loaded("php_debugger")` | ✅ true | ❌ false |
| `xdebug.*` INI settings | ✅ works | ✅ works |
| `xdebug_break()` | ✅ works | ✅ works |
| `XDEBUG_SESSION` trigger | ✅ works | ✅ works |
| Step debugging (DBGp) | ✅ | ✅ |
| Code coverage | ❌ use pcov | ✅ |
| Profiling | ❌ removed | ✅ |
| Tracing | ❌ removed | ✅ |
| Feature | PHP Debugger | Xdebug |
|------------------------------------|------------------------------------------------------------------------------|--------|
| `extension_loaded("xdebug")` | ✅ true | ✅ true |
| `extension_loaded("php_debugger")` | ✅ true | ❌ false |
| `xdebug.*` INI settings | ✅ works | ✅ works |
| `xdebug_break()` | ✅ works | ✅ works |
| `XDEBUG_SESSION` trigger | ✅ works | ✅ works |
| Step debugging (DBGp) | ✅ | ✅ |
| On-demand debugging | ✅ works if `on_demand_debugging_enabled`<br/>is set, does not work otherwise | ✅ |
| Code coverage | ❌ use pcov | ✅ |
| Profiling | ❌ removed | ✅ |
| Tracing | ❌ removed | ✅ |

### New names (optional)

Expand Down
44 changes: 23 additions & 21 deletions src/base/base.c
Original file line number Diff line number Diff line change
Expand Up @@ -715,8 +715,10 @@ static void xdebug_execute_user_code_begin(zend_execute_data *execute_data)

/* After first-call init, deactivate observer if no debugger connected */
if (!xdebug_is_debug_connection_active()) {
XG_BASE(observer_active) = 0;
return;
if (!XINI_DBG(on_demand_debugging_enabled)) {
XG_BASE(observer_active) = 0;
return;
}
}
}

Expand Down Expand Up @@ -1206,16 +1208,8 @@ void xdebug_base_post_startup()

void xdebug_base_rinit()
{
/* Hack: We check for a soap header here, if that's existing, we don't use
* Xdebug's error handler to keep soap fault from fucking up. */
if (
(XDEBUG_MODE_IS(XDEBUG_MODE_STEP_DEBUG))
&&
(zend_hash_str_find(Z_ARR(PG(http_globals)[TRACK_VARS_SERVER]), "HTTP_SOAPACTION", sizeof("HTTP_SOAPACTION") - 1) == NULL)
) {
xdebug_base_use_xdebug_error_cb();
xdebug_base_use_xdebug_throw_exception_hook();
}
XG_BASE(statement_handler_enabled) = true;
XG_BASE(observer_active) = true;

{
zend_string *fiber_key = create_key_for_fiber(EG(main_fiber_context));
Expand All @@ -1231,12 +1225,6 @@ void xdebug_base_rinit()
XG_BASE(last_eval_statement) = NULL;
XG_BASE(last_exception_trace) = NULL;

/* Enable statement handler only when needed */
XG_BASE(statement_handler_enabled) = false;
if (XDEBUG_MODE_IS(XDEBUG_MODE_STEP_DEBUG)) {
XG_BASE(statement_handler_enabled) = true;
}

/* Initialize start time */
XG_BASE(start_nanotime) = xdebug_get_nanotime();

Expand Down Expand Up @@ -1270,9 +1258,6 @@ void xdebug_base_rinit()
/* Signal that we're in a request now */
XG_BASE(in_execution) = 1;

/* Observer starts active to allow first-call debug init check */
XG_BASE(observer_active) = XDEBUG_MODE_IS(XDEBUG_MODE_STEP_DEBUG);

/* filters */
XG_BASE(filter_type_stack) = XDEBUG_FILTER_NONE;
XG_BASE(filters_stack) = xdebug_llist_alloc(xdebug_llist_string_dtor);
Expand All @@ -1283,6 +1268,23 @@ void xdebug_base_rinit()
}
}

void xdebug_base_rinit_if_enabled()
{
CG(compiler_options) = CG(compiler_options) | ZEND_COMPILE_EXTENDED_STMT;
xdebug_disable_opcache_optimizer();

/* Hack: We check for a soap header here, if that's existing, we don't use
* Xdebug's error handler to keep soap fault from fucking up. */
if (
(XDEBUG_MODE_IS(XDEBUG_MODE_STEP_DEBUG))
&&
(zend_hash_str_find(Z_ARR(PG(http_globals)[TRACK_VARS_SERVER]), "HTTP_SOAPACTION", sizeof("HTTP_SOAPACTION") - 1) == NULL)
) {
xdebug_base_use_xdebug_error_cb();
xdebug_base_use_xdebug_throw_exception_hook();
}
}

void xdebug_base_post_deactivate()
{
xdebug_hash_destroy(XG_BASE(fiber_stacks));
Expand Down
1 change: 1 addition & 0 deletions src/base/base.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ void xdebug_base_mshutdown();
void xdebug_base_post_startup();

void xdebug_base_rinit();
void xdebug_base_rinit_if_enabled();
void xdebug_base_post_deactivate();
void xdebug_base_rshutdown();

Expand Down
1 change: 1 addition & 0 deletions src/base/base_globals.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ typedef struct _xdebug_base_globals_t {
zend_string *last_eval_statement;
char *last_exception_trace;
zend_bool statement_handler_enabled;
zend_bool early_connection;

/* in-execution checking */
zend_bool in_execution;
Expand Down
2 changes: 2 additions & 0 deletions src/base/ctrl_socket.c
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ CTRL_FUNC(pause)
xdebug_xml_add_text(action, xdstrdup("IDE Connection Signalled"));

XG_DBG(context).do_connect_to_client = 1;

XG_BASE(statement_handler_enabled) = true;
} else {
action = xdebug_xml_node_init("action");
xdebug_xml_add_text(action, xdstrdup("Breakpoint Signalled"));
Expand Down
4 changes: 3 additions & 1 deletion src/debugger/com.c
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ static void xdebug_init_debugger()

/* If socket was already established by early connect at RINIT,
* skip straight to protocol initialization */
if (XG_DBG(context).socket >= 0) {
if (XG_BASE(early_connection)) {
xdebug_str_add_fmt(connection_attempts, "%s:%ld (through xdebug.client_host/xdebug.client_port)", XINI_DBG(client_host), XINI_DBG(client_port));
} else {
if (strcmp(XINI_DBG(cloud_id), "") != 0) {
Expand Down Expand Up @@ -654,6 +654,8 @@ static void xdebug_init_debugger()
xdebug_log_ex(XLOG_CHAN_DEBUG, XLOG_ERR, "NOPERM", "No permission connecting to debugging client (%s). This could be SELinux related.", connection_attempts->d);
}

XG_BASE(statement_handler_enabled) = XG_DBG(remote_connection_enabled);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the on-demand path where it was just flipped true by xdebug_connect_to_client(), will we reach this after the connection succeeds? otherwise we may end up with the flag bouncing false→true→false


xdebug_str_free(connection_attempts);
}

Expand Down
23 changes: 21 additions & 2 deletions src/debugger/debugger.c
Original file line number Diff line number Diff line change
Expand Up @@ -878,8 +878,6 @@ void xdebug_debugger_rinit(void)
{
char *idekey;

xdebug_disable_opcache_optimizer();

/* Get the ide key for this session */
XG_DBG(ide_key) = NULL;
idekey = xdebug_debugger_get_ide_key();
Expand Down Expand Up @@ -1223,6 +1221,16 @@ PHP_FUNCTION(xdebug_break)
{
RETURN_FALSE_IF_MODE_IS_NOT(XDEBUG_MODE_STEP_DEBUG);

if (!xdebug_is_debug_connection_active() && !XINI_DBG(on_demand_debugging_enabled)) {
xdebug_log_ex(XLOG_CHAN_DEBUG, XLOG_INFO, "JIT-OFF",
"xdebug_break() ignored: no active debug connection and "
"xdebug.on_demand_debugging_enabled is not enabled");
php_error(E_NOTICE,
"xdebug_break() ignored: no active debug session and on-demand debugging is disabled. "
"Set xdebug.on_demand_debugging_enabled=1 to enable mid-request debugging");
RETURN_FALSE;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add logging?

xdebug_log_ex(XLOG_CHAN_DEBUG, XLOG_INFO, "JIT-OFF",
        "xdebug_break() ignored: no active debug connection and "
        "xdebug.jit_debugging_enabled is not enabled");
php_error(E_NOTICE,
        "xdebug_break(): no active debug session and JIT debugging is disabled. "
        "Set xdebug.jit_debugging_enabled=1 to enable mid-request debugging");

or something along the lines

}

xdebug_debug_init_if_requested_on_xdebug_break();

if (!xdebug_is_debug_connection_active()) {
Expand All @@ -1239,8 +1247,19 @@ PHP_FUNCTION(xdebug_connect_to_client)
{
RETURN_FALSE_IF_MODE_IS_NOT(XDEBUG_MODE_STEP_DEBUG);

if (!XINI_DBG(on_demand_debugging_enabled)) {
xdebug_log_ex(XLOG_CHAN_DEBUG, XLOG_INFO, "ON-DEMAND-OFF",
"xdebug_connect_to_client() ignored: xdebug.on_demand_debugging_enabled is not enabled");
php_error(E_NOTICE,
"xdebug_connect_to_client() ignored: On-demand debugging is disabled. "
"Set xdebug.on_demand_debugging_enabled=1 to enable mid-request debugging");
RETURN_FALSE;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

}

XG_DBG(context).do_connect_to_client = 1;

XG_BASE(statement_handler_enabled) = true;

RETURN_TRUE;
}

Expand Down
2 changes: 2 additions & 0 deletions src/debugger/debugger.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ typedef struct _xdebug_debugger_settings_t {
zend_long connect_timeout_ms; /* Timeout in MS for remote connections */

char *ide_key_setting; /* Set through php.ini and friends */

zend_bool on_demand_debugging_enabled; /* Enable on-demand debugging */
} xdebug_debugger_settings_t;

PHP_INI_MH(OnUpdateDebugMode);
Expand Down
6 changes: 5 additions & 1 deletion tests/debugger/bug00622.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ $commands = array(
'detach'
);

dbgpRunFile( $filename, $commands );
$settings = [
'xdebug.on_demand_debugging_enabled' => 1
];

dbgpRunFile( $filename, $commands, $settings );
?>
--EXPECTF--
<?xml version="1.0" encoding="iso-8859-1"?>
Expand Down
3 changes: 1 addition & 2 deletions tests/debugger/bug00932.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ Test for bug #932: Show an error if Xdebug can't open the remote debug log
<?php
require __DIR__ . '/../utils.inc';
check_reqs('dbgp; !win');
if (is_stripped_debugger()) die('skip Needs develop mode');
?>
--INI--
xdebug.mode=develop
xdebug.mode=debug
xdebug.log=/doesnotexist/bug932.log
--FILE--
<?php
Expand Down
2 changes: 0 additions & 2 deletions tests/debugger/bug00964-001.phpt
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
--TEST--
Test for bug #964: IP retrival from X-Forwarded-For complies with RFC 7239 (without comma)
--XFAIL--
PHP 8.6 CGI changes affect how X-Forwarded-For header IP extraction works. The test expects the debugger to write the client IP to a temp file, but the file is never created.
--SKIPIF--
<?php
require __DIR__ . '/../utils.inc';
Expand Down
2 changes: 0 additions & 2 deletions tests/debugger/bug00964-002.phpt
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
--TEST--
Test for bug #964: IP retrival from X-Forwarded-For complies with RFC 7239 (with comma)
--XFAIL--
PHP 8.6 CGI changes affect how X-Forwarded-For header IP extraction works. The test expects the debugger to write the client IP to a temp file, but the file is never created.
--SKIPIF--
<?php
require __DIR__ . '/../utils.inc';
Expand Down
7 changes: 4 additions & 3 deletions tests/debugger/bug01101.phpt
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
--TEST--
Test for bug #1101: Debugger is not triggered on xdebug_break() in jit mode.
--XFAIL--
Phase 2 RINIT observer gating only sets ZEND_COMPILE_EXTENDED_STMT when a debug client connects at RINIT. In trigger/jit mode without a trigger, EXT_STMT opcodes are not emitted, so xdebug_break() cannot do line-level stepping on already-compiled code.
--SKIPIF--
<?php
require __DIR__ . '/../utils.inc';
Expand All @@ -19,7 +17,10 @@ $commands = array(
'detach',
);

dbgpRunFile( $filename, $commands, array( 'xdebug.start_with_request' => 'trigger' ) );
dbgpRunFile( $filename, $commands, [
'xdebug.start_with_request' => 'trigger',
'xdebug.on_demand_debugging_enabled' => 1
]);
?>
--EXPECT--
<?xml version="1.0" encoding="iso-8859-1"?>
Expand Down
3 changes: 1 addition & 2 deletions tests/debugger/bug01915.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ Test for bug #1915: Debugger should only start with XDEBUG_SESSION and not XDEBU
--SKIPIF--
<?php
require __DIR__ . "/../utils.inc";
if (is_stripped_debugger()) die("skip Needs profile mode");
?>
--ENV--
XDEBUG_PROFILE=1
Expand All @@ -14,7 +13,7 @@ require 'dbgp/dbgpclient.php';
dbgpRunFile(
dirname(__FILE__) . '/empty-echo.inc',
['step_into', 'step_into', 'property_get -n $e', 'detach'],
['xdebug.mode' => 'debug,profile', 'xdebug.start_with_request' => 'trigger'],
['xdebug.mode' => 'debug', 'xdebug.start_with_request' => 'trigger'],
['timeout' => 1]
);
?>
Expand Down
Loading
Loading