# xspeed/1.0.1/includes/class-hit-counter.php

xSpeed Cache: AI-Powered Performance Hub with MCP, Caching &amp; CDN, version 1.0.1. 153 lines.

- Page: https://pluginprobe.com/plugins/xspeed/1.0.1/code/includes/class-hit-counter.php
- Raw: https://pluginprobe.com/plugins/xspeed/1.0.1/raw/includes/class-hit-counter.php
- Modified: 2026-06-01T17:33:22+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/xspeed/1.0.1/code/includes/class-hit-counter.php#L10-L20`.

```php
<?php
/**
 * Hit_Counter — rolling 24h hits + misses for cache requests.
 *
 * Storage: one transient `xspeed_hit_buffer` containing a list of up to
 * 24 hourly buckets. Each bucket: [hour_start_ts, hits, misses]. Bucket
 * keyed by floor(time()/3600); old buckets drop off when we push a new
 * hour. Transient TTL set to 25 hours so an idle site doesn't lose its
 * history immediately after going quiet.
 *
 * Writes happen on every cached HIT and every MISS (Cache.php records
 * via the static record_* methods). We absorb the cost in an in-process
 * static accumulator that flushes to the transient once per request via
 * register_shutdown_function, so the served-from-disk hot path pays
 * nothing.
 *
 * @package XSpeed
 */

declare(strict_types=1);

namespace XSpeed;

defined( 'ABSPATH' ) || exit;

final class Hit_Counter {

	public const TRANSIENT_KEY = 'xspeed_hit_buffer';
	public const TTL           = 90000; // 25h
	public const MAX_BUCKETS   = 24;

	/**
	 * @var array<int,int> Pending increments keyed by metric ('hit'|'miss').
	 *                     Flushed to the transient on shutdown.
	 */
	private static $pending = array( 'hit' => 0, 'miss' => 0 );

	/**
	 * @var bool Whether the shutdown flush is already registered.
	 */
	private static $shutdown_registered = false;

	public static function record_hit(): void {
		++self::$pending['hit'];
		self::ensure_shutdown_flush();
	}

	public static function record_miss(): void {
		++self::$pending['miss'];
		self::ensure_shutdown_flush();
	}

	/**
	 * Returns up to MAX_BUCKETS most-recent hourly buckets oldest →
	 * newest. Each bucket: [ts => unix hour-start, hits => int, misses
	 * => int ].
	 *
	 * @return array<int,array{ts:int,hits:int,misses:int}>
	 */
	public static function buckets(): array {
		$buf = get_transient( self::TRANSIENT_KEY );
		if ( ! is_array( $buf ) ) {
			return array();
		}
		// Defensive — strip anything not shaped right.
		$out = array();
		foreach ( $buf as $b ) {
			if ( is_array( $b ) && isset( $b['ts'], $b['hits'], $b['misses'] ) ) {
				$out[] = array(
					'ts'     => (int) $b['ts'],
					'hits'   => (int) $b['hits'],
					'misses' => (int) $b['misses'],
				);
			}
		}
		return $out;
	}

	/**
	 * Totals over the last 24h (sum across all buckets).
	 *
	 * @return array{hits:int,misses:int,ratio:float}
	 */
	public static function totals_24h(): array {
		$buckets = self::buckets();
		$hits    = 0;
		$misses  = 0;
		foreach ( $buckets as $b ) {
			$hits   += $b['hits'];
			$misses += $b['misses'];
		}
		$total = $hits + $misses;
		return array(
			'hits'   => $hits,
			'misses' => $misses,
			'ratio'  => $total > 0 ? round( $hits / $total, 4 ) : 0.0,
		);
	}

	public static function reset(): void {
		delete_transient( self::TRANSIENT_KEY );
		self::$pending = array( 'hit' => 0, 'miss' => 0 );
	}

	/**
	 * One-shot register on first record_* call this request.
	 */
	private static function ensure_shutdown_flush(): void {
		if ( self::$shutdown_registered ) {
			return;
		}
		self::$shutdown_registered = true;
		register_shutdown_function( array( __CLASS__, 'flush_pending' ) );
	}

	/**
	 * Flush in-process counters into the transient. Bucketed by current
	 * hour. New hour → append a bucket and drop the oldest if we exceed
	 * MAX_BUCKETS.
	 */
	public static function flush_pending(): void {
		$pending = self::$pending;
		if ( 0 === $pending['hit'] && 0 === $pending['miss'] ) {
			return;
		}
		self::$pending = array( 'hit' => 0, 'miss' => 0 );

		$hour    = (int) ( time() - ( time() % 3600 ) );
		$buf     = self::buckets();
		$last    = end( $buf );
		$updated = false;

		if ( $last && $last['ts'] === $hour ) {
			$buf[ count( $buf ) - 1 ]['hits']   += $pending['hit'];
			$buf[ count( $buf ) - 1 ]['misses'] += $pending['miss'];
			$updated                              = true;
		}

		if ( ! $updated ) {
			$buf[] = array(
				'ts'     => $hour,
				'hits'   => $pending['hit'],
				'misses' => $pending['miss'],
			);
			while ( count( $buf ) > self::MAX_BUCKETS ) {
				array_shift( $buf );
			}
		}

		set_transient( self::TRANSIENT_KEY, $buf, self::TTL );
	}
}

```
