PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.0.7
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.0.7
1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
xspeed / includes / class-cache-benchmark.php

class-cache-benchmark.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.0.7, at includes/class-cache-benchmark.php

152 lines 4.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Cache_Benchmark — fetches home_url() twice (with + without cache) and
4 * returns side-by-side TTFB / total-time / transfer-bytes timings.
5 *
6 * Powers the "before vs after cache" widget on the wizard's Done step
7 * and the Cache panel. Synthetic — measures local HTTP only, no real
8 * RUM. Good enough for a directional "cache helps by X%" number; the
9 * RUM module is what you want for real percentiles.
10 *
11 * Mechanics:
12 * - "Without cache": HTTP GET home_url() with header
13 * `X-XSpeed-Bypass: 1`. The advanced-cache drop-in honors this
14 * header and short-circuits, so WordPress fully renders.
15 * - "With cache": HTTP GET home_url() with no special header. The
16 * drop-in serves the cached file (cache HIT) when available.
17 * - First "with-cache" call may MISS if no entry yet — we run a
18 * warm-up hit first so the timed pair is HIT vs render.
19 *
20 * Each measurement records:
21 * ttfb_ms — curl_getinfo CURLINFO_STARTTRANSFER_TIME (or our own
22 * "time before body read" fallback).
23 * time_ms — total response time.
24 * bytes — Content-Length or strlen(body).
25 * status — HTTP code.
26 *
27 * @package XSpeed
28 */
29
30 declare(strict_types=1);
31
32 namespace XSpeed;
33
34 defined( 'ABSPATH' ) || exit;
35
36 final class Cache_Benchmark {
37
38 /**
39 * @return array{
40 * url:string,
41 * without_cache:array{ttfb_ms:float,time_ms:float,bytes:int,status:int},
42 * with_cache:array{ttfb_ms:float,time_ms:float,bytes:int,status:int,was_hit:bool},
43 * savings_pct:?float,
44 * savings_ms:?float,
45 * cache_enabled:bool,
46 * }
47 */
48 public static function run( ?string $url = null ): array {
49 if ( null === $url ) {
50 $url = home_url( '/' );
51 }
52 // Cache enablement lives on the legacy Settings option
53 // (`cache_enabled` boolean). Cache::is_enabled() doesn't
54 // exist — read through Settings::get() instead.
55 $cache_enabled = false;
56 if ( class_exists( '\\XSpeed\\Settings' ) ) {
57 $opts = Settings::get();
58 $cache_enabled = ! empty( $opts['cache_enabled'] );
59 }
60
61 // Warm up so the "with cache" timing reflects a HIT, not the
62 // initial generation cost.
63 if ( $cache_enabled ) {
64 self::measure( $url, false );
65 }
66
67 $without = self::measure( $url, true ); // bypass
68 $with = self::measure( $url, false ); // normal — should HIT
69
70 $savings_ms = null;
71 $savings_pct = null;
72 if ( $cache_enabled && $without['time_ms'] > 0 && $with['time_ms'] > 0 ) {
73 $diff = $without['time_ms'] - $with['time_ms'];
74 $savings_ms = max( 0.0, round( $diff, 1 ) );
75 $savings_pct = round( ( $diff / $without['time_ms'] ) * 100, 1 );
76 if ( $savings_pct < 0 ) {
77 $savings_pct = 0.0;
78 }
79 }
80
81 return array(
82 'url' => $url,
83 'without_cache' => $without,
84 'with_cache' => $with + array( 'was_hit' => $cache_enabled ),
85 'savings_pct' => $savings_pct,
86 'savings_ms' => $savings_ms,
87 'cache_enabled' => $cache_enabled,
88 );
89 }
90
91 /**
92 * Single timed request. wp_remote_get's `args` don't expose curl-
93 * level timing on every transport, so we wrap the call ourselves
94 * with microtime — close enough for a directional comparison.
95 *
96 * @return array{ttfb_ms:float,time_ms:float,bytes:int,status:int}
97 */
98 private static function measure( string $url, bool $bypass ): array {
99 $headers = array(
100 'User-Agent' => 'xSpeed Benchmark/1.0',
101 );
102 if ( $bypass ) {
103 // The X-XSpeed-Bypass header only short-circuits the PHP
104 // drop-in. When nginx static-rewrite is firing it would
105 // still serve the cached file directly, so the "without
106 // cache" measurement would look identical to "with cache"
107 // (the bug visible on the dashboard's benchmark widget).
108 // Append a cache-buster query string so the nginx
109 // snippet's `if ($args)` check bails too, forcing the
110 // request all the way through to PHP.
111 $headers['X-XSpeed-Bypass'] = '1';
112 $bust_url = $url . ( false === strpos( $url, '?' ) ? '?' : '&' )
113 . 'xspeed_bypass=' . wp_generate_password( 12, false, false );
114 $url = $bust_url;
115 }
116 $start = microtime( true );
117 $res = wp_remote_get(
118 $url,
119 array(
120 'timeout' => 10,
121 'headers' => $headers,
122 // Disable WP's internal caching layer — every call MUST hit the network.
123 'reject_unsafe_urls' => false,
124 'sslverify' => false, // self-signed sandboxes
125 )
126 );
127 $elapsed = ( microtime( true ) - $start ) * 1000.0;
128
129 if ( is_wp_error( $res ) ) {
130 return array(
131 'ttfb_ms' => 0.0,
132 'time_ms' => round( $elapsed, 1 ),
133 'bytes' => 0,
134 'status' => 0,
135 );
136 }
137 $status = (int) wp_remote_retrieve_response_code( $res );
138 $body = wp_remote_retrieve_body( $res );
139 $bytes = is_string( $body ) ? strlen( $body ) : 0;
140
141 // wp_remote_get doesn't surface TTFB separately on the default
142 // transport. We surface total time as both fields and let the
143 // widget pick a sensible display.
144 return array(
145 'ttfb_ms' => round( $elapsed, 1 ),
146 'time_ms' => round( $elapsed, 1 ),
147 'bytes' => $bytes,
148 'status' => $status,
149 );
150 }
151 }
152