- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
256 lines
7.5 KiB
PHP
256 lines
7.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Discovery;
|
|
|
|
use App\Framework\Core\ValueObjects\Byte;
|
|
use App\Framework\Core\ValueObjects\GrowthRate;
|
|
use App\Framework\Core\ValueObjects\Percentage;
|
|
use App\Framework\DateTime\Clock;
|
|
use App\Framework\Discovery\ValueObjects\MemoryLeakInfo;
|
|
use App\Framework\Logging\Logger;
|
|
use App\Framework\Performance\MemoryMonitor;
|
|
|
|
/**
|
|
* Advanced memory leak prevention and detection system
|
|
*/
|
|
final class MemoryGuard
|
|
{
|
|
/** @var array<string, int> Tracking memory usage per operation */
|
|
private array $memoryCheckpoints = [];
|
|
|
|
/** @var array<string, MemoryLeakInfo> Detected memory leaks */
|
|
private array $detectedLeaks = [];
|
|
|
|
/** @var int Maximum allowed memory growth per operation (5MB) */
|
|
private const int MAX_MEMORY_GROWTH = 5 * 1024 * 1024;
|
|
|
|
/** @var float Memory leak detection threshold (50% growth) */
|
|
private const float LEAK_THRESHOLD = 1.5;
|
|
|
|
public function __construct(
|
|
private readonly MemoryMonitor $memoryMonitor,
|
|
private readonly Clock $clock,
|
|
private readonly ?Logger $logger = null
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Create a memory checkpoint before an operation
|
|
*/
|
|
public function checkpoint(string $operation): void
|
|
{
|
|
// Force garbage collection before checkpoint
|
|
$this->forceCleanup();
|
|
|
|
$currentMemory = $this->memoryMonitor->getCurrentMemory()->toBytes();
|
|
$this->memoryCheckpoints[$operation] = $currentMemory;
|
|
|
|
$this->logger?->debug("Memory checkpoint created for {$operation}: {$currentMemory} bytes");
|
|
}
|
|
|
|
/**
|
|
* Validate memory usage after an operation
|
|
* @return bool True if memory usage is acceptable, false if potential leak detected
|
|
*/
|
|
public function validate(string $operation): bool
|
|
{
|
|
if (! isset($this->memoryCheckpoints[$operation])) {
|
|
$this->logger?->warning("No checkpoint found for operation: {$operation}");
|
|
|
|
return true;
|
|
}
|
|
|
|
// Force cleanup before measuring
|
|
$this->forceCleanup();
|
|
|
|
$startMemory = $this->memoryCheckpoints[$operation];
|
|
$currentMemory = $this->memoryMonitor->getCurrentMemory()->toBytes();
|
|
$memoryGrowth = $currentMemory - $startMemory;
|
|
|
|
// Remove checkpoint
|
|
unset($this->memoryCheckpoints[$operation]);
|
|
|
|
// Check for excessive memory growth
|
|
if ($memoryGrowth > self::MAX_MEMORY_GROWTH) {
|
|
$leakInfo = new MemoryLeakInfo(
|
|
operation: $operation,
|
|
startMemory: Byte::fromBytes($startMemory),
|
|
endMemory: Byte::fromBytes($currentMemory),
|
|
growth: Byte::fromBytes($memoryGrowth),
|
|
growthPercentage: GrowthRate::fromMemoryValues($currentMemory, $startMemory),
|
|
timestamp: $this->clock->time()
|
|
);
|
|
|
|
$this->detectedLeaks[$operation] = $leakInfo;
|
|
|
|
$this->logger?->error("Potential memory leak detected in {$operation}: {$leakInfo}");
|
|
|
|
// Attempt aggressive cleanup
|
|
$this->aggressiveCleanup();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Check for percentage-based leak
|
|
if ($startMemory > 0 && $currentMemory > $startMemory * self::LEAK_THRESHOLD) {
|
|
$growthPercentage = ($currentMemory / $startMemory - 1) * 100;
|
|
$this->logger?->warning(
|
|
"High memory growth detected in {$operation}: {$growthPercentage}% increase"
|
|
);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Execute a callback with memory protection
|
|
* @template T
|
|
* @param callable(): T $callback
|
|
* @return T
|
|
*/
|
|
public function protect(string $operation, callable $callback): mixed
|
|
{
|
|
$this->checkpoint($operation);
|
|
|
|
try {
|
|
$result = $callback();
|
|
|
|
if (! $this->validate($operation)) {
|
|
$this->logger?->warning("Memory leak detected after {$operation}");
|
|
}
|
|
|
|
return $result;
|
|
} catch (\Throwable $e) {
|
|
// Clean up checkpoint on error
|
|
unset($this->memoryCheckpoints[$operation]);
|
|
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process data in chunks with memory protection
|
|
* @template T
|
|
* @param array<T> $items
|
|
* @param callable(T): void $processor
|
|
*/
|
|
public function processWithMemoryGuard(array $items, callable $processor, int $chunkSize = 100): void
|
|
{
|
|
$chunks = array_chunk($items, $chunkSize);
|
|
$chunkNumber = 0;
|
|
|
|
foreach ($chunks as $chunk) {
|
|
$chunkNumber++;
|
|
$operation = "chunk_{$chunkNumber}";
|
|
|
|
$this->protect($operation, function () use ($chunk, $processor) {
|
|
foreach ($chunk as $item) {
|
|
$processor($item);
|
|
}
|
|
});
|
|
|
|
// Check if we're approaching memory limits
|
|
if ($this->isMemoryPressureHigh()) {
|
|
$this->logger?->warning("High memory pressure detected, forcing cleanup");
|
|
$this->aggressiveCleanup();
|
|
|
|
// Reduce chunk size for next iteration
|
|
$chunkSize = max(10, (int)($chunkSize * 0.5));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Force garbage collection and cleanup
|
|
*/
|
|
public function forceCleanup(): void
|
|
{
|
|
// Multiple GC runs for thorough cleanup
|
|
gc_collect_cycles();
|
|
gc_collect_cycles();
|
|
|
|
// PHP 7.3+ memory cache cleanup
|
|
if (function_exists('gc_mem_caches')) {
|
|
gc_mem_caches();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Aggressive memory cleanup for critical situations
|
|
*/
|
|
public function aggressiveCleanup(): void
|
|
{
|
|
// Clear all internal caches
|
|
$this->clearInternalCaches();
|
|
|
|
// Force multiple GC runs
|
|
for ($i = 0; $i < 3; $i++) {
|
|
gc_collect_cycles();
|
|
usleep(10000); // 10ms pause between runs
|
|
}
|
|
|
|
// Clear opcache if available
|
|
if (function_exists('opcache_reset')) {
|
|
opcache_reset();
|
|
}
|
|
|
|
// Clear realpath cache
|
|
clearstatcache(true);
|
|
|
|
$this->logger?->info("Aggressive memory cleanup completed");
|
|
}
|
|
|
|
/**
|
|
* Check if memory pressure is high
|
|
*/
|
|
public function isMemoryPressureHigh(): bool
|
|
{
|
|
$usage = $this->memoryMonitor->getMemoryUsagePercentage();
|
|
|
|
return $usage->greaterThan(Percentage::from(80.0));
|
|
}
|
|
|
|
/**
|
|
* Get detected memory leaks
|
|
* @return array<string, MemoryLeakInfo>
|
|
*/
|
|
public function getDetectedLeaks(): array
|
|
{
|
|
return $this->detectedLeaks;
|
|
}
|
|
|
|
/**
|
|
* Clear detected leaks history
|
|
*/
|
|
public function clearLeakHistory(): void
|
|
{
|
|
$this->detectedLeaks = [];
|
|
}
|
|
|
|
/**
|
|
* Get memory guard statistics
|
|
*/
|
|
public function getStatistics(): array
|
|
{
|
|
return [
|
|
'active_checkpoints' => count($this->memoryCheckpoints),
|
|
'detected_leaks' => count($this->detectedLeaks),
|
|
'current_memory' => $this->memoryMonitor->getCurrentMemory()->toHumanReadable(),
|
|
'peak_memory' => $this->memoryMonitor->getPeakMemory()->toHumanReadable(),
|
|
'memory_usage' => $this->memoryMonitor->getMemoryUsagePercentage()->getValue() . '%',
|
|
'is_high_pressure' => $this->isMemoryPressureHigh(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Clear internal caches (to be extended by specific implementations)
|
|
*/
|
|
private function clearInternalCaches(): void
|
|
{
|
|
// Clear any static caches
|
|
// This is a hook for clearing application-specific caches
|
|
}
|
|
}
|