- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
368 lines
13 KiB
PHP
368 lines
13 KiB
PHP
<?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";
|
|
}
|
|
}
|