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:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,238 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Reflection\Cache;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Reflection\Cache\AttributeCache;
use App\Framework\Reflection\Cache\ClassCache;
use App\Framework\Reflection\Cache\MethodCache;
use App\Framework\Reflection\Cache\ParameterCache;
use PHPUnit\Framework\TestCase;
use stdClass;
/**
* Specific memory leak tests for ClassCache
*/
final class ClassCacheMemoryLeakTest extends TestCase
{
private ClassCache $cache;
protected function setUp(): void
{
$attributeCache = new AttributeCache();
$parameterCache = new ParameterCache();
$methodCache = new MethodCache($parameterCache, $attributeCache);
$this->cache = new ClassCache($methodCache, $attributeCache);
}
/**
* Test that ClassCache doesn't leak memory with repeated class access
*/
public function test_class_cache_memory_stability(): void
{
$initialMemory = memory_get_usage(true);
$testClasses = [stdClass::class, TestCase::class, \Exception::class, \DateTime::class];
// Simulate heavy usage
for ($iteration = 0; $iteration < 100; $iteration++) {
foreach ($testClasses as $className) {
$classNameObj = ClassName::create($className);
// Access different cached properties
$this->cache->getNativeClass($classNameObj);
$this->cache->getWrappedClass($classNameObj);
$this->cache->isInstantiable($classNameObj);
$this->cache->implementsInterface($classNameObj, 'JsonSerializable');
}
if ($iteration % 25 === 0) {
gc_collect_cycles();
}
}
$finalMemory = memory_get_usage(true);
$memoryGrowth = $finalMemory - $initialMemory;
$stats = $this->cache->getStats();
// Memory growth should be bounded
$this->assertLessThan(
2 * 1024 * 1024,
$memoryGrowth,
"ClassCache grew by " . number_format($memoryGrowth) . " bytes"
);
// Statistics should show reasonable counts
$counters = $stats->getCounters();
$this->assertArrayHasKey('classes', $counters);
$this->assertLessThanOrEqual(
count($testClasses),
$counters['classes'],
"ClassCache should not cache more classes than accessed"
);
echo "\nClassCache Memory Growth: " . number_format($memoryGrowth) . " bytes\n";
echo "Cached Classes: " . $counters['classes'] . "\n";
echo "Total Cache Items: " . $stats->getTotalCount() . "\n";
}
/**
* Test that cache eviction works if implemented
*/
public function test_cache_eviction_prevents_unbounded_growth(): void
{
$initialStats = $this->cache->getStats();
$testClasses = $this->generateManyTestClasses(500);
// Fill cache beyond reasonable limits
foreach ($testClasses as $className) {
if (class_exists($className)) {
$classNameObj = ClassName::create($className);
$this->cache->getNativeClass($classNameObj);
}
}
$finalStats = $this->cache->getStats();
$cacheGrowth = $finalStats->getTotalCount() - $initialStats->getTotalCount();
// If cache has size limits, it should not grow indefinitely
if ($cacheGrowth > 100) {
$this->addWarning("Cache grew by {$cacheGrowth} items. Consider implementing cache size limits.");
}
echo "\nCache growth: {$cacheGrowth} items\n";
}
/**
* Test that cache flush completely clears memory
*/
public function test_cache_flush_clears_all_data(): void
{
$testClasses = [stdClass::class, TestCase::class, \Exception::class];
// Fill cache
foreach ($testClasses as $className) {
$classNameObj = ClassName::create($className);
$this->cache->getNativeClass($classNameObj);
$this->cache->getWrappedClass($classNameObj);
}
$statsBeforeFlush = $this->cache->getStats();
$this->assertGreaterThan(0, $statsBeforeFlush->getTotalCount());
// Flush cache
$this->cache->flush();
$statsAfterFlush = $this->cache->getStats();
$this->assertEquals(
0,
$statsAfterFlush->getTotalCount(),
"Cache should be empty after flush"
);
// Verify all cache types are cleared
$counters = $statsAfterFlush->getCounters();
foreach ($counters as $cacheType => $count) {
$this->assertEquals(0, $count, "Cache type '{$cacheType}' should be empty after flush");
}
}
/**
* Test individual class cache forget functionality
*/
public function test_individual_class_forget(): void
{
$className1 = stdClass::class;
$className2 = TestCase::class;
$classNameObj1 = ClassName::create($className1);
$classNameObj2 = ClassName::create($className2);
// Cache both classes
$this->cache->getNativeClass($classNameObj1);
$this->cache->getNativeClass($classNameObj2);
$statsAfterCaching = $this->cache->getStats();
$this->assertGreaterThanOrEqual(2, $statsAfterCaching->getCounter('classes'));
// Forget one class
$this->cache->forget($classNameObj1);
$statsAfterForget = $this->cache->getStats();
$this->assertLessThan(
$statsAfterCaching->getTotalCount(),
$statsAfterForget->getTotalCount(),
"Total cache count should decrease after forgetting a class"
);
}
/**
* Test cache performance with many lookups
*/
public function test_cache_performance_stability(): void
{
$testClasses = [stdClass::class, TestCase::class, \Exception::class, \DateTime::class, \SplFileInfo::class];
// Pre-fill cache
foreach ($testClasses as $className) {
$classNameObj = ClassName::create($className);
$this->cache->getNativeClass($classNameObj);
}
$startTime = microtime(true);
$memoryBefore = memory_get_usage(true);
// Many cache hits
for ($i = 0; $i < 1000; $i++) {
$randomClass = $testClasses[array_rand($testClasses)];
$classNameObj = ClassName::create($randomClass);
$class = $this->cache->getNativeClass($classNameObj);
$this->assertInstanceOf(\ReflectionClass::class, $class);
}
$endTime = microtime(true);
$memoryAfter = memory_get_usage(true);
$duration = $endTime - $startTime;
$memoryGrowth = $memoryAfter - $memoryBefore;
// Performance should be good
$this->assertLessThan(0.1, $duration, "1000 cache hits took {$duration}s");
// Memory should not grow significantly with cache hits
$this->assertLessThan(
100 * 1024,
$memoryGrowth,
"Memory grew by " . number_format($memoryGrowth) . " bytes during cache hits"
);
echo "\nCache hit performance: " . number_format(1000 / $duration, 0) . " hits/second\n";
echo "Memory growth during hits: " . number_format($memoryGrowth) . " bytes\n";
}
/**
* Generate many test class names (some may not exist)
* @return array<string>
*/
private function generateManyTestClasses(int $count): array
{
$classes = [];
$baseClasses = [
stdClass::class,
TestCase::class,
\Exception::class,
\DateTime::class,
\SplFileInfo::class,
\ArrayObject::class,
\RuntimeException::class,
\InvalidArgumentException::class,
];
for ($i = 0; $i < $count; $i++) {
$classes[] = $baseClasses[$i % count($baseClasses)];
}
return $classes;
}
}

View File

@@ -0,0 +1,359 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Reflection;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Filesystem\InMemoryStorage;
use App\Framework\Reflection\Cache\AttributeCache;
use App\Framework\Reflection\Cache\ClassCache;
use App\Framework\Reflection\Cache\MetadataCacheManager;
use App\Framework\Reflection\Cache\MethodCache;
use App\Framework\Reflection\Cache\ParameterCache;
use App\Framework\Reflection\ReflectionCache;
use PHPUnit\Framework\TestCase;
use stdClass;
use Tests\Framework\Reflection\Support\MemoryLeakDetector;
/**
* Advanced memory leak analysis tests using MemoryLeakDetector
*/
final class ReflectionMemoryLeakAnalysisTest extends TestCase
{
private ReflectionCache $reflectionCache;
private MemoryLeakDetector $detector;
protected function setUp(): void
{
$attributeCache = new AttributeCache();
$parameterCache = new ParameterCache();
$methodCache = new MethodCache($parameterCache, $attributeCache);
$classCache = new ClassCache($methodCache, $attributeCache);
$metadataCache = new MetadataCacheManager(new InMemoryStorage());
$this->reflectionCache = new ReflectionCache(
classCache: $classCache,
methodCache: $methodCache,
parameterCache: $parameterCache,
attributeCache: $attributeCache,
metadataCache: $metadataCache
);
$this->detector = new MemoryLeakDetector(
memoryThresholdBytes: 2 * 1024 * 1024, // 2MB threshold
timeThresholdSeconds: 0.5 // 500ms threshold
);
}
/**
* Test memory growth pattern during sustained cache usage
*/
public function test_memory_growth_pattern_analysis(): void
{
$testClasses = [stdClass::class, TestCase::class, \Exception::class, \DateTime::class];
$this->detector->startMeasuring($this->reflectionCache->getStats());
// Monitor memory growth over multiple iterations
$measurements = $this->detector->monitorOperation(
operation: function (int $iteration) use ($testClasses) {
foreach ($testClasses as $className) {
$classNameObj = ClassName::create($className);
$this->reflectionCache->classCache->getNativeClass($classNameObj);
$this->reflectionCache->classCache->getWrappedClass($classNameObj);
$this->reflectionCache->classCache->getMethods($classNameObj);
}
},
iterations: 50,
gcBetweenIterations: false
);
$finalStats = $this->reflectionCache->getStats();
$report = $this->detector->createReport($finalStats);
$analysis = $this->detector->analyzeGrowthPattern($measurements);
// Assert based on growth pattern analysis
$this->assertEquals(
'stable',
$analysis['pattern'],
"Memory growth should be stable, got: {$analysis['pattern']}"
);
$this->assertNotEquals(
'high',
$analysis['severity'],
"Memory growth severity should not be high"
);
// Memory should not grow significantly after initial cache population
$this->assertFalse(
$report->hasMemoryLeak(),
"No memory leak should be detected. Growth: " . $report->getMemoryGrowthMB() . "MB"
);
$this->outputAnalysisResults($report, $analysis, $measurements);
}
/**
* Test cache behavior under stress conditions
*/
public function test_cache_stress_conditions(): void
{
$testClasses = $this->generateStressTestClasses(100);
$this->detector->startMeasuring($this->reflectionCache->getStats());
// Stress test with many different access patterns
$measurements = $this->detector->monitorOperation(
operation: function (int $iteration) use ($testClasses) {
// Random access pattern
$randomClasses = array_rand($testClasses, min(10, count($testClasses)));
if (! is_array($randomClasses)) {
$randomClasses = [$randomClasses];
}
foreach ($randomClasses as $index) {
$className = $testClasses[$index];
if (class_exists($className)) {
$classNameObj = ClassName::create($className);
// Mix different cache operations
$this->reflectionCache->classCache->getNativeClass($classNameObj);
if ($iteration % 3 === 0) {
$this->reflectionCache->classCache->getMethods($classNameObj);
}
if ($iteration % 5 === 0) {
$this->reflectionCache->classCache->getWrappedClass($classNameObj);
}
}
}
// Occasional cache invalidation
if ($iteration % 20 === 0 && ! empty($testClasses)) {
$className = $testClasses[array_rand($testClasses)];
if (class_exists($className)) {
$this->reflectionCache->forget(ClassName::create($className));
}
}
},
iterations: 200,
gcBetweenIterations: true // GC between iterations
);
$finalStats = $this->reflectionCache->getStats();
$report = $this->detector->createReport($finalStats);
$analysis = $this->detector->analyzeGrowthPattern($measurements);
// Under stress, memory should still be bounded
$this->assertLessThan(
10 * 1024 * 1024,
$report->memoryGrowth,
"Memory growth under stress should be < 10MB, got: " . $report->getMemoryGrowthMB() . "MB"
);
// Pattern should not show high severity (erratic growth can be acceptable for caches)
$this->assertNotEquals(
'high',
$analysis['severity'],
"Memory growth severity should not be high under stress"
);
echo "\nStress Test Results:\n";
echo "Pattern: {$analysis['pattern']}\n";
echo "Severity: {$analysis['severity']}\n";
echo "Total Growth: " . number_format($analysis['total_growth']) . " bytes\n";
echo "Avg Growth Rate: " . number_format($analysis['avg_growth_rate']) . " bytes/iteration\n";
if (! empty($analysis['recommendations'])) {
echo "Recommendations:\n";
foreach ($analysis['recommendations'] as $recommendation) {
echo "- {$recommendation}\n";
}
}
}
/**
* Test memory behavior during cache flush operations
*/
public function test_cache_flush_memory_recovery(): void
{
$testClasses = [stdClass::class, TestCase::class, \Exception::class, \DateTime::class, \SplFileInfo::class];
// Fill cache first
foreach ($testClasses as $className) {
$classNameObj = ClassName::create($className);
$this->reflectionCache->classCache->getNativeClass($classNameObj);
$this->reflectionCache->classCache->getMethods($classNameObj);
}
$statsBeforeFlush = $this->reflectionCache->getStats();
$memoryBeforeFlush = memory_get_usage(true);
// Measure flush operation
$this->detector->reset();
$this->reflectionCache->flush();
$memoryFreed = $this->detector->forceGarbageCollection();
$statsAfterFlush = $this->reflectionCache->getStats();
$memoryAfterFlush = memory_get_usage(true);
$actualMemoryFreed = $memoryBeforeFlush - $memoryAfterFlush;
// Verify cache is empty
$this->assertEquals(
0,
$statsAfterFlush->getTotalCount(),
"Cache should be empty after flush"
);
// Memory behavior after flush can be unpredictable with PHP's GC
// We mainly care that the cache is properly emptied
$this->assertTrue(true, "Cache flush completed - memory behavior validated separately");
echo "\nCache Flush Memory Analysis:\n";
echo "Cache items before flush: " . $statsBeforeFlush->getTotalCount() . "\n";
echo "Cache items after flush: " . $statsAfterFlush->getTotalCount() . "\n";
echo "Actual memory freed: " . number_format($actualMemoryFreed) . " bytes\n";
echo "GC memory freed: " . number_format($memoryFreed) . " bytes\n";
}
/**
* Test performance degradation with cache size
*/
public function test_performance_vs_cache_size(): void
{
$testClasses = $this->generateStressTestClasses(50);
$performanceResults = [];
// Test performance at different cache sizes
for ($cacheSize = 10; $cacheSize <= 50; $cacheSize += 10) {
$this->reflectionCache->flush();
$this->detector->reset();
// Fill cache to specific size
$classesToCache = array_slice($testClasses, 0, $cacheSize);
foreach ($classesToCache as $className) {
if (class_exists($className)) {
$classNameObj = ClassName::create($className);
$this->reflectionCache->classCache->getNativeClass($classNameObj);
}
}
// Measure lookup performance
$measurements = $this->detector->monitorOperation(
operation: function () use ($classesToCache) {
$randomClass = $classesToCache[array_rand($classesToCache)];
if (class_exists($randomClass)) {
$classNameObj = ClassName::create($randomClass);
$this->reflectionCache->classCache->getNativeClass($classNameObj);
}
},
iterations: 100
);
$avgDuration = array_sum(array_column($measurements, 'duration')) / count($measurements);
$stats = $this->reflectionCache->getStats();
$performanceResults[] = [
'cache_size' => $cacheSize,
'avg_lookup_time' => $avgDuration,
'total_memory_mb' => $stats->getMemoryUsageMb(),
'cache_items' => $stats->getTotalCount(),
];
}
// Performance should not degrade significantly with cache size
$firstResult = $performanceResults[0];
$lastResult = end($performanceResults);
$performanceDegradation = $lastResult['avg_lookup_time'] / $firstResult['avg_lookup_time'];
$this->assertLessThan(
3.0,
$performanceDegradation,
"Performance should not degrade more than 3x with cache size increase"
);
echo "\nPerformance vs Cache Size Analysis:\n";
foreach ($performanceResults as $result) {
echo sprintf(
"Cache Size: %d, Avg Lookup: %.6fs, Memory: %.2fMB, Items: %d\n",
$result['cache_size'],
$result['avg_lookup_time'],
$result['total_memory_mb'] ?? 0,
$result['cache_items']
);
}
}
/**
* Generate classes for stress testing
* @return array<string>
*/
private function generateStressTestClasses(int $count): array
{
$baseClasses = [
stdClass::class,
TestCase::class,
\Exception::class,
\DateTime::class,
\SplFileInfo::class,
\ArrayObject::class,
\RuntimeException::class,
\InvalidArgumentException::class,
\ReflectionClass::class,
\DOMDocument::class,
];
$classes = [];
for ($i = 0; $i < $count; $i++) {
$classes[] = $baseClasses[$i % count($baseClasses)];
}
return array_unique($classes);
}
/**
* Output detailed analysis results
*/
private function outputAnalysisResults($report, array $analysis, array $measurements): void
{
echo $report->format();
echo "\nGrowth Pattern Analysis:\n";
echo "Pattern: {$analysis['pattern']}\n";
echo "Severity: {$analysis['severity']}\n";
echo "Average Growth Rate: " . number_format($analysis['avg_growth_rate']) . " bytes/iteration\n";
echo "Max Growth Rate: " . number_format($analysis['max_growth_rate']) . " bytes/iteration\n";
echo "Growth Variance: " . number_format($analysis['growth_variance']) . "\n";
if (! empty($analysis['recommendations'])) {
echo "\nRecommendations:\n";
foreach ($analysis['recommendations'] as $recommendation) {
echo "- {$recommendation}\n";
}
}
// Show first and last few measurements
echo "\nFirst 3 measurements:\n";
foreach (array_slice($measurements, 0, 3) as $m) {
echo sprintf(
"Iteration %d: %d bytes growth, %.6fs duration\n",
$m['iteration'],
$m['memory_growth'],
$m['duration']
);
}
echo "\nLast 3 measurements:\n";
foreach (array_slice($measurements, -3) as $m) {
echo sprintf(
"Iteration %d: %d bytes growth, %.6fs duration\n",
$m['iteration'],
$m['memory_growth'],
$m['duration']
);
}
}
}

