testDir = '/home/michael/dev/michaelschiemer/tests/tmp/caching_perf_' . uniqid(); if (!is_dir($this->testDir)) { mkdir($this->testDir, 0755, true); } // Create PathProvider $this->pathProvider = new PathProvider($this->testDir); // Create Cache mock $this->cache = Mockery::mock(Cache::class); // Create TemplateLoader $this->loader = new TemplateLoader( pathProvider: $this->pathProvider, cache: $this->cache, templates: [], templatePath: '', cacheEnabled: true ); // Create Analyzer $this->analyzer = new SmartTemplateAnalyzer($this->loader); // Create FragmentCache mock $this->fragmentCache = Mockery::mock(FragmentCache::class); // Create CacheManager $this->cacheManager = new CacheManager( $this->cache, $this->analyzer, $this->fragmentCache ); // Store results for reporting $this->perfResults = []; }); afterEach(function () { // Cleanup test directory if (is_dir($this->testDir)) { $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($this->testDir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST ); foreach ($files as $file) { $file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath()); } rmdir($this->testDir); } Mockery::close(); // Print performance summary if (!empty($this->perfResults)) { echo "\n\n=== Performance Summary ===\n"; foreach ($this->perfResults as $test => $results) { echo "\n{$test}:\n"; // Check if this is timing stats or other metrics if (isset($results['min'])) { echo " Min: " . number_format($results['min'], 2) . " ms\n"; echo " Max: " . number_format($results['max'], 2) . " ms\n"; echo " Avg: " . number_format($results['avg'], 2) . " ms\n"; echo " Median: " . number_format($results['median'], 2) . " ms\n"; echo " P95: " . number_format($results['p95'], 2) . " ms\n"; echo " P99: " . number_format($results['p99'], 2) . " ms\n"; } if (isset($results['memory'])) { echo " Memory: " . number_format($results['memory'], 2) . " MB\n"; } if (isset($results['throughput_per_sec'])) { echo " Throughput: " . number_format($results['throughput_per_sec'], 0) . " renders/sec\n"; } } echo "\n"; } }); describe('Cache Hit vs Miss Performance', function () { it('benchmarks cache hit performance (FULL_PAGE strategy)', function () { // Create large static template $content = '
Static content paragraph.
', 100) . ''; $filePath = $this->testDir . '/pages/static.view.php'; $dir = dirname($filePath); if (!is_dir($dir)) mkdir($dir, 0755, true); file_put_contents($filePath, $content); $context = new TemplateContext( template: 'pages/static', data: ['title' => 'Test'] ); $renderedContent = $content; $measurements = []; // Warm up $this->cache->shouldReceive('get')->andReturn(CacheResult::fromItems(CacheItem::miss(CacheKey::fromString('test')))); $this->cache->shouldReceive('set')->andReturn(true); $this->cacheManager->render($context, fn() => $renderedContent); // Measure cache miss (10 iterations) $missMeasurements = []; for ($i = 0; $i < 10; $i++) { $this->cache->shouldReceive('get')->andReturn(CacheResult::fromItems(CacheItem::miss(CacheKey::fromString("test_{$i}")))); $this->cache->shouldReceive('set')->andReturn(true); $start = microtime(true); $this->cacheManager->render($context, fn() => $renderedContent); $missMeasurements[] = (microtime(true) - $start) * 1000; } // Measure cache hit (100 iterations) $hitMeasurements = []; $cacheKey = CacheKey::fromString('page:pages/static:' . md5(serialize(['title' => 'Test']))); $cacheItem = CacheItem::hit($cacheKey, $renderedContent); $this->cache->shouldReceive('get')->andReturn(CacheResult::fromItems($cacheItem)); for ($i = 0; $i < 100; $i++) { $start = microtime(true); $this->cacheManager->render($context, fn() => $renderedContent); $hitMeasurements[] = (microtime(true) - $start) * 1000; } // Calculate statistics $missStats = calculateStats($missMeasurements); $hitStats = calculateStats($hitMeasurements); $this->perfResults['FULL_PAGE Cache Miss'] = $missStats; $this->perfResults['FULL_PAGE Cache Hit'] = $hitStats; // Assert performance characteristics (info-gathering, not strict benchmarks with mocks) // Note: Mocked cache doesn't show real performance gains, but tests the workflow expect($hitStats['avg'])->toBeLessThan(10.0); // Should complete in reasonable time expect($missStats['avg'])->toBeLessThan(10.0); // Should complete in reasonable time }); it('benchmarks cache hit performance (COMPONENT strategy)', function () { // Create component template $content = ''; $filePath = $this->testDir . '/components/button.view.php'; $dir = dirname($filePath); if (!is_dir($dir)) mkdir($dir, 0755, true); file_put_contents($filePath, $content); $context = new TemplateContext( template: 'components/button', data: ['text' => 'Submit'] ); $renderedContent = $content; // Measure cache miss (10 iterations) $missMeasurements = []; for ($i = 0; $i < 10; $i++) { $this->cache->shouldReceive('get')->andReturn(CacheResult::fromItems(CacheItem::miss(CacheKey::fromString("test_{$i}")))); $this->cache->shouldReceive('set')->andReturn(true); $start = microtime(true); $this->cacheManager->render($context, fn() => $renderedContent); $missMeasurements[] = (microtime(true) - $start) * 1000; } // Measure cache hit (100 iterations) $hitMeasurements = []; $cacheKey = CacheKey::fromString('component:button:' . md5(serialize(['text' => 'Submit']))); $cacheItem = CacheItem::hit($cacheKey, $renderedContent); $this->cache->shouldReceive('get')->andReturn(CacheResult::fromItems($cacheItem)); for ($i = 0; $i < 100; $i++) { $start = microtime(true); $this->cacheManager->render($context, fn() => $renderedContent); $hitMeasurements[] = (microtime(true) - $start) * 1000; } $missStats = calculateStats($missMeasurements); $hitStats = calculateStats($hitMeasurements); $this->perfResults['COMPONENT Cache Miss'] = $missStats; $this->perfResults['COMPONENT Cache Hit'] = $hitStats; // Info-gathering: verify workflow completes in reasonable time expect($hitStats['avg'])->toBeLessThan(10.0); expect($missStats['avg'])->toBeLessThan(10.0); }); }); describe('SmartTemplateAnalyzer Performance', function () { it('benchmarks template analysis performance', function () { // Create various templates $templates = [ 'pages/simple.view.php' => 'Simple static page', 'pages/dynamic.view.php' => 'Hello {{ user.name }}, time: {{ now }}', 'components/card.view.php' => '{{ content }}
Paragraph content
', 50) . ''; $filePath = $this->testDir . '/pages/throughput.view.php'; $dir = dirname($filePath); if (!is_dir($dir)) mkdir($dir, 0755, true); file_put_contents($filePath, $content); $context = new TemplateContext(template: 'pages/throughput', data: []); // Simulate cache hits $cacheKey = CacheKey::fromString('page:pages/throughput:' . md5(serialize([]))); $cacheItem = CacheItem::hit($cacheKey, $content); $this->cache->shouldReceive('get')->andReturn(CacheResult::fromItems($cacheItem)); // Measure throughput $iterations = 1000; $start = microtime(true); for ($i = 0; $i < $iterations; $i++) { $this->cacheManager->render($context, fn() => $content); } $duration = microtime(true) - $start; $throughput = $iterations / $duration; $this->perfResults['Throughput (cached)']['throughput_per_sec'] = $throughput; echo "\nThroughput: " . number_format($throughput, 0) . " renders/sec\n"; // Info-gathering: verify reasonable throughput (> 500 renders/sec) expect($throughput)->toBeGreaterThan(500); }); it('benchmarks rendering throughput without caching', function () { // Create template $content = '' . str_repeat('Paragraph content
', 50) . ''; $filePath = $this->testDir . '/pages/throughput2.view.php'; $dir = dirname($filePath); if (!is_dir($dir)) mkdir($dir, 0755, true); file_put_contents($filePath, $content); $context = new TemplateContext(template: 'pages/throughput2', data: []); // Simulate cache misses $this->cache->shouldReceive('get')->andReturn(CacheResult::fromItems(CacheItem::miss(CacheKey::fromString('test')))); $this->cache->shouldReceive('set')->andReturn(true); // Measure throughput $iterations = 100; // Fewer iterations for uncached $start = microtime(true); for ($i = 0; $i < $iterations; $i++) { $this->cacheManager->render($context, fn() => $content); } $duration = microtime(true) - $start; $throughput = $iterations / $duration; $this->perfResults['Throughput (uncached)']['throughput_per_sec'] = $throughput; echo "\nThroughput (uncached): " . number_format($throughput, 0) . " renders/sec\n"; // Info-gathering: verify reasonable throughput (> 100 renders/sec) expect($throughput)->toBeGreaterThan(100); }); }); describe('Concurrent Load Simulation', function () { it('benchmarks performance under concurrent load', function () { // Create template $content = 'Concurrent test content'; $filePath = $this->testDir . '/pages/concurrent.view.php'; $dir = dirname($filePath); if (!is_dir($dir)) mkdir($dir, 0755, true); file_put_contents($filePath, $content); $context = new TemplateContext(template: 'pages/concurrent', data: []); // Simulate cache hits $cacheKey = CacheKey::fromString('page:pages/concurrent:' . md5(serialize([]))); $cacheItem = CacheItem::hit($cacheKey, $content); $this->cache->shouldReceive('get')->andReturn(CacheResult::fromItems($cacheItem)); // Simulate 10 concurrent requests with 100 renders each $measurements = []; for ($request = 0; $request < 10; $request++) { $requestMeasurements = []; for ($i = 0; $i < 100; $i++) { $start = microtime(true); $this->cacheManager->render($context, fn() => $content); $requestMeasurements[] = (microtime(true) - $start) * 1000; } $measurements = array_merge($measurements, $requestMeasurements); } $stats = calculateStats($measurements); $this->perfResults['Concurrent Load (10 requests)'] = $stats; // Performance should remain stable under load expect($stats['avg'])->toBeLessThan(5.0); // Average < 5ms (with test overhead) expect($stats['p99'])->toBeLessThan(100.0); // 99% < 100ms }); }); }); // Helper function for statistics calculation function calculateStats(array $measurements): array { sort($measurements); $count = count($measurements); return [ 'count' => $count, 'min' => min($measurements), 'max' => max($measurements), 'avg' => array_sum($measurements) / $count, 'median' => $measurements[(int)($count / 2)], 'p95' => $measurements[(int)($count * 0.95)], 'p99' => $measurements[(int)($count * 0.99)], 'stddev' => sqrt(array_sum(array_map(fn($x) => pow($x - (array_sum($measurements) / $count), 2), $measurements)) / $count) ]; }