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
This commit is contained in:
32
src/Framework/Discovery/Memory/BatchParameters.php
Normal file
32
src/Framework/Discovery/Memory/BatchParameters.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery\Memory;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
use App\Framework\Discovery\ValueObjects\MemoryStrategy;
|
||||
|
||||
/**
|
||||
* Batch processing parameters
|
||||
*/
|
||||
final readonly class BatchParameters
|
||||
{
|
||||
public function __construct(
|
||||
public int $chunkSize,
|
||||
public int $batchCount,
|
||||
public Byte $estimatedMemoryPerBatch,
|
||||
public MemoryStrategy $strategy
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'chunk_size' => $this->chunkSize,
|
||||
'batch_count' => $this->batchCount,
|
||||
'estimated_memory_per_batch' => $this->estimatedMemoryPerBatch->toHumanReadable(),
|
||||
'strategy' => $this->strategy->value,
|
||||
];
|
||||
}
|
||||
}
|
||||
387
src/Framework/Discovery/Memory/DiscoveryMemoryManager.php
Normal file
387
src/Framework/Discovery/Memory/DiscoveryMemoryManager.php
Normal file
@@ -0,0 +1,387 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery\Memory;
|
||||
|
||||
use App\Framework\Core\Events\EventDispatcher;
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\Discovery\Events\MemoryCleanupEvent;
|
||||
use App\Framework\Discovery\Events\MemoryLeakDetectedEvent;
|
||||
use App\Framework\Discovery\Events\MemoryPressureEvent;
|
||||
use App\Framework\Discovery\Events\MemoryStrategyChangedEvent;
|
||||
use App\Framework\Discovery\ValueObjects\MemoryStrategy;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Performance\MemoryMonitor;
|
||||
|
||||
/**
|
||||
* Centralized memory management for Discovery operations
|
||||
*
|
||||
* Provides intelligent memory monitoring, adaptive chunking, and leak detection
|
||||
* to ensure stable and efficient discovery processing regardless of codebase size.
|
||||
*/
|
||||
final readonly class DiscoveryMemoryManager
|
||||
{
|
||||
private Byte $memoryLimit;
|
||||
|
||||
private Byte $warningThreshold;
|
||||
|
||||
private Byte $criticalThreshold;
|
||||
|
||||
private int $leakDetectionWindow;
|
||||
|
||||
public function __construct(
|
||||
private MemoryStrategy $strategy,
|
||||
Byte $memoryLimit,
|
||||
private float $memoryPressureThreshold = 0.8,
|
||||
private ?MemoryMonitor $memoryMonitor = null,
|
||||
private ?Logger $logger = null,
|
||||
private ?EventDispatcher $eventDispatcher = null,
|
||||
private ?Clock $clock = null
|
||||
) {
|
||||
$this->memoryLimit = $memoryLimit;
|
||||
$this->warningThreshold = $memoryLimit->multiply($this->memoryPressureThreshold);
|
||||
$this->criticalThreshold = $memoryLimit->multiply(0.95);
|
||||
$this->leakDetectionWindow = 50; // Check for leaks every 50 operations
|
||||
}
|
||||
|
||||
/**
|
||||
* Create memory manager with auto-suggested strategy
|
||||
*/
|
||||
public static function createWithSuggestion(
|
||||
Byte $memoryLimit,
|
||||
int $estimatedFileCount,
|
||||
?MemoryMonitor $memoryMonitor = null,
|
||||
?Logger $logger = null
|
||||
): self {
|
||||
$strategy = MemoryStrategy::suggestForDiscovery(
|
||||
$estimatedFileCount,
|
||||
(int) $memoryLimit->toMegabytes(),
|
||||
$memoryMonitor !== null
|
||||
);
|
||||
|
||||
return new self($strategy, $memoryLimit, 0.8, $memoryMonitor, $logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current memory status and emit events if needed
|
||||
*/
|
||||
public function getMemoryStatus(?string $context = null): MemoryStatusInfo
|
||||
{
|
||||
$currentUsage = Byte::fromBytes(memory_get_usage(true));
|
||||
$peakUsage = Byte::fromBytes(memory_get_peak_usage(true));
|
||||
$availableMemory = $this->memoryLimit->subtract($currentUsage);
|
||||
$memoryPressure = $currentUsage->percentOf($this->memoryLimit);
|
||||
|
||||
$status = match (true) {
|
||||
$currentUsage->greaterThan($this->criticalThreshold) => MemoryStatus::CRITICAL,
|
||||
$currentUsage->greaterThan($this->warningThreshold) => MemoryStatus::WARNING,
|
||||
default => MemoryStatus::NORMAL
|
||||
};
|
||||
|
||||
// Emit memory pressure event if not normal
|
||||
if ($status !== MemoryStatus::NORMAL && $this->eventDispatcher && $this->clock) {
|
||||
$this->eventDispatcher->dispatch(new MemoryPressureEvent(
|
||||
status: $status,
|
||||
currentUsage: $currentUsage,
|
||||
memoryLimit: $this->memoryLimit,
|
||||
memoryPressure: $memoryPressure,
|
||||
strategy: $this->strategy,
|
||||
context: $context,
|
||||
timestamp: $this->clock->time()
|
||||
));
|
||||
}
|
||||
|
||||
return new MemoryStatusInfo(
|
||||
status: $status,
|
||||
currentUsage: $currentUsage,
|
||||
peakUsage: $peakUsage,
|
||||
memoryLimit: $this->memoryLimit,
|
||||
availableMemory: $availableMemory,
|
||||
memoryPressure: $memoryPressure,
|
||||
strategy: $this->strategy
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate optimal chunk size based on current memory pressure
|
||||
*/
|
||||
public function calculateOptimalChunkSize(int $totalItems): int
|
||||
{
|
||||
$memoryInfo = $this->getMemoryStatus();
|
||||
$baseChunkSize = $this->strategy->getDefaultChunkSize();
|
||||
|
||||
// Adaptive strategy adjusts chunk size based on memory pressure
|
||||
if ($this->strategy->supportsDynamicAdjustment()) {
|
||||
$pressureRatio = $memoryInfo->memoryPressure->toDecimal();
|
||||
|
||||
// Reduce chunk size as memory pressure increases
|
||||
$adjustmentFactor = match (true) {
|
||||
$pressureRatio > 0.9 => 0.25, // Critical: Very small chunks
|
||||
$pressureRatio > 0.8 => 0.5, // High pressure: Half size
|
||||
$pressureRatio > 0.6 => 0.75, // Medium pressure: 3/4 size
|
||||
default => 1.0 // Normal: Full size
|
||||
};
|
||||
|
||||
$adjustedChunkSize = (int) ($baseChunkSize * $adjustmentFactor);
|
||||
} else {
|
||||
$adjustedChunkSize = $baseChunkSize;
|
||||
}
|
||||
|
||||
// Ensure chunk size is reasonable
|
||||
$minChunkSize = 1;
|
||||
$maxChunkSize = min($totalItems, 1000);
|
||||
|
||||
return max($minChunkSize, min($adjustedChunkSize, $maxChunkSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if memory cleanup is needed
|
||||
*/
|
||||
public function shouldCleanup(int $processedItems): bool
|
||||
{
|
||||
$cleanupFrequency = $this->strategy->getCleanupFrequency();
|
||||
|
||||
// Always cleanup if memory pressure is high
|
||||
$memoryInfo = $this->getMemoryStatus();
|
||||
if ($memoryInfo->status === MemoryStatus::WARNING || $memoryInfo->status === MemoryStatus::CRITICAL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Regular cleanup based on strategy
|
||||
return $processedItems > 0 && ($processedItems % $cleanupFrequency) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform memory cleanup and emit cleanup event
|
||||
*/
|
||||
public function performCleanup(bool $isEmergency = false, ?string $triggerReason = null, ?string $context = null): MemoryCleanupResult
|
||||
{
|
||||
$beforeUsage = Byte::fromBytes(memory_get_usage(true));
|
||||
$beforePeak = Byte::fromBytes(memory_get_peak_usage(true));
|
||||
|
||||
// Force garbage collection
|
||||
$collectedCycles = gc_collect_cycles();
|
||||
|
||||
// Clear any internal caches that might be holding references
|
||||
if (function_exists('opcache_reset')) {
|
||||
opcache_reset();
|
||||
}
|
||||
|
||||
$afterUsage = Byte::fromBytes(memory_get_usage(true));
|
||||
$afterPeak = Byte::fromBytes(memory_get_peak_usage(true));
|
||||
|
||||
$memoryFreed = $beforeUsage->greaterThan($afterUsage)
|
||||
? $beforeUsage->subtract($afterUsage)
|
||||
: Byte::zero();
|
||||
|
||||
$result = new MemoryCleanupResult(
|
||||
beforeUsage: $beforeUsage,
|
||||
afterUsage: $afterUsage,
|
||||
memoryFreed: $memoryFreed,
|
||||
collectedCycles: $collectedCycles
|
||||
);
|
||||
|
||||
// Emit cleanup event
|
||||
if ($this->eventDispatcher && $this->clock) {
|
||||
$this->eventDispatcher->dispatch(new MemoryCleanupEvent(
|
||||
beforeUsage: $beforeUsage,
|
||||
afterUsage: $afterUsage,
|
||||
memoryFreed: $memoryFreed,
|
||||
collectedCycles: $collectedCycles,
|
||||
wasEmergency: $isEmergency,
|
||||
triggerReason: $triggerReason,
|
||||
context: $context,
|
||||
timestamp: $this->clock->time()
|
||||
));
|
||||
}
|
||||
|
||||
$this->logger?->debug('Memory cleanup performed', [
|
||||
'before_usage' => $beforeUsage->toHumanReadable(),
|
||||
'after_usage' => $afterUsage->toHumanReadable(),
|
||||
'memory_freed' => $memoryFreed->toHumanReadable(),
|
||||
'collected_cycles' => $collectedCycles,
|
||||
'strategy' => $this->strategy->value,
|
||||
'was_emergency' => $isEmergency,
|
||||
'trigger_reason' => $triggerReason,
|
||||
]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for potential memory leaks and emit leak event if detected
|
||||
*/
|
||||
public function checkForMemoryLeaks(array $memoryHistory, ?string $context = null): ?MemoryLeakInfo
|
||||
{
|
||||
if (count($memoryHistory) < $this->leakDetectionWindow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Take recent measurements
|
||||
$recentHistory = array_slice($memoryHistory, -$this->leakDetectionWindow);
|
||||
$usageValues = array_map(fn (Byte $usage) => $usage->toBytes(), $recentHistory);
|
||||
|
||||
// Calculate trend using linear regression
|
||||
$n = count($usageValues);
|
||||
$sumX = array_sum(range(0, $n - 1));
|
||||
$sumY = array_sum($usageValues);
|
||||
$sumXY = 0;
|
||||
$sumXX = 0;
|
||||
|
||||
foreach ($usageValues as $i => $y) {
|
||||
$sumXY += $i * $y;
|
||||
$sumXX += $i * $i;
|
||||
}
|
||||
|
||||
$slope = ($n * $sumXY - $sumX * $sumY) / ($n * $sumXX - $sumX * $sumX);
|
||||
$intercept = ($sumY - $slope * $sumX) / $n;
|
||||
|
||||
// If slope is significantly positive, we might have a leak
|
||||
$averageUsage = array_sum($usageValues) / $n;
|
||||
$leakThreshold = $averageUsage * 0.02; // 2% growth per measurement
|
||||
|
||||
if ($slope > $leakThreshold) {
|
||||
$firstUsage = Byte::fromBytes($usageValues[0]);
|
||||
$lastUsage = Byte::fromBytes($usageValues[$n - 1]);
|
||||
$growthRate = Byte::fromBytes((int) $slope);
|
||||
$severity = $this->calculateLeakSeverity($slope, $averageUsage);
|
||||
|
||||
$leakInfo = new MemoryLeakInfo(
|
||||
detectedAt: $lastUsage,
|
||||
growthRate: $growthRate,
|
||||
windowSize: $this->leakDetectionWindow,
|
||||
severity: $severity
|
||||
);
|
||||
|
||||
// Emit memory leak event
|
||||
if ($this->eventDispatcher && $this->clock) {
|
||||
$this->eventDispatcher->dispatch(new MemoryLeakDetectedEvent(
|
||||
detectedAt: $lastUsage,
|
||||
growthRate: $growthRate,
|
||||
severity: $severity,
|
||||
windowSize: $this->leakDetectionWindow,
|
||||
context: $context,
|
||||
timestamp: $this->clock->time()
|
||||
));
|
||||
}
|
||||
|
||||
return $leakInfo;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory guard for continuous monitoring
|
||||
*/
|
||||
public function createMemoryGuard(?callable $emergencyCallback = null): MemoryGuard
|
||||
{
|
||||
return new MemoryGuard(
|
||||
memoryManager: $this,
|
||||
emergencyCallback: $emergencyCallback ?? function () {
|
||||
$this->logger?->critical('Emergency memory cleanup triggered');
|
||||
$this->performCleanup();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate adaptive batch parameters for streaming operations
|
||||
*/
|
||||
public function calculateBatchParameters(int $totalItems, Byte $itemSizeEstimate): BatchParameters
|
||||
{
|
||||
$memoryInfo = $this->getMemoryStatus();
|
||||
$availableMemory = $memoryInfo->availableMemory;
|
||||
|
||||
// Reserve some memory for processing overhead
|
||||
$usableMemory = $availableMemory->multiply(0.8);
|
||||
|
||||
// Calculate how many items can fit in available memory
|
||||
$maxItemsInMemory = $itemSizeEstimate->isEmpty()
|
||||
? $this->strategy->getDefaultChunkSize()
|
||||
: (int) ($usableMemory->toBytes() / $itemSizeEstimate->toBytes());
|
||||
|
||||
$optimalChunkSize = $this->calculateOptimalChunkSize($totalItems);
|
||||
$finalChunkSize = min($maxItemsInMemory, $optimalChunkSize);
|
||||
|
||||
// Calculate number of batches needed
|
||||
$batchCount = (int) ceil($totalItems / $finalChunkSize);
|
||||
|
||||
return new BatchParameters(
|
||||
chunkSize: max(1, $finalChunkSize),
|
||||
batchCount: $batchCount,
|
||||
estimatedMemoryPerBatch: $itemSizeEstimate->multiply($finalChunkSize),
|
||||
strategy: $this->strategy
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get strategy description for debugging
|
||||
*/
|
||||
public function getStrategyDescription(): string
|
||||
{
|
||||
return $this->strategy->getDescription();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change memory strategy and emit strategy changed event
|
||||
*/
|
||||
public function changeStrategy(MemoryStrategy $newStrategy, string $changeReason, ?array $triggerMetrics = null, ?string $context = null): void
|
||||
{
|
||||
if ($newStrategy === $this->strategy) {
|
||||
return; // No change needed
|
||||
}
|
||||
|
||||
$previousStrategy = $this->strategy;
|
||||
$this->strategy = $newStrategy;
|
||||
|
||||
// Recalculate thresholds for new strategy
|
||||
$this->warningThreshold = $this->memoryLimit->multiply($this->memoryPressureThreshold);
|
||||
$this->criticalThreshold = $this->memoryLimit->multiply(0.95);
|
||||
|
||||
// Emit strategy changed event
|
||||
if ($this->eventDispatcher && $this->clock) {
|
||||
$this->eventDispatcher->dispatch(new MemoryStrategyChangedEvent(
|
||||
previousStrategy: $previousStrategy,
|
||||
newStrategy: $newStrategy,
|
||||
changeReason: $changeReason,
|
||||
triggerMetrics: $triggerMetrics,
|
||||
context: $context,
|
||||
timestamp: $this->clock->time()
|
||||
));
|
||||
}
|
||||
|
||||
$this->logger?->info('Memory strategy changed', [
|
||||
'previous_strategy' => $previousStrategy->value,
|
||||
'new_strategy' => $newStrategy->value,
|
||||
'change_reason' => $changeReason,
|
||||
'trigger_metrics' => $triggerMetrics,
|
||||
'context' => $context,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current memory strategy
|
||||
*/
|
||||
public function getCurrentStrategy(): MemoryStrategy
|
||||
{
|
||||
return $this->strategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate memory leak severity
|
||||
*/
|
||||
private function calculateLeakSeverity(float $slope, float $averageUsage): LeakSeverity
|
||||
{
|
||||
$relativeGrowth = $slope / $averageUsage;
|
||||
|
||||
return match (true) {
|
||||
$relativeGrowth > 0.05 => LeakSeverity::CRITICAL, // 5%+ growth
|
||||
$relativeGrowth > 0.03 => LeakSeverity::HIGH, // 3-5% growth
|
||||
$relativeGrowth > 0.02 => LeakSeverity::MEDIUM, // 2-3% growth
|
||||
default => LeakSeverity::LOW // < 2% growth
|
||||
};
|
||||
}
|
||||
}
|
||||
22
src/Framework/Discovery/Memory/GuardAction.php
Normal file
22
src/Framework/Discovery/Memory/GuardAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery\Memory;
|
||||
|
||||
/**
|
||||
* Guard actions that can be taken
|
||||
*/
|
||||
enum GuardAction: string
|
||||
{
|
||||
case WARNING_ISSUED = 'warning_issued';
|
||||
case EMERGENCY_CLEANUP = 'emergency_cleanup';
|
||||
case CLEANUP_SUGGESTED = 'cleanup_suggested';
|
||||
case CLEANUP_SUCCESSFUL = 'cleanup_successful';
|
||||
case CLEANUP_FAILED = 'cleanup_failed';
|
||||
case CRITICAL_LEAK_DETECTED = 'critical_leak_detected';
|
||||
case HIGH_LEAK_DETECTED = 'high_leak_detected';
|
||||
case MEDIUM_LEAK_DETECTED = 'medium_leak_detected';
|
||||
case LOW_LEAK_DETECTED = 'low_leak_detected';
|
||||
case EMERGENCY_MODE_RESET = 'emergency_mode_reset';
|
||||
}
|
||||
48
src/Framework/Discovery/Memory/GuardResult.php
Normal file
48
src/Framework/Discovery/Memory/GuardResult.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery\Memory;
|
||||
|
||||
/**
|
||||
* Result of a guard check
|
||||
*/
|
||||
final readonly class GuardResult
|
||||
{
|
||||
public function __construct(
|
||||
public MemoryStatusInfo $memoryStatus,
|
||||
public array $actions,
|
||||
public int $checkNumber,
|
||||
public bool $emergencyMode
|
||||
) {
|
||||
}
|
||||
|
||||
public function hasAction(GuardAction $action): bool
|
||||
{
|
||||
return in_array($action, $this->actions, true);
|
||||
}
|
||||
|
||||
public function hasAnyLeakDetection(): bool
|
||||
{
|
||||
return $this->hasAction(GuardAction::CRITICAL_LEAK_DETECTED) ||
|
||||
$this->hasAction(GuardAction::HIGH_LEAK_DETECTED) ||
|
||||
$this->hasAction(GuardAction::MEDIUM_LEAK_DETECTED) ||
|
||||
$this->hasAction(GuardAction::LOW_LEAK_DETECTED);
|
||||
}
|
||||
|
||||
public function requiresImmediateAction(): bool
|
||||
{
|
||||
return $this->hasAction(GuardAction::EMERGENCY_CLEANUP) ||
|
||||
$this->hasAction(GuardAction::CRITICAL_LEAK_DETECTED);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'memory_status' => $this->memoryStatus->toArray(),
|
||||
'actions' => array_map(fn ($action) => $action->value, $this->actions),
|
||||
'check_number' => $this->checkNumber,
|
||||
'emergency_mode' => $this->emergencyMode,
|
||||
];
|
||||
}
|
||||
}
|
||||
35
src/Framework/Discovery/Memory/GuardStatistics.php
Normal file
35
src/Framework/Discovery/Memory/GuardStatistics.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery\Memory;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
|
||||
/**
|
||||
* Guard statistics
|
||||
*/
|
||||
final readonly class GuardStatistics
|
||||
{
|
||||
public function __construct(
|
||||
public int $totalChecks,
|
||||
public MemoryStatusInfo $currentStatus,
|
||||
public Byte $averageUsage,
|
||||
public Byte $peakUsage,
|
||||
public int $historySize,
|
||||
public bool $emergencyMode
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'total_checks' => $this->totalChecks,
|
||||
'current_status' => $this->currentStatus->toArray(),
|
||||
'average_usage' => $this->averageUsage->toHumanReadable(),
|
||||
'peak_usage' => $this->peakUsage->toHumanReadable(),
|
||||
'history_size' => $this->historySize,
|
||||
'emergency_mode' => $this->emergencyMode,
|
||||
];
|
||||
}
|
||||
}
|
||||
16
src/Framework/Discovery/Memory/LeakSeverity.php
Normal file
16
src/Framework/Discovery/Memory/LeakSeverity.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery\Memory;
|
||||
|
||||
/**
|
||||
* Memory leak severity levels
|
||||
*/
|
||||
enum LeakSeverity: string
|
||||
{
|
||||
case LOW = 'low';
|
||||
case MEDIUM = 'medium';
|
||||
case HIGH = 'high';
|
||||
case CRITICAL = 'critical';
|
||||
}
|
||||
26
src/Framework/Discovery/Memory/MemoryCleanupResult.php
Normal file
26
src/Framework/Discovery/Memory/MemoryCleanupResult.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery\Memory;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
|
||||
/**
|
||||
* Memory cleanup result
|
||||
*/
|
||||
final readonly class MemoryCleanupResult
|
||||
{
|
||||
public function __construct(
|
||||
public Byte $beforeUsage,
|
||||
public Byte $afterUsage,
|
||||
public Byte $memoryFreed,
|
||||
public int $collectedCycles
|
||||
) {
|
||||
}
|
||||
|
||||
public function wasEffective(): bool
|
||||
{
|
||||
return $this->memoryFreed->greaterThan(Byte::zero()) || $this->collectedCycles > 0;
|
||||
}
|
||||
}
|
||||
205
src/Framework/Discovery/Memory/MemoryGuard.php
Normal file
205
src/Framework/Discovery/Memory/MemoryGuard.php
Normal file
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery\Memory;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
|
||||
/**
|
||||
* Memory guard for continuous memory monitoring during discovery operations
|
||||
*
|
||||
* Provides real-time memory monitoring with automatic emergency handling
|
||||
* to prevent out-of-memory conditions during long-running operations.
|
||||
*/
|
||||
final class MemoryGuard
|
||||
{
|
||||
private array $memoryHistory = [];
|
||||
|
||||
private int $checkCounter = 0;
|
||||
|
||||
private bool $emergencyMode = false;
|
||||
|
||||
private mixed $emergencyCallback = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly DiscoveryMemoryManager $memoryManager,
|
||||
?callable $emergencyCallback = null
|
||||
) {
|
||||
$this->emergencyCallback = $emergencyCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check memory status and take action if needed
|
||||
*/
|
||||
public function check(): GuardResult
|
||||
{
|
||||
$this->checkCounter++;
|
||||
$memoryStatus = $this->memoryManager->getMemoryStatus();
|
||||
|
||||
// Track memory history for leak detection
|
||||
$this->memoryHistory[] = $memoryStatus->currentUsage;
|
||||
|
||||
// Keep only recent history to prevent memory growth
|
||||
if (count($this->memoryHistory) > 100) {
|
||||
$this->memoryHistory = array_slice($this->memoryHistory, -50);
|
||||
}
|
||||
|
||||
$actions = [];
|
||||
|
||||
// Handle critical memory situations
|
||||
if ($memoryStatus->status === MemoryStatus::CRITICAL && ! $this->emergencyMode) {
|
||||
$this->emergencyMode = true;
|
||||
$actions[] = GuardAction::EMERGENCY_CLEANUP;
|
||||
|
||||
if ($this->emergencyCallback) {
|
||||
($this->emergencyCallback)();
|
||||
}
|
||||
|
||||
// Perform immediate cleanup
|
||||
$cleanupResult = $this->memoryManager->performCleanup();
|
||||
$actions[] = $cleanupResult->wasEffective()
|
||||
? GuardAction::CLEANUP_SUCCESSFUL
|
||||
: GuardAction::CLEANUP_FAILED;
|
||||
}
|
||||
|
||||
// Handle warning conditions
|
||||
if ($memoryStatus->status === MemoryStatus::WARNING) {
|
||||
$actions[] = GuardAction::WARNING_ISSUED;
|
||||
|
||||
// Suggest cleanup if it's time
|
||||
if ($this->memoryManager->shouldCleanup($this->checkCounter)) {
|
||||
$actions[] = GuardAction::CLEANUP_SUGGESTED;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for memory leaks periodically
|
||||
if ($this->checkCounter % 20 === 0) {
|
||||
$leakInfo = $this->memoryManager->checkForMemoryLeaks($this->memoryHistory);
|
||||
if ($leakInfo !== null) {
|
||||
$actions[] = match ($leakInfo->severity) {
|
||||
LeakSeverity::CRITICAL => GuardAction::CRITICAL_LEAK_DETECTED,
|
||||
LeakSeverity::HIGH => GuardAction::HIGH_LEAK_DETECTED,
|
||||
LeakSeverity::MEDIUM => GuardAction::MEDIUM_LEAK_DETECTED,
|
||||
LeakSeverity::LOW => GuardAction::LOW_LEAK_DETECTED
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Reset emergency mode if memory is back to normal
|
||||
if ($this->emergencyMode && $memoryStatus->status === MemoryStatus::NORMAL) {
|
||||
$this->emergencyMode = false;
|
||||
$actions[] = GuardAction::EMERGENCY_MODE_RESET;
|
||||
}
|
||||
|
||||
return new GuardResult(
|
||||
memoryStatus: $memoryStatus,
|
||||
actions: $actions,
|
||||
checkNumber: $this->checkCounter,
|
||||
emergencyMode: $this->emergencyMode
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force emergency cleanup
|
||||
*/
|
||||
public function forceEmergencyCleanup(): MemoryCleanupResult
|
||||
{
|
||||
$this->emergencyMode = true;
|
||||
|
||||
if ($this->emergencyCallback) {
|
||||
($this->emergencyCallback)();
|
||||
}
|
||||
|
||||
return $this->memoryManager->performCleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current memory statistics
|
||||
*/
|
||||
public function getStatistics(): GuardStatistics
|
||||
{
|
||||
$memoryStatus = $this->memoryManager->getMemoryStatus();
|
||||
|
||||
$historyCount = count($this->memoryHistory);
|
||||
$averageUsage = $historyCount > 0
|
||||
? Byte::fromBytes((int) (array_sum(array_map(fn ($byte) => $byte->toBytes(), $this->memoryHistory)) / $historyCount))
|
||||
: Byte::zero();
|
||||
|
||||
$peakUsage = $historyCount > 0
|
||||
? array_reduce($this->memoryHistory, fn ($max, $current) => $current->greaterThan($max ?? Byte::zero()) ? $current : $max, Byte::zero())
|
||||
: Byte::zero();
|
||||
|
||||
return new GuardStatistics(
|
||||
totalChecks: $this->checkCounter,
|
||||
currentStatus: $memoryStatus,
|
||||
averageUsage: $averageUsage,
|
||||
peakUsage: $peakUsage,
|
||||
historySize: $historyCount,
|
||||
emergencyMode: $this->emergencyMode
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset guard state
|
||||
*/
|
||||
public function reset(): void
|
||||
{
|
||||
$this->memoryHistory = [];
|
||||
$this->checkCounter = 0;
|
||||
$this->emergencyMode = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if it's safe to continue processing
|
||||
*/
|
||||
public function isSafeToProcess(): bool
|
||||
{
|
||||
$memoryStatus = $this->memoryManager->getMemoryStatus();
|
||||
|
||||
return $memoryStatus->status !== MemoryStatus::CRITICAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recommendations for current memory state
|
||||
* @return string[]
|
||||
*/
|
||||
public function getRecommendations(): array
|
||||
{
|
||||
$memoryStatus = $this->memoryManager->getMemoryStatus();
|
||||
$recommendations = [];
|
||||
|
||||
switch ($memoryStatus->status) {
|
||||
case MemoryStatus::CRITICAL:
|
||||
$recommendations[] = 'Immediate action required: Reduce batch size or stop processing';
|
||||
$recommendations[] = 'Consider switching to STREAMING memory strategy';
|
||||
$recommendations[] = 'Force garbage collection and cleanup caches';
|
||||
|
||||
break;
|
||||
|
||||
case MemoryStatus::WARNING:
|
||||
$recommendations[] = 'Consider reducing batch size';
|
||||
$recommendations[] = 'Enable more frequent cleanup cycles';
|
||||
$recommendations[] = 'Monitor for memory leaks';
|
||||
|
||||
break;
|
||||
|
||||
case MemoryStatus::NORMAL:
|
||||
if ($memoryStatus->memoryPressure->toDecimal() > 0.5) {
|
||||
$recommendations[] = 'Memory usage is moderate - consider optimization';
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for potential leaks
|
||||
if (count($this->memoryHistory) > 10) {
|
||||
$leakInfo = $this->memoryManager->checkForMemoryLeaks($this->memoryHistory);
|
||||
if ($leakInfo !== null) {
|
||||
$recommendations[] = "Memory leak detected ({$leakInfo->severity->value}): Review object retention";
|
||||
}
|
||||
}
|
||||
|
||||
return $recommendations;
|
||||
}
|
||||
}
|
||||
31
src/Framework/Discovery/Memory/MemoryLeakInfo.php
Normal file
31
src/Framework/Discovery/Memory/MemoryLeakInfo.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery\Memory;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
|
||||
/**
|
||||
* Memory leak detection information
|
||||
*/
|
||||
final readonly class MemoryLeakInfo
|
||||
{
|
||||
public function __construct(
|
||||
public Byte $detectedAt,
|
||||
public Byte $growthRate,
|
||||
public int $windowSize,
|
||||
public LeakSeverity $severity
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'detected_at' => $this->detectedAt->toHumanReadable(),
|
||||
'growth_rate' => $this->growthRate->toHumanReadable(),
|
||||
'window_size' => $this->windowSize,
|
||||
'severity' => $this->severity->value,
|
||||
];
|
||||
}
|
||||
}
|
||||
15
src/Framework/Discovery/Memory/MemoryStatus.php
Normal file
15
src/Framework/Discovery/Memory/MemoryStatus.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery\Memory;
|
||||
|
||||
/**
|
||||
* Memory status enumeration
|
||||
*/
|
||||
enum MemoryStatus: string
|
||||
{
|
||||
case NORMAL = 'normal';
|
||||
case WARNING = 'warning';
|
||||
case CRITICAL = 'critical';
|
||||
}
|
||||
39
src/Framework/Discovery/Memory/MemoryStatusInfo.php
Normal file
39
src/Framework/Discovery/Memory/MemoryStatusInfo.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery\Memory;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
use App\Framework\Core\ValueObjects\Percentage;
|
||||
use App\Framework\Discovery\ValueObjects\MemoryStrategy;
|
||||
|
||||
/**
|
||||
* Memory status information
|
||||
*/
|
||||
final readonly class MemoryStatusInfo
|
||||
{
|
||||
public function __construct(
|
||||
public MemoryStatus $status,
|
||||
public Byte $currentUsage,
|
||||
public Byte $peakUsage,
|
||||
public Byte $memoryLimit,
|
||||
public Byte $availableMemory,
|
||||
public Percentage $memoryPressure,
|
||||
public MemoryStrategy $strategy
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'status' => $this->status->value,
|
||||
'current_usage' => $this->currentUsage->toHumanReadable(),
|
||||
'peak_usage' => $this->peakUsage->toHumanReadable(),
|
||||
'memory_limit' => $this->memoryLimit->toHumanReadable(),
|
||||
'available_memory' => $this->availableMemory->toHumanReadable(),
|
||||
'memory_pressure' => $this->memoryPressure->toDecimal(),
|
||||
'strategy' => $this->strategy->value,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user