- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
380 lines
12 KiB
PHP
380 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Performance;
|
|
|
|
use App\Framework\Core\Events\EventDispatcher;
|
|
use App\Framework\Core\ValueObjects\Byte;
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
use App\Framework\DateTime\Clock;
|
|
use App\Framework\Logging\Logger;
|
|
use App\Framework\Performance\Events\OperationCompletedEvent;
|
|
use App\Framework\Performance\Events\OperationFailedEvent;
|
|
use App\Framework\Performance\Events\OperationStartedEvent;
|
|
use App\Framework\Performance\ValueObjects\PerformanceSnapshot;
|
|
|
|
/**
|
|
* Tracks individual operations with detailed performance snapshots
|
|
*
|
|
* Provides comprehensive operation lifecycle tracking with events,
|
|
* metrics collection, and performance analysis.
|
|
*/
|
|
final class OperationTracker
|
|
{
|
|
/** @var array<string, PerformanceSnapshot> */
|
|
private array $activeOperations = [];
|
|
|
|
/** @var array<string, PerformanceSnapshot> */
|
|
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';
|
|
}
|
|
}
|