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