Files
michaelschiemer/src/Framework/Discovery/MemoryGuard.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

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