PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.0.2
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.0.2
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.2, at includes/class-cache-benchmark.php

141 lines 4.3 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 $headers['X-XSpeed-Bypass'] = '1';
104 }
105 $start = microtime( true );
106 $res = wp_remote_get(
107 $url,
108 array(
109 'timeout' => 10,
110 'headers' => $headers,
111 // Disable WP's internal caching layer — every call MUST hit the network.
112 'reject_unsafe_urls' => false,
113 'sslverify' => false, // self-signed sandboxes
114 )
115 );
116 $elapsed = ( microtime( true ) - $start ) * 1000.0;
117
118 if ( is_wp_error( $res ) ) {
119 return array(
120 'ttfb_ms' => 0.0,
121 'time_ms' => round( $elapsed, 1 ),
122 'bytes' => 0,
123 'status' => 0,
124 );
125 }
126 $status = (int) wp_remote_retrieve_response_code( $res );
127 $body = wp_remote_retrieve_body( $res );
128 $bytes = is_string( $body ) ? strlen( $body ) : 0;
129
130 // wp_remote_get doesn't surface TTFB separately on the default
131 // transport. We surface total time as both fields and let the
132 // widget pick a sensible display.
133 return array(
134 'ttfb_ms' => round( $elapsed, 1 ),
135 'time_ms' => round( $elapsed, 1 ),
136 'bytes' => $bytes,
137 'status' => $status,
138 );
139 }
140 }
141