container = new DefaultContainer(); // Register required dependencies with concrete implementations $this->container->bind( \App\Framework\Cache\CacheDriver::class, \App\Framework\Cache\Driver\InMemoryCache::class ); $this->container->bind( \App\Framework\Serializer\Serializer::class, \App\Framework\Serializer\Json\JsonSerializer::class ); $this->container->bind(Cache::class, GeneralCache::class); $this->container->instance(PathProvider::class, new PathProvider('/var/www/html')); $this->bootstrapper = new DiscoveryServiceBootstrapper( $this->container, new SystemClock() ); // Take memory baseline $this->memoryBaseline = $this->captureMemoryStats(); } public function test_discovery_multiple_runs_memory_growth(): void { $iterations = 15; $memoryGrowthData = []; echo "\n=== Starting $iterations Discovery Runs ===\n"; for ($i = 1; $i <= $iterations; $i++) { echo "\n=== Discovery Run $i/$iterations ===\n"; $memoryBefore = memory_get_usage(true); $peakBefore = memory_get_peak_usage(true); // Run discovery $registry = $this->bootstrapper->bootstrap(); $memoryAfter = memory_get_usage(true); $peakAfter = memory_get_peak_usage(true); // Capture detailed stats $memoryStats = $this->captureMemoryStats(); $memoryStats['memory_before'] = $memoryBefore; $memoryStats['memory_after'] = $memoryAfter; $memoryStats['memory_diff'] = $memoryAfter - $memoryBefore; $memoryStats['peak_before'] = $peakBefore; $memoryStats['peak_after'] = $peakAfter; $memoryStats['peak_diff'] = $peakAfter - $peakBefore; $memoryGrowthData[$i] = $memoryStats; echo sprintf( "Run %d: Memory %s -> %s (diff: %s), Peak %s -> %s (diff: %s)\n", $i, $this->formatBytes($memoryBefore), $this->formatBytes($memoryAfter), $this->formatBytes($memoryStats['memory_diff']), $this->formatBytes($peakBefore), $this->formatBytes($peakAfter), $this->formatBytes($memoryStats['peak_diff']) ); // Test ReflectionProvider stats if available if ($i % 5 === 0) { $this->logReflectionStats($i); } // Force cleanup but keep container unset($registry); gc_collect_cycles(); // Check for immediate memory leak if ($memoryStats['memory_diff'] > 100 * 1024 * 1024) { // 100MB per run $this->fail("Run $i: Excessive memory usage detected: " . $this->formatBytes($memoryStats['memory_diff'])); } } // Comprehensive analysis $this->analyzeMemoryGrowth($memoryGrowthData); $this->analyzeMemoryTrends($memoryGrowthData); } private function logReflectionStats(int $iteration): void { try { $reflectionProvider = $this->container->has(ReflectionProvider::class) ? $this->container->get(ReflectionProvider::class) : new CachedReflectionProvider(); $stats = $reflectionProvider->getStats(); echo " ReflectionCache after run $iteration: " . json_encode($stats->toArray()['caches']['class']) . "\n"; } catch (\Throwable $e) { echo " ReflectionCache stats unavailable: " . $e->getMessage() . "\n"; } } public function test_reflection_cache_size_limits(): void { echo "\n=== Testing ReflectionProvider Cache Limits ===\n"; // Get or create ReflectionProvider $reflectionProvider = $this->container->has(ReflectionProvider::class) ? $this->container->get(ReflectionProvider::class) : new CachedReflectionProvider(); $initialStats = $reflectionProvider->getStats(); echo "Initial reflection stats: " . json_encode($initialStats->toArray()) . "\n"; // Run discovery multiple times without clearing cache for ($i = 1; $i <= 3; $i++) { $this->bootstrapper->bootstrap(); $stats = $reflectionProvider->getStats(); echo "After discovery run $i: " . json_encode($stats->toArray()) . "\n"; // Check if cache is growing unbounded $cacheSize = $stats->toArray()['total_cache_size'] ?? 0; if ($cacheSize > 50 * 1024 * 1024) { // 50MB limit $this->fail("ReflectionProvider cache exceeded 50MB: {$cacheSize} bytes"); } } } public function test_apcu_cache_growth(): void { if (! extension_loaded('apcu') || ! apcu_enabled()) { $this->markTestSkipped('APCu not available'); } echo "\n=== Testing APCu Cache Growth ===\n"; $initialInfo = apcu_cache_info(); echo "Initial APCu info: mem_size={$initialInfo['mem_size']}, num_entries={$initialInfo['num_entries']}\n"; // Run discovery multiple times for ($i = 1; $i <= 3; $i++) { $this->bootstrapper->bootstrap(); $info = apcu_cache_info(); echo "After run $i: mem_size={$info['mem_size']}, num_entries={$info['num_entries']}\n"; // Check for excessive growth if ($info['num_entries'] > $initialInfo['num_entries'] + 1000) { $this->fail("APCu entries grew excessively: {$info['num_entries']} entries"); } } } public function test_cache_system_memory_usage(): void { echo "\n=== Testing Cache System Memory Usage ===\n"; if (! $this->container->has(Cache::class)) { $this->markTestSkipped('Cache not available in container'); } $cache = $this->container->get(Cache::class); // Monitor cache usage during discovery runs for ($i = 1; $i <= 3; $i++) { $memoryBefore = memory_get_usage(true); $this->bootstrapper->bootstrap(); $memoryAfter = memory_get_usage(true); $memoryDiff = $memoryAfter - $memoryBefore; echo "Discovery run $i: Memory diff = " . $this->formatBytes($memoryDiff) . "\n"; // Check for excessive memory usage if ($memoryDiff > 100 * 1024 * 1024) { // 100MB limit per run $this->fail("Single discovery run used excessive memory: " . $this->formatBytes($memoryDiff)); } } } public function test_discovery_cache_invalidation(): void { echo "\n=== Testing Discovery Cache Invalidation ===\n"; // Run discovery first time $registry1 = $this->bootstrapper->bootstrap(); $initialMemory = memory_get_usage(true); // Run discovery second time (should use cache if enabled) $registry2 = $this->bootstrapper->bootstrap(); $secondMemory = memory_get_usage(true); // Run discovery third time with cache clearing if ($this->container->has(Cache::class)) { $cache = $this->container->get(Cache::class); if (method_exists($cache, 'flush')) { $cache->flush(); } } $registry3 = $this->bootstrapper->bootstrap(); $thirdMemory = memory_get_usage(true); echo "First run: " . $this->formatBytes($initialMemory) . "\n"; echo "Second run (cached): " . $this->formatBytes($secondMemory) . "\n"; echo "Third run (cache cleared): " . $this->formatBytes($thirdMemory) . "\n"; // Memory should not grow excessively between runs $maxGrowth = 20 * 1024 * 1024; // 20MB max growth if ($thirdMemory - $initialMemory > $maxGrowth) { $this->fail("Memory grew excessively between runs: " . $this->formatBytes($thirdMemory - $initialMemory)); } } private function captureMemoryStats(): array { return [ 'php_memory_usage' => memory_get_usage(true), 'php_memory_peak' => memory_get_peak_usage(true), 'php_memory_limit' => $this->parseMemoryLimit(ini_get('memory_limit')), 'timestamp' => microtime(true), 'apcu_info' => extension_loaded('apcu') && apcu_enabled() ? apcu_cache_info() : null, ]; } private function formatMemoryStats(array $stats): string { $usage = $this->formatBytes($stats['php_memory_usage']); $peak = $this->formatBytes($stats['php_memory_peak']); $limit = $this->formatBytes($stats['php_memory_limit']); $result = "Usage: $usage, Peak: $peak, Limit: $limit"; if ($stats['apcu_info']) { $entries = $stats['apcu_info']['num_entries']; $size = $this->formatBytes($stats['apcu_info']['mem_size']); $result .= ", APCu: $entries entries, $size"; } return $result; } private function analyzeMemoryGrowth(array $memoryData): void { echo "\n=== Memory Growth Analysis ===\n"; $baseline = $memoryData[1]['php_memory_usage']; $maxAllowedGrowth = 50 * 1024 * 1024; // 50MB max total growth foreach ($memoryData as $iteration => $stats) { $growth = $stats['php_memory_usage'] - $baseline; $percentage = ($growth / $baseline) * 100; echo "Run $iteration: Growth = " . $this->formatBytes($growth) . " (" . round($percentage, 1) . "%)\n"; if ($growth > $maxAllowedGrowth) { $this->fail("Memory growth exceeded limit at iteration $iteration: " . $this->formatBytes($growth)); } } // Check for linear growth (memory leak indicator) if (count($memoryData) >= 3) { $growthRates = []; for ($i = 2; $i <= count($memoryData); $i++) { $growthRates[] = $memoryData[$i]['php_memory_usage'] - $memoryData[$i - 1]['php_memory_usage']; } $avgGrowthRate = array_sum($growthRates) / count($growthRates); echo "Average growth per iteration: " . $this->formatBytes($avgGrowthRate) . "\n"; // If average growth > 10MB per iteration, likely memory leak if ($avgGrowthRate > 10 * 1024 * 1024) { $this->fail("Detected potential memory leak: average growth " . $this->formatBytes($avgGrowthRate) . " per iteration"); } } } private function analyzeMemoryTrends(array $memoryData): void { echo "\n=== Detailed Memory Trend Analysis ===\n"; $memoryDiffs = []; $peakDiffs = []; $cumulativeGrowth = 0; foreach ($memoryData as $iteration => $stats) { $memoryDiffs[] = $stats['memory_diff']; $peakDiffs[] = $stats['peak_diff']; $cumulativeGrowth += $stats['memory_diff']; if ($iteration % 3 === 0) { echo sprintf( "Run %d: Cumulative growth: %s, Avg per run: %s\n", $iteration, $this->formatBytes($cumulativeGrowth), $this->formatBytes($cumulativeGrowth / $iteration) ); } } $avgMemoryDiff = array_sum($memoryDiffs) / count($memoryDiffs); $avgPeakDiff = array_sum($peakDiffs) / count($peakDiffs); $maxMemoryDiff = max($memoryDiffs); $minMemoryDiff = min($memoryDiffs); echo "\n=== Memory Pattern Summary ===\n"; echo "Average memory per run: " . $this->formatBytes($avgMemoryDiff) . "\n"; echo "Average peak per run: " . $this->formatBytes($avgPeakDiff) . "\n"; echo "Max memory per run: " . $this->formatBytes($maxMemoryDiff) . "\n"; echo "Min memory per run: " . $this->formatBytes($minMemoryDiff) . "\n"; echo "Total cumulative growth: " . $this->formatBytes($cumulativeGrowth) . "\n"; // Check for linear growth pattern (memory leak indicator) $growthTrend = $this->calculateGrowthTrend($memoryDiffs); echo "Growth trend: " . ($growthTrend > 0 ? "INCREASING" : "STABLE") . " ($growthTrend per run)\n"; if ($growthTrend > 1024 * 1024) { // 1MB growth trend $this->fail("Detected linear memory growth trend: " . $this->formatBytes($growthTrend) . " per run"); } if ($avgMemoryDiff > 50 * 1024 * 1024) { // 50MB average $this->fail("Average memory usage per run too high: " . $this->formatBytes($avgMemoryDiff)); } } private function calculateGrowthTrend(array $values): float { $n = count($values); if ($n < 3) { return 0; } $sumX = 0; $sumY = 0; $sumXY = 0; $sumX2 = 0; for ($i = 0; $i < $n; $i++) { $x = $i + 1; $y = $values[$i]; $sumX += $x; $sumY += $y; $sumXY += $x * $y; $sumX2 += $x * $x; } // Calculate slope (linear regression) $slope = ($n * $sumXY - $sumX * $sumY) / ($n * $sumX2 - $sumX * $sumX); return $slope; } private function formatBytes(int|float $bytes): string { $units = ['B', 'KB', 'MB', 'GB']; $unitIndex = 0; while ($bytes >= 1024 && $unitIndex < count($units) - 1) { $bytes /= 1024; $unitIndex++; } return round($bytes, 2) . ' ' . $units[$unitIndex]; } private function parseMemoryLimit(string $memoryLimit): int { if ($memoryLimit === '-1') { return PHP_INT_MAX; } $unit = strtolower(substr($memoryLimit, -1)); $value = (int) substr($memoryLimit, 0, -1); return match($unit) { 'g' => $value * 1024 * 1024 * 1024, 'm' => $value * 1024 * 1024, 'k' => $value * 1024, default => (int) $memoryLimit, }; } }