Skip to content

Instantly share code, notes, and snippets.

@patrickfreitasdev
Last active November 1, 2026 01:08
Show Gist options
  • Select an option

  • Save patrickfreitasdev/d82716856dbdd06b04cd3928ae95f444 to your computer and use it in GitHub Desktop.

Select an option

Save patrickfreitasdev/d82716856dbdd06b04cd3928ae95f444 to your computer and use it in GitHub Desktop.
<?php
/**
* Plugin Name: OOM Elementor Query Trace
* Description: On a memory fatal, log the SELECT wpdb was fetching and who ran it.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
final class OOM_Query_Trace {
const RESERVE_BYTES = 8388608;
const MAX_QUERIES = 12;
private static $reserve = '';
private static $queries = array();
public static function boot() {
if ( ! self::should_trace_request() ) {
return;
}
self::$reserve = str_repeat( 'x', self::RESERVE_BYTES );
add_filter( 'query', array( __CLASS__, 'capture' ), PHP_INT_MAX );
register_shutdown_function( array( __CLASS__, 'shutdown' ) );
}
public static function capture( $query ) {
$sql = (string) $query;
$item = array(
'bytes' => strlen( $sql ),
'sql' => self::clip( $sql, 800 ),
'mem' => memory_get_usage( true ),
'callers' => array(),
);
self::$queries[] = $item;
if ( count( self::$queries ) > self::MAX_QUERIES ) {
array_shift( self::$queries );
}
$index = count( self::$queries ) - 1;
self::$queries[ $index ]['callers'] = self::callers();
return $query;
}
public static function shutdown() {
self::$reserve = '';
$err = error_get_last();
if ( ! is_array( $err ) || empty( $err['message'] ) ) {
return;
}
if ( false === stripos( $err['message'], 'Allowed memory size' ) ) {
return;
}
global $wpdb;
$lines = array();
$lines[] = '=== ' . gmdate( 'c' ) . ' memory exhausted ===';
$lines[] = 'message: ' . $err['message'];
$lines[] = 'file: ' . ( isset( $err['file'] ) ? $err['file'] : '' ) . ':' . ( isset( $err['line'] ) ? $err['line'] : '' );
$lines[] = 'memory_limit: ' . ini_get( 'memory_limit' );
$lines[] = 'peak: ' . self::bytes( memory_get_peak_usage( true ) );
$lines[] = 'request: ' . ( isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] : '' ) . ' ' . self::clip( isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '', 500 );
$lines[] = 'script: ' . ( isset( $_SERVER['SCRIPT_NAME'] ) ? $_SERVER['SCRIPT_NAME'] : '' );
$lines[] = 'post_action: ' . self::scalar_request( 'action' );
$lines[] = 'editor_post_id: ' . self::scalar_request( 'editor_post_id' );
$lines[] = 'post_id: ' . self::scalar_request( 'post_id' );
$lines[] = self::elementor_actions();
if ( isset( $GLOBALS['wp_current_filter'] ) && is_array( $GLOBALS['wp_current_filter'] ) ) {
$lines[] = 'current_filter: ' . implode( ' > ', $GLOBALS['wp_current_filter'] );
}
if ( isset( $wpdb ) && is_object( $wpdb ) ) {
$last = isset( $wpdb->last_query ) ? (string) $wpdb->last_query : '';
$fetched = ( isset( $wpdb->last_result ) && is_array( $wpdb->last_result ) ) ? count( $wpdb->last_result ) : 0;
$lines[] = 'last_query_bytes: ' . strlen( $last );
$lines[] = 'rows_fetched_before_crash: ' . $fetched;
$lines[] = 'last_query: ' . self::clip( $last, 1500 );
if ( isset( $wpdb->result ) && $wpdb->result instanceof mysqli_result ) {
$lines[] = 'mysqli_num_rows: ' . mysqli_num_rows( $wpdb->result );
$fields = mysqli_fetch_fields( $wpdb->result );
if ( is_array( $fields ) ) {
$parts = array();
foreach ( $fields as $field ) {
$parts[] = $field->table . '.' . $field->name . ' max_bytes=' . $field->max_length;
}
$lines[] = 'columns: ' . implode( ' | ', $parts );
}
}
if ( $fetched > 0 ) {
$lines[] = 'first_row: ' . self::row_profile( $wpdb->last_result[0] );
if ( $fetched > 1 ) {
$lines[] = 'last_stored_row: ' . self::row_profile( $wpdb->last_result[ $fetched - 1 ] );
}
}
}
$lines[] = 'recent_queries:';
$i = 1;
foreach ( self::$queries as $query ) {
$lines[] = $i . '. bytes=' . $query['bytes'] . ' mem=' . self::bytes( $query['mem'] );
$lines[] = ' sql: ' . $query['sql'];
foreach ( $query['callers'] as $caller ) {
$lines[] = ' at ' . $caller;
}
$i++;
}
$lines[] = '';
$report = implode( "\n", $lines );
$dir = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR : dirname( __DIR__ );
file_put_contents( $dir . '/oom-query-trace.log', $report, FILE_APPEND | LOCK_EX );
error_log( 'OOM trace written to wp-content/oom-query-trace.log; ' . self::clip( str_replace( "\n", ' | ', $report ), 1800 ) );
}
private static function should_trace_request() {
$uri = isset( $_SERVER['REQUEST_URI'] ) ? (string) $_SERVER['REQUEST_URI'] : '';
$action = isset( $_REQUEST['action'] ) ? (string) $_REQUEST['action'] : '';
if ( false !== strpos( $action, 'elementor' ) ) {
return true;
}
foreach ( array( 'wp-admin', 'admin-ajax.php', 'wp-json', 'elementor' ) as $needle ) {
if ( false !== strpos( $uri, $needle ) ) {
return true;
}
}
return false;
}
private static function callers() {
$frames = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 12 );
$skip = array(
'capture',
'callers',
'apply_filters',
'apply_filters_ref_array',
'query',
'_do_query',
'get_results',
'get_row',
'get_var',
'get_col',
);
$out = array();
foreach ( $frames as $frame ) {
$fn = isset( $frame['function'] ) ? $frame['function'] : '';
if ( in_array( $fn, $skip, true ) ) {
continue;
}
$file = isset( $frame['file'] ) ? $frame['file'] : '';
if ( defined( 'ABSPATH' ) && $file && 0 === strpos( $file, ABSPATH ) ) {
$file = substr( $file, strlen( ABSPATH ) );
}
$class = isset( $frame['class'] ) ? $frame['class'] . $frame['type'] : '';
$out[] = $file . ':' . ( isset( $frame['line'] ) ? $frame['line'] : '' ) . ' ' . $class . $fn;
if ( count( $out ) >= 6 ) {
break;
}
}
return $out;
}
private static function elementor_actions() {
if ( empty( $_POST['actions'] ) || ! is_string( $_POST['actions'] ) ) {
return 'elementor_actions: (none)';
}
$raw = $_POST['actions'];
preg_match_all( '/"action"\s*:\s*"([A-Za-z0-9_\\\\]+)"/', substr( $raw, 0, 80000 ), $matches );
$names = array_slice( array_unique( $matches[1] ), 0, 20 );
return 'elementor_actions_bytes: ' . strlen( $raw ) . ' names: ' . implode( ',', $names );
}
private static function row_profile( $row ) {
if ( ! is_object( $row ) ) {
return '';
}
$parts = array();
$seen = 0;
foreach ( $row as $key => $value ) {
if ( $seen >= 30 ) {
$parts[] = '...';
break;
}
$parts[] = $key . '=' . ( is_string( $value ) ? strlen( $value ) . 'b' : gettype( $value ) );
$seen++;
}
return implode( ', ', $parts );
}
private static function scalar_request( $key ) {
if ( ! isset( $_REQUEST[ $key ] ) || is_array( $_REQUEST[ $key ] ) ) {
return '';
}
return self::clip( (string) $_REQUEST[ $key ], 80 );
}
private static function clip( $text, $limit ) {
$text = (string) $text;
if ( strlen( $text ) <= $limit ) {
return $text;
}
return substr( $text, 0, $limit ) . '…[' . strlen( $text ) . ' bytes]';
}
private static function bytes( $bytes ) {
return round( $bytes / 1048576, 1 ) . 'MB';
}
}
OOM_Query_Trace::boot();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment