| @@ -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 | |