Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
43
tests/Framework/Discovery/DiscoveryCacheTest.php
Normal file
43
tests/Framework/Discovery/DiscoveryCacheTest.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\Discovery\Storage\DiscoveryCacheManager;
|
||||
use App\Framework\Discovery\ValueObjects\DiscoveryContext;
|
||||
use App\Framework\Discovery\ValueObjects\DiscoveryOptions;
|
||||
use App\Framework\Discovery\ValueObjects\ScanType;
|
||||
use App\Framework\Filesystem\FileSystemService;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->mockCache = Mockery::mock(Cache::class);
|
||||
$this->mockClock = Mockery::mock(\App\Framework\DateTime\Clock::class);
|
||||
$this->realFileSystemService = new FileSystemService(); // Use real service since it's final
|
||||
$this->mockClock->shouldReceive('time')->andReturn(Timestamp::fromFloat(microtime(true)))->byDefault();
|
||||
$this->mockClock->shouldReceive('now')->andReturn(new \DateTimeImmutable())->byDefault();
|
||||
|
||||
$this->discoveryCacheManager = new DiscoveryCacheManager(
|
||||
$this->mockCache,
|
||||
$this->mockClock,
|
||||
$this->realFileSystemService
|
||||
);
|
||||
|
||||
// Create test context
|
||||
$this->testContext = new DiscoveryContext(
|
||||
paths: ['/test/path'],
|
||||
scanType: ScanType::FULL,
|
||||
options: new DiscoveryOptions(),
|
||||
startTime: $this->mockClock->now()
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Mockery::close();
|
||||
});
|
||||
|
||||
describe('Basic Cache Operations', function () {
|
||||
it('cache manager can be instantiated', function () {
|
||||
expect($this->discoveryCacheManager)->toBeInstanceOf(DiscoveryCacheManager::class);
|
||||
});
|
||||
});
|
||||
415
tests/Framework/Discovery/MemoryLeakTest.php
Normal file
415
tests/Framework/Discovery/MemoryLeakTest.php
Normal file
@@ -0,0 +1,415 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Discovery;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\GeneralCache;
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\Discovery\DiscoveryServiceBootstrapper;
|
||||
use App\Framework\Reflection\CachedReflectionProvider;
|
||||
use App\Framework\Reflection\ReflectionProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Memory leak test for Discovery system and related caches
|
||||
*
|
||||
* Tests:
|
||||
* - Discovery Cache growth over multiple runs
|
||||
* - ReflectionProvider cache size limits
|
||||
* - APCU cache growth monitoring
|
||||
* - Overall memory usage patterns
|
||||
*/
|
||||
final class MemoryLeakTest extends TestCase
|
||||
{
|
||||
private DefaultContainer $container;
|
||||
|
||||
private DiscoveryServiceBootstrapper $bootstrapper;
|
||||
|
||||
private array $memoryBaseline = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->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,
|
||||
};
|
||||
}
|
||||
}
|
||||
271
tests/Framework/Discovery/SimpleMemoryTest.php
Normal file
271
tests/Framework/Discovery/SimpleMemoryTest.php
Normal file
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Discovery;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\CacheIdentifier;
|
||||
use App\Framework\Cache\CacheItem;
|
||||
use App\Framework\Cache\CacheResult;
|
||||
use App\Framework\Cache\Driver\InMemoryCache;
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
use App\Framework\Discovery\UnifiedDiscoveryService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Simple Cache implementation for testing
|
||||
*/
|
||||
final class SimpleCacheWrapper implements Cache
|
||||
{
|
||||
public function __construct(private InMemoryCache $driver)
|
||||
{
|
||||
}
|
||||
|
||||
public function get(CacheIdentifier ...$identifiers): CacheResult
|
||||
{
|
||||
$keys = array_filter($identifiers, fn ($id) => $id instanceof \App\Framework\Cache\CacheKey);
|
||||
|
||||
return $this->driver->get(...$keys);
|
||||
}
|
||||
|
||||
public function set(CacheItem ...$items): bool
|
||||
{
|
||||
return $this->driver->set(...$items);
|
||||
}
|
||||
|
||||
public function has(CacheIdentifier ...$identifiers): array
|
||||
{
|
||||
$keys = array_filter($identifiers, fn ($id) => $id instanceof \App\Framework\Cache\CacheKey);
|
||||
|
||||
return $this->driver->has(...$keys);
|
||||
}
|
||||
|
||||
public function forget(CacheIdentifier ...$identifiers): bool
|
||||
{
|
||||
$keys = array_filter($identifiers, fn ($id) => $id instanceof \App\Framework\Cache\CacheKey);
|
||||
|
||||
return $this->driver->forget(...$keys);
|
||||
}
|
||||
|
||||
public function clear(): bool
|
||||
{
|
||||
return $this->driver->clear();
|
||||
}
|
||||
|
||||
public function remember(\App\Framework\Cache\CacheKey $key, callable $callback, ?Duration $ttl = null): CacheItem
|
||||
{
|
||||
$result = $this->driver->get($key);
|
||||
$item = $result->getItem($key);
|
||||
if ($item->isHit) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
$value = $callback();
|
||||
$newItem = CacheItem::forSet($key, $value, $ttl);
|
||||
$this->driver->set($newItem);
|
||||
|
||||
return CacheItem::hit($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified memory test focusing only on Discovery core without container dependencies
|
||||
*/
|
||||
final class SimpleMemoryTest extends TestCase
|
||||
{
|
||||
public function test_discovery_memory_usage_15_runs(): void
|
||||
{
|
||||
$iterations = 15;
|
||||
$memoryData = [];
|
||||
|
||||
echo "\n=== Testing Discovery Memory Usage - $iterations Runs ===\n";
|
||||
|
||||
// Create discovery service directly with mock dependencies
|
||||
$pathProvider = new PathProvider('/var/www/html');
|
||||
|
||||
// Use InMemoryCache with wrapper to implement Cache interface
|
||||
$cacheDriver = new InMemoryCache();
|
||||
$cache = new SimpleCacheWrapper($cacheDriver);
|
||||
$clock = new SystemClock();
|
||||
|
||||
// Create reflection provider without dependencies
|
||||
$reflectionProvider = new \App\Framework\Reflection\CachedReflectionProvider();
|
||||
|
||||
$config = new \App\Framework\Discovery\ValueObjects\DiscoveryConfiguration(
|
||||
paths: ['/var/www/html/src'],
|
||||
attributeMappers: [
|
||||
new \App\Framework\Core\RouteMapper(),
|
||||
new \App\Framework\DI\InitializerMapper(),
|
||||
],
|
||||
targetInterfaces: [],
|
||||
useCache: false
|
||||
);
|
||||
|
||||
$discoveryService = new UnifiedDiscoveryService(
|
||||
pathProvider: $pathProvider,
|
||||
cache: $cache,
|
||||
clock: $clock,
|
||||
reflectionProvider: $reflectionProvider,
|
||||
configuration: $config,
|
||||
attributeMappers: [
|
||||
new \App\Framework\Core\RouteMapper(),
|
||||
new \App\Framework\DI\InitializerMapper(),
|
||||
],
|
||||
targetInterfaces: []
|
||||
);
|
||||
|
||||
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 = $discoveryService->discover();
|
||||
|
||||
$memoryAfter = memory_get_usage(true);
|
||||
$peakAfter = memory_get_peak_usage(true);
|
||||
|
||||
$memoryDiff = $memoryAfter - $memoryBefore;
|
||||
$peakDiff = $peakAfter - $peakBefore;
|
||||
|
||||
$memoryData[$i] = [
|
||||
'memory_before' => $memoryBefore,
|
||||
'memory_after' => $memoryAfter,
|
||||
'memory_diff' => $memoryDiff,
|
||||
'peak_before' => $peakBefore,
|
||||
'peak_after' => $peakAfter,
|
||||
'peak_diff' => $peakDiff,
|
||||
];
|
||||
|
||||
echo sprintf(
|
||||
"Run %2d: Memory %s -> %s (diff: %s), Peak %s -> %s (diff: %s)\n",
|
||||
$i,
|
||||
$this->formatBytes($memoryBefore),
|
||||
$this->formatBytes($memoryAfter),
|
||||
$this->formatBytes($memoryDiff),
|
||||
$this->formatBytes($peakBefore),
|
||||
$this->formatBytes($peakAfter),
|
||||
$this->formatBytes($peakDiff)
|
||||
);
|
||||
|
||||
// Log reflection stats every 5 runs
|
||||
if ($i % 5 === 0) {
|
||||
try {
|
||||
$health = $discoveryService->getHealthStatus();
|
||||
echo " Health Status: " . json_encode($health) . "\n";
|
||||
} catch (\Throwable $e) {
|
||||
echo " Health Status: Error - " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
unset($registry);
|
||||
gc_collect_cycles();
|
||||
|
||||
// Check for excessive memory usage
|
||||
if ($memoryDiff > 100 * 1024 * 1024) { // 100MB limit
|
||||
$this->fail("Run $i: Excessive memory usage: " . $this->formatBytes($memoryDiff));
|
||||
}
|
||||
}
|
||||
|
||||
$this->analyzeResults($memoryData);
|
||||
}
|
||||
|
||||
private function analyzeResults(array $memoryData): void
|
||||
{
|
||||
echo "\n=== Memory Analysis Results ===\n";
|
||||
|
||||
$memoryDiffs = array_column($memoryData, 'memory_diff');
|
||||
$peakDiffs = array_column($memoryData, 'peak_diff');
|
||||
|
||||
$avgMemory = array_sum($memoryDiffs) / count($memoryDiffs);
|
||||
$avgPeak = array_sum($peakDiffs) / count($peakDiffs);
|
||||
$maxMemory = max($memoryDiffs);
|
||||
$minMemory = min($memoryDiffs);
|
||||
$totalGrowth = array_sum($memoryDiffs);
|
||||
|
||||
echo "Average memory per run: " . $this->formatBytes($avgMemory) . "\n";
|
||||
echo "Average peak per run: " . $this->formatBytes($avgPeak) . "\n";
|
||||
echo "Max memory per run: " . $this->formatBytes($maxMemory) . "\n";
|
||||
echo "Min memory per run: " . $this->formatBytes($minMemory) . "\n";
|
||||
echo "Total cumulative growth: " . $this->formatBytes($totalGrowth) . "\n";
|
||||
|
||||
// Calculate growth trend
|
||||
$growthTrend = $this->calculateLinearTrend($memoryDiffs);
|
||||
echo "Growth trend (bytes per run): " . $this->formatBytes($growthTrend) . "\n";
|
||||
|
||||
// Memory leak detection
|
||||
if ($growthTrend > 2 * 1024 * 1024) { // 2MB growth per run
|
||||
$this->fail("Detected memory leak: " . $this->formatBytes($growthTrend) . " growth per run");
|
||||
}
|
||||
|
||||
if ($avgMemory > 50 * 1024 * 1024) { // 50MB average
|
||||
$this->fail("Average memory usage too high: " . $this->formatBytes($avgMemory));
|
||||
}
|
||||
|
||||
// Check for stability - memory usage should be consistent
|
||||
$memoryVariance = $this->calculateVariance($memoryDiffs);
|
||||
echo "Memory usage variance: " . $this->formatBytes($memoryVariance) . "\n";
|
||||
|
||||
if ($memoryVariance > 10 * 1024 * 1024) { // 10MB variance
|
||||
echo "WARNING: High memory usage variance detected\n";
|
||||
}
|
||||
|
||||
echo "\n✅ Memory test completed successfully!\n";
|
||||
echo "✅ No significant memory leaks detected\n";
|
||||
echo "✅ Average memory usage within acceptable limits\n";
|
||||
}
|
||||
|
||||
private function calculateLinearTrend(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;
|
||||
}
|
||||
|
||||
return ($n * $sumXY - $sumX * $sumY) / ($n * $sumX2 - $sumX * $sumX);
|
||||
}
|
||||
|
||||
private function calculateVariance(array $values): float
|
||||
{
|
||||
$mean = array_sum($values) / count($values);
|
||||
$squaredDiffs = array_map(fn ($value) => pow($value - $mean, 2), $values);
|
||||
|
||||
return array_sum($squaredDiffs) / count($squaredDiffs);
|
||||
}
|
||||
|
||||
private function formatBytes(int|float $bytes): string
|
||||
{
|
||||
if ($bytes < 0) {
|
||||
return '-' . $this->formatBytes(abs($bytes));
|
||||
}
|
||||
|
||||
$units = ['B', 'KB', 'MB', 'GB'];
|
||||
$unitIndex = 0;
|
||||
|
||||
while ($bytes >= 1024 && $unitIndex < count($units) - 1) {
|
||||
$bytes /= 1024;
|
||||
$unitIndex++;
|
||||
}
|
||||
|
||||
return round($bytes, 1) . ' ' . $units[$unitIndex];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user