(int) round( ( microtime( true ) - $started ) * 1000 ), 'method' => strtoupper( (string) ( $args['method'] ?? 'GET' ) ), 'url' => strtok( (string) $url, '?' ), 'status' => is_wp_error( $response ) ? 'error' : (int) wp_remote_retrieve_response_code( $response ), ); } /** * Store the sample. */ public static function record(): void { global $wpdb, $timestart; if ( ! self::$active ) { return; } self::$active = false; $user_id = get_current_user_id(); if ( 'visit' === self::$source && ! $user_id ) { return; } $total_ms = isset( $timestart ) ? (int) round( ( microtime( true ) - $timestart ) * 1000 ) : null; [ $queries, $sql_ms, $slow, $duplicates ] = self::query_stats(); $row = array( 'request_type' => self::request_type(), 'path' => self::request_path(), 'template' => self::$template, 'user_id' => $user_id, 'status' => (int) http_response_code(), 'total_ms' => $total_ms, 'queries' => $queries, 'sql_ms' => $sql_ms, 'http_count' => count( self::$http_calls ), 'http_ms' => (int) array_sum( array_column( self::$http_calls, 'ms' ) ), 'memory_kb' => (int) round( memory_get_peak_usage() / 1024 ), 'html_bytes' => self::$html_bytes ? self::$html_bytes : null, ); if ( 'run' === self::$source || $total_ms >= (int) Settings::get( 'detail_threshold_ms' ) ) { usort( self::$http_calls, static fn( $a, $b ) => $b['ms'] <=> $a['ms'] ); $row['detail'] = wp_json_encode( array( 'queries' => $slow, 'duplicates' => $duplicates, 'http' => array_slice( self::$http_calls, 0, self::DETAIL_LIMIT ), ) ); } if ( 'run' === self::$source ) { // The runner created this row before requesting the page. $wpdb->update( Schema::samples_table(), $row, array( 'req_id' => self::$req_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- writes a sample to the plugin's own table. return; } $row['created_at'] = current_time( 'mysql', true ); $row['source'] = 'visit'; $wpdb->insert( Schema::samples_table(), $row ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- writes a sample to the plugin's own table. } /** * Query count, total SQL time, slowest queries and repeated queries. * * @return array{0:int, 1:int|null, 2:array, 3:array} */ private static function query_stats(): array { global $wpdb; if ( ! self::$query_timing || ! is_array( $wpdb->queries ) ) { return array( (int) $wpdb->num_queries, null, array(), array() ); } $queries = $wpdb->queries; $total = 0.0; $counts = array(); foreach ( $queries as $query ) { $total += (float) $query[1]; $key = self::normalize_sql( (string) $query[0] ); $counts[ $key ] = ( $counts[ $key ] ?? 0 ) + 1; } usort( $queries, static fn( $a, $b ) => $b[1] <=> $a[1] ); $slow = array(); foreach ( array_slice( $queries, 0, self::DETAIL_LIMIT ) as $query ) { $slow[] = array( 'ms' => round( (float) $query[1] * 1000, 1 ), 'sql' => substr( (string) preg_replace( '/\s+/', ' ', (string) $query[0] ), 0, 500 ), 'caller' => self::caller( (string) ( $query[2] ?? '' ) ), ); } arsort( $counts ); $duplicates = array(); foreach ( array_slice( array_filter( $counts, static fn( $count ) => $count > 1 ), 0, 5, true ) as $sql => $count ) { $duplicates[] = array( 'count' => $count, 'sql' => substr( $sql, 0, 300 ), ); } return array( count( $wpdb->queries ), (int) round( $total * 1000 ), $slow, $duplicates ); } /** * The theme or plugin file that ran a query, from the backtrace summary. * * @param string $backtrace wpdb's caller summary. */ private static function caller( string $backtrace ): string { $frames = array_reverse( array_map( 'trim', explode( ',', $backtrace ) ) ); foreach ( $frames as $frame ) { if ( preg_match( "#(?:include|require)(?:_once)?\\('([^']+)'\\)#", $frame, $match ) && preg_match( '#/(themes|plugins)/#', $match[1] ) ) { return self::relative_path( $match[1] ); } } return $frames[0] ?? ''; } /** * SQL with literals replaced, so repeats with different values group together. * * @param string $sql Query. */ private static function normalize_sql( string $sql ): string { $sql = (string) preg_replace( "/'(?:[^'\\\\]|\\\\.)*'/", '?', $sql ); $sql = (string) preg_replace( '/\b\d+\b/', '?', $sql ); return trim( (string) preg_replace( '/\s+/', ' ', $sql ) ); } /** * Path relative to wp-content (or the original path when outside it). * * @param string $path File path. */ private static function relative_path( string $path ): string { $path = str_replace( '\\', '/', $path ); if ( preg_match( '#(?:^|/)((?:mu-plugins|plugins|themes)/.+)$#', $path, $match ) ) { return $match[1]; } return basename( $path ); } /** * Page, ajax, rest or admin. */ private static function request_type(): string { if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return 'rest'; } if ( wp_doing_ajax() ) { return 'ajax'; } return is_admin() ? 'admin' : 'page'; } /** * Request path used to group samples: query strings are dropped, AJAX keeps * its action, and numeric path segments become ":id". */ public static function request_path(): string { $uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '/'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- normalised below. $path = (string) wp_parse_url( $uri, PHP_URL_PATH ); $path = '' === $path ? '/' : $path; if ( wp_doing_ajax() && isset( $_REQUEST['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only grouping key. $path .= '?action=' . sanitize_key( wp_unslash( $_REQUEST['action'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } $path = (string) preg_replace( '#/\d+(?=/|$|\?)#', '/:id', $path ); return substr( sanitize_text_field( $path ), 0, 255 ); } /** * Requests never profiled: CLI, cron, static files, Heartbeat, this plugin's * own AJAX, and anything the settings exclude. */ private static function is_excluded_request(): bool { if ( ( defined( 'WP_CLI' ) && WP_CLI ) || wp_doing_cron() || ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) ) { return true; } $uri = isset( $_SERVER['REQUEST_URI'] ) ? (string) wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- only inspected. $path = (string) wp_parse_url( $uri, PHP_URL_PATH ); $extension = strtolower( pathinfo( $path, PATHINFO_EXTENSION ) ); if ( in_array( $extension, self::STATIC_EXTENSIONS, true ) ) { return true; } if ( wp_doing_ajax() ) { $action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only. if ( 'heartbeat' === $action || 0 === strpos( $action, 'psbench_' ) || ! Settings::get( 'track_ajax' ) ) { return true; } } elseif ( is_admin() && ! Settings::get( 'track_admin' ) ) { return true; } foreach ( Settings::excluded_paths() as $prefix ) { if ( 0 === strpos( $path, $prefix ) ) { return true; } } return false; } /** * Only logged-in users are sampled; the real check happens at shutdown. */ private static function has_login_cookie(): bool { foreach ( array_keys( $_COOKIE ) as $name ) { if ( 0 === strpos( (string) $name, 'wordpress_logged_in_' ) ) { return true; } } return false; } /** * One request in sample_rate is profiled. */ private static function sampled(): bool { $rate = max( 1, (int) Settings::get( 'sample_rate' ) ); return 1 === $rate || 1 === random_int( 1, $rate ); } }