Files
michaelschiemer/tests/Framework/Reflection/Support/MemoryLeakDetector.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

370 lines
11 KiB
PHP

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