Files
michaelschiemer/tests/Framework/Reflection/Cache/ClassCacheMemoryLeakTest.php
Michael Schiemer 55a330b223 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
2025-08-11 20:13:26 +02:00

239 lines
7.8 KiB
PHP

<?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;
}
}