PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.0.1
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.0.1
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-hit-counter.php

class-hit-counter.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.0.1, at includes/class-hit-counter.php

153 lines 4.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Hit_Counter — rolling 24h hits + misses for cache requests.
4 *
5 * Storage: one transient `xspeed_hit_buffer` containing a list of up to
6 * 24 hourly buckets. Each bucket: [hour_start_ts, hits, misses]. Bucket
7 * keyed by floor(time()/3600); old buckets drop off when we push a new
8 * hour. Transient TTL set to 25 hours so an idle site doesn't lose its
9 * history immediately after going quiet.
10 *
11 * Writes happen on every cached HIT and every MISS (Cache.php records
12 * via the static record_* methods). We absorb the cost in an in-process
13 * static accumulator that flushes to the transient once per request via
14 * register_shutdown_function, so the served-from-disk hot path pays
15 * nothing.
16 *
17 * @package XSpeed
18 */
19
20 declare(strict_types=1);
21
22 namespace XSpeed;
23
24 defined( 'ABSPATH' ) || exit;
25
26 final class Hit_Counter {
27
28 public const TRANSIENT_KEY = 'xspeed_hit_buffer';
29 public const TTL = 90000; // 25h
30 public const MAX_BUCKETS = 24;
31
32 /**
33 * @var array<int,int> Pending increments keyed by metric ('hit'|'miss').
34 * Flushed to the transient on shutdown.
35 */
36 private static $pending = array( 'hit' => 0, 'miss' => 0 );
37
38 /**
39 * @var bool Whether the shutdown flush is already registered.
40 */
41 private static $shutdown_registered = false;
42
43 public static function record_hit(): void {
44 ++self::$pending['hit'];
45 self::ensure_shutdown_flush();
46 }
47
48 public static function record_miss(): void {
49 ++self::$pending['miss'];
50 self::ensure_shutdown_flush();
51 }
52
53 /**
54 * Returns up to MAX_BUCKETS most-recent hourly buckets oldest →
55 * newest. Each bucket: [ts => unix hour-start, hits => int, misses
56 * => int ].
57 *
58 * @return array<int,array{ts:int,hits:int,misses:int}>
59 */
60 public static function buckets(): array {
61 $buf = get_transient( self::TRANSIENT_KEY );
62 if ( ! is_array( $buf ) ) {
63 return array();
64 }
65 // Defensive — strip anything not shaped right.
66 $out = array();
67 foreach ( $buf as $b ) {
68 if ( is_array( $b ) && isset( $b['ts'], $b['hits'], $b['misses'] ) ) {
69 $out[] = array(
70 'ts' => (int) $b['ts'],
71 'hits' => (int) $b['hits'],
72 'misses' => (int) $b['misses'],
73 );
74 }
75 }
76 return $out;
77 }
78
79 /**
80 * Totals over the last 24h (sum across all buckets).
81 *
82 * @return array{hits:int,misses:int,ratio:float}
83 */
84 public static function totals_24h(): array {
85 $buckets = self::buckets();
86 $hits = 0;
87 $misses = 0;
88 foreach ( $buckets as $b ) {
89 $hits += $b['hits'];
90 $misses += $b['misses'];
91 }
92 $total = $hits + $misses;
93 return array(
94 'hits' => $hits,
95 'misses' => $misses,
96 'ratio' => $total > 0 ? round( $hits / $total, 4 ) : 0.0,
97 );
98 }
99
100 public static function reset(): void {
101 delete_transient( self::TRANSIENT_KEY );
102 self::$pending = array( 'hit' => 0, 'miss' => 0 );
103 }
104
105 /**
106 * One-shot register on first record_* call this request.
107 */
108 private static function ensure_shutdown_flush(): void {
109 if ( self::$shutdown_registered ) {
110 return;
111 }
112 self::$shutdown_registered = true;
113 register_shutdown_function( array( __CLASS__, 'flush_pending' ) );
114 }
115
116 /**
117 * Flush in-process counters into the transient. Bucketed by current
118 * hour. New hour → append a bucket and drop the oldest if we exceed
119 * MAX_BUCKETS.
120 */
121 public static function flush_pending(): void {
122 $pending = self::$pending;
123 if ( 0 === $pending['hit'] && 0 === $pending['miss'] ) {
124 return;
125 }
126 self::$pending = array( 'hit' => 0, 'miss' => 0 );
127
128 $hour = (int) ( time() - ( time() % 3600 ) );
129 $buf = self::buckets();
130 $last = end( $buf );
131 $updated = false;
132
133 if ( $last && $last['ts'] === $hour ) {
134 $buf[ count( $buf ) - 1 ]['hits'] += $pending['hit'];
135 $buf[ count( $buf ) - 1 ]['misses'] += $pending['miss'];
136 $updated = true;
137 }
138
139 if ( ! $updated ) {
140 $buf[] = array(
141 'ts' => $hour,
142 'hits' => $pending['hit'],
143 'misses' => $pending['miss'],
144 );
145 while ( count( $buf ) > self::MAX_BUCKETS ) {
146 array_shift( $buf );
147 }
148 }
149
150 set_transient( self::TRANSIENT_KEY, $buf, self::TTL );
151 }
152 }
153