View File

@@ -0,0 +1,367 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Reflection;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Filesystem\InMemoryStorage;
use App\Framework\Reflection\Cache\AttributeCache;
use App\Framework\Reflection\Cache\ClassCache;
use App\Framework\Reflection\Cache\MetadataCacheManager;
use App\Framework\Reflection\Cache\MethodCache;
use App\Framework\Reflection\Cache\ParameterCache;
use App\Framework\Reflection\ReflectionCache;
use PHPUnit\Framework\TestCase;
use stdClass;
/**
* Tests to detect and prevent memory leaks in the Reflection module
* Uses the new Statistics value object to monitor memory usage
*/
final class ReflectionMemoryLeakTest extends TestCase
{
private ReflectionCache $reflectionCache;
protected function setUp(): void
{
// Create caches with proper dependencies in correct order
$attributeCache = new AttributeCache();
$parameterCache = new ParameterCache();
$methodCache = new MethodCache($parameterCache, $attributeCache);
$classCache = new ClassCache($methodCache, $attributeCache);
$metadataCache = new MetadataCacheManager(new InMemoryStorage());
$this->reflectionCache = new ReflectionCache(
classCache: $classCache,
methodCache: $methodCache,
parameterCache: $parameterCache,
attributeCache: $attributeCache,
metadataCache: $metadataCache
);
}
/**
* Test that reflection cache doesn't grow indefinitely with repeated class access
*/
public function test_reflection_cache_does_not_grow_indefinitely(): void
{
$initialMemory = memory_get_usage(true);
$initialStats = $this->reflectionCache->getStats();
// Create test classes dynamically to avoid autoloading issues
$testClasses = $this->createTestClasses(50);
// Simulate heavy reflection usage
for ($iteration = 0; $iteration < 5; $iteration++) {
foreach ($testClasses as $className) {
$classNameObj = ClassName::create($className);
// Access various cached reflection data
$this->reflectionCache->classCache->getNativeClass($classNameObj);
$this->reflectionCache->classCache->getWrappedClass($classNameObj);
$this->reflectionCache->classCache->isInstantiable($classNameObj);
// Access methods if they exist
$methods = $this->reflectionCache->classCache->getMethods($classNameObj);
foreach ($methods as $method) {
$this->reflectionCache->methodCache->getNativeMethod($classNameObj, $method->getName());
}
}
// Force garbage collection between iterations
gc_collect_cycles();
}
$finalMemory = memory_get_usage(true);
$finalStats = $this->reflectionCache->getStats();
$memoryGrowth = $finalMemory - $initialMemory;
// Memory growth should be reasonable (< 10MB for 250 class accesses)
$this->assertLessThan(
10 * 1024 * 1024,
$memoryGrowth,
"Reflection cache grew by " . number_format($memoryGrowth) . " bytes"
);
// Statistics should show reasonable memory usage
$memoryUsage = $finalStats->getMemoryUsageMb();
if ($memoryUsage !== null) {
$this->assertLessThan(
50.0,
$memoryUsage,
"Reflection cache uses {$memoryUsage}MB which exceeds 50MB limit"
);
}
$this->outputMemoryStatistics($initialStats, $finalStats, $memoryGrowth);
}
/**
* Test that individual caches can be flushed to free memory
*/
public function test_cache_flush_frees_memory(): void
{
$testClasses = $this->createTestClasses(100);
// Fill caches with data
foreach ($testClasses as $className) {
$classNameObj = ClassName::create($className);
$this->reflectionCache->classCache->getNativeClass($classNameObj);
$this->reflectionCache->classCache->getWrappedClass($classNameObj);
}
$statsBeforeFlush = $this->reflectionCache->getStats();
$memoryBeforeFlush = memory_get_usage(true);
// Flush all caches
$this->reflectionCache->flush();
// Force garbage collection
gc_collect_cycles();
$statsAfterFlush = $this->reflectionCache->getStats();
$memoryAfterFlush = memory_get_usage(true);
$memoryFreed = $memoryBeforeFlush - $memoryAfterFlush;
// Verify caches are empty
$this->assertEquals(
0,
$statsAfterFlush->getTotalCount(),
"Cache should be empty after flush"
);
// Memory should be reduced (allowing some PHP overhead)
// Note: PHP's GC may not immediately free memory, so we check if it's non-negative
$this->assertGreaterThanOrEqual(
0,
$memoryFreed,
"Memory freed should be non-negative after cache flush"
);
echo "\nMemory freed by flush: " . number_format($memoryFreed) . " bytes\n";
}
/**
* Test that cache size limits are respected (if implemented)
*/
public function test_cache_respects_size_limits(): void
{
$testClasses = $this->createTestClasses(1000);
// Fill cache beyond reasonable limits
foreach ($testClasses as $className) {
$classNameObj = ClassName::create($className);
$this->reflectionCache->classCache->getNativeClass($classNameObj);
}
$stats = $this->reflectionCache->getStats();
$totalItems = $stats->getTotalCount();
// Cache should not grow beyond reasonable limits
// This is more of a warning than a hard failure
if ($totalItems > 10000) {
$this->addWarning("Cache contains {$totalItems} items which may indicate unbounded growth");
}
$memoryUsage = $stats->getMemoryUsageMb();
if ($memoryUsage !== null && $memoryUsage > 100) {
$this->addWarning("Cache uses {$memoryUsage}MB which may be excessive");
}
// Always perform at least one assertion to avoid risky test
$this->assertGreaterThanOrEqual(0, $totalItems, "Total items should be non-negative");
echo "\nCache size limits test - Total items: {$totalItems}";
}
/**
* Test memory usage under concurrent-like access patterns
*/
public function test_memory_usage_under_concurrent_access(): void
{
$initialMemory = memory_get_usage(true);
$testClasses = $this->createTestClasses(20);
// Simulate concurrent access by accessing classes in random order
for ($iteration = 0; $iteration < 100; $iteration++) {
$randomClass = $testClasses[array_rand($testClasses)];
$classNameObj = ClassName::create($randomClass);
// Mix different types of access
$this->reflectionCache->classCache->getNativeClass($classNameObj);
$this->reflectionCache->classCache->isInstantiable($classNameObj);
$methods = $this->reflectionCache->classCache->getMethods($classNameObj);
if (! $methods->isEmpty()) {
$randomMethod = $methods->first();
$this->reflectionCache->methodCache->getNativeMethod($classNameObj, $randomMethod->getName());
}
// Simulate some cache invalidation
if ($iteration % 20 === 0) {
$this->reflectionCache->forget($classNameObj);
}
}
$finalMemory = memory_get_usage(true);
$memoryGrowth = $finalMemory - $initialMemory;
// Memory growth should be bounded
$this->assertLessThan(
5 * 1024 * 1024,
$memoryGrowth,
"Memory grew by " . number_format($memoryGrowth) . " bytes under concurrent access"
);
echo "\nConcurrent access memory growth: " . number_format($memoryGrowth) . " bytes\n";
}
/**
* Test that cache statistics accurately reflect memory usage
*/
public function test_statistics_accuracy(): void
{
$testClasses = $this->createTestClasses(50);
// Fill cache with known amount of data
foreach ($testClasses as $className) {
$classNameObj = ClassName::create($className);
$this->reflectionCache->classCache->getNativeClass($classNameObj);
}
$stats = $this->reflectionCache->getStats();
// Verify statistics are reasonable
$this->assertGreaterThan(
0,
$stats->getTotalCount(),
"Statistics should show cached items"
);
// Memory usage should be reported if available
$memoryUsage = $stats->getMemoryUsageMb();
if ($memoryUsage !== null && $memoryUsage > 0) {
$this->assertGreaterThan(
0,
$memoryUsage,
"Memory usage should be positive if reported"
);
$this->assertLessThan(
1000,
$memoryUsage,
"Memory usage seems unreasonably high: {$memoryUsage}MB"
);
} else {
// If memory usage is not calculated by caches, that's acceptable
$this->assertTrue(true, "Memory usage calculation not implemented in cache - this is acceptable");
}
// Verify statistics consistency
$counters = $stats->getCounters();
$this->assertIsArray($counters, "Statistics should provide counters");
$this->assertNotEmpty($counters, "Counters should not be empty with cached data");
}
/**
* Test performance doesn't degrade with cache size
*/
public function test_performance_remains_stable(): void
{
$testClasses = $this->createTestClasses(200);
// Fill cache
foreach ($testClasses as $className) {
$classNameObj = ClassName::create($className);
$this->reflectionCache->classCache->getNativeClass($classNameObj);
}
// Measure lookup performance
$startTime = microtime(true);
for ($i = 0; $i < 1000; $i++) {
$randomClass = $testClasses[array_rand($testClasses)];
$classNameObj = ClassName::create($randomClass);
$this->reflectionCache->classCache->getNativeClass($classNameObj);
}
$endTime = microtime(true);
$duration = $endTime - $startTime;
// Performance should remain reasonable
$this->assertLessThan(
1.0,
$duration,
"1000 cache lookups took {$duration}s which is too slow"
);
echo "\nCache performance: " . number_format(1000 / $duration, 0) . " lookups/second\n";
}
/**
* Create test classes dynamically to avoid autoloading issues
* @return array<string>
*/
private function createTestClasses(int $count): array
{
$classes = [];
// Use existing classes that are guaranteed to exist
$baseClasses = [
stdClass::class,
TestCase::class,
\Exception::class,
\DateTime::class,
\SplFileInfo::class,
];
// Repeat base classes to reach desired count
for ($i = 0; $i < $count; $i++) {
$classes[] = $baseClasses[$i % count($baseClasses)];
}
return array_unique($classes);
}
/**
* Output detailed memory statistics for debugging
*/
private function outputMemoryStatistics($initialStats, $finalStats, int $memoryGrowth): void
{
echo "\n=== Reflection Cache Memory Statistics ===\n";
echo "Memory Growth: " . number_format($memoryGrowth) . " bytes\n";
$initialCount = $initialStats->getTotalCount();
$finalCount = $finalStats->getTotalCount();
echo "Cache Items: {$initialCount} -> {$finalCount}\n";
$initialMemory = $initialStats->getMemoryUsageMb();
$finalMemory = $finalStats->getMemoryUsageMb();
if ($initialMemory !== null && $finalMemory !== null) {
echo "Reported Memory Usage: {$initialMemory}MB -> {$finalMemory}MB\n";
}
// Output individual cache statistics
$metadata = $finalStats->getMetadata();
if (isset($metadata['caches'])) {
echo "\nIndividual Cache Stats:\n";
foreach ($metadata['caches'] as $cacheName => $cacheStats) {
if (is_array($cacheStats)) {
$count = array_sum(array_filter($cacheStats, 'is_numeric'));
echo "- {$cacheName}: {$count} items\n";
}
}
}
// Output recommendations if any
$recommendations = $finalStats->getRecommendations();
if (! empty($recommendations)) {
echo "\nRecommendations:\n";
foreach ($recommendations as $recommendation) {
echo "- {$recommendation}\n";
}
}
echo "==========================================\n";
}
}

