Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
360 lines
13 KiB
PHP
360 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\ReflectionLegacy\Cache\AttributeCache;
|
|
use App\Framework\ReflectionLegacy\Cache\ClassCache;
|
|
use App\Framework\ReflectionLegacy\Cache\MetadataCacheManager;
|
|
use App\Framework\ReflectionLegacy\Cache\MethodCache;
|
|
use App\Framework\ReflectionLegacy\Cache\ParameterCache;
|
|
use App\Framework\ReflectionLegacy\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']
|
|
);
|
|
}
|
|
}
|
|
}
|