//detailed//.jpg does not exist until * the first visitor requests it. The request falls through to PHP * (fn_generate_thumbnail -> fn_resize_image), which decodes the source, * resizes and writes the file. On a catalog of thousands of SKUs this means: * - first page views after "Clear cache" or theme size change are slow * (every product card waits for GD/ImageMagick), * - CPU spikes right when a promo email lands traffic on a cold cache, * - Google crawls cold thumbnails and records bad TTFB. * * This tool pre-generates every thumbnail size ahead of time (cron friendly), * so visitors always hit static files served by nginx. * * Demo mode scans images/detailed/ on disk. In a live CS-Cart install the * image list comes from the DB instead: * db_get_array("SELECT pair_id, image_id, object_type FROM ?:images_links"); * and sizes come from the active theme settings (fn_get_theme_settings). * * Usage: * php thumb_warmup.php --warm pre-generate all missing thumbnails * php thumb_warmup.php --bench cold vs warm benchmark (full report) * php thumb_warmup.php --purge drop thumbnail cache (simulate "Clear cache") */ error_reporting(E_ALL); $ROOT = __DIR__; $SRC_DIR = $ROOT . '/images/detailed'; $THUMB_ROOT = $ROOT . '/images/thumbnails'; /* Thumbnail sizes used by a typical responsive theme (category grid, * product page, cart mini). In CS-Cart these come from theme manifest. */ $SIZES = [ [200, 200], // category / grid listing [500, 500], // product page main image [80, 80], // mini cart / admin lists ]; $JPEG_QUALITY = 85; function list_sources($src_dir) { $out = []; foreach (glob($src_dir . '/*/*.{jpg,jpeg,png}', GLOB_BRACE) as $f) { $out[] = $f; } sort($out); return $out; } function thumb_path($thumb_root, $src_dir, $src, $w, $h) { /* Mirrors CS-Cart layout: images/thumbnails///detailed// */ $rel = substr($src, strlen(dirname($src_dir)) + 1); // detailed// return sprintf('%s/%d/%d/%s', $thumb_root, $w, $h, $rel); } function generate_thumbnail($src, $dest, $w, $h, $quality) { /* Same core steps as fn_resize_image: decode, fit inside WxH keeping * aspect ratio, resample, save. */ $info = getimagesize($src); if ($info === false) return false; [$sw, $sh] = $info; $img = ($info[2] === IMAGETYPE_PNG) ? imagecreatefrompng($src) : imagecreatefromjpeg($src); if (!$img) return false; $scale = min($w / $sw, $h / $sh, 1); $nw = max(1, (int) round($sw * $scale)); $nh = max(1, (int) round($sh * $scale)); $thumb = imagecreatetruecolor($nw, $nh); imagecopyresampled($thumb, $img, 0, 0, 0, 0, $nw, $nh, $sw, $sh); $dir = dirname($dest); if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) return false; $ok = imagejpeg($thumb, $dest, $quality); imagedestroy($img); imagedestroy($thumb); return $ok; } function warm($sources, $SIZES, $THUMB_ROOT, $SRC_DIR, $JPEG_QUALITY, &$stats) { $t0 = microtime(true); $made = 0; $skipped = 0; foreach ($sources as $src) { foreach ($SIZES as [$w, $h]) { $dest = thumb_path($THUMB_ROOT, $SRC_DIR, $src, $w, $h); if (file_exists($dest)) { $skipped++; continue; } if (generate_thumbnail($src, $dest, $w, $h, $JPEG_QUALITY)) $made++; } } $stats = ['generated' => $made, 'already_cached' => $skipped, 'seconds' => microtime(true) - $t0]; return $stats; } function serve_warm($sources, $SIZES, $THUMB_ROOT, $SRC_DIR) { /* Warm path: what nginx does per request, approximated as stat+read. */ $t0 = microtime(true); $bytes = 0; $n = 0; foreach ($sources as $src) { foreach ($SIZES as [$w, $h]) { $dest = thumb_path($THUMB_ROOT, $SRC_DIR, $src, $w, $h); if (file_exists($dest)) { $bytes += strlen(file_get_contents($dest)); $n++; } } } return ['served' => $n, 'mb' => round($bytes / 1048576, 1), 'seconds' => microtime(true) - $t0]; } function purge($THUMB_ROOT) { $n = 0; if (is_dir($THUMB_ROOT)) { $it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($THUMB_ROOT, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST ); foreach ($it as $f) { if ($f->isFile()) { unlink($f->getPathname()); $n++; } elseif ($f->isDir()) { rmdir($f->getPathname()); } } } return $n; } $mode = $argv[1] ?? '--bench'; $sources = list_sources($SRC_DIR); if (!$sources) { fwrite(STDERR, "no source images in $SRC_DIR\n"); exit(1); } printf("CS-Cart thumbnail warmer (test stand) | %d source images x %d sizes = %d thumbnails\n", count($sources), count($SIZES), count($sources) * count($SIZES)); if ($mode === '--purge') { printf("purged %d cached thumbnails (simulated admin 'Clear cache')\n", purge($THUMB_ROOT)); exit(0); } if ($mode === '--warm') { warm($sources, $SIZES, $THUMB_ROOT, $SRC_DIR, $JPEG_QUALITY, $stats); printf("generated: %d, already cached: %d, took %.2fs\n", $stats['generated'], $stats['already_cached'], $stats['seconds']); exit(0); } /* --bench: full cold vs warm story */ echo str_repeat('=', 64) . "\n"; echo "STEP 1. Purge thumbnail cache (what admin 'Clear cache' does)\n"; printf(" removed %d files\n", purge($THUMB_ROOT)); echo "STEP 2. COLD: generate every thumbnail (lazy path visitors hit)\n"; warm($sources, $SIZES, $THUMB_ROOT, $SRC_DIR, $JPEG_QUALITY, $cold); $total = $cold['generated']; printf(" %d thumbnails in %.2fs => %.1f ms per thumbnail\n", $total, $cold['seconds'], 1000 * $cold['seconds'] / max(1, $total)); $per_page = 24; // product cards on one category page $page_cost = ($cold['seconds'] / max(1, $total)) * $per_page; printf(" => first view of a %d-card category page pays ~%.2fs of image work alone\n", $per_page, $page_cost); echo "STEP 3. WARM: same catalog again (files exist, nginx static path)\n"; $w = serve_warm($sources, $SIZES, $THUMB_ROOT, $SRC_DIR); printf(" %d thumbnails (%.1f MB) read in %.3fs => %.2f ms per thumbnail\n", $w['served'], $w['mb'], $w['seconds'], 1000 * $w['seconds'] / max(1, $w['served'])); $speedup = ($cold['seconds'] / max(1, $total)) / ($w['seconds'] / max(1, $w['served'])); printf("RESULT: cold lazy generation is ~%.0fx slower than serving a warmed cache.\n", $speedup); printf(" Pre-warm after every cache clear / size change, and visitors never pay it.\n");