PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.1
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.1
1.4.1 1.4.0 1.3.7 1.3.6 1.3.5 1.3.4 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 All 35 releases
← All changes | includes/class-cache-gc.php +52 -3 1.3.2 → 1.4.1 View file →
@@ -8,11 +8,13 @@
8 8 * deletes anything. Expiry still works (both serve paths age-check before
9 9 * using a file, so nobody is served a stale page), but the expired bodies sit
10 10 * on disk forever and the admin's Cache Size figure only ever climbs.
11 11 *
12 - * Minified assets are worse: the key is md5(source path | source mtime)
13 - * (Minifier::rewrite_asset), so every plugin or theme update mints a new
14 - * min/ file and orphans the old one permanently.
12 + * Minified assets are worse: they are named by content (Minifier::
13 + * rewrite_asset), so every plugin or theme update mints a new min/ file and
14 + * orphans the old one permanently. The per-source manifests that map a
15 + * source to its current name (min/manifests/<blog>/*.json) orphan the same
16 + * way when a source goes away or its output is collected.
15 17 *
16 18 * This adds the missing time-driven collector — a daily `xspeed_gc` cron that
17 19 * sweeps in three phases:
18 20 *
@@ -18,8 +20,9 @@
18 20 *
19 21 * flat wp-content/cache/xspeed/<md5>.html per-entry TTL
20 22 * static wp-content/cache/xspeed-static/**\/index.html global TTL
21 23 * min wp-content/cache/xspeed/min/**\/*.css|js long max-age
24 + * plus min/manifests/**\/*.json long max-age, orphans only
22 25 *
23 26 * Deliberately NOT swept: `rest/*.json`. A REST entry's TTL is resolved per
24 27 * request through the `xspeed_rest_cache_ttl` filter and is never written to
25 28 * disk (Rest_Cache::ttl_for), so nothing on disk tells GC when one expired.
@@ -233,8 +236,19 @@
233 236 if ( ! self::is_stale( $path, $now, $max_age ) ) {
234 237 continue;
235 238 }
236 239
240 + // A manifest is never linked from a page, so the reference index
241 + // has nothing to say about it. It goes only when it can no longer
242 + // be used: its source is gone, or the output it names is.
243 + if ( 'min' === $phase && '.json' === substr( $path, -5 ) ) {
244 + if ( self::manifest_is_orphan( $path ) ) {
245 + wp_delete_file( $path );
246 + ++$removed;
247 + }
248 + continue;
249 + }
250 +
237 251 // An asset a live cached page still links to is NOT collectable,
238 252 // however old it is. Age is a hint about orphanhood; this is the
239 253 // fact. Without it GC deleted files every cached page pointed at
240 254 // and left the pages in place, so the site served 200s full of
@@ -309,9 +323,44 @@
309 323 return true;
310 324 case 'static':
311 325 return 'index.html' === $name;
312 326 case 'min':
327 + if ( '.json' === substr( $name, -5 ) ) {
328 + $manifests = self::phase_root( 'min' ) . '/' . Asset_Manifest::SUBDIR . '/';
329 + return 0 === strpos( $path, $manifests );
330 + }
313 331 return '.css' === substr( $name, -4 ) || '.js' === substr( $name, -3 );
332 + }
333 + return false;
334 + }
335 +
336 + /**
337 + * Is this manifest useless now?
338 + *
339 + * True when its source no longer exists, or when it names a minified
340 + * output that is no longer on disk. The second is safe to drop even while
341 + * the source lives: the next render rebuilds both. A combine-part
342 + * manifest names no output, so only its source decides. An unreadable
343 + * manifest is useless by definition.
344 + *
345 + * @param string $path Absolute manifest path.
346 + */
347 + private static function manifest_is_orphan( string $path ): bool {
348 + $manifest = Asset_Manifest::read( $path );
349 + if ( null === $manifest || empty( $manifest['src'] ) || ! is_string( $manifest['src'] ) ) {
350 + return true;
351 + }
352 + if ( ! file_exists( $manifest['src'] ) ) {
353 + return true;
354 + }
355 + $root = self::phase_root( 'min' );
356 + $key = isset( $manifest['key'] ) && is_string( $manifest['key'] ) ? $manifest['key'] : '';
357 + if ( '' === $key || null === $root ) {
358 + return true;
359 + }
360 + $kind = isset( $manifest['kind'] ) && is_string( $manifest['kind'] ) ? $manifest['kind'] : '';
361 + if ( 'css' === $kind || 'js' === $kind ) {
362 + return ! file_exists( $root . '/' . $key . '.' . $kind );
314 363 }
315 364 return false;
316 365 }
317 366