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