- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
413 lines
17 KiB
PHP
413 lines
17 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Cache\Cache;
|
|
use App\Framework\Cache\CacheItem;
|
|
use App\Framework\Cache\CacheKey;
|
|
use App\Framework\Cache\CacheResult;
|
|
use App\Framework\View\Caching\Analysis\CacheStrategy;
|
|
use App\Framework\View\Caching\Analysis\SmartTemplateAnalyzer;
|
|
use App\Framework\View\Caching\CacheManager;
|
|
use App\Framework\View\Caching\FragmentCache;
|
|
use App\Framework\View\Caching\TemplateContext;
|
|
use App\Framework\View\Loading\TemplateLoader;
|
|
use App\Framework\Core\PathProvider;
|
|
|
|
describe('Caching Performance Benchmarks', function () {
|
|
beforeEach(function () {
|
|
// Create test templates directory
|
|
$this->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 = '<html><head><title>Static Page</title></head><body>' . str_repeat('<p>Static content paragraph.</p>', 100) . '</body></html>';
|
|
$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 = '<button class="btn btn-primary">' . str_repeat('Click me ', 10) . '</button>';
|
|
$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' => '<html><body>Simple static page</body></html>',
|
|
'pages/dynamic.view.php' => '<html><body>Hello {{ user.name }}, time: {{ now }}</body></html>',
|
|
'components/card.view.php' => '<div class="card"><h3>{{ title }}</h3><p>{{ content }}</p></div>',
|
|
'partials/nav.view.php' => '<nav><ul>' . str_repeat('<li>{{ item }}</li>', 5) . '</ul></nav>',
|
|
];
|
|
|
|
foreach ($templates as $path => $content) {
|
|
$filePath = $this->testDir . '/' . $path;
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
|
file_put_contents($filePath, $content);
|
|
}
|
|
|
|
// Measure analysis performance
|
|
$measurements = [];
|
|
foreach ($templates as $path => $content) {
|
|
$templateName = str_replace('.view.php', '', $path);
|
|
|
|
$start = microtime(true);
|
|
$analysis = $this->analyzer->analyze($templateName);
|
|
$measurements[] = (microtime(true) - $start) * 1000;
|
|
}
|
|
|
|
// Repeat for statistical significance (25 iterations per template)
|
|
for ($i = 0; $i < 24; $i++) {
|
|
foreach ($templates as $path => $content) {
|
|
$templateName = str_replace('.view.php', '', $path);
|
|
|
|
$start = microtime(true);
|
|
$this->analyzer->analyze($templateName);
|
|
$measurements[] = (microtime(true) - $start) * 1000;
|
|
}
|
|
}
|
|
|
|
$stats = calculateStats($measurements);
|
|
$this->perfResults['Template Analysis'] = $stats;
|
|
|
|
// Analysis should be fast (< 5ms average)
|
|
expect($stats['avg'])->toBeLessThan(5.0);
|
|
expect($stats['p99'])->toBeLessThan(10.0);
|
|
});
|
|
});
|
|
|
|
describe('Memory Usage Benchmarks', function () {
|
|
it('benchmarks memory usage across caching strategies', function () {
|
|
$memoryBefore = memory_get_usage(true);
|
|
|
|
// Create large templates
|
|
$largeContent = str_repeat('<div class="item">Static content item</div>', 1000);
|
|
|
|
// FULL_PAGE template
|
|
$filePath = $this->testDir . '/pages/large.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
|
file_put_contents($filePath, $largeContent);
|
|
|
|
$context = new TemplateContext(template: 'pages/large', data: []);
|
|
|
|
// Mock cache responses
|
|
$this->cache->shouldReceive('get')->andReturn(CacheResult::fromItems(CacheItem::miss(CacheKey::fromString('test'))));
|
|
$this->cache->shouldReceive('set')->andReturn(true);
|
|
|
|
// Render 100 times
|
|
for ($i = 0; $i < 100; $i++) {
|
|
$this->cacheManager->render($context, fn() => $largeContent);
|
|
}
|
|
|
|
$memoryAfter = memory_get_usage(true);
|
|
$memoryUsed = ($memoryAfter - $memoryBefore) / 1024 / 1024; // MB
|
|
|
|
$this->perfResults['Memory Usage (100 renders)']['memory'] = $memoryUsed;
|
|
|
|
// Memory usage should be reasonable (< 50MB for 100 renders with test overhead)
|
|
expect($memoryUsed)->toBeLessThan(50.0);
|
|
});
|
|
});
|
|
|
|
describe('Throughput Benchmarks', function () {
|
|
it('benchmarks rendering throughput with caching', function () {
|
|
// Create template
|
|
$content = '<html><body>' . str_repeat('<p>Paragraph content</p>', 50) . '</body></html>';
|
|
$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 = '<html><body>' . str_repeat('<p>Paragraph content</p>', 50) . '</body></html>';
|
|
$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 = '<html><body>Concurrent test content</body></html>';
|
|
$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)
|
|
];
|
|
}
|