Files
michaelschiemer/src/Framework/Performance/OperationTracker.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

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