From 509c25971a82988c41b912187f6c68e95cd4988f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Feb 2026 01:53:29 +0000 Subject: [PATCH 1/2] Add xdebug integration: query timing and slow query breakpoints Adds two key features to the MariaDB profiler: 1. Query execution time measurement: All mysqlnd hooks (query, send_query, prepare, execute) now measure wall-clock duration using gettimeofday. Duration is recorded in both JSONL logs (as "duration_ms" field) and raw logs (as [X.XXXms] column). 2. xdebug slow query breakpoint: When xdebug is loaded with step-debug mode active, queries exceeding the configured threshold automatically trigger xdebug_break(), pausing the debugger at the exact point of a slow query for interactive inspection. New INI setting: mariadb_profiler.xdebug_break_threshold = 0 Threshold in seconds (e.g. 0.5). Set to 0 to disable. PHP_INI_ALL so it can be changed per-request via ini_set(). New files: profiler_xdebug.c - xdebug detection (module registry + mode check) with per-request caching, and xdebug_break() caller profiler_xdebug.h - public API declarations Compatible with PHP 5.3-8.4+ and xdebug 2.x/3.x. https://claude.ai/code/session_01Ku8336fyCEdnwAVszkVmFS --- ext/mariadb_profiler/config.m4 | 2 +- ext/mariadb_profiler/config.w32 | 2 +- ext/mariadb_profiler/mariadb_profiler.c | 17 +++ ext/mariadb_profiler/php_mariadb_profiler.h | 19 ++- ext/mariadb_profiler/profiler_log.c | 62 ++++++--- .../profiler_mysqlnd_plugin.c | 87 ++++++++++-- ext/mariadb_profiler/profiler_xdebug.c | 130 ++++++++++++++++++ ext/mariadb_profiler/profiler_xdebug.h | 37 +++++ 8 files changed, 323 insertions(+), 33 deletions(-) create mode 100644 ext/mariadb_profiler/profiler_xdebug.c create mode 100644 ext/mariadb_profiler/profiler_xdebug.h diff --git a/ext/mariadb_profiler/config.m4 b/ext/mariadb_profiler/config.m4 index b1fea48..55e7e2f 100644 --- a/ext/mariadb_profiler/config.m4 +++ b/ext/mariadb_profiler/config.m4 @@ -19,7 +19,7 @@ if test "$PHP_MARIADB_PROFILER" != "no"; then fi PHP_NEW_EXTENSION(mariadb_profiler, - mariadb_profiler.c profiler_mysqlnd_plugin.c profiler_job.c profiler_log.c profiler_tag.c profiler_trace.c, + mariadb_profiler.c profiler_mysqlnd_plugin.c profiler_job.c profiler_log.c profiler_tag.c profiler_trace.c profiler_xdebug.c, $ext_shared,, $PROFILER_CFLAGS) dnl Require mysqlnd diff --git a/ext/mariadb_profiler/config.w32 b/ext/mariadb_profiler/config.w32 index e1449b5..d4e47cd 100644 --- a/ext/mariadb_profiler/config.w32 +++ b/ext/mariadb_profiler/config.w32 @@ -4,7 +4,7 @@ ARG_ENABLE('mariadb_profiler', 'MariaDB Query Profiler support', 'no'); if (PHP_MARIADB_PROFILER != 'no') { EXTENSION('mariadb_profiler', - 'mariadb_profiler.c profiler_mysqlnd_plugin.c profiler_job.c profiler_log.c profiler_tag.c profiler_trace.c', + 'mariadb_profiler.c profiler_mysqlnd_plugin.c profiler_job.c profiler_log.c profiler_tag.c profiler_trace.c profiler_xdebug.c', PHP_MARIADB_PROFILER_SHARED, '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1'); ADD_EXTENSION_DEP('mariadb_profiler', 'mysqlnd', true); diff --git a/ext/mariadb_profiler/mariadb_profiler.c b/ext/mariadb_profiler/mariadb_profiler.c index f173742..47579aa 100644 --- a/ext/mariadb_profiler/mariadb_profiler.c +++ b/ext/mariadb_profiler/mariadb_profiler.c @@ -15,6 +15,7 @@ #include "php_ini.h" #include "ext/standard/info.h" #include "php_mariadb_profiler.h" +#include "profiler_xdebug.h" #include #include @@ -62,6 +63,14 @@ PHP_INI_BEGIN() trace_depth, zend_mariadb_profiler_globals, mariadb_profiler_globals) + + STD_PHP_INI_ENTRY("mariadb_profiler.xdebug_break_threshold", + "0", + PHP_INI_ALL, + OnUpdateReal, + xdebug_break_threshold, + zend_mariadb_profiler_globals, + mariadb_profiler_globals) PHP_INI_END() /* }}} */ @@ -226,6 +235,8 @@ PHP_RINIT_FUNCTION(mariadb_profiler) /* {{{ PHP_RSHUTDOWN_FUNCTION */ PHP_RSHUTDOWN_FUNCTION(mariadb_profiler) { + profiler_xdebug_request_shutdown(); + if (PROFILER_G(enabled)) { profiler_tag_clear_all(); profiler_job_free_active_jobs(); @@ -256,6 +267,12 @@ PHP_MINFO_FUNCTION(mariadb_profiler) php_info_print_table_row(2, "Log directory", PROFILER_G(log_dir)); php_info_print_table_row(2, "Raw logging", PROFILER_G(raw_log) ? "Yes" : "No"); php_info_print_table_row(2, "Trace depth", trace_depth_str); + { + char threshold_str[32]; + snprintf(threshold_str, sizeof(threshold_str), "%.3f", + PROFILER_G(xdebug_break_threshold)); + php_info_print_table_row(2, "xdebug break threshold (s)", threshold_str); + } php_info_print_table_end(); DISPLAY_INI_ENTRIES(); diff --git a/ext/mariadb_profiler/php_mariadb_profiler.h b/ext/mariadb_profiler/php_mariadb_profiler.h index ca432f5..4e41d2f 100644 --- a/ext/mariadb_profiler/php_mariadb_profiler.h +++ b/ext/mariadb_profiler/php_mariadb_profiler.h @@ -62,6 +62,8 @@ ZEND_BEGIN_MODULE_GLOBALS(mariadb_profiler) int tag_depth; /* Trace settings */ zend_long trace_depth; /* 0=disabled, N=capture N frames */ + /* xdebug integration */ + double xdebug_break_threshold; /* seconds; 0=disabled */ #if PHP_VERSION_ID >= 70000 /* Prepared statement query template storage (PHP 7.0+) */ HashTable *stmt_queries; /* stmt ptr -> query template string */ @@ -99,13 +101,17 @@ void profiler_job_free_active_jobs(void); int profiler_job_is_any_active(void); char **profiler_job_get_active_list(int *count); -/* Logging – status is "ok" or "err" (NULL treated as "ok") */ -void profiler_log_query(const char *query, size_t query_len, const char *status); +/* Logging – status is "ok" or "err" (NULL treated as "ok") + * duration_ms: query execution time in milliseconds, negative if not measured */ +void profiler_log_query(const char *query, size_t query_len, + const char *status, double duration_ms); void profiler_log_query_with_params(const char *query, size_t query_len, - const char *params_json, const char *status); + const char *params_json, const char *status, + double duration_ms); void profiler_log_raw(const char *job_key, const char *query, size_t query_len, const char *tag, const char *trace_json, - const char *params_json, const char *status); + const char *params_json, const char *status, + double duration_ms); void profiler_log_init(void); void profiler_log_shutdown(void); @@ -119,6 +125,11 @@ void profiler_tag_clear_all(void); /* Trace */ char *profiler_trace_capture_json(void); +/* xdebug integration */ +int profiler_xdebug_is_debugger_active(void); +void profiler_xdebug_break(void); +void profiler_xdebug_request_shutdown(void); + /* PHP functions */ PHP_FUNCTION(mariadb_profiler_tag); PHP_FUNCTION(mariadb_profiler_untag); diff --git a/ext/mariadb_profiler/profiler_log.c b/ext/mariadb_profiler/profiler_log.c index 8f317d5..2247d51 100644 --- a/ext/mariadb_profiler/profiler_log.c +++ b/ext/mariadb_profiler/profiler_log.c @@ -110,10 +110,12 @@ static double profiler_log_get_microtime(void) /* {{{ profiler_log_raw * Write raw query to job's raw log file. - * tag, trace_json, params_json, and status may be NULL. */ + * tag, trace_json, params_json, and status may be NULL. + * duration_ms: query execution time in milliseconds, negative if not measured. */ void profiler_log_raw(const char *job_key, const char *query, size_t query_len, const char *tag, const char *trace_json, - const char *params_json, const char *status) + const char *params_json, const char *status, + double duration_ms) { char *filepath; FILE *fp; @@ -134,12 +136,24 @@ void profiler_log_raw(const char *job_key, const char *query, size_t query_len, timestamp = profiler_log_get_timestamp(); - if (tag) { - fprintf(fp, "[%s] [%s] [%s] %.*s\n", timestamp, - status ? status : "ok", tag, (int)query_len, query); + if (duration_ms >= 0) { + if (tag) { + fprintf(fp, "[%s] [%s] [%.3fms] [%s] %.*s\n", timestamp, + status ? status : "ok", duration_ms, tag, + (int)query_len, query); + } else { + fprintf(fp, "[%s] [%s] [%.3fms] %.*s\n", timestamp, + status ? status : "ok", duration_ms, + (int)query_len, query); + } } else { - fprintf(fp, "[%s] [%s] %.*s\n", timestamp, - status ? status : "ok", (int)query_len, query); + if (tag) { + fprintf(fp, "[%s] [%s] [%s] %.*s\n", timestamp, + status ? status : "ok", tag, (int)query_len, query); + } else { + fprintf(fp, "[%s] [%s] %.*s\n", timestamp, + status ? status : "ok", (int)query_len, query); + } } /* Append bound parameter values if present */ @@ -207,10 +221,12 @@ void profiler_log_raw(const char *job_key, const char *query, size_t query_len, /* {{{ profiler_log_jsonl * Write JSON line to job's parsed log file. * tag, trace_json, params_json, and status may be NULL. + * duration_ms: query execution time in milliseconds, negative if not measured. * SQL parsing (table/column extraction) is done by the CLI tool. */ static void profiler_log_jsonl(const char *job_key, const char *query, size_t query_len, const char *tag, const char *trace_json, - const char *params_json, const char *status) + const char *params_json, const char *status, + double duration_ms) { char *filepath; FILE *fp; @@ -259,6 +275,10 @@ static void profiler_log_jsonl(const char *job_key, const char *query, size_t qu fprintf(fp, ",\"s\":\"%s\"", status); } + if (duration_ms >= 0) { + fprintf(fp, ",\"duration_ms\":%.3f", duration_ms); + } + fprintf(fp, ",\"ts\":%.6f}\n", ts); efree(escaped_query); @@ -274,10 +294,12 @@ static void profiler_log_jsonl(const char *job_key, const char *query, size_t qu /* {{{ profiler_log_query_internal * Internal: log a query to all active jobs with optional params and status. - * Captures the current context tag and PHP trace once, shared across all jobs. */ + * Captures the current context tag and PHP trace once, shared across all jobs. + * duration_ms: query execution time in milliseconds, negative if not measured. */ static void profiler_log_query_internal(const char *query, size_t query_len, const char *params_json, - const char *status) + const char *status, + double duration_ms) { char **jobs; int job_count; @@ -298,11 +320,11 @@ static void profiler_log_query_internal(const char *query, size_t query_len, for (i = 0; i < job_count; i++) { /* Write JSONL entry */ - profiler_log_jsonl(jobs[i], query, query_len, tag, trace_json, params_json, status); + profiler_log_jsonl(jobs[i], query, query_len, tag, trace_json, params_json, status, duration_ms); /* Write raw log if enabled */ if (PROFILER_G(raw_log)) { - profiler_log_raw(jobs[i], query, query_len, tag, trace_json, params_json, status); + profiler_log_raw(jobs[i], query, query_len, tag, trace_json, params_json, status, duration_ms); } } @@ -314,20 +336,24 @@ static void profiler_log_query_internal(const char *query, size_t query_len, /* {{{ profiler_log_query * Main entry point: log a query (without params) to all active jobs. - * status is "ok" or "err" (NULL treated as "ok"). */ -void profiler_log_query(const char *query, size_t query_len, const char *status) + * status is "ok" or "err" (NULL treated as "ok"). + * duration_ms: query execution time in milliseconds, negative if not measured. */ +void profiler_log_query(const char *query, size_t query_len, + const char *status, double duration_ms) { - profiler_log_query_internal(query, query_len, NULL, status); + profiler_log_query_internal(query, query_len, NULL, status, duration_ms); } /* }}} */ /* {{{ profiler_log_query_with_params * Log a prepared statement query with bound parameter values to all active jobs. - * status is "ok" or "err" (NULL treated as "ok"). */ + * status is "ok" or "err" (NULL treated as "ok"). + * duration_ms: query execution time in milliseconds, negative if not measured. */ void profiler_log_query_with_params(const char *query, size_t query_len, - const char *params_json, const char *status) + const char *params_json, const char *status, + double duration_ms) { - profiler_log_query_internal(query, query_len, params_json, status); + profiler_log_query_internal(query, query_len, params_json, status, duration_ms); } /* }}} */ diff --git a/ext/mariadb_profiler/profiler_mysqlnd_plugin.c b/ext/mariadb_profiler/profiler_mysqlnd_plugin.c index c9f57c1..1ae3ba6 100644 --- a/ext/mariadb_profiler/profiler_mysqlnd_plugin.c +++ b/ext/mariadb_profiler/profiler_mysqlnd_plugin.c @@ -14,6 +14,37 @@ #include "php.h" #include "php_mariadb_profiler.h" #include "profiler_log.h" +#include "profiler_xdebug.h" + +#ifndef PHP_WIN32 +# include +#endif + +/* {{{ profiler_get_microtime + * Get current time in seconds with microsecond precision. */ +static double profiler_get_microtime(void) +{ + struct timeval tv; + gettimeofday(&tv, NULL); + return (double)tv.tv_sec + (double)tv.tv_usec / 1000000.0; +} +/* }}} */ + +/* {{{ profiler_check_slow_query + * If xdebug is active and duration exceeds the threshold, trigger a breakpoint. */ +static void profiler_check_slow_query(double duration_ms) +{ + double threshold; + TSRMLS_FETCH(); + + threshold = PROFILER_G(xdebug_break_threshold); + if (threshold > 0 && duration_ms >= threshold * 1000.0) { + if (profiler_xdebug_is_debugger_active()) { + profiler_xdebug_break(); + } + } +} +/* }}} */ /* * mysqlnd internal API changed across PHP versions: @@ -62,15 +93,24 @@ MYSQLND_METHOD(profiler_conn, query)( PROFILER_QUERY_LEN_T query_len TSRMLS_DC) { enum_func_status result; + double t0, duration_ms; + + t0 = profiler_get_microtime(); /* Call the original method first */ result = orig_conn_data_methods->query(conn, query, query_len TSRMLS_CC); - /* Log the query with execution status */ + duration_ms = (profiler_get_microtime() - t0) * 1000.0; + + /* Log the query with execution status and duration */ if (PROFILER_G(enabled) && profiler_job_is_any_active()) { - profiler_log_query(query, query_len, result == PASS ? "ok" : "err"); + profiler_log_query(query, query_len, + result == PASS ? "ok" : "err", duration_ms); } + /* Check slow query threshold for xdebug break */ + profiler_check_slow_query(duration_ms); + return result; } /* }}} */ @@ -92,10 +132,15 @@ MYSQLND_METHOD(profiler_conn, send_query)( zval *err_cb) { enum_func_status result; + double t0, duration_ms; + t0 = profiler_get_microtime(); result = orig_conn_data_methods->send_query(conn, query, query_len, read_cb, err_cb); + duration_ms = (profiler_get_microtime() - t0) * 1000.0; if (PROFILER_G(enabled) && profiler_job_is_any_active()) { - profiler_log_query(query, query_len, result == PASS ? "ok" : "err"); + profiler_log_query(query, query_len, + result == PASS ? "ok" : "err", duration_ms); } + profiler_check_slow_query(duration_ms); return result; } #elif PHP_VERSION_ID >= 70000 @@ -110,10 +155,15 @@ MYSQLND_METHOD(profiler_conn, send_query)( zval *err_cb) { enum_func_status result; + double t0, duration_ms; + t0 = profiler_get_microtime(); result = orig_conn_data_methods->send_query(conn, query, query_len, type, read_cb, err_cb); + duration_ms = (profiler_get_microtime() - t0) * 1000.0; if (PROFILER_G(enabled) && profiler_job_is_any_active()) { - profiler_log_query(query, query_len, result == PASS ? "ok" : "err"); + profiler_log_query(query, query_len, + result == PASS ? "ok" : "err", duration_ms); } + profiler_check_slow_query(duration_ms); return result; } #else @@ -125,10 +175,15 @@ MYSQLND_METHOD(profiler_conn, send_query)( unsigned int query_len TSRMLS_DC) { enum_func_status result; + double t0, duration_ms; + t0 = profiler_get_microtime(); result = orig_conn_data_methods->send_query(conn, query, query_len TSRMLS_CC); + duration_ms = (profiler_get_microtime() - t0) * 1000.0; if (PROFILER_G(enabled) && profiler_job_is_any_active()) { - profiler_log_query(query, query_len, result == PASS ? "ok" : "err"); + profiler_log_query(query, query_len, + result == PASS ? "ok" : "err", duration_ms); } + profiler_check_slow_query(duration_ms); return result; } #endif @@ -145,8 +200,11 @@ MYSQLND_METHOD(profiler_stmt, prepare)( PROFILER_QUERY_LEN_T query_len TSRMLS_DC) { enum_func_status result; + double t0, duration_ms; + t0 = profiler_get_microtime(); result = orig_stmt_methods->prepare(stmt, query, query_len TSRMLS_CC); + duration_ms = (profiler_get_microtime() - t0) * 1000.0; #if PHP_VERSION_ID >= 70000 if (PROFILER_G(enabled)) { @@ -161,14 +219,17 @@ MYSQLND_METHOD(profiler_stmt, prepare)( ); } else if (result != PASS && profiler_job_is_any_active()) { /* Failed prepare has no subsequent execute(); log immediately with err status */ - profiler_log_query(query, query_len, "err"); + profiler_log_query(query, query_len, "err", duration_ms); + profiler_check_slow_query(duration_ms); } } #else /* PHP 5.x: log template at prepare time (no param support) */ if (PROFILER_G(enabled) && profiler_job_is_any_active()) { - profiler_log_query(query, query_len, result == PASS ? "ok" : "err"); + profiler_log_query(query, query_len, + result == PASS ? "ok" : "err", duration_ms); } + profiler_check_slow_query(duration_ms); #endif return result; @@ -359,11 +420,16 @@ MYSQLND_METHOD(profiler_stmt, execute)( MYSQLND_STMT * const stmt) { enum_func_status result; + double t0, duration_ms; + + t0 = profiler_get_microtime(); /* Call the original method first */ result = orig_stmt_methods->execute(stmt); - /* Log with status and params after execution */ + duration_ms = (profiler_get_microtime() - t0) * 1000.0; + + /* Log with status, params, and duration after execution */ if (PROFILER_G(enabled) && profiler_job_is_any_active() && PROFILER_G(stmt_queries)) { zval *entry = zend_hash_index_find( PROFILER_G(stmt_queries), @@ -373,7 +439,7 @@ MYSQLND_METHOD(profiler_stmt, execute)( char *params_json = profiler_build_params_json(stmt); profiler_log_query_with_params( Z_STRVAL_P(entry), Z_STRLEN_P(entry), params_json, - result == PASS ? "ok" : "err" + result == PASS ? "ok" : "err", duration_ms ); if (params_json) { efree(params_json); @@ -381,6 +447,9 @@ MYSQLND_METHOD(profiler_stmt, execute)( } } + /* Check slow query threshold for xdebug break */ + profiler_check_slow_query(duration_ms); + return result; } /* }}} */ diff --git a/ext/mariadb_profiler/profiler_xdebug.c b/ext/mariadb_profiler/profiler_xdebug.c new file mode 100644 index 0000000..598f7be --- /dev/null +++ b/ext/mariadb_profiler/profiler_xdebug.c @@ -0,0 +1,130 @@ +/* + +----------------------------------------------------------------------+ + | MariaDB Query Profiler - xdebug Integration | + +----------------------------------------------------------------------+ + | Detects xdebug and calls xdebug_break() on slow queries | + | Compatible with PHP 5.3 - 8.4+ | + +----------------------------------------------------------------------+ +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "php.h" +#include "php_mariadb_profiler.h" +#include "profiler_xdebug.h" + +/* + * Per-request cache for xdebug detection. + * 0 = not yet checked + * 1 = xdebug step-debug is available + * -1 = xdebug not available / no step-debug + */ +static int xdebug_detected = 0; + +/* {{{ profiler_xdebug_detect + * Perform the actual detection of xdebug. + * Checks the module registry for the "xdebug" extension. + * On xdebug 3.x, also checks if xdebug.mode includes "debug". */ +static int profiler_xdebug_detect(void) +{ +#if PHP_VERSION_ID >= 70000 + zend_string *name; + zval *mode_val; + + /* Check if xdebug module is loaded */ + name = zend_string_init("xdebug", sizeof("xdebug") - 1, 0); + if (!zend_hash_exists(&module_registry, name)) { + zend_string_release(name); + return 0; + } + zend_string_release(name); + + /* + * xdebug 3.x uses xdebug.mode to control which features are active. + * If mode does not include "debug", step-debugging is disabled. + * xdebug 2.x does not have xdebug.mode; step-debug is always available. + */ + mode_val = zend_ini_str("xdebug.mode", sizeof("xdebug.mode") - 1, 0); + if (mode_val && Z_TYPE_P(mode_val) == IS_STRING) { + /* xdebug 3.x: check for "debug" in the mode string */ + if (strstr(Z_STRVAL_P(mode_val), "debug") != NULL) { + return 1; + } + /* mode is set but doesn't include "debug" */ + return 0; + } + + /* No xdebug.mode setting: either xdebug 2.x or mode defaults to "develop". + * For xdebug 2.x, step-debug is always available. + * For xdebug 3.x with default mode ("develop"), step-debug is off. + * We check if xdebug_break function exists as the definitive test. */ + { + zend_string *fn_name = zend_string_init("xdebug_break", sizeof("xdebug_break") - 1, 0); + int exists = zend_hash_exists(EG(function_table), fn_name); + zend_string_release(fn_name); + return exists ? 1 : 0; + } +#else + /* PHP 5.x: check module registry with old API */ + if (zend_hash_exists(&module_registry, "xdebug", sizeof("xdebug")) == 0) { + return 0; + } + /* xdebug 2.x on PHP 5: step-debug is always available */ + return 1; +#endif +} +/* }}} */ + +/* {{{ profiler_xdebug_is_debugger_active */ +int profiler_xdebug_is_debugger_active(void) +{ + if (xdebug_detected == 0) { + xdebug_detected = profiler_xdebug_detect() ? 1 : -1; + } + return (xdebug_detected == 1); +} +/* }}} */ + +/* {{{ profiler_xdebug_break + * Call xdebug_break() to trigger a debugger breakpoint. + * Works by looking up and calling the PHP function. */ +void profiler_xdebug_break(void) +{ +#if PHP_VERSION_ID >= 70000 + zval retval; + zval fname; + + ZVAL_STRING(&fname, "xdebug_break"); + + if (call_user_function(EG(function_table), NULL, &fname, &retval, 0, NULL) == SUCCESS) { + zval_ptr_dtor(&retval); + } + + zval_ptr_dtor(&fname); +#else + zval *retval_ptr = NULL; + zval *fname; + zval retval; + TSRMLS_FETCH(); + + MAKE_STD_ZVAL(fname); + ZVAL_STRING(fname, "xdebug_break", 1); + + INIT_ZVAL(retval); + if (call_user_function(EG(function_table), NULL, fname, &retval, 0, NULL TSRMLS_CC) == SUCCESS) { + zval_dtor(&retval); + } + + zval_ptr_dtor(&fname); +#endif +} +/* }}} */ + +/* {{{ profiler_xdebug_request_shutdown */ +void profiler_xdebug_request_shutdown(void) +{ + xdebug_detected = 0; +} +/* }}} */ diff --git a/ext/mariadb_profiler/profiler_xdebug.h b/ext/mariadb_profiler/profiler_xdebug.h new file mode 100644 index 0000000..5555aa9 --- /dev/null +++ b/ext/mariadb_profiler/profiler_xdebug.h @@ -0,0 +1,37 @@ +/* + +----------------------------------------------------------------------+ + | MariaDB Query Profiler - xdebug Integration | + +----------------------------------------------------------------------+ + | Detects xdebug presence and triggers breakpoints on slow queries | + +----------------------------------------------------------------------+ +*/ + +#ifndef PROFILER_XDEBUG_H +#define PROFILER_XDEBUG_H + +/* + * Check whether the xdebug extension is loaded and has the step-debug + * mode active. Result is cached per-request so the module registry + * lookup only happens once. + * + * Returns 1 if xdebug step-debug is available, 0 otherwise. + */ +int profiler_xdebug_is_debugger_active(void); + +/* + * Call xdebug_break() from the PHP userland function table. + * This causes the xdebug debugger (IDE) to pause execution at the + * current point, letting the developer inspect the context of a slow + * query interactively. + * + * No-op if xdebug is not loaded or xdebug_break function is absent. + */ +void profiler_xdebug_break(void); + +/* + * Reset the per-request cached detection state. + * Must be called during RSHUTDOWN. + */ +void profiler_xdebug_request_shutdown(void); + +#endif /* PROFILER_XDEBUG_H */ From 1fa4ce8a7cb685f30b300dbfa919305acd70c899 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Feb 2026 02:46:15 +0000 Subject: [PATCH 2/2] Fix build: replace non-existent zend_ini_str with zend_ini_string zend_ini_str() does not exist in any PHP version. The correct API is zend_ini_string() which returns char* (NULL if setting absent). Also removes an unused variable in the PHP 5.x code path. https://claude.ai/code/session_01Ku8336fyCEdnwAVszkVmFS --- ext/mariadb_profiler/profiler_xdebug.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ext/mariadb_profiler/profiler_xdebug.c b/ext/mariadb_profiler/profiler_xdebug.c index 598f7be..f93fd9f 100644 --- a/ext/mariadb_profiler/profiler_xdebug.c +++ b/ext/mariadb_profiler/profiler_xdebug.c @@ -31,7 +31,7 @@ static int profiler_xdebug_detect(void) { #if PHP_VERSION_ID >= 70000 zend_string *name; - zval *mode_val; + char *mode_str; /* Check if xdebug module is loaded */ name = zend_string_init("xdebug", sizeof("xdebug") - 1, 0); @@ -45,11 +45,13 @@ static int profiler_xdebug_detect(void) * xdebug 3.x uses xdebug.mode to control which features are active. * If mode does not include "debug", step-debugging is disabled. * xdebug 2.x does not have xdebug.mode; step-debug is always available. + * + * zend_ini_string() returns char* (NULL if setting doesn't exist). */ - mode_val = zend_ini_str("xdebug.mode", sizeof("xdebug.mode") - 1, 0); - if (mode_val && Z_TYPE_P(mode_val) == IS_STRING) { + mode_str = zend_ini_string("xdebug.mode", sizeof("xdebug.mode") - 1, 0); + if (mode_str && mode_str[0] != '\0') { /* xdebug 3.x: check for "debug" in the mode string */ - if (strstr(Z_STRVAL_P(mode_val), "debug") != NULL) { + if (strstr(mode_str, "debug") != NULL) { return 1; } /* mode is set but doesn't include "debug" */ @@ -104,7 +106,6 @@ void profiler_xdebug_break(void) zval_ptr_dtor(&fname); #else - zval *retval_ptr = NULL; zval *fname; zval retval; TSRMLS_FETCH();