*/ private array $activeOperations = []; /** @var array */ private array $completedOperations = []; private int $maxHistorySize = 1000; public function __construct( private readonly Clock $clock, private readonly MemoryMonitor $memoryMonitor, private readonly ?Logger $logger = null, private readonly ?EventDispatcher $eventDispatcher = null ) { } /** * Start tracking an operation */ public function startOperation( string $operationId, PerformanceCategory $category, array $contextData = [] ): PerformanceSnapshot { $snapshot = PerformanceSnapshot::start( operationId: $operationId, category: $category, startTime: $this->clock->time(), startMemory: $this->memoryMonitor->getCurrentMemory(), contextData: $contextData ); $this->activeOperations[$operationId] = $snapshot; // Emit start event $this->eventDispatcher?->dispatch(new OperationStartedEvent( operationId: $operationId, category: $category, contextData: $contextData, timestamp: $snapshot->startTime )); $this->logger?->debug('Operation tracking started', [ 'operation_id' => $operationId, 'category' => $category->value, 'start_memory' => $snapshot->startMemory->toHumanReadable(), ]); return $snapshot; } /** * Update operation metrics during execution */ public function updateOperation( string $operationId, array $updates = [] ): ?PerformanceSnapshot { if (! isset($this->activeOperations[$operationId])) { $this->logger?->warning('Attempted to update non-existent operation', [ 'operation_id' => $operationId, ]); return null; } $snapshot = $this->activeOperations[$operationId]; // Update metrics based on provided data foreach ($updates as $key => $value) { $snapshot = match ($key) { 'cache_hits' => $snapshot->withCacheHits($snapshot->cacheHits + $value), 'cache_misses' => $snapshot->withCacheMisses($snapshot->cacheMisses + $value), 'items_processed' => $snapshot->withItemsProcessed($snapshot->itemsProcessed + $value), 'io_operations' => $snapshot->withIoOperations($snapshot->ioOperations + $value), 'errors' => $snapshot->withErrorsEncountered($snapshot->errorsEncountered + $value), default => $snapshot->withCustomMetric($key, $value) }; } // Update peak memory if current usage is higher $currentMemory = $this->memoryMonitor->getCurrentMemory(); if ($currentMemory->greaterThan($snapshot->peakMemory)) { $snapshot = $snapshot->withPeakMemory($currentMemory); } $this->activeOperations[$operationId] = $snapshot; return $snapshot; } /** * Complete operation tracking and generate final snapshot */ public function completeOperation(string $operationId): ?PerformanceSnapshot { if (! isset($this->activeOperations[$operationId])) { $this->logger?->warning('Attempted to complete non-existent operation', [ 'operation_id' => $operationId, ]); return null; } $snapshot = $this->activeOperations[$operationId]; $endTime = $this->clock->time(); $endMemory = $this->memoryMonitor->getCurrentMemory(); $duration = Duration::fromSeconds($endTime->toFloat() - $snapshot->startTime->toFloat()); $memoryDelta = $endMemory->subtract($snapshot->startMemory); // Complete the snapshot $completedSnapshot = $snapshot ->withEndTime($endTime) ->withEndMemory($endMemory) ->withDuration($duration) ->withMemoryDelta($memoryDelta); // Store in completed operations $this->addToHistory($completedSnapshot); // Emit completion event $this->eventDispatcher?->dispatch(new OperationCompletedEvent( snapshot: $completedSnapshot, timestamp: $endTime )); // Clean up active operation unset($this->activeOperations[$operationId]); $this->logger?->info('Operation tracking completed', [ 'operation_id' => $operationId, 'category' => $completedSnapshot->category->value, 'duration' => $duration->toMilliseconds() . 'ms', 'memory_used' => $memoryDelta->toHumanReadable(), 'items_processed' => $completedSnapshot->itemsProcessed, 'throughput' => round($completedSnapshot->getThroughput(), 2) . ' items/s', ]); return $completedSnapshot; } /** * Mark operation as failed */ public function failOperation(string $operationId, \Throwable $exception): ?PerformanceSnapshot { if (! isset($this->activeOperations[$operationId])) { $this->logger?->warning('Attempted to fail non-existent operation', [ 'operation_id' => $operationId, ]); return null; } // Complete the operation first $snapshot = $this->completeOperation($operationId); if ($snapshot !== null) { // Emit failure event $this->eventDispatcher?->dispatch(new OperationFailedEvent( snapshot: $snapshot, exception: $exception, timestamp: $this->clock->time() )); $this->logger?->error('Operation failed', [ 'operation_id' => $operationId, 'category' => $snapshot->category->value, 'error' => $exception->getMessage(), 'duration' => $snapshot->duration?->toMilliseconds() . 'ms', ]); } return $snapshot; } /** * Get active operation snapshot */ public function getActiveOperation(string $operationId): ?PerformanceSnapshot { return $this->activeOperations[$operationId] ?? null; } /** * Get completed operation snapshot */ public function getCompletedOperation(string $operationId): ?PerformanceSnapshot { return $this->completedOperations[$operationId] ?? null; } /** * Get all active operations */ public function getActiveOperations(): array { return $this->activeOperations; } /** * Get recent completed operations */ public function getRecentOperations(int $limit = 50): array { return array_slice($this->completedOperations, -$limit, preserve_keys: true); } /** * Get operations by category */ public function getOperationsByCategory(PerformanceCategory $category): array { return array_filter( $this->completedOperations, fn (PerformanceSnapshot $snapshot) => $snapshot->category === $category ); } /** * Get performance statistics */ public function getStatistics(): array { $activeCount = count($this->activeOperations); $completedCount = count($this->completedOperations); if ($completedCount === 0) { return [ 'active_operations' => $activeCount, 'completed_operations' => 0, 'average_duration' => 0.0, 'average_throughput' => 0.0, 'average_memory_usage' => '0 B', 'success_rate' => 0.0, ]; } // Calculate averages $totalDuration = 0; $totalThroughput = 0; $totalMemory = 0; $successfulOperations = 0; foreach ($this->completedOperations as $snapshot) { if ($snapshot->duration !== null) { $totalDuration += $snapshot->duration->toSeconds(); } $totalThroughput += $snapshot->getThroughput(); $totalMemory += $snapshot->peakMemory->toBytes(); if ($snapshot->getErrorRate() < 0.1) { // Less than 10% error rate considered success $successfulOperations++; } } $avgDuration = $totalDuration / $completedCount; $avgThroughput = $totalThroughput / $completedCount; $avgMemory = Byte::fromBytes((int) ($totalMemory / $completedCount)); $successRate = $successfulOperations / $completedCount; return [ 'active_operations' => $activeCount, 'completed_operations' => $completedCount, 'average_duration' => round($avgDuration, 3), 'average_throughput' => round($avgThroughput, 2), 'average_memory_usage' => $avgMemory->toHumanReadable(), 'success_rate' => round($successRate * 100, 1), ]; } /** * Get performance trends */ public function getTrends(int $samples = 20): array { $recent = array_slice($this->completedOperations, -$samples, preserve_keys: true); if (count($recent) < 2) { return ['trend' => 'insufficient_data']; } $durations = []; $throughputs = []; $memoryUsages = []; foreach ($recent as $snapshot) { if ($snapshot->duration !== null) { $durations[] = $snapshot->duration->toSeconds(); } $throughputs[] = $snapshot->getThroughput(); $memoryUsages[] = $snapshot->peakMemory->toBytes(); } return [ 'duration_trend' => $this->calculateTrend($durations), 'throughput_trend' => $this->calculateTrend($throughputs), 'memory_trend' => $this->calculateTrend($memoryUsages), 'sample_size' => count($recent), ]; } /** * Clear operation history */ public function clearHistory(): void { $this->completedOperations = []; $this->logger?->info('Operation history cleared'); } /** * Add completed operation to history with size management */ private function addToHistory(PerformanceSnapshot $snapshot): void { $this->completedOperations[$snapshot->operationId] = $snapshot; // Trim history to prevent memory growth if (count($this->completedOperations) > $this->maxHistorySize) { $this->completedOperations = array_slice( $this->completedOperations, -($this->maxHistorySize / 2), preserve_keys: true ); } } /** * Calculate trend for a series of values */ private function calculateTrend(array $values): string { if (count($values) < 3) { return 'stable'; } $midpoint = (int) (count($values) / 2); $firstHalf = array_slice($values, 0, $midpoint); $secondHalf = array_slice($values, $midpoint); $firstAvg = array_sum($firstHalf) / count($firstHalf); $secondAvg = array_sum($secondHalf) / count($secondHalf); $difference = ($secondAvg - $firstAvg) / max($firstAvg, 0.001); // Avoid division by zero if ($difference > 0.15) { return 'increasing'; } elseif ($difference < -0.15) { return 'decreasing'; } return 'stable'; } }