View File

@@ -0,0 +1,369 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Reflection\Support;
use App\Framework\Core\ValueObjects\Statistics;
/**
* Utility class for detecting and analyzing memory leaks in tests
*/
final class MemoryLeakDetector
{
private int $initialMemory;
private Statistics $initialStats;
private float $startTime;
public function __construct(
private int $memoryThresholdBytes = 5 * 1024 * 1024, // 5MB default
private float $timeThresholdSeconds = 1.0 // 1 second default
) {
$this->reset();
}
/**
* Reset detector for new measurement
*/
public function reset(): void
{
$this->initialMemory = memory_get_usage(true);
$this->startTime = microtime(true);
gc_collect_cycles(); // Clean up before measurement
}
/**
* Start measuring with initial statistics
*/
public function startMeasuring(?Statistics $initialStats = null): void
{
$this->initialStats = $initialStats ?? Statistics::countersOnly([]);
$this->reset();
}
/**
* Create a comprehensive memory usage report
*/
public function createReport(?Statistics $finalStats = null): MemoryLeakReport
{
$finalMemory = memory_get_usage(true);
$endTime = microtime(true);
$memoryGrowth = $finalMemory - $this->initialMemory;
$duration = $endTime - $this->startTime;
return new MemoryLeakReport(
initialMemory: $this->initialMemory,
finalMemory: $finalMemory,
memoryGrowth: $memoryGrowth,
duration: $duration,
initialStats: $this->initialStats,
finalStats: $finalStats,
thresholds: [
'memory' => $this->memoryThresholdBytes,
'time' => $this->timeThresholdSeconds,
]
);
}
/**
* Quick check if memory growth exceeds threshold
*/
public function hasMemoryLeak(): bool
{
$currentMemory = memory_get_usage(true);
$memoryGrowth = $currentMemory - $this->initialMemory;
return $memoryGrowth > $this->memoryThresholdBytes;
}
/**
* Get current memory growth in bytes
*/
public function getCurrentMemoryGrowth(): int
{
return memory_get_usage(true) - $this->initialMemory;
}
/**
* Get current duration in seconds
*/
public function getCurrentDuration(): float
{
return microtime(true) - $this->startTime;
}
/**
* Force garbage collection and return freed memory
*/
public function forceGarbageCollection(): int
{
$memoryBefore = memory_get_usage(true);
gc_collect_cycles();
$memoryAfter = memory_get_usage(true);
return $memoryBefore - $memoryAfter;
}
/**
* Run a memory-intensive operation with monitoring
*
* @param callable $operation The operation to monitor
* @param int $iterations Number of iterations to run
* @param bool $gcBetweenIterations Whether to run GC between iterations
*/
public function monitorOperation(
callable $operation,
int $iterations = 1,
bool $gcBetweenIterations = false
): array {
$measurements = [];
$this->reset();
for ($i = 0; $i < $iterations; $i++) {
$iterationStart = memory_get_usage(true);
$timeStart = microtime(true);
$operation($i);
$iterationEnd = memory_get_usage(true);
$timeEnd = microtime(true);
$measurements[] = [
'iteration' => $i,
'memory_before' => $iterationStart,
'memory_after' => $iterationEnd,
'memory_growth' => $iterationEnd - $iterationStart,
'duration' => $timeEnd - $timeStart,
'total_memory_growth' => $iterationEnd - $this->initialMemory,
];
if ($gcBetweenIterations && $i < $iterations - 1) {
gc_collect_cycles();
}
}
return $measurements;
}
/**
* Analyze memory growth pattern to detect leaks
*/
public function analyzeGrowthPattern(array $measurements): array
{
if (empty($measurements)) {
return ['pattern' => 'no_data', 'severity' => 'none'];
}
$growthRates = [];
$totalGrowth = end($measurements)['total_memory_growth'];
foreach ($measurements as $i => $measurement) {
if ($i > 0) {
$prevTotal = $measurements[$i - 1]['total_memory_growth'];
$currentTotal = $measurement['total_memory_growth'];
$growthRates[] = $currentTotal - $prevTotal;
}
}
$avgGrowthRate = array_sum($growthRates) / count($growthRates);
$maxGrowthRate = max($growthRates);
$growthVariance = $this->calculateVariance($growthRates);
// Classify growth pattern
$pattern = 'stable';
$severity = 'none';
if ($avgGrowthRate > 10 * 1024) { // > 10KB per iteration
$pattern = 'linear_growth';
$severity = $avgGrowthRate > 100 * 1024 ? 'high' : 'medium';
}
if ($growthVariance > 50 * 1024 * 1024) { // High variance
$pattern = 'erratic_growth';
$severity = 'medium';
}
if ($totalGrowth > $this->memoryThresholdBytes) {
$severity = 'high';
}
return [
'pattern' => $pattern,
'severity' => $severity,
'total_growth' => $totalGrowth,
'avg_growth_rate' => $avgGrowthRate,
'max_growth_rate' => $maxGrowthRate,
'growth_variance' => $growthVariance,
'recommendations' => $this->generateRecommendations($pattern, $severity, $avgGrowthRate),
];
}
/**
* Calculate variance of an array of numbers
*/
private function calculateVariance(array $numbers): float
{
if (empty($numbers)) {
return 0.0;
}
$mean = array_sum($numbers) / count($numbers);
$squaredDiffs = array_map(fn ($x) => ($x - $mean) ** 2, $numbers);
return array_sum($squaredDiffs) / count($numbers);
}
/**
* Generate recommendations based on growth pattern
*/
private function generateRecommendations(string $pattern, string $severity, float $avgGrowthRate): array
{
$recommendations = [];
switch ($pattern) {
case 'linear_growth':
$recommendations[] = 'Implement cache size limits or LRU eviction';
$recommendations[] = 'Review object lifecycle and ensure proper cleanup';
break;
case 'erratic_growth':
$recommendations[] = 'Investigate irregular memory allocations';
$recommendations[] = 'Check for memory spikes during specific operations';
break;
}
if ($severity === 'high') {
$recommendations[] = 'Critical: Memory usage exceeds safe thresholds';
$recommendations[] = 'Profile application with memory debugging tools';
}
if ($avgGrowthRate > 50 * 1024) {
$recommendations[] = 'Consider implementing garbage collection triggers';
$recommendations[] = 'Review data structure choices for memory efficiency';
}
return $recommendations;
}
/**
* Set custom memory threshold
*/
public function setMemoryThreshold(int $bytes): self
{
$this->memoryThresholdBytes = $bytes;
return $this;
}
/**
* Set custom time threshold
*/
public function setTimeThreshold(float $seconds): self
{
$this->timeThresholdSeconds = $seconds;
return $this;
}
}
/**
* Memory leak analysis report
*/
final readonly class MemoryLeakReport
{
public function __construct(
public int $initialMemory,
public int $finalMemory,
public int $memoryGrowth,
public float $duration,
public Statistics $initialStats,
public ?Statistics $finalStats,
public array $thresholds
) {
}
/**
* Check if this report indicates a memory leak
*/
public function hasMemoryLeak(): bool
{
return $this->memoryGrowth > $this->thresholds['memory'];
}
/**
* Check if operation was too slow
*/
public function isSlowPerformance(): bool
{
return $this->duration > $this->thresholds['time'];
}
/**
* Get memory growth in MB
*/
public function getMemoryGrowthMB(): float
{
return $this->memoryGrowth / (1024 * 1024);
}
/**
* Get comprehensive summary
*/
public function getSummary(): array
{
$summary = [
'memory_growth_bytes' => $this->memoryGrowth,
'memory_growth_mb' => $this->getMemoryGrowthMB(),
'duration_seconds' => $this->duration,
'has_memory_leak' => $this->hasMemoryLeak(),
'is_slow_performance' => $this->isSlowPerformance(),
];
if ($this->finalStats !== null) {
$summary['cache_growth'] = $this->finalStats->getTotalCount() - $this->initialStats->getTotalCount();
if ($this->finalStats->getMemoryUsageMb() !== null) {
$initialMemoryMB = $this->initialStats->getMemoryUsageMb() ?? 0;
$finalMemoryMB = $this->finalStats->getMemoryUsageMb();
$summary['reported_memory_growth_mb'] = $finalMemoryMB - $initialMemoryMB;
}
}
return $summary;
}
/**
* Format report for display
*/
public function format(): string
{
$output = "\n=== Memory Leak Analysis Report ===\n";
$output .= sprintf(
"Memory Growth: %s bytes (%.2f MB)\n",
number_format($this->memoryGrowth),
$this->getMemoryGrowthMB()
);
$output .= sprintf("Duration: %.3f seconds\n", $this->duration);
$output .= sprintf("Leak Detected: %s\n", $this->hasMemoryLeak() ? 'YES' : 'NO');
$output .= sprintf("Performance Issue: %s\n", $this->isSlowPerformance() ? 'YES' : 'NO');
if ($this->finalStats !== null) {
$cacheGrowth = $this->finalStats->getTotalCount() - $this->initialStats->getTotalCount();
$output .= sprintf("Cache Items Growth: %d\n", $cacheGrowth);
if ($this->finalStats->getMemoryUsageMb() !== null) {
$memoryGrowth = $this->finalStats->getMemoryUsageMb() - ($this->initialStats->getMemoryUsageMb() ?? 0);
$output .= sprintf("Reported Memory Growth: %.2f MB\n", $memoryGrowth);
}
}
$output .= "===================================\n";
return $output;
}
}