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:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,235 @@
# Performance Module Architecture
## Overview
This document describes the architecture and component structure of the Performance Module after the refactoring from a monolithic to a modular middleware-based system.
## Directory Structure
```
src/Framework/Performance/
├── README.md # Main documentation
├── ARCHITECTURE.md # This file - Architecture overview
├── PerformanceCategory.php # Enum for performance categories
├── PerformanceCollector.php # Central metrics collection
├── PerformanceConfig.php # Configuration for tracking settings
├── PerformanceMetric.php # Individual metric data structure
├── PerformanceReporter.php # Report generation (HTML/JSON/Text)
├── PerformanceService.php # Simplified API for developers
└── Middleware/ # Specialized middleware components
├── RequestPerformanceMiddleware.php
├── ControllerPerformanceMiddleware.php
├── RoutingPerformanceMiddleware.php
├── DatabasePerformanceMiddleware.php
├── CachePerformanceMiddleware.php
└── PerformanceDebugMiddleware.php
```
## Component Relationships
### Data Flow
```
Request → RequestPerformanceMiddleware
→ RoutingPerformanceMiddleware
→ ControllerPerformanceMiddleware
→ [Database/Cache Operations via their Middleware]
→ PerformanceDebugMiddleware
→ Response with Performance Data
```
### Core Components
#### PerformanceCollector
- **Purpose**: Central collection point for all metrics
- **Responsibilities**:
- Store timing data
- Track memory usage
- Manage active measurements
- Provide metric retrieval APIs
- **Usage**: Used by all middleware components to store metrics
#### PerformanceMetric
- **Purpose**: Represents a single performance metric
- **Data Stored**:
- Timing measurements (duration, start/end times)
- Memory usage
- Count statistics
- Context information
- **Features**:
- Statistical calculations (min, max, average)
- Serialization support
#### PerformanceConfig
- **Purpose**: Configuration for the entire performance system
- **Settings**:
- Enable/disable tracking per category
- Slow query thresholds
- Excluded paths
- Output formats
- **Flexibility**: Allows fine-grained control over what gets tracked
#### PerformanceReporter
- **Purpose**: Generate performance reports in multiple formats
- **Formats Supported**:
- HTML (with interactive debugging)
- JSON (for APIs and external tools)
- Text (for logs and CLI)
- Array (for programmatic use)
#### PerformanceService
- **Purpose**: Simplified API for application developers
- **Features**:
- Convenience methods for common operations
- High-level abstractions
- Easy integration into existing code
### Middleware Components
#### HTTP Layer Middleware
**RequestPerformanceMiddleware**
- **Priority**: `FIRST` (runs earliest)
- **Tracks**: Overall request performance, memory usage
- **Metrics**: Request time, memory consumption, success/failure rates
**RoutingPerformanceMiddleware**
- **Priority**: `ROUTING + 10` (after routing)
- **Tracks**: Route resolution performance
- **Metrics**: Route matching time, pattern usage, 404 rates
**ControllerPerformanceMiddleware**
- **Priority**: `CONTROLLER - 10` (before controller execution)
- **Tracks**: Controller and action performance
- **Metrics**: Controller execution time, action-specific performance
**PerformanceDebugMiddleware**
- **Priority**: `LAST` (runs latest)
- **Purpose**: Inject debug reports into responses
- **Features**: HTML injection, HTTP headers, AJAX support
#### Data Layer Middleware
**DatabasePerformanceMiddleware**
- **Interface**: Implements `Database\Middleware`
- **Tracks**: Database query performance
- **Metrics**: Query types, execution time, slow queries
- **Features**: Query sanitization, sensitive data protection
**CachePerformanceMiddleware**
- **Interface**: Implements `Cache\CacheMiddleware`
- **Tracks**: Cache operation performance
- **Metrics**: Hit/miss ratios, operation timing, data sizes
## Design Principles
### Modularity
- Each middleware focuses on a specific concern
- Components can be enabled/disabled independently
- Clear separation of responsibilities
### Performance
- Minimal overhead when disabled
- Efficient data structures
- Lazy report generation
### Flexibility
- Multiple output formats
- Configurable tracking levels
- Extensible middleware system
### Framework Integration
- Uses existing middleware patterns
- Follows framework conventions
- Integrates with DI container
## Migration from Old System
### Removed Components
- `PerformanceMeter` → Replaced by `PerformanceCollector`
- `PerformanceMarker` → Integrated into `PerformanceMetric`
- `PerformanceMeasurement` → Integrated into `PerformanceMetric`
- `MemoryUsageTracker` → Functionality moved to `PerformanceCollector`
- `PerformanceMiddleware` → Split into specialized middleware
- Example files → Replaced by comprehensive documentation
- Job-based logging → Simplified to in-memory collection
### Backwards Compatibility
- `PerformanceCategory` enum maintained
- Core concepts preserved
- API improvements without breaking changes
### Benefits of New Architecture
1. **Better Separation of Concerns**: Each middleware handles specific metrics
2. **Improved Performance**: More efficient data collection and storage
3. **Enhanced Flexibility**: Fine-grained control over tracking
4. **Better Integration**: Follows framework middleware patterns
5. **Easier Testing**: Smaller, focused components
6. **Reduced Complexity**: Eliminated redundant code
7. **Better Documentation**: Comprehensive guides and examples
## Extension Points
### Adding New Middleware
```php
class CustomPerformanceMiddleware implements HttpMiddleware
{
public function __construct(
private PerformanceCollector $collector,
private PerformanceConfig $config
) {}
public function __invoke(MiddlewareContext $context, callable $next, RequestStateManager $stateManager): MiddlewareContext
{
if (!$this->config->isTrackingEnabled(PerformanceCategory::CUSTOM)) {
return $next($context);
}
$this->collector->startTiming('custom_operation', PerformanceCategory::CUSTOM);
$result = $next($context);
$this->collector->endTiming('custom_operation');
return $result;
}
}
```
### Adding New Categories
```php
// Extend PerformanceCategory enum
enum PerformanceCategory: string
{
// ... existing cases
case THIRD_PARTY = 'third_party';
case EXTERNAL_API = 'external_api';
}
// Update PerformanceConfig to handle new categories
public function isTrackingEnabled(PerformanceCategory $category): bool
{
return match ($category) {
// ... existing cases
PerformanceCategory::THIRD_PARTY => $this->thirdPartyTracking,
PerformanceCategory::EXTERNAL_API => $this->externalApiTracking,
};
}
```
### Custom Report Formats
```php
// Extend PerformanceReporter
public function generateCustomReport(): string
{
$data = $this->collectReportData();
return $this->formatAsCustomFormat($data);
}
```
## Future Enhancements
- **Persistent Storage**: Optional database storage for metrics
- **Real-time Monitoring**: WebSocket-based live performance data
- **Alerting**: Threshold-based notifications
- **Aggregation**: Cross-request performance analysis
- **Export Integration**: Prometheus, Grafana, etc.
- **Machine Learning**: Anomaly detection and predictions

View File

@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Analysis;
use App\Framework\Core\ValueObjects\Score;
use App\Framework\Performance\ValueObjects\PerformanceSnapshot;
/**
* Result of bottleneck analysis for a performance snapshot
*/
final readonly class BottleneckAnalysis
{
public function __construct(
public PerformanceSnapshot $snapshot,
public array $bottlenecks,
public array $recommendations,
public string $severity,
public Score $performanceScore,
public \DateTimeImmutable $analyzedAt
) {
}
/**
* Check if there are performance issues
*/
public function hasIssues(): bool
{
return ! empty($this->bottlenecks);
}
/**
* Check if there are critical issues
*/
public function hasCriticalIssues(): bool
{
return $this->severity === 'critical';
}
/**
* Get bottlenecks by severity
*/
public function getBottlenecksBySeverity(string $severity): array
{
return array_filter(
$this->bottlenecks,
fn (array $bottleneck) => $bottleneck['severity'] === $severity
);
}
/**
* Get bottlenecks by type
*/
public function getBottlenecksByType(string $type): array
{
return array_filter(
$this->bottlenecks,
fn (array $bottleneck) => $bottleneck['type'] === $type
);
}
/**
* Get efficiency rating
*/
public function getEfficiencyRating(): string
{
$score = $this->performanceScore->toDecimal();
return match (true) {
$score >= 0.9 => 'excellent',
$score >= 0.8 => 'good',
$score >= 0.6 => 'fair',
$score >= 0.4 => 'poor',
default => 'critical'
};
}
/**
* Get summary of issues
*/
public function getSummary(): string
{
if (empty($this->bottlenecks)) {
return 'No performance issues detected';
}
$criticalCount = count($this->getBottlenecksBySeverity('critical'));
$warningCount = count($this->getBottlenecksBySeverity('warning'));
$infoCount = count($this->getBottlenecksBySeverity('info'));
$parts = [];
if ($criticalCount > 0) {
$parts[] = "{$criticalCount} critical issue" . ($criticalCount > 1 ? 's' : '');
}
if ($warningCount > 0) {
$parts[] = "{$warningCount} warning" . ($warningCount > 1 ? 's' : '');
}
if ($infoCount > 0) {
$parts[] = "{$infoCount} minor issue" . ($infoCount > 1 ? 's' : '');
}
return implode(', ', $parts) . ' detected';
}
/**
* Convert to array for serialization
*/
public function toArray(): array
{
return [
'operation_id' => $this->snapshot->operationId,
'category' => $this->snapshot->category->value,
'analyzed_at' => $this->analyzedAt->format('Y-m-d H:i:s'),
'severity' => $this->severity,
'performance_score' => $this->performanceScore->toDecimal(),
'efficiency_rating' => $this->getEfficiencyRating(),
'summary' => $this->getSummary(),
'has_issues' => $this->hasIssues(),
'has_critical_issues' => $this->hasCriticalIssues(),
'bottlenecks' => $this->bottlenecks,
'recommendations' => $this->recommendations,
'snapshot' => $this->snapshot->toArray(),
];
}
}

View File

@@ -0,0 +1,502 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Analysis;
use App\Framework\Core\ValueObjects\Score;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\Performance\ValueObjects\PerformanceSnapshot;
/**
* Analyzes performance snapshots to identify bottlenecks and optimization opportunities
*
* Provides intelligent analysis of performance data to identify
* system bottlenecks, inefficiencies, and areas for improvement.
*/
final class BottleneckAnalyzer
{
// Performance thresholds for different categories
private const THRESHOLDS = [
'duration' => [
PerformanceCategory::DISCOVERY => 10.0, // 10 seconds
PerformanceCategory::DATABASE => 1.0, // 1 second
PerformanceCategory::CACHE => 0.1, // 100ms
PerformanceCategory::FILESYSTEM => 2.0, // 2 seconds
'default' => 5.0,
],
'memory_mb' => [
PerformanceCategory::DISCOVERY => 256, // 256MB
PerformanceCategory::DATABASE => 64, // 64MB
PerformanceCategory::CACHE => 32, // 32MB
PerformanceCategory::FILESYSTEM => 128, // 128MB
'default' => 128,
],
'throughput' => [
PerformanceCategory::DISCOVERY => 10.0, // 10 items/sec
PerformanceCategory::DATABASE => 100.0, // 100 ops/sec
PerformanceCategory::CACHE => 1000.0, // 1000 ops/sec
PerformanceCategory::FILESYSTEM => 50.0, // 50 files/sec
'default' => 50.0,
],
'cache_hit_rate' => 0.8, // 80% minimum
'error_rate' => 0.05, // 5% maximum
'memory_pressure' => 0.8, // 80% maximum
];
/**
* Analyze snapshot for bottlenecks
*/
public function analyze(PerformanceSnapshot $snapshot): BottleneckAnalysis
{
$bottlenecks = [];
$recommendations = [];
$severity = 'info';
// Duration analysis
$durationBottleneck = $this->analyzeDuration($snapshot);
if ($durationBottleneck !== null) {
$bottlenecks[] = $durationBottleneck;
$severity = $this->escalateSeverity($severity, $durationBottleneck['severity']);
}
// Memory analysis
$memoryBottleneck = $this->analyzeMemory($snapshot);
if ($memoryBottleneck !== null) {
$bottlenecks[] = $memoryBottleneck;
$severity = $this->escalateSeverity($severity, $memoryBottleneck['severity']);
}
// Throughput analysis
$throughputBottleneck = $this->analyzeThroughput($snapshot);
if ($throughputBottleneck !== null) {
$bottlenecks[] = $throughputBottleneck;
$severity = $this->escalateSeverity($severity, $throughputBottleneck['severity']);
}
// Cache performance analysis
$cacheBottleneck = $this->analyzeCachePerformance($snapshot);
if ($cacheBottleneck !== null) {
$bottlenecks[] = $cacheBottleneck;
$severity = $this->escalateSeverity($severity, $cacheBottleneck['severity']);
}
// Error rate analysis
$errorBottleneck = $this->analyzeErrorRate($snapshot);
if ($errorBottleneck !== null) {
$bottlenecks[] = $errorBottleneck;
$severity = $this->escalateSeverity($severity, $errorBottleneck['severity']);
}
// Generate recommendations
$recommendations = $this->generateRecommendations($bottlenecks, $snapshot);
// Calculate overall performance score
$performanceScore = $this->calculatePerformanceScore($snapshot, $bottlenecks);
return new BottleneckAnalysis(
snapshot: $snapshot,
bottlenecks: $bottlenecks,
recommendations: $recommendations,
severity: $severity,
performanceScore: $performanceScore,
analyzedAt: new \DateTimeImmutable()
);
}
/**
* Analyze multiple snapshots for patterns
*/
public function analyzePattern(array $snapshots): array
{
if (empty($snapshots)) {
return ['pattern' => 'no_data'];
}
$analyses = array_map([$this, 'analyze'], $snapshots);
// Count bottleneck types
$bottleneckTypes = [];
$severityCounts = [];
foreach ($analyses as $analysis) {
foreach ($analysis->bottlenecks as $bottleneck) {
$type = $bottleneck['type'];
$bottleneckTypes[$type] = ($bottleneckTypes[$type] ?? 0) + 1;
}
$severity = $analysis->severity;
$severityCounts[$severity] = ($severityCounts[$severity] ?? 0) + 1;
}
// Find most common bottlenecks
arsort($bottleneckTypes);
$topBottlenecks = array_slice($bottleneckTypes, 0, 3, true);
// Calculate trend
$performanceScores = array_map(
fn ($analysis) => $analysis->performanceScore->toDecimal(),
$analyses
);
$trend = $this->calculateTrend($performanceScores);
return [
'total_operations' => count($snapshots),
'top_bottlenecks' => $topBottlenecks,
'severity_distribution' => $severityCounts,
'performance_trend' => $trend,
'average_score' => array_sum($performanceScores) / count($performanceScores),
'pattern_recommendations' => $this->generatePatternRecommendations($topBottlenecks, $trend),
];
}
/**
* Analyze duration bottlenecks
*/
private function analyzeDuration(PerformanceSnapshot $snapshot): ?array
{
if ($snapshot->duration === null) {
return null;
}
$duration = $snapshot->duration->toSeconds();
$threshold = $this->getThreshold('duration', $snapshot->category);
if ($duration <= $threshold) {
return null;
}
$severity = match (true) {
$duration > $threshold * 3 => 'critical',
$duration > $threshold * 2 => 'warning',
default => 'info'
};
return [
'type' => 'slow_execution',
'severity' => $severity,
'metric' => 'duration',
'value' => $duration,
'threshold' => $threshold,
'impact' => 'High response times affect user experience',
'description' => "Operation took {$duration}s (threshold: {$threshold}s)",
];
}
/**
* Analyze memory bottlenecks
*/
private function analyzeMemory(PerformanceSnapshot $snapshot): ?array
{
$memoryUsage = $snapshot->peakMemory->toMegabytes();
$threshold = $this->getThreshold('memory_mb', $snapshot->category);
$memoryPressure = $snapshot->getMemoryPressure();
// Check both absolute usage and pressure
$isHighUsage = $memoryUsage > $threshold;
$isHighPressure = $memoryPressure > self::THRESHOLDS['memory_pressure'];
if (! $isHighUsage && ! $isHighPressure) {
return null;
}
$severity = match (true) {
$memoryUsage > $threshold * 2 || $memoryPressure > 0.95 => 'critical',
$memoryUsage > $threshold * 1.5 || $memoryPressure > 0.9 => 'warning',
default => 'info'
};
$description = $isHighUsage
? "High memory usage: {$memoryUsage}MB (threshold: {$threshold}MB)"
: "High memory pressure: " . round($memoryPressure * 100, 1) . "%";
return [
'type' => 'high_memory_usage',
'severity' => $severity,
'metric' => 'memory',
'value' => $memoryUsage,
'threshold' => $threshold,
'pressure' => $memoryPressure,
'impact' => 'High memory usage can lead to system instability',
'description' => $description,
];
}
/**
* Analyze throughput bottlenecks
*/
private function analyzeThroughput(PerformanceSnapshot $snapshot): ?array
{
$throughput = $snapshot->getThroughput();
$threshold = $this->getThreshold('throughput', $snapshot->category);
if ($throughput >= $threshold || $snapshot->itemsProcessed === 0) {
return null;
}
$severity = match (true) {
$throughput < $threshold * 0.3 => 'critical',
$throughput < $threshold * 0.5 => 'warning',
default => 'info'
};
return [
'type' => 'low_throughput',
'severity' => $severity,
'metric' => 'throughput',
'value' => $throughput,
'threshold' => $threshold,
'impact' => 'Low throughput affects system capacity',
'description' => "Low throughput: " . round($throughput, 2) . " items/s (threshold: {$threshold} items/s)",
];
}
/**
* Analyze cache performance bottlenecks
*/
private function analyzeCachePerformance(PerformanceSnapshot $snapshot): ?array
{
$totalCacheOps = $snapshot->cacheHits + $snapshot->cacheMisses;
if ($totalCacheOps === 0) {
return null; // No cache operations
}
$hitRate = $snapshot->getCacheHitRate();
$threshold = self::THRESHOLDS['cache_hit_rate'];
if ($hitRate >= $threshold) {
return null;
}
$severity = match (true) {
$hitRate < $threshold * 0.5 => 'critical',
$hitRate < $threshold * 0.7 => 'warning',
default => 'info'
};
return [
'type' => 'poor_cache_performance',
'severity' => $severity,
'metric' => 'cache_hit_rate',
'value' => $hitRate,
'threshold' => $threshold,
'impact' => 'Poor cache performance increases response times',
'description' => "Low cache hit rate: " . round($hitRate * 100, 1) . "% (threshold: " . round($threshold * 100, 1) . "%)",
];
}
/**
* Analyze error rate bottlenecks
*/
private function analyzeErrorRate(PerformanceSnapshot $snapshot): ?array
{
if ($snapshot->itemsProcessed === 0) {
return null;
}
$errorRate = $snapshot->getErrorRate();
$threshold = self::THRESHOLDS['error_rate'];
if ($errorRate <= $threshold) {
return null;
}
$severity = match (true) {
$errorRate > $threshold * 4 => 'critical',
$errorRate > $threshold * 2 => 'warning',
default => 'info'
};
return [
'type' => 'high_error_rate',
'severity' => $severity,
'metric' => 'error_rate',
'value' => $errorRate,
'threshold' => $threshold,
'impact' => 'High error rates indicate system reliability issues',
'description' => "High error rate: " . round($errorRate * 100, 1) . "% (threshold: " . round($threshold * 100, 1) . "%)",
];
}
/**
* Generate recommendations based on bottlenecks
*/
private function generateRecommendations(array $bottlenecks, PerformanceSnapshot $snapshot): array
{
$recommendations = [];
foreach ($bottlenecks as $bottleneck) {
$recommendations[] = match ($bottleneck['type']) {
'slow_execution' => $this->getExecutionRecommendations($snapshot),
'high_memory_usage' => $this->getMemoryRecommendations($snapshot),
'low_throughput' => $this->getThroughputRecommendations($snapshot),
'poor_cache_performance' => $this->getCacheRecommendations($snapshot),
'high_error_rate' => $this->getErrorRecommendations($snapshot),
default => 'Review system configuration and optimize accordingly'
};
}
return array_unique(array_filter($recommendations));
}
/**
* Generate recommendations for execution performance
*/
private function getExecutionRecommendations(PerformanceSnapshot $snapshot): string
{
return match ($snapshot->category) {
PerformanceCategory::DISCOVERY => 'Consider enabling parallel processing or reducing batch size',
PerformanceCategory::DATABASE => 'Optimize queries, add indexes, or implement connection pooling',
PerformanceCategory::CACHE => 'Review cache strategy and consider warming frequently accessed data',
PerformanceCategory::FILESYSTEM => 'Optimize file I/O patterns or consider async processing',
default => 'Profile and optimize the slowest code paths'
};
}
/**
* Generate recommendations for memory usage
*/
private function getMemoryRecommendations(PerformanceSnapshot $snapshot): string
{
return match ($snapshot->category) {
PerformanceCategory::DISCOVERY => 'Enable memory-aware processing and increase cleanup frequency',
PerformanceCategory::DATABASE => 'Reduce result set sizes or implement streaming',
PerformanceCategory::CACHE => 'Implement cache eviction policies and compression',
default => 'Review memory allocation patterns and implement cleanup strategies'
};
}
/**
* Generate recommendations for throughput
*/
private function getThroughputRecommendations(PerformanceSnapshot $snapshot): string
{
return match ($snapshot->category) {
PerformanceCategory::DISCOVERY => 'Enable concurrent processing and optimize file scanning',
PerformanceCategory::DATABASE => 'Implement batch operations and optimize queries',
PerformanceCategory::CACHE => 'Review cache configuration and consider clustering',
default => 'Implement parallel processing and optimize algorithms'
};
}
/**
* Generate recommendations for cache performance
*/
private function getCacheRecommendations(PerformanceSnapshot $snapshot): string
{
return 'Review cache strategy, implement cache warming, and optimize cache keys';
}
/**
* Generate recommendations for error rates
*/
private function getErrorRecommendations(PerformanceSnapshot $snapshot): string
{
return 'Investigate error causes, improve error handling, and add input validation';
}
/**
* Generate pattern-based recommendations
*/
private function generatePatternRecommendations(array $topBottlenecks, string $trend): array
{
$recommendations = [];
// Recommendations based on most common bottlenecks
foreach (array_keys($topBottlenecks) as $bottleneck) {
$recommendations[] = match ($bottleneck) {
'slow_execution' => 'Implement performance monitoring and systematic optimization',
'high_memory_usage' => 'Review memory management strategy across the system',
'low_throughput' => 'Consider architectural changes for better scalability',
'poor_cache_performance' => 'Implement comprehensive caching strategy review',
'high_error_rate' => 'Improve system reliability and error handling',
default => 'Monitor and optimize the most frequently occurring bottleneck'
};
}
// Recommendations based on trend
if ($trend === 'degrading') {
$recommendations[] = 'Performance is degrading - investigate recent changes and resource usage';
} elseif ($trend === 'stable_poor') {
$recommendations[] = 'Consistent performance issues detected - consider infrastructure upgrades';
}
return array_unique($recommendations);
}
/**
* Calculate overall performance score
*/
private function calculatePerformanceScore(PerformanceSnapshot $snapshot, array $bottlenecks): Score
{
$baseScore = 100;
// Deduct points based on bottlenecks
foreach ($bottlenecks as $bottleneck) {
$deduction = match ($bottleneck['severity']) {
'critical' => 30,
'warning' => 15,
'info' => 5,
default => 0
};
$baseScore -= $deduction;
}
// Bonus for good performance indicators
if ($snapshot->getErrorRate() === 0.0) {
$baseScore += 5;
}
if ($snapshot->getCacheHitRate() > 0.9) {
$baseScore += 5;
}
return Score::fromRatio(max(0, min(100, $baseScore)), 100);
}
/**
* Get threshold for category
*/
private function getThreshold(string $metric, PerformanceCategory $category): float
{
return self::THRESHOLDS[$metric][$category] ?? self::THRESHOLDS[$metric]['default'];
}
/**
* Escalate severity level
*/
private function escalateSeverity(string $current, string $new): string
{
$levels = ['info' => 1, 'warning' => 2, 'critical' => 3];
return $levels[$new] > $levels[$current] ? $new : $current;
}
/**
* Calculate trend for performance scores
*/
private function calculateTrend(array $scores): string
{
if (count($scores) < 3) {
return 'insufficient_data';
}
$midpoint = (int) (count($scores) / 2);
$firstHalf = array_slice($scores, 0, $midpoint);
$secondHalf = array_slice($scores, $midpoint);
$firstAvg = array_sum($firstHalf) / count($firstHalf);
$secondAvg = array_sum($secondHalf) / count($secondHalf);
$difference = $secondAvg - $firstAvg;
if ($difference > 0.1) {
return 'improving';
} elseif ($difference < -0.1) {
return $secondAvg < 0.5 ? 'stable_poor' : 'degrading';
}
return $secondAvg < 0.5 ? 'stable_poor' : 'stable';
}
}

View File

@@ -0,0 +1,365 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Analysis;
use App\Framework\Performance\ValueObjects\PerformanceSnapshot;
/**
* Result of trend analysis for performance snapshots
*
* Contains comprehensive trend analysis results including
* individual metric trends, predictions, recommendations,
* and anomaly detection results.
*/
final readonly class TrendAnalysis
{
public function __construct(
public array $snapshots,
public array $metricTrends,
public string $overallTrend,
public array $predictions,
public array $recommendations,
public array $anomalies,
public \DateTimeImmutable $analysisTimestamp,
public float $confidence
) {
}
/**
* Create analysis for insufficient data
*/
public static function insufficient(array $snapshots): self
{
return new self(
snapshots: $snapshots,
metricTrends: [],
overallTrend: 'insufficient_data',
predictions: [],
recommendations: ['Collect more performance data for trend analysis'],
anomalies: [],
analysisTimestamp: new \DateTimeImmutable(),
confidence: 0.0
);
}
/**
* Check if trend analysis has valid trends
*/
public function hasValidTrend(): bool
{
return $this->overallTrend !== 'insufficient_data' && ! empty($this->metricTrends);
}
/**
* Check if performance is improving
*/
public function isImproving(): bool
{
return $this->overallTrend === 'improving';
}
/**
* Check if performance is degrading
*/
public function isDegrading(): bool
{
return $this->overallTrend === 'degrading';
}
/**
* Check if performance is stable
*/
public function isStable(): bool
{
return $this->overallTrend === 'stable';
}
/**
* Get trends by direction
*/
public function getTrendsByDirection(string $direction): array
{
return array_filter(
$this->metricTrends,
fn (array $trend) => $trend['direction'] === $direction
);
}
/**
* Get metrics with concerning predictions
*/
public function getConcerningPredictions(): array
{
return array_filter(
$this->predictions,
fn (array $prediction) => isset($prediction['alert'])
);
}
/**
* Get anomalies by severity
*/
public function getAnomaliesBySeverity(string $severity): array
{
return array_filter(
$this->anomalies,
fn (array $anomaly) => $anomaly['severity'] === $severity
);
}
/**
* Get high severity anomalies
*/
public function getHighSeverityAnomalies(): array
{
return $this->getAnomaliesBySeverity('high');
}
/**
* Get recommendations by priority
*/
public function getPriorityRecommendations(): array
{
// High priority recommendations for degrading trends
if ($this->isDegrading()) {
return array_merge(
['URGENT: Performance is degrading - immediate investigation required'],
$this->recommendations
);
}
// Medium priority for concerning predictions
if (! empty($this->getConcerningPredictions())) {
return array_merge(
['WARNING: Concerning performance predictions detected'],
$this->recommendations
);
}
return $this->recommendations;
}
/**
* Get trend strength for a metric
*/
public function getTrendStrength(string $metric): float
{
return $this->metricTrends[$metric]['strength'] ?? 0.0;
}
/**
* Get trend direction for a metric
*/
public function getTrendDirection(string $metric): string
{
return $this->metricTrends[$metric]['direction'] ?? 'unknown';
}
/**
* Get prediction for a metric
*/
public function getPrediction(string $metric): ?array
{
return $this->predictions[$metric] ?? null;
}
/**
* Check if there are any critical issues
*/
public function hasCriticalIssues(): bool
{
return $this->isDegrading()
|| ! empty($this->getHighSeverityAnomalies())
|| ! empty($this->getConcerningPredictions());
}
/**
* Get performance health rating
*/
public function getHealthRating(): string
{
if ($this->confidence < 0.3) {
return 'insufficient_data';
}
if ($this->hasCriticalIssues()) {
return 'critical';
}
if ($this->isDegrading()) {
return 'poor';
}
if ($this->isImproving()) {
return 'excellent';
}
return 'good';
}
/**
* Get time period covered by analysis
*/
public function getTimePeriod(): array
{
if (empty($this->snapshots)) {
return ['start' => null, 'end' => null, 'duration' => 0];
}
$timestamps = array_map(
fn (PerformanceSnapshot $snapshot) => $snapshot->startTime->toFloat(),
$this->snapshots
);
$start = new \DateTimeImmutable('@' . (int) min($timestamps));
$end = new \DateTimeImmutable('@' . (int) max($timestamps));
$duration = $end->getTimestamp() - $start->getTimestamp();
return [
'start' => $start,
'end' => $end,
'duration' => $duration,
];
}
/**
* Get summary of trend analysis
*/
public function getSummary(): string
{
if (! $this->hasValidTrend()) {
return 'Insufficient data for trend analysis';
}
$period = $this->getTimePeriod();
$durationHours = round($period['duration'] / 3600, 1);
$sampleCount = count($this->snapshots);
$summary = "Analyzed {$sampleCount} performance samples over {$durationHours} hours. ";
$summary .= match ($this->overallTrend) {
'improving' => 'Performance trends are positive with measurable improvements.',
'degrading' => 'Performance is degrading and requires immediate attention.',
'stable' => 'Performance is stable with no significant changes.',
default => 'Mixed performance trends detected.'
};
if (! empty($this->anomalies)) {
$anomalyCount = count($this->anomalies);
$summary .= " {$anomalyCount} performance anomalies detected.";
}
if (! empty($this->getConcerningPredictions())) {
$concernCount = count($this->getConcerningPredictions());
$summary .= " {$concernCount} concerning predictions require attention.";
}
return $summary;
}
/**
* Convert to array for serialization
*/
public function toArray(): array
{
$period = $this->getTimePeriod();
return [
'analysis_timestamp' => $this->analysisTimestamp->format('Y-m-d H:i:s'),
'sample_count' => count($this->snapshots),
'time_period' => [
'start' => $period['start']?->format('Y-m-d H:i:s'),
'end' => $period['end']?->format('Y-m-d H:i:s'),
'duration_hours' => round($period['duration'] / 3600, 1),
],
'overall_trend' => $this->overallTrend,
'confidence' => $this->confidence,
'health_rating' => $this->getHealthRating(),
'has_valid_trend' => $this->hasValidTrend(),
'has_critical_issues' => $this->hasCriticalIssues(),
'summary' => $this->getSummary(),
'metric_trends' => $this->metricTrends,
'predictions' => $this->predictions,
'concerning_predictions' => $this->getConcerningPredictions(),
'recommendations' => $this->recommendations,
'priority_recommendations' => $this->getPriorityRecommendations(),
'anomalies' => $this->anomalies,
'high_severity_anomalies' => $this->getHighSeverityAnomalies(),
'trend_directions' => [
'improving' => count($this->getTrendsByDirection('increasing')),
'degrading' => count($this->getTrendsByDirection('decreasing')),
'stable' => count($this->getTrendsByDirection('stable')),
],
];
}
/**
* Create detailed report
*/
public function createReport(): string
{
$report = "Performance Trend Analysis Report\n";
$report .= str_repeat('=', 40) . "\n\n";
$report .= "Analysis Date: " . $this->analysisTimestamp->format('Y-m-d H:i:s') . "\n";
$report .= "Confidence Level: " . round($this->confidence * 100, 1) . "%\n\n";
// Summary
$report .= "Summary:\n";
$report .= $this->getSummary() . "\n\n";
// Overall Trend
$report .= "Overall Performance Trend: " . strtoupper($this->overallTrend) . "\n";
$report .= "Health Rating: " . strtoupper($this->getHealthRating()) . "\n\n";
// Metric Trends
if (! empty($this->metricTrends)) {
$report .= "Metric Trends:\n";
foreach ($this->metricTrends as $metric => $trend) {
$direction = strtoupper($trend['direction']);
$strength = round($trend['strength'] * 100, 1);
$report .= " - {$metric}: {$direction} (strength: {$strength}%)\n";
}
$report .= "\n";
}
// Predictions
if (! empty($this->predictions)) {
$report .= "Predictions:\n";
foreach ($this->predictions as $metric => $prediction) {
$value = round($prediction['predicted_value'], 2);
$confidence = round($prediction['confidence'] * 100, 1);
$report .= " - {$metric}: {$value} (confidence: {$confidence}%)\n";
if (isset($prediction['alert'])) {
$report .= " WARNING: " . $prediction['alert'] . "\n";
}
}
$report .= "\n";
}
// Recommendations
if (! empty($this->recommendations)) {
$report .= "Recommendations:\n";
foreach ($this->getPriorityRecommendations() as $i => $recommendation) {
$report .= " " . ($i + 1) . ". {$recommendation}\n";
}
$report .= "\n";
}
// Anomalies
if (! empty($this->anomalies)) {
$report .= "Anomalies Detected:\n";
foreach ($this->anomalies as $anomaly) {
$severity = strtoupper($anomaly['severity']);
$metric = $anomaly['metric'];
$timestamp = date('Y-m-d H:i:s', (int) $anomaly['timestamp']);
$report .= " - [{$severity}] {$metric} at {$timestamp}\n";
}
}
return $report;
}
}

View File

@@ -0,0 +1,618 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Analysis;
use App\Framework\Core\ValueObjects\Score;
/**
* Analyzes performance trends over time to identify patterns and predict issues
*
* Provides statistical analysis of performance data to understand
* system behavior patterns, predict performance degradation, and
* recommend proactive optimization strategies.
*/
final class TrendAnalyzer
{
public function __construct(
private int $minSamplesForTrend = 5,
private float $significanceThreshold = 0.15 // 15% change threshold
) {
}
/**
* Analyze trend for a series of performance snapshots
*/
public function analyzeTrend(array $snapshots): TrendAnalysis
{
if (count($snapshots) < $this->minSamplesForTrend) {
return TrendAnalysis::insufficient($snapshots);
}
// Sort by timestamp
usort(
$snapshots,
fn ($a, $b) =>
$a->startTime->toFloat() <=> $b->startTime->toFloat()
);
// Extract metrics over time
$metrics = $this->extractMetricsTimeSeries($snapshots);
// Analyze each metric trend
$trendResults = [];
foreach ($metrics as $metricName => $values) {
$trendResults[$metricName] = $this->calculateTrend($values);
}
// Calculate overall trend health
$overallTrend = $this->calculateOverallTrend($trendResults);
// Generate predictions and recommendations
$predictions = $this->generatePredictions($snapshots, $trendResults);
$recommendations = $this->generateTrendRecommendations($trendResults, $overallTrend);
// Detect anomalies
$anomalies = $this->detectAnomalies($snapshots, $metrics);
return new TrendAnalysis(
snapshots: $snapshots,
metricTrends: $trendResults,
overallTrend: $overallTrend,
predictions: $predictions,
recommendations: $recommendations,
anomalies: $anomalies,
analysisTimestamp: new \DateTimeImmutable(),
confidence: $this->calculateConfidence($snapshots, $trendResults)
);
}
/**
* Analyze trends by category
*/
public function analyzeTrendsByCategory(array $snapshots): array
{
$categorized = [];
// Group snapshots by category
foreach ($snapshots as $snapshot) {
$category = $snapshot->category->value;
$categorized[$category] = $categorized[$category] ?? [];
$categorized[$category][] = $snapshot;
}
// Analyze trend for each category
$results = [];
foreach ($categorized as $category => $categorySnapshots) {
$results[$category] = $this->analyzeTrend($categorySnapshots);
}
return $results;
}
/**
* Compare trends between different time periods
*/
public function comparePeriods(array $period1, array $period2): array
{
$trend1 = $this->analyzeTrend($period1);
$trend2 = $this->analyzeTrend($period2);
if (! $trend1->hasValidTrend() || ! $trend2->hasValidTrend()) {
return ['comparison' => 'insufficient_data'];
}
$comparison = [];
foreach ($trend1->metricTrends as $metric => $trend1Data) {
if (isset($trend2->metricTrends[$metric])) {
$trend2Data = $trend2->metricTrends[$metric];
$comparison[$metric] = [
'period1_trend' => $trend1Data['direction'],
'period2_trend' => $trend2Data['direction'],
'improvement' => $this->compareMetricTrends($trend1Data, $trend2Data, $metric),
'change_magnitude' => $this->calculateChangeMagnitude($trend1Data, $trend2Data),
];
}
}
return [
'comparison' => $comparison,
'overall_change' => $this->calculateOverallChange($trend1, $trend2),
'confidence' => min($trend1->confidence, $trend2->confidence),
];
}
/**
* Extract time series data for key metrics
*/
private function extractMetricsTimeSeries(array $snapshots): array
{
$metrics = [
'duration' => [],
'memory_peak' => [],
'throughput' => [],
'cache_hit_rate' => [],
'error_rate' => [],
'memory_efficiency' => [],
];
foreach ($snapshots as $snapshot) {
$timestamp = $snapshot->startTime->toFloat();
// Duration
if ($snapshot->duration !== null) {
$metrics['duration'][] = [
'timestamp' => $timestamp,
'value' => $snapshot->duration->toSeconds(),
];
}
// Memory peak
$metrics['memory_peak'][] = [
'timestamp' => $timestamp,
'value' => $snapshot->peakMemory->toMegabytes(),
];
// Throughput
$metrics['throughput'][] = [
'timestamp' => $timestamp,
'value' => $snapshot->getThroughput(),
];
// Cache hit rate
$metrics['cache_hit_rate'][] = [
'timestamp' => $timestamp,
'value' => $snapshot->getCacheHitRate(),
];
// Error rate
$metrics['error_rate'][] = [
'timestamp' => $timestamp,
'value' => $snapshot->getErrorRate(),
];
// Memory efficiency
$metrics['memory_efficiency'][] = [
'timestamp' => $timestamp,
'value' => $snapshot->getMemoryEfficiency(),
];
}
return array_filter($metrics, fn ($values) => ! empty($values));
}
/**
* Calculate trend for a metric time series
*/
private function calculateTrend(array $timeSeries): array
{
if (count($timeSeries) < 2) {
return ['direction' => 'insufficient_data', 'strength' => 0.0, 'slope' => 0.0];
}
// Extract values and calculate linear regression
$values = array_column($timeSeries, 'value');
$timestamps = array_column($timeSeries, 'timestamp');
// Normalize timestamps to start from 0
$minTimestamp = min($timestamps);
$normalizedTimestamps = array_map(fn ($t) => $t - $minTimestamp, $timestamps);
// Calculate linear regression
$regression = $this->calculateLinearRegression($normalizedTimestamps, $values);
// Determine trend direction and strength
$slope = $regression['slope'];
$rSquared = $regression['r_squared'];
$direction = match (true) {
$slope > $this->significanceThreshold => 'increasing',
$slope < -$this->significanceThreshold => 'decreasing',
default => 'stable'
};
return [
'direction' => $direction,
'strength' => $rSquared, // R-squared indicates how well the trend fits
'slope' => $slope,
'start_value' => $values[0],
'end_value' => end($values),
'min_value' => min($values),
'max_value' => max($values),
'average_value' => array_sum($values) / count($values),
'variance' => $this->calculateVariance($values),
'data_points' => count($values),
];
}
/**
* Calculate overall trend from individual metric trends
*/
private function calculateOverallTrend(array $metricTrends): string
{
$scores = [];
$weights = [
'duration' => 0.3, // Performance is critical
'memory_peak' => 0.2, // Memory usage important
'throughput' => 0.25, // Throughput critical
'cache_hit_rate' => 0.15, // Cache performance matters
'error_rate' => 0.1, // Errors are concerning
];
foreach ($metricTrends as $metric => $trend) {
if ($trend['direction'] === 'insufficient_data') {
continue;
}
$weight = $weights[$metric] ?? 0.1;
// Score based on trend direction and metric type
$score = match ($metric) {
'duration', 'memory_peak', 'error_rate' => match ($trend['direction']) {
'decreasing' => 1.0, // Good: decreasing duration/memory/errors
'stable' => 0.5, // Neutral
'increasing' => -1.0 // Bad: increasing duration/memory/errors
},
'throughput', 'cache_hit_rate' => match ($trend['direction']) {
'increasing' => 1.0, // Good: increasing throughput/cache hits
'stable' => 0.5, // Neutral
'decreasing' => -1.0 // Bad: decreasing throughput/cache hits
},
default => 0.0
};
// Weight by trend strength (confidence)
$weightedScore = $score * $trend['strength'] * $weight;
$scores[] = $weightedScore;
}
if (empty($scores)) {
return 'unknown';
}
$averageScore = array_sum($scores) / count($scores);
return match (true) {
$averageScore > 0.3 => 'improving',
$averageScore < -0.3 => 'degrading',
default => 'stable'
};
}
/**
* Generate predictions based on trends
*/
private function generatePredictions(array $snapshots, array $trendResults): array
{
$predictions = [];
foreach ($trendResults as $metric => $trend) {
if ($trend['direction'] === 'insufficient_data' || $trend['strength'] < 0.5) {
continue; // Skip metrics with weak trends
}
$prediction = $this->predictMetricValue($trend, 3600); // Predict 1 hour ahead
if ($prediction !== null) {
$predictions[$metric] = [
'predicted_value' => $prediction,
'confidence' => $trend['strength'],
'time_horizon' => '1 hour',
'trend_basis' => $trend['direction'],
];
// Add alerts for concerning predictions
if ($this->isPredictionConcerning($metric, $prediction, $trend)) {
$predictions[$metric]['alert'] = $this->generatePredictionAlert($metric, $prediction, $trend);
}
}
}
return $predictions;
}
/**
* Generate trend-based recommendations
*/
private function generateTrendRecommendations(array $trendResults, string $overallTrend): array
{
$recommendations = [];
// Overall trend recommendations
$recommendations[] = match ($overallTrend) {
'improving' => 'Performance trends are positive - continue current optimization strategies',
'degrading' => 'Performance is degrading - investigate recent changes and implement optimization measures',
'stable' => 'Performance is stable - consider proactive optimization for better efficiency',
default => 'Insufficient data for trend-based recommendations'
};
// Metric-specific recommendations
foreach ($trendResults as $metric => $trend) {
if ($trend['direction'] === 'insufficient_data' || $trend['strength'] < 0.3) {
continue;
}
$metricRecommendation = $this->getMetricTrendRecommendation($metric, $trend);
if ($metricRecommendation !== null) {
$recommendations[] = $metricRecommendation;
}
}
return array_unique($recommendations);
}
/**
* Detect anomalies in the performance data
*/
private function detectAnomalies(array $snapshots, array $metrics): array
{
$anomalies = [];
foreach ($metrics as $metricName => $timeSeries) {
$values = array_column($timeSeries, 'value');
$timestamps = array_column($timeSeries, 'timestamp');
if (count($values) < 5) {
continue; // Need sufficient data for anomaly detection
}
$mean = array_sum($values) / count($values);
$stdDev = sqrt($this->calculateVariance($values));
// Detect outliers using z-score
for ($i = 0; $i < count($values); $i++) {
$zScore = abs(($values[$i] - $mean) / max($stdDev, 0.001));
if ($zScore > 2.5) { // 2.5 standard deviations
$anomalies[] = [
'metric' => $metricName,
'timestamp' => $timestamps[$i],
'value' => $values[$i],
'expected_range' => [$mean - 2 * $stdDev, $mean + 2 * $stdDev],
'z_score' => $zScore,
'severity' => $zScore > 3.0 ? 'high' : 'medium',
];
}
}
}
return $anomalies;
}
/**
* Calculate linear regression
*/
private function calculateLinearRegression(array $x, array $y): array
{
$n = count($x);
if ($n < 2) {
return ['slope' => 0.0, 'intercept' => 0.0, 'r_squared' => 0.0];
}
$sumX = array_sum($x);
$sumY = array_sum($y);
$sumXY = 0;
$sumXX = 0;
$sumYY = 0;
for ($i = 0; $i < $n; $i++) {
$sumXY += $x[$i] * $y[$i];
$sumXX += $x[$i] * $x[$i];
$sumYY += $y[$i] * $y[$i];
}
$slope = ($n * $sumXY - $sumX * $sumY) / max($n * $sumXX - $sumX * $sumX, 0.001);
$intercept = ($sumY - $slope * $sumX) / $n;
// Calculate R-squared
$meanY = $sumY / $n;
$ssTotal = $sumYY - $n * $meanY * $meanY;
$ssRes = 0;
for ($i = 0; $i < $n; $i++) {
$predicted = $slope * $x[$i] + $intercept;
$ssRes += ($y[$i] - $predicted) ** 2;
}
$rSquared = $ssTotal > 0 ? 1 - ($ssRes / $ssTotal) : 0;
return [
'slope' => $slope,
'intercept' => $intercept,
'r_squared' => max(0, min(1, $rSquared)),
];
}
/**
* Calculate variance
*/
private function calculateVariance(array $values): float
{
$n = count($values);
if ($n < 2) {
return 0.0;
}
$mean = array_sum($values) / $n;
$sumSquaredDiffs = 0;
foreach ($values as $value) {
$sumSquaredDiffs += ($value - $mean) ** 2;
}
return $sumSquaredDiffs / ($n - 1);
}
/**
* Predict metric value based on trend
*/
private function predictMetricValue(array $trend, int $secondsAhead): ?float
{
if ($trend['direction'] === 'stable' || $trend['strength'] < 0.5) {
return $trend['average_value'];
}
// Simple linear extrapolation
$timeSteps = $secondsAhead; // Assuming 1 second steps
$predictedValue = $trend['end_value'] + ($trend['slope'] * $timeSteps);
// Bound predictions to reasonable ranges
$minBound = min($trend['min_value'] * 0.5, $trend['min_value'] - $trend['variance']);
$maxBound = max($trend['max_value'] * 2.0, $trend['max_value'] + $trend['variance']);
return max($minBound, min($maxBound, $predictedValue));
}
/**
* Check if prediction is concerning
*/
private function isPredictionConcerning(string $metric, float $prediction, array $trend): bool
{
return match ($metric) {
'duration' => $prediction > $trend['max_value'] * 1.5,
'memory_peak' => $prediction > $trend['max_value'] * 1.3,
'error_rate' => $prediction > 0.1, // 10% error rate threshold
'throughput' => $prediction < $trend['min_value'] * 0.7,
'cache_hit_rate' => $prediction < 0.5, // 50% hit rate threshold
default => false
};
}
/**
* Generate prediction alert
*/
private function generatePredictionAlert(string $metric, float $prediction, array $trend): string
{
return match ($metric) {
'duration' => "Duration expected to increase to " . round($prediction, 2) . "s",
'memory_peak' => "Memory usage expected to reach " . round($prediction, 1) . "MB",
'error_rate' => "Error rate may increase to " . round($prediction * 100, 1) . "%",
'throughput' => "Throughput may decrease to " . round($prediction, 1) . " items/s",
'cache_hit_rate' => "Cache hit rate may drop to " . round($prediction * 100, 1) . "%",
default => "Concerning trend detected for {$metric}"
};
}
/**
* Get metric-specific trend recommendation
*/
private function getMetricTrendRecommendation(string $metric, array $trend): ?string
{
if ($trend['strength'] < 0.3) {
return null; // Weak trend, no recommendation
}
return match ($metric) {
'duration' => match ($trend['direction']) {
'increasing' => 'Duration is increasing - investigate performance bottlenecks and optimize slow operations',
'decreasing' => 'Duration improvements detected - continue current optimization strategies',
default => null
},
'memory_peak' => match ($trend['direction']) {
'increasing' => 'Memory usage trending upward - implement memory management and investigate leaks',
'decreasing' => 'Good memory optimization trend - maintain current memory practices',
default => null
},
'throughput' => match ($trend['direction']) {
'decreasing' => 'Throughput declining - optimize processing algorithms and consider parallel processing',
'increasing' => 'Throughput improvements detected - scaling strategies are working well',
default => null
},
'cache_hit_rate' => match ($trend['direction']) {
'decreasing' => 'Cache performance declining - review cache strategy and implement cache warming',
'increasing' => 'Cache performance improving - current caching strategy is effective',
default => null
},
'error_rate' => match ($trend['direction']) {
'increasing' => 'Error rate trending upward - investigate root causes and improve error handling',
'decreasing' => 'Error rate improvements detected - continue current reliability practices',
default => null
},
default => null
};
}
/**
* Compare metric trends between periods
*/
private function compareMetricTrends(array $trend1, array $trend2, string $metric): string
{
// For metrics where lower is better (duration, memory, errors)
$lowerIsBetter = in_array($metric, ['duration', 'memory_peak', 'error_rate']);
$value1 = $trend1['average_value'];
$value2 = $trend2['average_value'];
$change = ($value2 - $value1) / max($value1, 0.001);
if ($lowerIsBetter) {
return match (true) {
$change < -0.1 => 'improved',
$change > 0.1 => 'degraded',
default => 'stable'
};
} else {
return match (true) {
$change > 0.1 => 'improved',
$change < -0.1 => 'degraded',
default => 'stable'
};
}
}
/**
* Calculate change magnitude between trends
*/
private function calculateChangeMagnitude(array $trend1, array $trend2): float
{
$value1 = $trend1['average_value'];
$value2 = $trend2['average_value'];
return abs(($value2 - $value1) / max($value1, 0.001));
}
/**
* Calculate overall change between two trend analyses
*/
private function calculateOverallChange(TrendAnalysis $trend1, TrendAnalysis $trend2): string
{
$improvements = 0;
$degradations = 0;
foreach ($trend1->metricTrends as $metric => $trend1Data) {
if (isset($trend2->metricTrends[$metric])) {
$trend2Data = $trend2->metricTrends[$metric];
$comparison = $this->compareMetricTrends($trend1Data, $trend2Data, $metric);
match ($comparison) {
'improved' => $improvements++,
'degraded' => $degradations++,
default => null
};
}
}
return match (true) {
$improvements > $degradations => 'overall_improvement',
$degradations > $improvements => 'overall_degradation',
default => 'overall_stable'
};
}
/**
* Calculate confidence in trend analysis
*/
private function calculateConfidence(array $snapshots, array $trendResults): float
{
$sampleSize = count($snapshots);
$sampleScore = min(1.0, $sampleSize / 20); // Full confidence at 20+ samples
$trendStrengths = array_column($trendResults, 'strength');
$avgTrendStrength = ! empty($trendStrengths) ? array_sum($trendStrengths) / count($trendStrengths) : 0;
return ($sampleScore * 0.4) + ($avgTrendStrength * 0.6);
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Contracts;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\Performance\PerformanceMetric;
interface PerformanceCollectorInterface
{
/**
* Start timing an operation
*/
public function startTiming(string $key, PerformanceCategory $category, array $context = []): void;
/**
* End timing an operation
*/
public function endTiming(string $key): void;
/**
* Measure a callable's execution time and memory usage
*/
public function measure(string $key, PerformanceCategory $category, callable $callback, array $context = []): mixed;
/**
* Record a metric value
*/
public function recordMetric(string $key, PerformanceCategory $category, float $value, array $context = []): void;
/**
* Increment a counter
*/
public function increment(string $key, PerformanceCategory $category, int $amount = 1, array $context = []): void;
/**
* Get all metrics, optionally filtered by category
*
* @return array<string, PerformanceMetric>
*/
public function getMetrics(?PerformanceCategory $category = null): array;
/**
* Get a specific metric by key
*/
public function getMetric(string $key): ?PerformanceMetric;
/**
* Get total request time in milliseconds
*/
public function getTotalRequestTime(): float;
/**
* Get total request memory usage in bytes
*/
public function getTotalRequestMemory(): int;
/**
* Get peak memory usage in bytes
*/
public function getPeakMemory(): int;
/**
* Reset all collected metrics
*/
public function reset(): void;
/**
* Check if performance tracking is enabled
*/
public function isEnabled(): bool;
/**
* Enable or disable performance tracking
*/
public function setEnabled(bool $enabled): void;
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Contracts;
use App\Framework\Filesystem\FilePath;
use RuntimeException;
interface PerformanceReporterInterface
{
/**
* Generate a performance report in the specified format
*
* @param string $format Supported formats: 'array', 'json', 'html', 'text'
* @return string|array Returns array for 'array' format, string for others
* @throws \InvalidArgumentException If format is not supported
*/
public function generateReport(string $format = 'array'): string|array;
/**
* Get performance summary data
*
* @return array{
* total_request_time_ms: float,
* total_request_memory_bytes: int,
* peak_memory_bytes: int,
* metrics_count: int
* }
*/
public function getSummary(): array;
/**
* Get top metrics by execution time
*
* @param int $limit Maximum number of metrics to return
* @return array Array of metric data sorted by total duration descending
*/
public function getTopMetricsByTime(int $limit = 10): array;
/**
* Get top metrics by memory usage
*
* @param int $limit Maximum number of metrics to return
* @return array Array of metric data sorted by total memory descending
*/
public function getTopMetricsByMemory(int $limit = 10): array;
/**
* Get metrics grouped by category with aggregated statistics
*
* @return array<string, array{
* metrics_count: int,
* total_time_ms: float,
* total_calls: int,
* avg_time_ms: float,
* metrics: array
* }>
*/
public function getMetricsByCategory(): array;
/**
* Export report to file
*
* @param FilePath $filepath Path to export file
* @param string $format Export format
* @throws RuntimeException If file cannot be written
*/
public function exportToFile(FilePath $filepath, string $format = 'json'): void;
}

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Contracts;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\Performance\PerformanceConfig;
use App\Framework\Performance\PerformanceMetric;
interface PerformanceServiceInterface
{
/**
* Measure a callable's execution time and memory usage
*/
public function measure(string $key, callable $callback, PerformanceCategory $category = PerformanceCategory::CUSTOM, array $context = []): mixed;
/**
* Start timing an operation
*/
public function startTiming(string $key, PerformanceCategory $category = PerformanceCategory::CUSTOM, array $context = []): void;
/**
* End timing an operation
*/
public function endTiming(string $key): void;
/**
* Record a metric value
*/
public function recordMetric(string $key, float $value, PerformanceCategory $category = PerformanceCategory::CUSTOM, array $context = []): void;
/**
* Increment a counter
*/
public function increment(string $key, int $amount = 1, PerformanceCategory $category = PerformanceCategory::CUSTOM, array $context = []): void;
/**
* Get all metrics for a category
*
* @return array<string, PerformanceMetric>
*/
public function getMetrics(?PerformanceCategory $category = null): array;
/**
* Get a specific metric
*/
public function getMetric(string $key): ?PerformanceMetric;
/**
* Generate a performance report
*
* @param string $format Supported formats: 'array', 'json', 'html', 'text'
* @return string|array
*/
public function generateReport(string $format = 'array'): string|array;
/**
* Get current request statistics
*
* @return array{
* time_ms: float,
* memory_bytes: int,
* peak_memory_bytes: int,
* metrics_count: int
* }
*/
public function getRequestStats(): array;
/**
* Get performance summary
*
* @return array
*/
public function getSummary(): array;
/**
* Get slowest operations
*
* @param int $limit Maximum number of operations to return
* @return array
*/
public function getSlowestOperations(int $limit = 10): array;
/**
* Export metrics to array for external processing
*
* @return array
*/
public function exportMetrics(): array;
/**
* Reset all metrics
*/
public function reset(): void;
/**
* Check if performance tracking is enabled
*/
public function isEnabled(): bool;
/**
* Enable or disable performance tracking
*/
public function setEnabled(bool $enabled): void;
/**
* Get configuration
*/
public function getConfig(): PerformanceConfig;
// Convenience methods for common operations
/**
* Measure database query timing
*/
public function measureDatabaseQuery(string $queryType, callable $callback, array $context = []): mixed;
/**
* Measure cache operation timing
*/
public function measureCacheOperation(string $operation, callable $callback, array $context = []): mixed;
/**
* Measure view rendering timing
*/
public function measureViewRender(string $view, callable $callback, array $context = []): mixed;
}

View File

@@ -0,0 +1,557 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance;
use App\Framework\Attributes\Singleton;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\HighResolutionClock;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Performance\ValueObjects\Measurement;
/**
* Enhanced performance collector with nanosecond-precision timing using HighResolutionClock
*/
#[Singleton]
final class EnhancedPerformanceCollector implements PerformanceCollectorInterface
{
/** @var array<string, PerformanceMetric> */
private array $metrics = [];
/** @var array<string, array{start_time: Duration, start_memory: int, timestamp: Timestamp, parent?: string, depth: int}> */
private array $activeTimers = [];
/** @var string[] Call stack of active operation keys */
private array $callStack = [];
/** @var array<string, array{key: string, parent?: string, depth: int, children: string[], self_time?: float, total_time?: float}> */
private array $nestedStructure = [];
private bool $enabled;
private Duration $requestStartNanos;
#private Timestamp $requestStart;
private int $requestMemoryStart;
public function __construct(
private readonly Clock $clock,
private readonly HighResolutionClock $highResClock,
private readonly MemoryMonitor $memoryMonitor,
bool $enabled = false // Temporarily disabled to fix memory issues
) {
$this->enabled = $enabled;
if ($enabled) {
$this->requestStartNanos = $this->highResClock->hrtime();
// Record initial memory usage only if enabled
$this->requestMemoryStart = $this->memoryMonitor->getCurrentMemory()->toBytes();
} else {
$this->requestStartNanos = Duration::zero();
$this->requestMemoryStart = 0;
}
}
public function startTiming(string $key, PerformanceCategory $category, array $context = []): void
{
if (! $this->enabled) {
return;
}
// Get current parent from call stack
$parent = end($this->callStack) ?: null;
$depth = count($this->callStack);
$this->activeTimers[$key] = [
'start_time' => $this->highResClock->hrtime(),
'start_memory' => $this->memoryMonitor->getCurrentMemory()->toBytes(),
'timestamp' => $this->clock->time(),
'parent' => $parent,
'depth' => $depth,
];
// Add to call stack
$this->callStack[] = $key;
// Initialize nested structure
$this->nestedStructure[$key] = [
'key' => $key,
'parent' => $parent,
'depth' => $depth,
'children' => [],
];
// Add as child to parent if exists
if ($parent && isset($this->nestedStructure[$parent])) {
$this->nestedStructure[$parent]['children'][] = $key;
}
if (! isset($this->metrics[$key])) {
$this->metrics[$key] = PerformanceMetric::create($key, $category, array_merge($context, [
'depth' => $depth,
'parent' => $parent,
]));
}
}
public function endTiming(string $key): void
{
if (! $this->enabled || ! isset($this->activeTimers[$key])) {
return;
}
$startData = $this->activeTimers[$key];
$measurement = Measurement::endHighResTiming(
[
'start_time' => $startData['start_time'],
'start_memory' => $startData['start_memory'],
],
$this->highResClock,
$this->memoryMonitor,
$this->clock
);
$totalTime = $measurement->getDuration()->toMilliseconds();
// Calculate self time (total time minus children time)
$childrenTime = $this->calculateChildrenTime($key);
$selfTime = max(0, $totalTime - $childrenTime);
// Update nested structure with timing info
if (isset($this->nestedStructure[$key])) {
$this->nestedStructure[$key]['total_time'] = $totalTime;
$this->nestedStructure[$key]['self_time'] = $selfTime;
}
if (isset($this->metrics[$key])) {
$this->metrics[$key]->addMeasurementObject($measurement);
}
// Remove from call stack
$this->removeFromCallStack($key);
unset($this->activeTimers[$key]);
}
public function measure(string $key, PerformanceCategory $category, callable $callback, array $context = []): mixed
{
if (! $this->enabled) {
return $callback();
}
$this->startTiming($key, $category, $context);
try {
$result = $callback();
} finally {
$this->endTiming($key);
}
return $result;
}
public function recordMetric(string $key, PerformanceCategory $category, float $value, array $context = []): void
{
if (! $this->enabled) {
return;
}
if (! isset($this->metrics[$key])) {
$this->metrics[$key] = PerformanceMetric::create($key, $category, $context);
}
$this->metrics[$key]->addValue($value);
}
public function increment(string $key, PerformanceCategory $category, int $amount = 1, array $context = []): void
{
if (! $this->enabled) {
return;
}
if (! isset($this->metrics[$key])) {
$this->metrics[$key] = PerformanceMetric::create($key, $category, $context);
}
$this->metrics[$key]->increment($amount);
}
public function getMetrics(?PerformanceCategory $category = null): array
{
if ($category === null) {
return $this->metrics;
}
return array_filter(
$this->metrics,
fn (PerformanceMetric $metric) => $metric->getCategory() === $category
);
}
public function getMetric(string $key): ?PerformanceMetric
{
return $this->metrics[$key] ?? null;
}
public function getTotalRequestTime(): float
{
$currentTime = $this->highResClock->hrtime();
$duration = $currentTime->subtract($this->requestStartNanos);
return $duration->toMilliseconds();
}
public function getTotalRequestMemory(): int
{
$currentMemory = $this->memoryMonitor->getCurrentMemory()->toBytes();
return max(0, $currentMemory - $this->requestMemoryStart);
}
public function getPeakMemory(): int
{
return $this->memoryMonitor->getPeakMemory()->toBytes();
}
public function reset(): void
{
$this->metrics = [];
$this->activeTimers = [];
$this->callStack = [];
$this->nestedStructure = [];
$this->requestStartNanos = $this->highResClock->hrtime();
#$this->requestStart = Timestamp::fromClock($this->clock);
$this->requestMemoryStart = $this->memoryMonitor->getCurrentMemory()->toBytes();
}
public function isEnabled(): bool
{
return $this->enabled;
}
public function setEnabled(bool $enabled): void
{
$this->enabled = $enabled;
}
/**
* Get all active timers (useful for debugging)
*/
public function getActiveTimers(): array
{
return array_keys($this->activeTimers);
}
/**
* Check if a timer is active
*/
public function hasActiveTimer(string $key): bool
{
return isset($this->activeTimers[$key]);
}
/**
* Benchmark a callable with nanosecond precision
*/
public function benchmark(string $key, callable $callback, int $iterations = 1): array
{
if (! $this->enabled || $iterations < 1) {
return [
'total' => Duration::zero(),
'average' => Duration::zero(),
'min' => Duration::zero(),
'max' => Duration::zero(),
'result' => $iterations === 1 ? $callback() : null,
];
}
$results = $this->highResClock->benchmark($callback, $iterations);
// Store as performance metric
$category = PerformanceCategory::BENCHMARK;
if (! isset($this->metrics[$key])) {
$this->metrics[$key] = PerformanceMetric::create($key, $category, [
'iterations' => $iterations,
'benchmark' => true,
]);
}
// Add the average duration as a measurement
$avgMeasurement = Measurement::create(
$results['average'],
Byte::fromBytes(0), // Benchmarks don't track memory by default
$this->clock->time()
);
$this->metrics[$key]->addMeasurementObject($avgMeasurement);
return $results;
}
/**
* Measure duration of a callable with nanosecond precision
*/
public function measureDuration(callable $callback): array
{
if (! $this->enabled) {
return [
'result' => $callback(),
'duration' => Duration::zero(),
];
}
return $this->highResClock->measureDuration($callback);
}
/**
* Get current high-resolution time as Duration
*/
public function getHighResTime(): Duration
{
return $this->highResClock->hrtime();
}
/**
* Get total request duration as Duration object with nanosecond precision
*/
public function getTotalRequestDuration(): Duration
{
$currentTime = $this->highResClock->hrtime();
return $currentTime->subtract($this->requestStartNanos);
}
// ========== NESTED MEASUREMENT FUNCTIONALITY ==========
/**
* Get current call stack depth
*/
public function getCallStackDepth(): int
{
return count($this->callStack);
}
/**
* Get current operation being measured
*/
public function getCurrentOperation(): ?string
{
return end($this->callStack) ?: null;
}
/**
* Get call stack
*/
public function getCallStack(): array
{
return $this->callStack;
}
/**
* Get nested structure with hierarchy information
*/
public function getNestedStructure(): array
{
return $this->nestedStructure;
}
/**
* Get hierarchical performance report
*/
public function getHierarchicalReport(): array
{
$rootOperations = array_filter(
$this->nestedStructure,
fn ($structure) => $structure['parent'] === null
);
$hierarchicalReport = [];
foreach ($rootOperations as $root) {
$hierarchicalReport[] = $this->buildHierarchicalNode($root);
}
return [
'operations' => $hierarchicalReport,
'call_stack_depth' => $this->getCallStackDepth(),
'active_operations' => count($this->activeTimers),
'total_operations' => count($this->nestedStructure),
'request_stats' => [
'total_time_ms' => $this->getTotalRequestTime(),
'total_memory_bytes' => $this->getTotalRequestMemory(),
'peak_memory_bytes' => $this->getPeakMemory(),
],
];
}
/**
* Get flat list of all operations with nested information
*/
public function getFlatReport(): array
{
$flatOperations = [];
foreach ($this->nestedStructure as $key => $structure) {
$metric = $this->metrics[$key] ?? null;
$flatOperations[] = [
'key' => $key,
'parent' => $structure['parent'],
'depth' => $structure['depth'],
'children_count' => count($structure['children']),
'total_time_ms' => $structure['total_time'] ?? null,
'self_time_ms' => $structure['self_time'] ?? null,
'metric_data' => $metric?->toArray(),
'is_active' => isset($this->activeTimers[$key]),
];
}
return $flatOperations;
}
/**
* Get execution tree as string (for debugging)
*/
public function getExecutionTree(): string
{
$tree = "=== Performance Execution Tree ===\n";
if (! empty($this->callStack)) {
$tree .= "Active Call Stack:\n";
foreach ($this->callStack as $i => $key) {
$structure = $this->nestedStructure[$key] ?? [];
$indent = str_repeat(' ', $structure['depth'] ?? 0);
$tree .= sprintf("%d. %s%s (running)\n", $i + 1, $indent, $key);
}
$tree .= "\n";
}
$rootOperations = array_filter(
$this->nestedStructure,
fn ($structure) => $structure['parent'] === null
);
if (! empty($rootOperations)) {
$tree .= "Completed Operations:\n";
foreach ($rootOperations as $root) {
$tree .= $this->buildExecutionTreeNode($root);
}
}
return $tree;
}
/**
* Calculate total time spent in children of an operation
*/
private function calculateChildrenTime(string $parentKey): float
{
if (! isset($this->nestedStructure[$parentKey])) {
return 0.0;
}
$totalChildTime = 0.0;
$children = $this->nestedStructure[$parentKey]['children'];
foreach ($children as $childKey) {
$childStructure = $this->nestedStructure[$childKey] ?? [];
$totalChildTime += $childStructure['total_time'] ?? 0.0;
}
return $totalChildTime;
}
/**
* Remove operation from call stack
*/
private function removeFromCallStack(string $key): void
{
$index = array_search($key, $this->callStack, true);
if ($index !== false) {
array_splice($this->callStack, $index, 1);
}
}
/**
* Build hierarchical node for report
*/
private function buildHierarchicalNode(array $structure): array
{
$node = [
'key' => $structure['key'],
'depth' => $structure['depth'],
'total_time_ms' => $structure['total_time'] ?? null,
'self_time_ms' => $structure['self_time'] ?? null,
'children' => [],
];
// Add metric data if available
if (isset($this->metrics[$structure['key']])) {
$node['metric_data'] = $this->metrics[$structure['key']]->toArray();
}
// Add children recursively
foreach ($structure['children'] as $childKey) {
if (isset($this->nestedStructure[$childKey])) {
$node['children'][] = $this->buildHierarchicalNode($this->nestedStructure[$childKey]);
}
}
return $node;
}
/**
* Build execution tree node as string
*/
private function buildExecutionTreeNode(array $structure, string $prefix = ''): string
{
$key = $structure['key'];
$totalTime = $structure['total_time'] ?? 0;
$selfTime = $structure['self_time'] ?? 0;
$isActive = isset($this->activeTimers[$key]);
$line = sprintf(
"%s%s: %.2fms (self: %.2fms)%s\n",
$prefix,
$key,
$totalTime,
$selfTime,
$isActive ? ' [ACTIVE]' : ''
);
// Add children
foreach ($structure['children'] as $childKey) {
if (isset($this->nestedStructure[$childKey])) {
$line .= $this->buildExecutionTreeNode(
$this->nestedStructure[$childKey],
$prefix . ' '
);
}
}
return $line;
}
/**
* Get performance summary with nested information
*/
public function getNestedSummary(): array
{
$summary = [
'total_operations' => count($this->nestedStructure),
'active_operations' => count($this->activeTimers),
'max_depth' => 0,
'total_request_time_ms' => $this->getTotalRequestTime(),
];
// Calculate max depth and other stats
foreach ($this->nestedStructure as $structure) {
$summary['max_depth'] = max($summary['max_depth'], $structure['depth']);
}
return $summary;
}
}

View File

@@ -1,18 +0,0 @@
<?php
namespace App\Framework\Performance\EventHandler;
use App\Framework\Core\Events\ApplicationBooted;
use App\Framework\Core\Events\OnEvent;
use App\Framework\Performance\PerformanceMeter;
final readonly class MeasureApplicationBootHandler
{
public function __construct(private PerformanceMeter $meter) {}
#[OnEvent]
public function onApplicationBooted(ApplicationBooted $event): void
{
$this->meter->mark('application:booted');
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Events;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Performance\ValueObjects\PerformanceSnapshot;
/**
* Event emitted when a performance-tracked operation completes successfully
*/
final readonly class OperationCompletedEvent
{
public function __construct(
public PerformanceSnapshot $snapshot,
public Timestamp $timestamp
) {
}
/**
* Get telemetry data for monitoring systems
*/
public function toTelemetryData(): array
{
return array_merge(
$this->snapshot->toTelemetryData(),
[
'event_type' => 'operation_completed',
'status' => 'success',
]
);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Events;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Performance\ValueObjects\PerformanceSnapshot;
/**
* Event emitted when a performance-tracked operation fails
*/
final readonly class OperationFailedEvent
{
public function __construct(
public PerformanceSnapshot $snapshot,
public \Throwable $exception,
public Timestamp $timestamp
) {
}
/**
* Get telemetry data for monitoring systems
*/
public function toTelemetryData(): array
{
return array_merge(
$this->snapshot->toTelemetryData(),
[
'event_type' => 'operation_failed',
'status' => 'error',
'error_type' => get_class($this->exception),
'error_message' => $this->exception->getMessage(),
'error_code' => $this->exception->getCode(),
]
);
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Events;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Performance\PerformanceCategory;
/**
* Event emitted when a performance-tracked operation starts
*/
final readonly class OperationStartedEvent
{
public function __construct(
public string $operationId,
public PerformanceCategory $category,
public array $contextData,
public Timestamp $timestamp
) {
}
}

View File

@@ -1,455 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Examples;
use App\Framework\Performance\PerformanceMeter;
use App\Framework\Performance\PerformanceCategory;
/**
* Beispiel für Database-Performance-Tracking
*
* Zeigt, wie Database-Operationen mit dem PerformanceMeter
* gemessen und analysiert werden können.
*/
class DatabasePerformanceExample
{
private PerformanceMeter $meter;
private array $queryLog = [];
private array $config;
public function __construct(PerformanceMeter $meter, array $config = [])
{
$this->meter = $meter;
$this->config = array_merge([
'log_all_queries' => false,
'log_slow_queries_only' => true,
'slow_query_threshold_ms' => 100,
'track_query_patterns' => true,
'track_connection_pool' => true,
], $config);
}
/**
* Query-Ausführung mit Performance-Tracking
*/
public function executeQuery(string $sql, array $params = []): array
{
$queryId = uniqid('query_');
$queryType = $this->getQueryType($sql);
$measurementId = "db_query_{$queryType}_{$queryId}";
// Query-Start markieren
$this->meter->mark("query_start_{$queryType}", PerformanceCategory::DATABASE);
// Query-Kontext sammeln
$queryContext = [
'sql' => $sql,
'params' => $params,
'type' => $queryType,
'start_time' => microtime(true),
];
$this->queryLog[$queryId] = $queryContext;
// Hauptmessung starten
$this->meter->startMeasure($measurementId, PerformanceCategory::DATABASE);
try {
// Query-Preparation messen
$statement = $this->meter->measure('query_preparation', function() use ($sql) {
return $this->prepareStatement($sql);
}, PerformanceCategory::DATABASE);
// Parameter-Binding messen
if (!empty($params)) {
$this->meter->measure('parameter_binding', function() use ($statement, $params) {
$this->bindParameters($statement, $params);
}, PerformanceCategory::DATABASE);
}
// Query-Execution messen
$result = $this->meter->measure('query_execution', function() use ($statement) {
return $this->executeStatement($statement);
}, PerformanceCategory::DATABASE);
// Result-Fetching messen (bei SELECT)
if ($queryType === 'SELECT') {
$data = $this->meter->measure('result_fetching', function() use ($result) {
return $this->fetchResults($result);
}, PerformanceCategory::DATABASE);
} else {
$data = $result;
}
// Query-Ende markieren
$this->meter->mark("query_end_{$queryType}", PerformanceCategory::DATABASE);
// Performance-Analyse
$this->analyzeQueryPerformance($queryId, $queryContext);
return $data;
} catch (\Exception $e) {
$this->meter->mark("query_error_{$queryType}", PerformanceCategory::DATABASE);
throw $e;
} finally {
$this->meter->endMeasure($measurementId);
unset($this->queryLog[$queryId]);
}
}
/**
* Transaction-Performance-Tracking
*/
public function executeTransaction(callable $operations): mixed
{
$transactionId = uniqid('trans_');
$measurementId = "db_transaction_{$transactionId}";
$this->meter->mark('transaction_begin', PerformanceCategory::DATABASE);
$this->meter->startMeasure($measurementId, PerformanceCategory::DATABASE);
// Transaction-Start messen
$this->meter->measure('transaction_start', function() {
$this->beginTransaction();
}, PerformanceCategory::DATABASE);
try {
// Transaction-Operationen ausführen
$result = $this->meter->measure('transaction_operations', function() use ($operations) {
return $operations();
}, PerformanceCategory::DATABASE);
// Commit messen
$this->meter->measure('transaction_commit', function() {
$this->commitTransaction();
}, PerformanceCategory::DATABASE);
$this->meter->mark('transaction_committed', PerformanceCategory::DATABASE);
return $result;
} catch (\Exception $e) {
// Rollback messen
$this->meter->measure('transaction_rollback', function() {
$this->rollbackTransaction();
}, PerformanceCategory::DATABASE);
$this->meter->mark('transaction_rolled_back', PerformanceCategory::DATABASE);
throw $e;
} finally {
$this->meter->endMeasure($measurementId);
}
}
/**
* Bulk-Insert mit Performance-Tracking
*/
public function bulkInsert(string $table, array $data): bool
{
$recordCount = count($data);
$measurementId = "bulk_insert_{$table}_{$recordCount}";
$this->meter->mark("bulk_insert_start_{$table}", PerformanceCategory::DATABASE);
$this->meter->startMeasure($measurementId, PerformanceCategory::DATABASE);
try {
// Data-Preparation
$preparedData = $this->meter->measure('bulk_data_preparation', function() use ($data) {
return $this->prepareBulkData($data);
}, PerformanceCategory::DATABASE);
// SQL-Generation
$sql = $this->meter->measure('bulk_sql_generation', function() use ($table, $preparedData) {
return $this->generateBulkInsertSql($table, $preparedData);
}, PerformanceCategory::DATABASE);
// Bulk-Execution
$result = $this->meter->measure('bulk_execution', function() use ($sql, $preparedData) {
return $this->executeBulkInsert($sql, $preparedData);
}, PerformanceCategory::DATABASE);
$this->meter->mark("bulk_insert_completed_{$table}", PerformanceCategory::DATABASE);
// Performance-Statistiken
$this->logBulkInsertStats($table, $recordCount);
return $result;
} finally {
$this->meter->endMeasure($measurementId);
}
}
/**
* Connection-Pool Performance-Tracking
*/
public function withConnection(callable $operation): mixed
{
if (!$this->config['track_connection_pool']) {
return $operation();
}
$connectionId = uniqid('conn_');
// Connection-Acquisition messen
$connection = $this->meter->measure('connection_acquisition', function() {
return $this->acquireConnection();
}, PerformanceCategory::DATABASE);
$this->meter->mark('connection_acquired', PerformanceCategory::DATABASE);
try {
// Operation ausführen
return $this->meter->measure("connection_operation_{$connectionId}", function() use ($operation, $connection) {
return $operation($connection);
}, PerformanceCategory::DATABASE);
} finally {
// Connection-Release messen
$this->meter->measure('connection_release', function() use ($connection) {
$this->releaseConnection($connection);
}, PerformanceCategory::DATABASE);
$this->meter->mark('connection_released', PerformanceCategory::DATABASE);
}
}
/**
* Query-Performance analysieren
*/
private function analyzeQueryPerformance(string $queryId, array $context): void
{
$duration = (microtime(true) - $context['start_time']) * 1000;
$queryType = $context['type'];
// Slow-Query Detection
if ($duration > $this->config['slow_query_threshold_ms']) {
$this->meter->mark("slow_query_{$queryType}", PerformanceCategory::DATABASE);
$this->handleSlowQuery($context, $duration);
}
// Query-Pattern-Tracking
if ($this->config['track_query_patterns']) {
$this->trackQueryPattern($context, $duration);
}
// Logging-Entscheidung
$shouldLog = $this->config['log_all_queries'] ||
($this->config['log_slow_queries_only'] && $duration > $this->config['slow_query_threshold_ms']);
if ($shouldLog) {
$this->logQueryPerformance($context, $duration);
}
}
/**
* Langsame Queries behandeln
*/
private function handleSlowQuery(array $context, float $duration): void
{
error_log("SLOW QUERY DETECTED: {$context['type']} took {$duration}ms");
error_log("SQL: " . $this->sanitizeSqlForLogging($context['sql']));
// Detailed Performance-Report für slow queries
$report = $this->meter->generateReport();
$dbMeasurements = array_filter($report['measurements'], function($m) {
return $m['category'] === 'database';
});
if (!empty($dbMeasurements)) {
error_log("DB Performance breakdown: " . json_encode($dbMeasurements));
}
}
/**
* Query-Pattern-Tracking
*/
private function trackQueryPattern(array $context, float $duration): void
{
$pattern = $this->extractQueryPattern($context['sql']);
$patternMeasurement = "query_pattern_{$pattern}";
// Pattern-spezifische Statistiken in separater Messung
static $patternMeasurements = [];
if (!isset($patternMeasurements[$pattern])) {
$patternMeasurements[$pattern] = [
'count' => 0,
'total_time' => 0,
'measurement_id' => $patternMeasurement,
];
}
$patternMeasurements[$pattern]['count']++;
$patternMeasurements[$pattern]['total_time'] += $duration;
// Bei häufigen Patterns: Marker setzen
if ($patternMeasurements[$pattern]['count'] % 10 === 0) {
$avgTime = $patternMeasurements[$pattern]['total_time'] / $patternMeasurements[$pattern]['count'];
$this->meter->mark("frequent_pattern_{$pattern}_avg_{$avgTime}ms", PerformanceCategory::DATABASE);
}
}
/**
* Bulk-Insert-Statistiken loggen
*/
private function logBulkInsertStats(string $table, int $recordCount): void
{
$report = $this->meter->generateReport();
// Bulk-Insert-spezifische Messungen finden
$bulkMeasurements = array_filter($report['measurements'], function($m) {
return strpos($m['category'], 'bulk') !== false ||
strpos($m['category'], 'database') !== false;
});
$totalTime = array_sum(array_column($bulkMeasurements, 'total_time_ms'));
$recordsPerSecond = $recordCount / ($totalTime / 1000);
error_log("BULK INSERT STATS: {$table} - {$recordCount} records in {$totalTime}ms ({$recordsPerSecond} records/sec)");
}
/**
* Query-Performance loggen
*/
private function logQueryPerformance(array $context, float $duration): void
{
$logData = [
'query_type' => $context['type'],
'duration_ms' => round($duration, 2),
'sql_pattern' => $this->extractQueryPattern($context['sql']),
'param_count' => count($context['params']),
'timestamp' => microtime(true),
];
// Sensitive Parameter entfernen
if (!empty($context['params'])) {
$logData['has_params'] = true;
}
error_log('DB_PERFORMANCE: ' . json_encode($logData));
}
// === HELPER-METHODEN ===
private function getQueryType(string $sql): string
{
$sql = trim(strtoupper($sql));
if (strpos($sql, 'SELECT') === 0) return 'SELECT';
if (strpos($sql, 'INSERT') === 0) return 'INSERT';
if (strpos($sql, 'UPDATE') === 0) return 'UPDATE';
if (strpos($sql, 'DELETE') === 0) return 'DELETE';
if (strpos($sql, 'CREATE') === 0) return 'CREATE';
if (strpos($sql, 'ALTER') === 0) return 'ALTER';
if (strpos($sql, 'DROP') === 0) return 'DROP';
return 'OTHER';
}
private function extractQueryPattern(string $sql): string
{
// SQL normalisieren für Pattern-Erkennung
$pattern = preg_replace('/\s+/', ' ', trim($sql));
$pattern = preg_replace('/\d+/', '?', $pattern); // Zahlen durch ? ersetzen
$pattern = preg_replace("/'[^']*'/", '?', $pattern); // Strings durch ? ersetzen
$pattern = preg_replace('/\([^)]*\)/', '(?)', $pattern); // Parameter-Listen normalisieren
return substr($pattern, 0, 100); // Auf 100 Zeichen kürzen
}
private function sanitizeSqlForLogging(string $sql): string
{
// Sensitive Daten für Logging entfernen
$sanitized = preg_replace("/'[^']*'/", "'***'", $sql);
return substr($sanitized, 0, 200) . (strlen($sanitized) > 200 ? '...' : '');
}
// === DUMMY-IMPLEMENTIERUNGEN ===
private function prepareStatement(string $sql): object
{
usleep(2000); // 2ms für Statement-Preparation
return (object)['sql' => $sql, 'prepared' => true];
}
private function bindParameters(object $statement, array $params): void
{
usleep(1000 * count($params)); // 1ms pro Parameter
}
private function executeStatement(object $statement): object
{
$queryType = $this->getQueryType($statement->sql);
$executionTimes = [
'SELECT' => rand(5000, 50000), // 5-50ms
'INSERT' => rand(2000, 15000), // 2-15ms
'UPDATE' => rand(3000, 25000), // 3-25ms
'DELETE' => rand(3000, 20000), // 3-20ms
'OTHER' => rand(1000, 10000), // 1-10ms
];
usleep($executionTimes[$queryType] ?? 5000);
return (object)[
'success' => true,
'affected_rows' => rand(1, 100),
'data' => range(1, rand(1, 1000)),
];
}
private function fetchResults(object $result): array
{
$rowCount = count($result->data);
usleep(100 * $rowCount); // 0.1ms pro Row
return $result->data;
}
private function beginTransaction(): void
{
usleep(1000); // 1ms für Transaction-Start
}
private function commitTransaction(): void
{
usleep(3000); // 3ms für Commit
}
private function rollbackTransaction(): void
{
usleep(2000); // 2ms für Rollback
}
private function prepareBulkData(array $data): array
{
usleep(500 * count($data)); // 0.5ms pro Record
return $data;
}
private function generateBulkInsertSql(string $table, array $data): string
{
usleep(2000); // 2ms für SQL-Generation
return "INSERT INTO {$table} VALUES " . str_repeat('(?),', count($data));
}
private function executeBulkInsert(string $sql, array $data): bool
{
usleep(1000 * count($data)); // 1ms pro Record
return true;
}
private function acquireConnection(): object
{
usleep(rand(1000, 10000)); // 1-10ms für Connection-Akquisition
return (object)['id' => uniqid('conn_'), 'acquired_at' => microtime(true)];
}
private function releaseConnection(object $connection): void
{
usleep(500); // 0.5ms für Connection-Release
}
}

View File

@@ -1,538 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Examples;
use App\Framework\Performance\PerformanceMeter;
use App\Framework\Performance\PerformanceCategory;
/**
* Beispiel-Event-Handler für Event-Performance-Tracking
*
* Dieser Handler zeigt, wie Performance-Messungen bei verschiedenen
* Event-Typen implementiert werden können.
*/
class PerformanceEventHandlerExample
{
private PerformanceMeter $meter;
private array $activeEventMeasurements = [];
private array $eventStatistics = [];
private array $config;
public function __construct(PerformanceMeter $meter, array $config = [])
{
$this->meter = $meter;
$this->config = array_merge([
'track_all_events' => true,
'track_slow_events_only' => false,
'slow_event_threshold_ms' => 100,
'detailed_sub_measurements' => true,
'enable_event_statistics' => true,
], $config);
}
/**
* Event-Start-Handler - beginnt Performance-Tracking
*/
public function onEventStart($event): void
{
$eventName = $this->getEventName($event);
$eventId = $this->getEventId($event);
$measurementId = "event_{$eventName}_{$eventId}";
// Event-Start markieren
$this->meter->mark("event_start_{$eventName}", PerformanceCategory::EVENT);
// Hauptmessung für das Event starten
$this->meter->startMeasure($measurementId, PerformanceCategory::EVENT);
// Für spätere Referenz speichern
$this->activeEventMeasurements[$eventId] = [
'measurement_id' => $measurementId,
'event_name' => $eventName,
'start_time' => microtime(true),
'start_memory' => memory_get_usage(true),
];
// Event-spezifische Performance-Messungen
$this->trackEventSpecificMetrics($event, $eventName);
}
/**
* Event-Ende-Handler - beendet Performance-Tracking
*/
public function onEventEnd($event): void
{
$eventId = $this->getEventId($event);
if (!isset($this->activeEventMeasurements[$eventId])) {
return; // Event wurde nicht getrackt
}
$measurement = $this->activeEventMeasurements[$eventId];
$eventName = $measurement['event_name'];
$measurementId = $measurement['measurement_id'];
// Hauptmessung beenden
$this->meter->endMeasure($measurementId);
// Event-Ende markieren
$this->meter->mark("event_end_{$eventName}", PerformanceCategory::EVENT);
// Performance-Analyse
$duration = (microtime(true) - $measurement['start_time']) * 1000;
$memoryUsed = (memory_get_usage(true) - $measurement['start_memory']) / 1024 / 1024;
// Statistiken aktualisieren
if ($this->config['enable_event_statistics']) {
$this->updateEventStatistics($eventName, $duration, $memoryUsed);
}
// Langsame Events kennzeichnen
if ($duration > $this->config['slow_event_threshold_ms']) {
$this->meter->mark("slow_event_{$eventName}", PerformanceCategory::EVENT);
$this->handleSlowEvent($event, $duration);
}
// Performance-Daten ausgeben/loggen
$this->logEventPerformance($event, $duration, $memoryUsed);
// Cleanup
unset($this->activeEventMeasurements[$eventId]);
}
/**
* Spezifische Event-Handler mit detaillierten Messungen
*/
public function handleUserRegistrationEvent($event): void
{
$this->meter->startMeasure('user_registration_processing', PerformanceCategory::USER);
// Email-Validierung messen
$this->meter->measure('email_validation', function() use ($event) {
$this->validateUserEmail($event->getEmail());
}, PerformanceCategory::VALIDATION);
// Password-Hashing messen
$this->meter->measure('password_hashing', function() use ($event) {
$this->hashUserPassword($event->getPassword());
}, PerformanceCategory::SECURITY);
// Database-Insert messen
$this->meter->measure('user_db_insert', function() use ($event) {
$this->insertUserToDatabase($event->getUserData());
}, PerformanceCategory::DATABASE);
// Welcome-Email senden (async)
$this->meter->measure('welcome_email_queue', function() use ($event) {
$this->queueWelcomeEmail($event->getUser());
}, PerformanceCategory::EXTERNAL);
$this->meter->endMeasure('user_registration_processing');
// Zusätzliche Marker für wichtige Schritte
$this->meter->mark('user_registered', PerformanceCategory::USER);
}
/**
* E-Commerce Order Event Handler
*/
public function handleOrderCreatedEvent($event): void
{
$this->meter->startMeasure('order_processing', PerformanceCategory::BUSINESS);
// Inventory-Check
$this->meter->measure('inventory_check', function() use ($event) {
$this->checkOrderInventory($event->getOrderItems());
}, PerformanceCategory::DATABASE);
// Payment-Processing
$this->meter->measure('payment_processing', function() use ($event) {
$this->processOrderPayment($event->getPaymentData());
}, PerformanceCategory::EXTERNAL);
// Tax-Calculation
$this->meter->measure('tax_calculation', function() use ($event) {
$this->calculateOrderTax($event->getOrder());
}, PerformanceCategory::BUSINESS);
// Order-Confirmation
$this->meter->measure('order_confirmation', function() use ($event) {
$this->sendOrderConfirmation($event->getOrder());
}, PerformanceCategory::EXTERNAL);
// Inventory-Update
$this->meter->measure('inventory_update', function() use ($event) {
$this->updateInventoryAfterOrder($event->getOrderItems());
}, PerformanceCategory::DATABASE);
$this->meter->endMeasure('order_processing');
$this->meter->mark('order_completed', PerformanceCategory::BUSINESS);
}
/**
* File-Upload Event Handler
*/
public function handleFileUploadEvent($event): void
{
$this->meter->startMeasure('file_upload_processing', PerformanceCategory::FILE);
$fileSize = $event->getFileSize();
$fileName = $event->getFileName();
// File-Validation
$this->meter->measure('file_validation', function() use ($event) {
$this->validateUploadedFile($event->getFile());
}, PerformanceCategory::VALIDATION);
// Virus-Scan (bei großen Files)
if ($fileSize > 1024 * 1024) { // > 1MB
$this->meter->measure('virus_scan', function() use ($event) {
$this->scanFileForViruses($event->getFile());
}, PerformanceCategory::SECURITY);
}
// File-Resize/Processing (bei Images)
if ($this->isImage($fileName)) {
$this->meter->measure('image_processing', function() use ($event) {
$this->processUploadedImage($event->getFile());
}, PerformanceCategory::FILE);
}
// File-Storage
$this->meter->measure('file_storage', function() use ($event) {
$this->storeFile($event->getFile(), $event->getStoragePath());
}, PerformanceCategory::FILE);
// Metadata-Extraction
$this->meter->measure('metadata_extraction', function() use ($event) {
$this->extractFileMetadata($event->getFile());
}, PerformanceCategory::FILE);
$this->meter->endMeasure('file_upload_processing');
// File-Size-spezifische Marker
if ($fileSize > 10 * 1024 * 1024) { // > 10MB
$this->meter->mark('large_file_uploaded', PerformanceCategory::FILE);
}
}
/**
* Cache-Event Handler
*/
public function handleCacheEvent($event): void
{
$operation = $event->getOperation(); // 'get', 'set', 'delete', 'flush'
$cacheKey = $event->getCacheKey();
$measurementId = "cache_{$operation}_{$cacheKey}";
$result = $this->meter->measure($measurementId, function() use ($event) {
return $this->executeCacheOperation($event);
}, PerformanceCategory::CACHE);
// Cache-Hit/Miss markieren
if ($operation === 'get') {
$hitMiss = $result !== null ? 'hit' : 'miss';
$this->meter->mark("cache_{$hitMiss}_{$cacheKey}", PerformanceCategory::CACHE);
}
// Cache-Statistiken aktualisieren
$this->updateCacheStatistics($operation, $cacheKey, $result);
}
/**
* Event-spezifische Metriken tracken
*/
private function trackEventSpecificMetrics($event, string $eventName): void
{
if (!$this->config['detailed_sub_measurements']) {
return;
}
// Event-Serialization messen (falls Event serialisiert wird)
if (method_exists($event, 'serialize')) {
$this->meter->measure("event_serialization_{$eventName}", function() use ($event) {
$event->serialize();
}, PerformanceCategory::EVENT);
}
// Event-Validation messen
if (method_exists($event, 'validate')) {
$this->meter->measure("event_validation_{$eventName}", function() use ($event) {
$event->validate();
}, PerformanceCategory::VALIDATION);
}
// Listener-Count als Marker
$listenerCount = $this->getListenerCount($eventName);
if ($listenerCount > 5) {
$this->meter->mark("many_listeners_{$eventName}", PerformanceCategory::EVENT);
}
}
/**
* Langsame Events behandeln
*/
private function handleSlowEvent($event, float $duration): void
{
$eventName = $this->getEventName($event);
// Warnung loggen
error_log("SLOW EVENT WARNING: {$eventName} took {$duration}ms");
// Detaillierte Informationen sammeln
$report = $this->meter->generateReport();
$slowOperations = [];
foreach ($report['measurements'] as $label => $data) {
if ($data['avg_time_ms'] > 50) { // Operationen > 50ms
$slowOperations[] = "{$label}: {$data['avg_time_ms']}ms";
}
}
if (!empty($slowOperations)) {
error_log("SLOW EVENT DETAILS: " . implode(', ', $slowOperations));
}
// Für kritische Events: Alert senden
if ($this->isCriticalEvent($eventName) && $duration > 1000) {
$this->sendPerformanceAlert($eventName, $duration);
}
}
/**
* Event-Performance loggen
*/
private function logEventPerformance($event, float $duration, float $memoryMb): void
{
$eventName = $this->getEventName($event);
// Entscheidung: Wann loggen?
$shouldLog = $this->config['track_all_events'] ||
($this->config['track_slow_events_only'] && $duration > $this->config['slow_event_threshold_ms']);
if (!$shouldLog) {
return;
}
$logData = [
'event_name' => $eventName,
'event_id' => $this->getEventId($event),
'duration_ms' => round($duration, 2),
'memory_mb' => round($memoryMb, 2),
'timestamp' => microtime(true),
];
// Event-spezifische Daten hinzufügen
if (method_exists($event, 'getLogData')) {
$logData['event_data'] = $event->getLogData();
}
error_log('EVENT_PERFORMANCE: ' . json_encode($logData));
}
/**
* Event-Statistiken aktualisieren
*/
private function updateEventStatistics(string $eventName, float $duration, float $memory): void
{
if (!isset($this->eventStatistics[$eventName])) {
$this->eventStatistics[$eventName] = [
'count' => 0,
'total_time_ms' => 0,
'total_memory_mb' => 0,
'min_time_ms' => PHP_FLOAT_MAX,
'max_time_ms' => 0,
];
}
$stats = &$this->eventStatistics[$eventName];
$stats['count']++;
$stats['total_time_ms'] += $duration;
$stats['total_memory_mb'] += $memory;
$stats['min_time_ms'] = min($stats['min_time_ms'], $duration);
$stats['max_time_ms'] = max($stats['max_time_ms'], $duration);
$stats['avg_time_ms'] = $stats['total_time_ms'] / $stats['count'];
$stats['avg_memory_mb'] = $stats['total_memory_mb'] / $stats['count'];
}
/**
* Event-Statistiken abrufen
*/
public function getEventStatistics(): array
{
return $this->eventStatistics;
}
/**
* Performance-Bericht für alle Events
*/
public function generateEventPerformanceReport(): string
{
$report = "EVENT PERFORMANCE STATISTICS\n";
$report .= str_repeat('=', 50) . "\n";
foreach ($this->eventStatistics as $eventName => $stats) {
$report .= sprintf(
"%-30s Count: %4d | Avg: %6.2fms | Min: %6.2fms | Max: %6.2fms | Mem: %5.2fMB\n",
$eventName,
$stats['count'],
$stats['avg_time_ms'],
$stats['min_time_ms'],
$stats['max_time_ms'],
$stats['avg_memory_mb']
);
}
return $report;
}
// === HELPER-METHODEN ===
private function getEventName($event): string
{
return get_class($event);
}
private function getEventId($event): string
{
return method_exists($event, 'getId') ? $event->getId() : uniqid();
}
private function getListenerCount(string $eventName): int
{
// Dummy-Implementation - in echter Anwendung würde hier
// die Anzahl der registrierten Listener zurückgegeben
return rand(1, 10);
}
private function isCriticalEvent(string $eventName): bool
{
$criticalEvents = [
'OrderCreatedEvent',
'PaymentProcessedEvent',
'UserRegistrationEvent',
'SecurityAlertEvent',
];
return in_array(basename($eventName), $criticalEvents);
}
private function sendPerformanceAlert(string $eventName, float $duration): void
{
// Dummy - in echter Implementierung: Slack, Email, SMS, etc.
error_log("CRITICAL PERFORMANCE ALERT: {$eventName} took {$duration}ms");
}
// === DUMMY-IMPLEMENTIERUNGEN FÜR VOLLSTÄNDIGE BEISPIELE ===
private function validateUserEmail(string $email): bool
{
usleep(5000); // 5ms simulierte Email-Validierung
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
private function hashUserPassword(string $password): string
{
usleep(100000); // 100ms simuliertes Password-Hashing (realistisch)
return password_hash($password, PASSWORD_DEFAULT);
}
private function insertUserToDatabase(array $userData): bool
{
usleep(25000); // 25ms simulierter DB-Insert
return true;
}
private function queueWelcomeEmail($user): void
{
usleep(2000); // 2ms für Queue-Operation
}
private function checkOrderInventory(array $items): bool
{
usleep(15000); // 15ms simulierte Inventory-Prüfung
return true;
}
private function processOrderPayment(array $paymentData): bool
{
usleep(200000); // 200ms simulierte Payment-Verarbeitung (realistisch)
return true;
}
private function calculateOrderTax(array $order): float
{
usleep(10000); // 10ms simulierte Tax-Berechnung
return 19.99;
}
private function sendOrderConfirmation(array $order): void
{
usleep(50000); // 50ms simulierte Email-Versendung
}
private function updateInventoryAfterOrder(array $items): void
{
usleep(20000); // 20ms simuliertes Inventory-Update
}
private function validateUploadedFile($file): bool
{
usleep(8000); // 8ms simulierte File-Validierung
return true;
}
private function scanFileForViruses($file): bool
{
usleep(150000); // 150ms simulierter Virus-Scan
return true;
}
private function isImage(string $fileName): bool
{
$imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
return in_array($extension, $imageExtensions);
}
private function processUploadedImage($file): void
{
usleep(75000); // 75ms simulierte Image-Verarbeitung
}
private function storeFile($file, string $path): bool
{
usleep(30000); // 30ms simulierte File-Storage
return true;
}
private function extractFileMetadata($file): array
{
usleep(12000); // 12ms simulierte Metadata-Extraktion
return ['size' => 1024, 'type' => 'image/jpeg'];
}
private function executeCacheOperation($event): mixed
{
$operation = $event->getOperation();
$delay = [
'get' => 2000, // 2ms
'set' => 5000, // 5ms
'delete' => 3000, // 3ms
'flush' => 50000, // 50ms
];
usleep($delay[$operation] ?? 1000);
return $operation === 'get' ? (rand(0, 1) ? 'cached_value' : null) : true;
}
private function updateCacheStatistics(string $operation, string $key, $result): void
{
// Dummy - Cache-Statistiken würden hier aktualisiert
}
}

View File

@@ -1,406 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Examples;
use App\Framework\Performance\PerformanceMeter;
use App\Framework\Performance\PerformanceCategory;
/**
* Beispiel-Middleware für HTTP-Request Performance-Tracking
*
* Diese Middleware zeigt, wie Performance-Messungen in einer typischen
* HTTP-Request-Pipeline implementiert werden können.
*/
class PerformanceMiddlewareExample
{
private PerformanceMeter $meter;
private array $config;
public function __construct(PerformanceMeter $meter, array $config = [])
{
$this->meter = $meter;
$this->config = array_merge([
'track_all_requests' => true,
'track_slow_requests_only' => false,
'slow_threshold_ms' => 1000,
'add_response_headers' => true,
'detailed_measurement' => true,
], $config);
}
/**
* Hauptmethode der Middleware - umschließt den Request-Prozess
*/
public function handle($request, callable $next)
{
// === REQUEST START TRACKING ===
$this->meter->mark('request_received', PerformanceCategory::FRAMEWORK);
$startMemory = memory_get_usage(true);
$requestId = $this->generateRequestId();
// Request-Kontext für spätere Analyse sammeln
$requestContext = $this->captureRequestContext($request);
// === PRE-PROCESSING MEASUREMENT ===
$this->meter->startMeasure('request_preprocessing', PerformanceCategory::FRAMEWORK);
$this->preprocessRequest($request);
$this->meter->endMeasure('request_preprocessing');
// === AUTHENTICATION MEASUREMENT ===
if ($this->shouldTrackAuth($request)) {
$this->meter->startMeasure('authentication', PerformanceCategory::SECURITY);
$authResult = $this->authenticateRequest($request);
$this->meter->endMeasure('authentication');
$this->meter->mark('auth_completed', PerformanceCategory::SECURITY);
}
// === AUTHORIZATION MEASUREMENT ===
if ($this->shouldTrackAuthz($request)) {
$this->meter->startMeasure('authorization', PerformanceCategory::SECURITY);
$authzResult = $this->authorizeRequest($request);
$this->meter->endMeasure('authorization');
if (!$authzResult) {
$this->meter->mark('authorization_failed', PerformanceCategory::SECURITY);
}
}
// === CORE REQUEST PROCESSING ===
$this->meter->mark('core_processing_start', PerformanceCategory::FRAMEWORK);
$this->meter->startMeasure('core_request_processing', PerformanceCategory::CONTROLLER);
// Hier würde der eigentliche Request-Handler aufgerufen
$response = $next($request);
$this->meter->endMeasure('core_request_processing');
$this->meter->mark('core_processing_end', PerformanceCategory::FRAMEWORK);
// === RESPONSE PROCESSING MEASUREMENT ===
$this->meter->startMeasure('response_processing', PerformanceCategory::FRAMEWORK);
$response = $this->processResponse($response, $requestContext);
$this->meter->endMeasure('response_processing');
// === LOGGING & CLEANUP MEASUREMENT ===
$this->meter->startMeasure('logging_cleanup', PerformanceCategory::FRAMEWORK);
$this->logRequestMetrics($requestId, $requestContext, $startMemory);
$this->meter->endMeasure('logging_cleanup');
// === FINAL MARKERS ===
$this->meter->mark('request_completed', PerformanceCategory::FRAMEWORK);
// === PERFORMANCE HEADERS HINZUFÜGEN ===
if ($this->config['add_response_headers']) {
$this->addPerformanceHeaders($response);
}
// === PERFORMANCE-DATEN AUSGEBEN/LOGGEN ===
$this->outputPerformanceData($requestId, $requestContext);
return $response;
}
/**
* Request-Preprocessing mit detaillierter Messung
*/
private function preprocessRequest($request): void
{
// Input-Validierung messen
$this->meter->measure('input_validation', function() use ($request) {
$this->validateInput($request);
}, PerformanceCategory::VALIDATION);
// Request-Parsing messen
$this->meter->measure('request_parsing', function() use ($request) {
$this->parseRequestData($request);
}, PerformanceCategory::FRAMEWORK);
// Rate-Limiting prüfen
$this->meter->measure('rate_limiting', function() use ($request) {
$this->checkRateLimit($request);
}, PerformanceCategory::SECURITY);
}
/**
* Authentication mit Performance-Tracking
*/
private function authenticateRequest($request): bool
{
// Session-Check
$sessionValid = $this->meter->measure('session_validation', function() use ($request) {
return $this->validateSession($request);
}, PerformanceCategory::SECURITY);
if ($sessionValid) {
$this->meter->mark('session_auth_success', PerformanceCategory::SECURITY);
return true;
}
// Token-Authentication
$tokenValid = $this->meter->measure('token_validation', function() use ($request) {
return $this->validateToken($request);
}, PerformanceCategory::SECURITY);
if ($tokenValid) {
$this->meter->mark('token_auth_success', PerformanceCategory::SECURITY);
return true;
}
// Database-Authentication (langsam)
$dbAuth = $this->meter->measure('database_authentication', function() use ($request) {
return $this->authenticateAgainstDatabase($request);
}, PerformanceCategory::DATABASE);
if ($dbAuth) {
$this->meter->mark('db_auth_success', PerformanceCategory::SECURITY);
} else {
$this->meter->mark('auth_failed', PerformanceCategory::SECURITY);
}
return $dbAuth;
}
/**
* Response-Processing mit Messungen
*/
private function processResponse($response, array $context)
{
// Content-Transformation
$this->meter->measure('content_transformation', function() use ($response) {
$this->transformResponseContent($response);
}, PerformanceCategory::VIEW);
// Caching-Headers setzen
$this->meter->measure('cache_headers', function() use ($response, $context) {
$this->setCacheHeaders($response, $context);
}, PerformanceCategory::CACHE);
// Compression
if ($this->shouldCompress($response)) {
$this->meter->measure('response_compression', function() use ($response) {
$this->compressResponse($response);
}, PerformanceCategory::FRAMEWORK);
}
return $response;
}
/**
* Performance-Headers zum Response hinzufügen
*/
private function addPerformanceHeaders($response): void
{
$report = $this->meter->generateReport();
// Basis-Performance-Daten
$response->headers['X-Performance-Time'] = sprintf('%.2fms', $report['summary']['total_time_ms']);
$response->headers['X-Performance-Memory'] = sprintf('%.2fMB', $report['summary']['total_memory_mb']);
$response->headers['X-Performance-Markers'] = (string) $report['summary']['marker_count'];
// Detaillierte Kategorie-Zeiten
foreach ($report['categories'] as $category => $data) {
if (isset($data['total_time_ms'])) {
$headerName = 'X-Performance-' . ucfirst($category);
$response->headers[$headerName] = sprintf('%.2fms', $data['total_time_ms']);
}
}
// Langsame Operationen hervorheben
$slowOperations = $this->findSlowOperations($report);
if (!empty($slowOperations)) {
$response->headers['X-Performance-Slow-Ops'] = implode(',', $slowOperations);
}
// Memory-Peak
$response->headers['X-Performance-Memory-Peak'] = sprintf('%.2fMB', memory_get_peak_usage(true) / 1024 / 1024);
}
/**
* Performance-Daten ausgeben oder loggen
*/
private function outputPerformanceData(string $requestId, array $context): void
{
$report = $this->meter->generateReport();
$totalTime = $report['summary']['total_time_ms'];
// Entscheidung: Wann sollen Performance-Daten ausgegeben werden?
$shouldOutput = $this->config['track_all_requests'] ||
($this->config['track_slow_requests_only'] && $totalTime > $this->config['slow_threshold_ms']);
if (!$shouldOutput) {
return;
}
// Für Development: Detaillierte Ausgabe
if ($this->isDebugMode()) {
echo "\n" . str_repeat('=', 50) . "\n";
echo "PERFORMANCE REPORT - Request: $requestId\n";
echo str_repeat('=', 50) . "\n";
echo $this->meter->generateTextReport();
echo str_repeat('=', 50) . "\n\n";
}
// Für Production: Strukturiertes Logging
$logData = [
'request_id' => $requestId,
'url' => $context['url'],
'method' => $context['method'],
'total_time_ms' => $totalTime,
'memory_mb' => $report['summary']['total_memory_mb'],
'slow_operations' => $this->findSlowOperations($report),
'category_breakdown' => $this->getCategoryBreakdown($report),
'context' => $context,
];
error_log('PERFORMANCE: ' . json_encode($logData));
// Bei sehr langsamen Requests: Warnung
if ($totalTime > $this->config['slow_threshold_ms'] * 2) {
error_log("SLOW REQUEST WARNING: {$context['url']} took {$totalTime}ms");
}
}
/**
* Request-Kontext für Analyse sammeln
*/
private function captureRequestContext($request): array
{
return [
'url' => $request->getUri ?? $_SERVER['REQUEST_URI'] ?? 'unknown',
'method' => $request->getMethod ?? $_SERVER['REQUEST_METHOD'] ?? 'unknown',
'user_agent' => $request->getUserAgent ?? $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
'ip' => $request->getClientIp ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'content_length' => $request->getContentLength ?? $_SERVER['CONTENT_LENGTH'] ?? 0,
'timestamp' => microtime(true),
];
}
/**
* Langsame Operationen identifizieren
*/
private function findSlowOperations(array $report): array
{
$slowOps = [];
$threshold = 100; // ms
foreach ($report['measurements'] as $label => $data) {
if ($data['avg_time_ms'] > $threshold) {
$slowOps[] = "{$label}:{$data['avg_time_ms']}ms";
}
}
return $slowOps;
}
/**
* Kategorie-Breakdown für Logging
*/
private function getCategoryBreakdown(array $report): array
{
$breakdown = [];
foreach ($report['categories'] as $category => $data) {
if (isset($data['total_time_ms'])) {
$breakdown[$category] = [
'time_ms' => $data['total_time_ms'],
'count' => $data['count'],
'avg_ms' => $data['avg_time_ms'],
];
}
}
return $breakdown;
}
// === DUMMY-METHODEN FÜR VOLLSTÄNDIGES BEISPIEL ===
private function generateRequestId(): string
{
return uniqid('req_', true);
}
private function shouldTrackAuth($request): bool
{
return true; // In echter Implementierung: Prüfung ob Auth nötig
}
private function shouldTrackAuthz($request): bool
{
return true; // In echter Implementierung: Prüfung ob Authz nötig
}
private function validateInput($request): void
{
// Dummy: Input-Validierung
usleep(5000); // 5ms simulierte Verarbeitung
}
private function parseRequestData($request): void
{
// Dummy: Request-Parsing
usleep(2000); // 2ms simulierte Verarbeitung
}
private function checkRateLimit($request): void
{
// Dummy: Rate-Limiting
usleep(1000); // 1ms simulierte Verarbeitung
}
private function validateSession($request): bool
{
usleep(10000); // 10ms simulierte Session-Validierung
return rand(0, 1) === 1;
}
private function validateToken($request): bool
{
usleep(5000); // 5ms simulierte Token-Validierung
return rand(0, 1) === 1;
}
private function authenticateAgainstDatabase($request): bool
{
usleep(50000); // 50ms simulierte DB-Authentication
return true;
}
private function authorizeRequest($request): bool
{
usleep(15000); // 15ms simulierte Authorization
return true;
}
private function transformResponseContent($response): void
{
usleep(8000); // 8ms simulierte Content-Transformation
}
private function setCacheHeaders($response, $context): void
{
usleep(1000); // 1ms simulierte Cache-Header
}
private function shouldCompress($response): bool
{
return true;
}
private function compressResponse($response): void
{
usleep(20000); // 20ms simulierte Kompression
}
private function logRequestMetrics($requestId, $context, $startMemory): void
{
usleep(3000); // 3ms simuliertes Logging
}
private function isDebugMode(): bool
{
return $_ENV['DEBUG'] ?? false;
}
}

View File

@@ -1,18 +0,0 @@
# Performance Monitoring Examples
Dieser Ordner enthält praktische Beispiele für die Verwendung des Performance-Monitoring-Systems in verschiedenen Szenarien.
## Beispiele
### 1. PerformanceMiddlewareExample.php
**Zweck**: Zeigt, wie Performance-Tracking in HTTP-Request-Middleware implementiert wird.
**Key Features**:
- ✅ Request-Lifecycle-Tracking
- ✅ Authentication/Authorization-Messungen
- ✅ Response-Processing-Tracking
- ✅ Performance-Headers
- ✅ Slow-Request-Detection
**Verwendung**:

View File

@@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Jobs;
use App\Framework\Jobs\Job;
use App\Framework\Performance\PerformanceDatabaseWorker;
use App\Framework\Core\Container;
class ProcessPerformanceLogsJob extends Job
{
private string $logFile;
public function __construct(string $logFile)
{
$this->logFile = $logFile;
}
public function handle(Container $container): void
{
$worker = $container->get(PerformanceDatabaseWorker::class);
$worker->processLogFile($this->logFile);
}
public function getLogFile(): string
{
return $this->logFile;
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Performance\ValueObjects\MemorySummary;
final readonly class MemoryMonitor
{
public function getCurrentMemory(): Byte
{
return Byte::fromBytes(memory_get_usage(true));
}
public function getPeakMemory(): Byte
{
return Byte::fromBytes(memory_get_peak_usage(true));
}
public function getMemoryLimit(): Byte
{
$limit = ini_get('memory_limit');
if ($limit === '-1') {
return Byte::fromGigabytes(1024); // Assume 1TB for unlimited
}
return Byte::parse($limit);
}
public function getMemoryUsagePercentage(): Percentage
{
$current = $this->getCurrentMemory();
$limit = $this->getMemoryLimit();
return $current->percentOf($limit);
}
public function isMemoryLimitApproaching(float $threshold = 80.0): bool
{
$usage = $this->getMemoryUsagePercentage();
$thresholdPercentage = Percentage::from($threshold);
return $usage->greaterThan($thresholdPercentage) || $usage->equals($thresholdPercentage);
}
public function getSummary(): MemorySummary
{
$current = $this->getCurrentMemory();
$peak = $this->getPeakMemory();
$limit = $this->getMemoryLimit();
$percentage = $this->getMemoryUsagePercentage();
return new MemorySummary(
current: $current,
peak: $peak,
limit: $limit,
usagePercentage: $percentage,
isApproachingLimit: $this->isMemoryLimitApproaching()
);
}
// Static helper methods for quick checks
public static function quickCheck(): MemorySummary
{
$monitor = new self();
return $monitor->getSummary();
}
public static function isMemoryLow(float $threshold = 20.0): bool
{
$monitor = new self();
$usage = $monitor->getMemoryUsagePercentage();
return $usage->lessThan(Percentage::from($threshold));
}
}

View File

@@ -1,132 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance;
/**
* Tracking von Speicherverbrauch und Performance-Metriken.
*/
final class MemoryUsageTracker
{
/**
* @var array<string, float> Speichert Start-Zeitstempel für Messungen
*/
private array $timers = [];
/**
* @var array<string, int> Speichert Speicherverbrauch zu Beginn einer Messung
*/
private array $memoryUsage = [];
/**
* @var array<string, array{time: float, memory: int}> Gespeicherte Messpunkte
*/
private array $measurements = [];
/**
* Startet eine neue Zeitmessung.
*/
public function startTimer(string $name): void
{
$this->timers[$name] = microtime(true);
$this->memoryUsage[$name] = memory_get_usage(true);
}
/**
* Beendet eine Zeitmessung und gibt die verstrichene Zeit in Sekunden zurück.
*/
public function stopTimer(string $name): float
{
if (!isset($this->timers[$name])) {
return 0.0;
}
$startTime = $this->timers[$name];
$startMemory = $this->memoryUsage[$name];
$endTime = microtime(true);
$endMemory = memory_get_usage(true);
$timeElapsed = $endTime - $startTime;
$memoryDiff = $endMemory - $startMemory;
$this->measurements[$name] = [
'time' => $timeElapsed,
'memory' => $memoryDiff,
];
unset($this->timers[$name]);
unset($this->memoryUsage[$name]);
return $timeElapsed;
}
/**
* Gibt alle gespeicherten Messungen zurück.
*
* @return array<string, array{time: float, memory: int}>
*/
public function getMeasurements(): array
{
return $this->measurements;
}
/**
* Gibt die Messung für einen bestimmten Namen zurück.
*
* @return array{time: float, memory: int}|null
*/
public function getMeasurement(string $name): ?array
{
return $this->measurements[$name] ?? null;
}
/**
* Misst die Ausführungszeit einer Funktion.
*
* @param callable $callback Die auszuführende Funktion
* @param string $name Ein Name für diese Messung
* @return mixed Der Rückgabewert der Funktion
*/
public function measure(callable $callback, string $name): mixed
{
$this->startTimer($name);
$result = $callback();
$this->stopTimer($name);
return $result;
}
/**
* Gibt den aktuellen Speicherverbrauch in einem lesbaren Format zurück.
*/
public function getCurrentMemoryUsage(bool $realUsage = true): string
{
return $this->formatBytes(memory_get_usage($realUsage));
}
/**
* Gibt den maximalen Speicherverbrauch in einem lesbaren Format zurück.
*/
public function getPeakMemoryUsage(bool $realUsage = true): string
{
return $this->formatBytes(memory_get_peak_usage($realUsage));
}
/**
* Formatiert Bytes in ein lesbares Format.
*/
public function formatBytes(int $bytes, int $precision = 2): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, $precision) . ' ' . $units[$pow];
}
}

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Middleware;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\Next;
use App\Framework\Http\RequestStateManager;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\Performance\PerformanceConfig;
#[MiddlewarePriorityAttribute(MiddlewarePriority::CONTROLLER, -10)]
final readonly class ControllerPerformanceMiddleware implements HttpMiddleware
{
public function __construct(
private PerformanceCollectorInterface $collector,
private PerformanceConfig $config
) {
}
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
{
if (! $this->config->isTrackingEnabled(PerformanceCategory::CONTROLLER)) {
return $next($context);
}
$request = $context->request;
// Try to get controller info from state manager or request
$controllerInfo = $this->extractControllerInfo($request, $stateManager);
$controllerKey = $controllerInfo['controller'] ?? 'unknown_controller';
$actionKey = $controllerInfo['action'] ?? 'unknown_action';
$performanceContext = [
'controller' => $controllerKey,
'action' => $actionKey,
'method' => $request->method,
'path' => $request->path,
];
$timingKey = "controller_{$controllerKey}_{$actionKey}";
$this->collector->startTiming($timingKey, PerformanceCategory::CONTROLLER, $performanceContext);
// Count controller invocations
$this->collector->increment(
"controller_calls_{$controllerKey}",
PerformanceCategory::CONTROLLER,
1,
$performanceContext
);
try {
$result = $next($context);
// Record successful controller execution
$this->collector->increment(
"controller_success_{$controllerKey}",
PerformanceCategory::CONTROLLER,
1,
$performanceContext
);
return $result;
} catch (\Throwable $e) {
// Record controller error
$this->collector->increment(
"controller_errors_{$controllerKey}",
PerformanceCategory::CONTROLLER,
1,
array_merge($performanceContext, ['error' => $e::class])
);
throw $e;
} finally {
$this->collector->endTiming($timingKey);
}
}
private function extractControllerInfo(mixed $request, RequestStateManager $stateManager): array
{
// Try to get controller info from various sources
// Method 1: From state manager
$controllerData = $stateManager->get('controller_info');
if ($controllerData) {
return $controllerData;
}
// Method 2: From request attributes/route
if (isset($request->route)) {
$handler = $request->route['handler'] ?? null;
if ($handler && is_string($handler)) {
return $this->parseControllerString($handler);
}
}
// Method 3: From request path (fallback)
$path = $request->path ?? '/';
$segments = explode('/', trim($path, '/'));
return [
'controller' => $segments[0] ?? 'home',
'action' => $segments[1] ?? 'index',
];
}
private function parseControllerString(string $handler): array
{
// Parse strings like "App\Controller\HomeController@index"
if (str_contains($handler, '@')) {
[$controller, $action] = explode('@', $handler, 2);
$controller = basename(str_replace('\\', '/', $controller));
return ['controller' => $controller, 'action' => $action];
}
// Parse class names
if (str_contains($handler, '\\')) {
$controller = basename(str_replace('\\', '/', $handler));
return ['controller' => $controller, 'action' => '__invoke'];
}
return ['controller' => $handler, 'action' => 'unknown'];
}
}

View File

@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Middleware;
use App\Framework\Database\Middleware\QueryContext;
use App\Framework\Database\Middleware\QueryMiddleware;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\Performance\PerformanceConfig;
final readonly class DatabasePerformanceMiddleware implements QueryMiddleware
{
public function __construct(
private PerformanceCollectorInterface $collector,
private PerformanceConfig $config
) {
}
public function process(QueryContext $context, callable $next): mixed
{
if (! $this->config->isTrackingEnabled(PerformanceCategory::DATABASE)) {
return $next();
}
$query = $context->sql;
$bindings = $context->parameters;
$queryType = $this->getQueryType($query);
$queryKey = "db_query_{$queryType}";
$performanceContext = [
'query_type' => $queryType,
'bindings_count' => count($bindings),
'query_hash' => md5($query),
];
// Add query text for detailed reports (careful with sensitive data)
if ($this->config->detailedReports) {
$performanceContext['query'] = $this->sanitizeQuery($query);
$performanceContext['bindings'] = $this->sanitizeBindings($bindings);
}
$this->collector->startTiming($queryKey, PerformanceCategory::DATABASE, $performanceContext);
// Count database queries
$this->collector->increment('database_queries_total', PerformanceCategory::DATABASE, 1, $performanceContext);
$this->collector->increment("database_queries_{$queryType}", PerformanceCategory::DATABASE, 1, $performanceContext);
try {
$result = $next();
// Record successful query
$this->collector->increment('database_queries_success', PerformanceCategory::DATABASE, 1, $performanceContext);
return $result;
} catch (\Throwable $e) {
// Record failed query
$this->collector->increment(
'database_queries_failed',
PerformanceCategory::DATABASE,
1,
array_merge($performanceContext, ['error' => $e::class])
);
throw $e;
} finally {
$duration = $this->collector->endTiming($queryKey);
// Check for slow queries
if ($this->config->isSlowQuery($duration)) {
$this->collector->increment(
'database_slow_queries',
PerformanceCategory::DATABASE,
1,
array_merge($performanceContext, ['duration_ms' => $duration])
);
}
}
}
public function getPriority(): int
{
return 100; // Performance middleware should run with moderate priority
}
private function getQueryType(string $query): string
{
$query = trim(strtoupper($query));
if (str_starts_with($query, 'SELECT')) {
return 'select';
}
if (str_starts_with($query, 'INSERT')) {
return 'insert';
}
if (str_starts_with($query, 'UPDATE')) {
return 'update';
}
if (str_starts_with($query, 'DELETE')) {
return 'delete';
}
if (str_starts_with($query, 'CREATE')) {
return 'create';
}
if (str_starts_with($query, 'DROP')) {
return 'drop';
}
if (str_starts_with($query, 'ALTER')) {
return 'alter';
}
if (str_starts_with($query, 'SHOW')) {
return 'show';
}
if (str_starts_with($query, 'DESCRIBE') || str_starts_with($query, 'DESC')) {
return 'describe';
}
return 'other';
}
private function sanitizeQuery(string $query): string
{
// Remove potential sensitive data from queries for logging
$query = preg_replace('/\b\d{4}[-\s]\d{4}[-\s]\d{4}[-\s]\d{4}\b/', '****-****-****-****', $query);
$query = preg_replace('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', '***@***.***', $query);
// Truncate very long queries
if (strlen($query) > 500) {
$query = substr($query, 0, 500) . '...';
}
return $query;
}
private function sanitizeBindings(array $bindings): array
{
// Sanitize binding values to prevent sensitive data exposure
return array_map(function ($value) {
if (is_string($value)) {
// Hide potential sensitive strings
if (strlen($value) > 50) {
return '[LONG_STRING:' . strlen($value) . '_chars]';
}
if (preg_match('/^\d{4}[-\s]\d{4}[-\s]\d{4}[-\s]\d{4}$/', $value)) {
return '[CREDIT_CARD]';
}
if (filter_var($value, FILTER_VALIDATE_EMAIL)) {
return '[EMAIL]';
}
}
return $value;
}, $bindings);
}
}

View File

@@ -0,0 +1,696 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Middleware;
use App\Framework\Http\Headers;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\HttpResponse;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\Next;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\Session\SessionInterface;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Performance\EnhancedPerformanceCollector;
use App\Framework\Performance\PerformanceConfig;
use App\Framework\Performance\PerformanceReporter;
#[MiddlewarePriorityAttribute(MiddlewarePriority::LAST)]
final readonly class PerformanceDebugMiddleware implements HttpMiddleware
{
public function __construct(
private PerformanceCollectorInterface $collector,
private PerformanceConfig $config,
private PerformanceReporter $reporter
) {
}
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
{
// Always execute the request first
$context = $next($context);
// Handle performance output and headers
return $this->handlePerformanceOutput($context, $stateManager);
}
private function handlePerformanceOutput(MiddlewareContext $context, RequestStateManager $stateManager): MiddlewareContext
{
// Check if performance tracking is enabled
if (! $this->config->enabled) {
return $context;
}
$request = $context->request;
$response = $context->response ?? null;
// Skip if no response
if (! $response) {
return $context;
}
// Skip for excluded paths
$path = $request->path ?? '/';
if ($this->config->shouldExcludePath($path)) {
return $context;
}
// Check content type - only add to HTML responses
$contentType = $response->headers->get('Content-Type', '');
if (is_string($contentType) && ! empty($contentType) && ! str_contains($contentType, 'text/html')) {
// Add performance headers for non-HTML responses
$headersWithPerformance = $this->addPerformanceHeaders($response->headers);
$newResponse = new HttpResponse(
status: $response->status,
headers: $headersWithPerformance,
body: $response->body
);
return $context->withResponse($newResponse);
}
// Skip for AJAX requests unless specifically enabled
if ($this->isAjaxRequest($request) && ! $this->config->detailedReports) {
return $context;
}
if (! $context->response instanceof HttpResponse) {
return $context;
}
// Add full performance output to HTML responses
return $this->addPerformanceOutput($context, $stateManager);
}
private function isAjaxRequest(mixed $request): bool
{
$xhr = $request->headers->get('X-Requested-With', '');
return is_string($xhr) && strtolower($xhr) === 'xmlhttprequest';
}
private function addPerformanceOutput(MiddlewareContext $context, RequestStateManager $stateManager): MiddlewareContext
{
$response = $context->response;
if ($context->response->headers->getFirst('Content-Type') === 'application/json') {
return $context;
}
if (! $response || ! isset($response->body)) {
return $context;
}
$body = $response->body;
// Generate performance report
$reportHtml = $this->reporter->generateReport('html');
assert(is_string($reportHtml), 'HTML format should return string');
// Add hierarchical performance report if using EnhancedPerformanceCollector
if ($this->collector instanceof EnhancedPerformanceCollector) {
$hierarchicalHtml = $this->generateHierarchicalReport();
$reportHtml .= $hierarchicalHtml;
}
// Always add session debug information as collapsible details
if ($this->config->detailedReports) {
$sessionDebugHtml = $this->generateSessionDebugInfo($stateManager);
$reportHtml .= $sessionDebugHtml;
}
// Try to find closing body tag
$closingBodyPos = strripos($body, '</body>');
if ($closingBodyPos !== false) {
// Insert before closing body tag
$newBody = substr($body, 0, $closingBodyPos);
$newBody .= $this->wrapReportForInsertion($reportHtml);
$newBody .= substr($body, $closingBodyPos);
} else {
// Append to end if no body tag found
$newBody = $body . $this->wrapReportForInsertion($reportHtml);
}
// Add performance headers to the headers
$headersWithPerformance = $this->addPerformanceHeaders($response->headers);
// Create new response with modified body and headers
$newResponse = new HttpResponse(
status: $response->status,
headers: $headersWithPerformance,
body: $newBody
);
// Return new context with new response
return $context->withResponse($newResponse);
}
private function wrapReportForInsertion(string $reportHtml): string
{
return "
<!-- Performance Debug Report -->
<style>
.perf-debug-theme-light {
--perf-bg-primary: #ffffff;
--perf-bg-secondary: #f8f9fa;
--perf-bg-tertiary: #e9ecef;
--perf-text-primary: #212529;
--perf-text-secondary: #6c757d;
--perf-border: #dee2e6;
--perf-accent: #007bff;
--perf-success: #28a745;
--perf-warning: #ffc107;
--perf-danger: #dc3545;
}
.perf-debug-theme-dark {
--perf-bg-primary: #1a1a1a;
--perf-bg-secondary: #2d2d2d;
--perf-bg-tertiary: #404040;
--perf-text-primary: #ffffff;
--perf-text-secondary: #b0b0b0;
--perf-border: #404040;
--perf-accent: #0d6efd;
--perf-success: #198754;
--perf-warning: #fd7e14;
--perf-danger: #dc3545;
}
.perf-debug-container {
color-scheme: light dark;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
color: var(--perf-text-primary, black);
}
.perf-debug-container * {
box-sizing: border-box;
}
.perf-debug-toggle {
background: var(--perf-accent);
color: var(--perf-bg-primary);
border: none;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
font-family: inherit;
font-size: 12px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 6px;
}
.perf-debug-toggle:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(0,0,0,0.2);
}
.perf-debug-modal {
background: var(--perf-bg-primary);
border: 1px solid var(--perf-border);
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.15);
color: var(--perf-text-primary);
backdrop-filter: blur(10px);
min-width: 400px;
max-width: 80vw;
}
.perf-debug-header {
background: var(--perf-bg-secondary);
padding: 16px 20px;
border-radius: 12px 12px 0 0;
border-bottom: 1px solid var(--perf-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.perf-debug-title {
font-weight: 700;
font-size: 14px;
margin: 0;
display: flex;
align-items: center;
gap: 8px;
}
.perf-debug-controls {
display: flex;
align-items: center;
gap: 8px;
}
.perf-debug-theme-toggle {
background: var(--perf-bg-tertiary);
color: var(--perf-text-primary);
border: 1px solid var(--perf-border);
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.perf-debug-theme-toggle:hover {
background: var(--perf-accent);
color: var(--perf-bg-primary);
}
.perf-debug-close {
background: var(--perf-danger);
color: white;
border: none;
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.2s ease;
}
.perf-debug-close:hover {
background: #c82333;
transform: scale(1.05);
}
.perf-debug-content {
color: var(--perf-text-primary);
}
.perf-debug-content table {
color: var(--perf-text-primary);
background: var(--perf-bg-primary);
}
.perf-debug-content th {
background: var(--perf-bg-secondary) !important;
color: var(--perf-text-primary) !important;
border-color: var(--perf-border) !important;
}
.perf-debug-content td {
background: var(--perf-bg-primary) !important;
color: var(--perf-text-primary) !important;
border-color: var(--perf-border) !important;
}
.perf-debug-content tr:nth-child(even) td {
background: var(--perf-bg-secondary) !important;
}
</style>
<div id=\"performance-debug-report\" class=\"perf-debug-container perf-debug-theme-light\" style=\"position: relative; z-index: 999999;\">
<div style=\"position: fixed; bottom: 20px; right: 20px; z-index: 1000000;\">
<button onclick=\"togglePerformanceReport()\" class=\"perf-debug-toggle\">
<span>📊</span>
<span>Performance</span>
</button>
</div>
<div id=\"performance-report-content\" class=\"perf-debug-modal\" style=\"display: none; position: fixed; top: 20px; right: 20px; z-index: 1000000; max-height: 80vh; overflow-y: auto;\">
<div class=\"perf-debug-header\">
<h3 class=\"perf-debug-title\">
<span>⚡</span>
<span>Performance Debug</span>
</h3>
<div class=\"perf-debug-controls\">
<button onclick=\"toggleTheme()\" class=\"perf-debug-theme-toggle\" title=\"Toggle Dark/Light Mode\">
<span id=\"theme-icon\">🌙</span>
</button>
<button onclick=\"closePerformanceReport()\" class=\"perf-debug-close\">
</button>
</div>
</div>
<div class=\"perf-debug-content\">
{$reportHtml}
</div>
</div>
</div>
<script>
function togglePerformanceReport() {
const content = document.getElementById('performance-report-content');
content.style.display = content.style.display === 'none' ? 'block' : 'none';
}
function closePerformanceReport() {
document.getElementById('performance-report-content').style.display = 'none';
}
function toggleTheme() {
const container = document.getElementById('performance-debug-report');
const themeIcon = document.getElementById('theme-icon');
const isDark = container.classList.contains('perf-debug-theme-dark');
if (isDark) {
container.classList.remove('perf-debug-theme-dark');
container.classList.add('perf-debug-theme-light');
themeIcon.textContent = '🌙';
localStorage.setItem('perf-debug-theme', 'light');
} else {
container.classList.remove('perf-debug-theme-light');
container.classList.add('perf-debug-theme-dark');
themeIcon.textContent = '☀️';
localStorage.setItem('perf-debug-theme', 'dark');
}
}
// Load saved theme preference
(function() {
const savedTheme = localStorage.getItem('perf-debug-theme');
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const container = document.getElementById('performance-debug-report');
const themeIcon = document.getElementById('theme-icon');
const shouldUseDark = savedTheme === 'dark' || (!savedTheme && prefersDark);
if (shouldUseDark) {
container.classList.remove('perf-debug-theme-light');
container.classList.add('perf-debug-theme-dark');
themeIcon.textContent = '☀️';
}
})();
</script>
<!-- End Performance Debug Report -->
";
}
private function addPerformanceHeaders(Headers $headers): Headers
{
$data = $this->reporter->generateReport('array');
$summary = $data['summary'];
$newHeaders = $headers
->with('X-Performance-Time', sprintf('%.2f ms', $summary['total_request_time_ms']))
->with('X-Performance-Memory', $this->formatBytes($summary['total_request_memory_bytes']))
->with('X-Performance-Peak-Memory', $this->formatBytes($summary['peak_memory_bytes']))
->with('X-Performance-Metrics-Count', (string) $summary['metrics_count']);
// Add category summaries
foreach ($data['categories'] as $categoryName => $categoryData) {
$headerName = 'X-Performance-' . ucfirst($categoryName);
$headerValue = sprintf('%.2f ms (%d calls)', $categoryData['total_time_ms'], $categoryData['total_calls']);
$newHeaders = $newHeaders->with($headerName, $headerValue);
}
return $newHeaders;
}
private function formatBytes(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, 2) . ' ' . $units[$pow];
}
private function generateSessionDebugInfo(RequestStateManager $stateManager): string
{
$session = $stateManager->get('session');
if (! $session instanceof SessionInterface) {
return '<details style="margin: 20px;">
<summary style="cursor: pointer; padding: 12px 16px; background: var(--perf-bg-secondary); border: 1px solid var(--perf-border); border-radius: 8px; font-weight: 600; color: var(--perf-text-primary);">
<span style="margin-right: 8px;">🔐</span>Session Debug Information
</summary>
<div style="padding: 20px; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; border-radius: 4px; margin-top: 10px;">
<strong>⚠️ Session Debug:</strong> No session found in RequestStateManager
</div>
</details>';
}
$sessionData = $session->all();
$sessionId = $session->id->toString();
// Count form data and validation errors
$formDataCount = isset($sessionData['__form']) ? count($sessionData['__form']) : 0;
$validationErrorCount = isset($sessionData['__validation']) ? count($sessionData['__validation']) : 0;
// Build summary text
$summaryText = '🔐 Session Debug';
if ($formDataCount > 0) {
$summaryText .= ' • 📝 ' . $formDataCount . ' form(s)';
}
if ($validationErrorCount > 0) {
$summaryText .= ' • ❌ ' . $validationErrorCount . ' error(s)';
}
// Format session data as HTML
$html = '<details style="margin: 20px;">
<summary style="cursor: pointer; padding: 12px 16px; background: var(--perf-bg-secondary); border: 1px solid var(--perf-border); border-radius: 8px; font-weight: 600; color: var(--perf-text-primary);">
' . $summaryText . '
</summary>
<div style="padding: 20px; background: var(--perf-bg-secondary); border: 1px solid var(--perf-border); border-radius: 0 0 8px 8px; margin-top: -1px;">
<table style="width: 100%; border-collapse: collapse; font-family: monospace; font-size: 12px;">
<tr>
<td style="padding: 8px; background: var(--perf-bg-tertiary); font-weight: bold; width: 200px;">Session ID</td>
<td style="padding: 8px; background: var(--perf-bg-primary); word-break: break-all;">' . htmlspecialchars($sessionId) . '</td>
</tr>
<tr>
<td style="padding: 8px; background: var(--perf-bg-tertiary); font-weight: bold; vertical-align: top;">Session Data</td>
<td style="padding: 8px; background: var(--perf-bg-primary);">
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">' . htmlspecialchars(json_encode($sessionData, JSON_PRETTY_PRINT) ?: 'Unable to encode') . '</pre>
</td>
</tr>';
// Check for form data specifically
if (isset($sessionData['__form'])) {
$html .= '<tr>
<td style="padding: 8px; background: var(--perf-bg-tertiary); font-weight: bold; vertical-align: top;">📝 Form Data</td>
<td style="padding: 8px; background: var(--perf-bg-primary);">
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">' . htmlspecialchars(json_encode($sessionData['__form'], JSON_PRETTY_PRINT) ?: 'Unable to encode') . '</pre>
</td>
</tr>';
}
// Check for validation errors
if (isset($sessionData['__validation'])) {
$html .= '<tr>
<td style="padding: 8px; background: var(--perf-bg-tertiary); font-weight: bold; vertical-align: top;">❌ Validation Errors</td>
<td style="padding: 8px; background: var(--perf-bg-primary);">
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">' . htmlspecialchars(json_encode($sessionData['__validation'], JSON_PRETTY_PRINT) ?: 'Unable to encode') . '</pre>
</td>
</tr>';
}
$html .= '</table>
</div>
</details>';
return $html;
}
/**
* Generate hierarchical performance report HTML
*/
private function generateHierarchicalReport(): string
{
if (! $this->collector instanceof EnhancedPerformanceCollector) {
return '';
}
$hierarchicalData = $this->collector->getHierarchicalReport();
$executionTree = $this->collector->getExecutionTree();
$summary = $this->collector->getNestedSummary();
$html = '
<details style="margin: 20px;" open>
<summary style="cursor: pointer; padding: 12px 16px; background: var(--perf-bg-secondary); border: 1px solid var(--perf-border); border-radius: 8px; font-weight: 600; color: var(--perf-text-primary);">
<span style="margin-right: 8px;">🌳</span>Performance Execution Tree
<span style="font-size: 11px; opacity: 0.7; margin-left: 8px;">
• Max Depth: ' . $summary['max_depth'] . '
• Total: ' . $summary['total_operations'] . ' ops
• Active: ' . $summary['active_operations'] . ' ops
</span>
</summary>
<div style="padding: 20px; background: var(--perf-bg-secondary); border: 1px solid var(--perf-border); border-radius: 0 0 8px 8px; margin-top: -1px;">
<!-- Execution Tree -->
<div style="margin-bottom: 20px;">
<h4 style="margin: 0 0 10px 0; color: var(--perf-text-primary); font-size: 13px; font-weight: 600;">
📊 Execution Tree (Text View)
</h4>
<pre style="
background: var(--perf-bg-primary);
border: 1px solid var(--perf-border);
border-radius: 6px;
padding: 16px;
margin: 0;
font-family: \'SF Mono\', Monaco, \'Cascadia Code\', \'Roboto Mono\', Consolas, \'Courier New\', monospace;
font-size: 11px;
line-height: 1.4;
color: var(--perf-text-primary);
overflow-x: auto;
white-space: pre;
">' . htmlspecialchars($executionTree) . '</pre>
</div>
<!-- Hierarchical Table -->
<div>
<h4 style="margin: 0 0 10px 0; color: var(--perf-text-primary); font-size: 13px; font-weight: 600;">
📋 Hierarchical Operations (Table View)
</h4>
<div style="overflow-x: auto;">
' . $this->buildHierarchicalTable($hierarchicalData['operations'] ?? []) . '
</div>
</div>
<!-- Call Stack Info -->
' . $this->buildCallStackInfo() . '
</div>
</details>';
return $html;
}
/**
* Build hierarchical table from operations data
*/
private function buildHierarchicalTable(array $operations): string
{
if (empty($operations)) {
return '<p style="color: var(--perf-text-secondary); font-style: italic;">No hierarchical operations recorded.</p>';
}
$html = '
<table style="
width: 100%;
border-collapse: collapse;
font-family: \'SF Mono\', Monaco, \'Cascadia Code\', \'Roboto Mono\', Consolas, \'Courier New\', monospace;
font-size: 11px;
background: var(--perf-bg-primary);
border: 1px solid var(--perf-border);
border-radius: 6px;
overflow: hidden;
">
<thead>
<tr style="background: var(--perf-bg-tertiary);">
<th style="padding: 10px 12px; text-align: left; font-weight: 600; color: var(--perf-text-primary); border-bottom: 1px solid var(--perf-border);">Operation</th>
<th style="padding: 10px 12px; text-align: right; font-weight: 600; color: var(--perf-text-primary); border-bottom: 1px solid var(--perf-border);">Total Time</th>
<th style="padding: 10px 12px; text-align: right; font-weight: 600; color: var(--perf-text-primary); border-bottom: 1px solid var(--perf-border);">Self Time</th>
<th style="padding: 10px 12px; text-align: center; font-weight: 600; color: var(--perf-text-primary); border-bottom: 1px solid var(--perf-border);">Children</th>
</tr>
</thead>
<tbody>';
$html .= $this->buildHierarchicalRows($operations);
$html .= '
</tbody>
</table>';
return $html;
}
/**
* Build hierarchical table rows recursively
*/
private function buildHierarchicalRows(array $operations, int $depth = 0): string
{
$html = '';
foreach ($operations as $operation) {
$indent = str_repeat('&nbsp;&nbsp;', $depth * 2);
$depthIndicator = $depth > 0 ? str_repeat('┗', 1) . '━ ' : '';
$totalTime = $operation['total_time_ms'] ?? 0;
$selfTime = $operation['self_time_ms'] ?? 0;
$childrenCount = count($operation['children'] ?? []);
// Color coding for performance
$timeColor = $this->getTimeColor($totalTime);
$selfTimeColor = $this->getTimeColor($selfTime);
$html .= '
<tr style="' . ($depth % 2 === 1 ? 'background: var(--perf-bg-secondary);' : 'background: var(--perf-bg-primary);') . '">
<td style="padding: 8px 12px; color: var(--perf-text-primary); border-bottom: 1px solid var(--perf-border);">
<span style="font-family: monospace;">' . $indent . $depthIndicator . '</span>
<strong style="color: var(--perf-text-primary);">' . htmlspecialchars($operation['key']) . '</strong>
' . ($depth > 0 ? '<span style="opacity: 0.6; font-size: 10px; margin-left: 4px;">(depth: ' . $depth . ')</span>' : '') . '
</td>
<td style="padding: 8px 12px; text-align: right; color: ' . $timeColor . '; font-weight: 600; border-bottom: 1px solid var(--perf-border);">
' . ($totalTime > 0 ? number_format($totalTime, 2) . 'ms' : '—') . '
</td>
<td style="padding: 8px 12px; text-align: right; color: ' . $selfTimeColor . '; font-weight: 600; border-bottom: 1px solid var(--perf-border);">
' . ($selfTime > 0 ? number_format($selfTime, 2) . 'ms' : '—') . '
</td>
<td style="padding: 8px 12px; text-align: center; color: var(--perf-text-secondary); border-bottom: 1px solid var(--perf-border);">
' . ($childrenCount > 0 ? $childrenCount : '—') . '
</td>
</tr>';
// Recursively add children
if (! empty($operation['children'])) {
$html .= $this->buildHierarchicalRows($operation['children'], $depth + 1);
}
}
return $html;
}
/**
* Get color for time values based on performance thresholds
*/
private function getTimeColor(float $timeMs): string
{
if ($timeMs >= 1000) {
return 'var(--perf-danger)'; // Red for >= 1s
} elseif ($timeMs >= 500) {
return 'var(--perf-warning)'; // Orange for >= 500ms
} elseif ($timeMs >= 100) {
return '#fd7e14'; // Yellow for >= 100ms
} else {
return 'var(--perf-success)'; // Green for < 100ms
}
}
/**
* Build call stack information
*/
private function buildCallStackInfo(): string
{
if (! $this->collector instanceof EnhancedPerformanceCollector) {
return '';
}
$callStack = $this->collector->getCallStack();
$currentOp = $this->collector->getCurrentOperation();
$depth = $this->collector->getCallStackDepth();
if (empty($callStack)) {
return '
<div style="margin-top: 20px; padding: 12px 16px; background: var(--perf-bg-tertiary); border: 1px solid var(--perf-border); border-radius: 6px;">
<h4 style="margin: 0 0 8px 0; color: var(--perf-text-primary); font-size: 12px; font-weight: 600;">
📚 Call Stack Status
</h4>
<p style="margin: 0; color: var(--perf-success); font-size: 11px;">
✅ All operations completed (stack is empty)
</p>
</div>';
}
$stackHtml = '
<div style="margin-top: 20px; padding: 12px 16px; background: var(--perf-bg-tertiary); border: 1px solid var(--perf-border); border-radius: 6px;">
<h4 style="margin: 0 0 8px 0; color: var(--perf-text-primary); font-size: 12px; font-weight: 600;">
📚 Active Call Stack (Depth: ' . $depth . ')
</h4>
<div style="font-family: \'SF Mono\', Monaco, \'Cascadia Code\', \'Roboto Mono\', Consolas, \'Courier New\', monospace; font-size: 11px; line-height: 1.4;">';
foreach ($callStack as $i => $operation) {
$indent = str_repeat('&nbsp;&nbsp;', $i);
$isCurrentOp = $operation === $currentOp;
$stackHtml .= '
<div style="margin: 4px 0; color: var(--perf-text-primary);">
' . $indent . '
<span style="color: var(--perf-text-secondary);">' . ($i + 1) . '.</span>
<strong style="color: ' . ($isCurrentOp ? 'var(--perf-accent)' : 'var(--perf-text-primary)') . ';">' . htmlspecialchars($operation) . '</strong>
' . ($isCurrentOp ? '<span style="color: var(--perf-accent); font-size: 10px;"> ← CURRENT</span>' : '') . '
<span style="opacity: 0.6; font-size: 10px; margin-left: 8px;">(active)</span>
</div>';
}
$stackHtml .= '</div></div>';
return $stackHtml;
}
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Middleware;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\Next;
use App\Framework\Http\RequestStateManager;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Performance\MemoryMonitor;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\Performance\PerformanceConfig;
#[MiddlewarePriorityAttribute(MiddlewarePriority::FIRST)]
final readonly class RequestPerformanceMiddleware implements HttpMiddleware
{
public function __construct(
private PerformanceCollectorInterface $collector,
private PerformanceConfig $config
) {
}
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
{
if (! $this->config->isTrackingEnabled(PerformanceCategory::SYSTEM)) {
return $next($context);
}
$request = $context->request;
$path = $request->path ?? '/';
if ($this->config->shouldExcludePath($path)) {
return $next($context);
}
$requestKey = 'http_request';
$performanceContext = [
'method' => $request->method,
'path' => $path,
'user_agent' => $request->headers->getFirst('User-Agent', ''),
'ip' => $request->server->getClientIp()->value,
];
$this->collector->startTiming($requestKey, PerformanceCategory::SYSTEM, $performanceContext);
// Initialize memory monitoring
$memoryMonitor = new MemoryMonitor();
// Memory usage before request
$memoryBefore = $memoryMonitor->getCurrentMemory();
$this->collector->recordMetric(
'memory_before_request',
PerformanceCategory::SYSTEM,
$memoryBefore->toBytes(),
array_merge($performanceContext, [
'memory_human' => $memoryBefore->toHumanReadable(),
'memory_percentage' => $memoryMonitor->getMemoryUsagePercentage(),
])
);
try {
$result = $next($context);
// Record successful request
$this->collector->increment('requests_total', PerformanceCategory::SYSTEM, 1, $performanceContext);
$this->collector->increment('requests_success', PerformanceCategory::SYSTEM, 1, $performanceContext);
return $result;
} catch (\Throwable $e) {
// Record failed request
$this->collector->increment('requests_total', PerformanceCategory::SYSTEM, 1, $performanceContext);
$this->collector->increment('requests_failed', PerformanceCategory::SYSTEM, 1, $performanceContext);
$this->collector->recordMetric(
'request_error',
PerformanceCategory::SYSTEM,
1,
array_merge($performanceContext, ['error' => $e::class])
);
throw $e;
} finally {
$this->collector->endTiming($requestKey);
// Memory monitoring after request
$memoryAfter = $memoryMonitor->getCurrentMemory();
$memoryPeak = $memoryMonitor->getPeakMemory();
$memoryDiff = $memoryAfter->greaterThan($memoryBefore)
? $memoryAfter->subtract($memoryBefore)
: Byte::zero();
$this->collector->recordMetric(
'memory_after_request',
PerformanceCategory::SYSTEM,
$memoryAfter->toBytes(),
array_merge($performanceContext, [
'memory_human' => $memoryAfter->toHumanReadable(),
'memory_percentage' => $memoryMonitor->getMemoryUsagePercentage(),
])
);
if ($memoryDiff->isNotEmpty()) {
$this->collector->recordMetric(
'memory_usage_request',
PerformanceCategory::SYSTEM,
$memoryDiff->toBytes(),
array_merge($performanceContext, [
'memory_diff_human' => $memoryDiff->toHumanReadable(),
])
);
}
// Peak memory
$this->collector->recordMetric(
'memory_peak',
PerformanceCategory::SYSTEM,
$memoryPeak->toBytes(),
array_merge($performanceContext, [
'memory_peak_human' => $memoryPeak->toHumanReadable(),
'is_approaching_limit' => $memoryMonitor->isMemoryLimitApproaching(),
])
);
}
}
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Middleware;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\Next;
use App\Framework\Http\RequestStateManager;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\Performance\PerformanceConfig;
#[MiddlewarePriorityAttribute(MiddlewarePriority::ROUTING, 10)]
final readonly class RoutingPerformanceMiddleware implements HttpMiddleware
{
public function __construct(
private PerformanceCollectorInterface $collector,
private PerformanceConfig $config
) {
}
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
{
if (! $this->config->isTrackingEnabled(PerformanceCategory::ROUTING)) {
return $next($context);
}
$request = $context->request;
$path = $request->path ?? '/';
$method = $request->method;
$routingContext = [
'method' => $method,
'path' => $path,
];
$routingKey = 'route_resolution';
$this->collector->startTiming($routingKey, PerformanceCategory::ROUTING, $routingContext);
// Count routing attempts
$this->collector->increment('routing_attempts', PerformanceCategory::ROUTING, 1, $routingContext);
try {
$result = $next($context);
// Check if route was found (this is framework-specific logic)
$routeFound = $this->isRouteFound($result, $stateManager);
if ($routeFound) {
$this->collector->increment('routes_found', PerformanceCategory::ROUTING, 1, $routingContext);
// Get route pattern if available
$routePattern = $this->getRoutePattern($result, $stateManager);
if ($routePattern) {
$this->collector->increment(
"route_pattern_" . md5($routePattern),
PerformanceCategory::ROUTING,
1,
array_merge($routingContext, ['pattern' => $routePattern])
);
}
} else {
$this->collector->increment('routes_not_found', PerformanceCategory::ROUTING, 1, $routingContext);
}
return $result;
} catch (\Throwable $e) {
$this->collector->increment(
'routing_errors',
PerformanceCategory::ROUTING,
1,
array_merge($routingContext, ['error' => $e::class])
);
throw $e;
} finally {
$this->collector->endTiming($routingKey);
}
}
private function isRouteFound(MiddlewareContext $context, RequestStateManager $stateManager): bool
{
// Check various indicators that a route was found
// Method 1: Check state manager
if ($stateManager->has('route_found')) {
return $stateManager->get('route_found');
}
// Method 2: Check if controller info is available
if ($stateManager->has('controller_info')) {
return true;
}
// Method 3: Check response status (if not 404)
if (isset($context->response) && $context->response->status->value !== 404) {
return true;
}
// Default: assume found if we got here without exceptions
return true;
}
private function getRoutePattern(MiddlewareContext $context, RequestStateManager $stateManager): ?string
{
// Try to get route pattern from various sources
// Method 1: From state manager
$routeData = $stateManager->get('route_data');
if ($routeData && isset($routeData['pattern'])) {
return $routeData['pattern'];
}
// Method 2: From request route
if (isset($context->request->route['pattern'])) {
return $context->request->route['pattern'];
}
// Method 3: From matched route
$matchedRoute = $stateManager->get('matched_route');
if ($matchedRoute && isset($matchedRoute['pattern'])) {
return $matchedRoute['pattern'];
}
return null;
}
}

View File

@@ -0,0 +1,427 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\HighResolutionClock;
use App\Framework\Logging\Logger;
use App\Framework\Performance\ValueObjects\NestedMeasurement;
/**
* Verwaltet verschachtelte Performance-Messungen mit Hierarchie-Support
*/
final class NestedPerformanceTracker
{
/** @var NestedMeasurement[] Call stack der aktuell laufenden Operationen */
private array $callStack = [];
/** @var NestedMeasurement[] Abgeschlossene Top-Level Operationen */
private array $completedOperations = [];
/** @var array<string, NestedMeasurement> Lookup für alle aktiven Operationen */
private array $activeOperations = [];
private int $operationCounter = 0;
private bool $enabled = true;
public function __construct(
private readonly Clock $clock,
private readonly HighResolutionClock $highResClock,
private readonly MemoryMonitor $memoryMonitor,
private readonly ?Logger $logger = null
) {
}
/**
* Start eine neue verschachtelte Operation
*/
public function startOperation(
string $name,
PerformanceCategory $category,
array $context = []
): string {
if (! $this->enabled) {
return '';
}
$operationId = $this->generateOperationId($name);
$currentParent = $this->getCurrentOperation();
$depth = count($this->callStack);
$measurement = NestedMeasurement::start(
operationId: $operationId,
name: $name,
category: $category,
startTime: $this->clock->time(),
startMemory: $this->memoryMonitor->getCurrentMemory(),
context: $context,
parentId: $currentParent?->operationId,
depth: $depth
);
// Zum Call Stack hinzufügen
$this->callStack[] = $measurement;
$this->activeOperations[$operationId] = $measurement;
$this->logger?->debug('Nested operation started', [
'operation_id' => $operationId,
'name' => $name,
'category' => $category->value,
'depth' => $depth,
'parent_id' => $currentParent?->operationId,
]);
return $operationId;
}
/**
* Beende eine Operation
*/
public function endOperation(string $operationId): ?NestedMeasurement
{
if (! $this->enabled || ! isset($this->activeOperations[$operationId])) {
return null;
}
$measurement = $this->activeOperations[$operationId];
// Vervollständige die Messung
$completedMeasurement = $measurement->complete(
$this->clock->time(),
$this->memoryMonitor->getCurrentMemory()
);
// Aus aktiven Operationen entfernen
unset($this->activeOperations[$operationId]);
// Vom Call Stack entfernen
$this->removeFromCallStack($operationId);
// Zu Parent hinzufügen oder als Top-Level speichern
$parent = $this->findParentOperation($completedMeasurement->parentId);
if ($parent) {
// Als Child zum Parent hinzufügen
$updatedParent = $parent->addChild($completedMeasurement);
$this->updateActiveOperation($parent->operationId, $updatedParent);
} else {
// Als Top-Level Operation speichern
$this->completedOperations[$operationId] = $completedMeasurement;
}
$this->logger?->info('Nested operation completed', [
'operation_id' => $operationId,
'name' => $completedMeasurement->name,
'duration_ms' => $completedMeasurement->duration?->toMilliseconds(),
'self_time_ms' => $completedMeasurement->getSelfTime()->toMilliseconds(),
'depth' => $completedMeasurement->depth,
'children_count' => count($completedMeasurement->children),
]);
return $completedMeasurement;
}
/**
* Messe eine Operation mit automatischem Start/End
*/
public function measure(
string $name,
PerformanceCategory $category,
callable $callback,
array $context = []
): mixed {
if (! $this->enabled) {
return $callback();
}
$operationId = $this->startOperation($name, $category, $context);
try {
return $callback();
} finally {
$this->endOperation($operationId);
}
}
/**
* Get aktuell laufende Operation (Top of stack)
*/
public function getCurrentOperation(): ?NestedMeasurement
{
return end($this->callStack) ?: null;
}
/**
* Get Call Stack depth
*/
public function getCallStackDepth(): int
{
return count($this->callStack);
}
/**
* Get alle aktiven Operationen
*/
public function getActiveOperations(): array
{
return $this->activeOperations;
}
/**
* Get abgeschlossene Top-Level Operationen
*/
public function getCompletedOperations(): array
{
return $this->completedOperations;
}
/**
* Get Operation nach ID
*/
public function getOperation(string $operationId): ?NestedMeasurement
{
// Zuerst in aktiven suchen
if (isset($this->activeOperations[$operationId])) {
return $this->activeOperations[$operationId];
}
// Dann in abgeschlossenen suchen
if (isset($this->completedOperations[$operationId])) {
return $this->completedOperations[$operationId];
}
// In Children von abgeschlossenen suchen
foreach ($this->completedOperations as $completed) {
$found = $completed->findChild($operationId);
if ($found) {
return $found;
}
}
return null;
}
/**
* Get Performance-Bericht als hierarchische Struktur
*/
public function getHierarchicalReport(): array
{
$report = [
'active_operations' => [],
'completed_operations' => [],
'call_stack_depth' => $this->getCallStackDepth(),
'total_operations' => count($this->activeOperations) + count($this->completedOperations),
'summary' => $this->getSummary(),
];
// Aktive Operationen
foreach ($this->activeOperations as $active) {
$report['active_operations'][] = $active->toArray();
}
// Abgeschlossene Operationen (hierarchisch)
foreach ($this->completedOperations as $completed) {
$report['completed_operations'][] = $completed->toHierarchicalArray();
}
return $report;
}
/**
* Get flachen Performance-Bericht (alle Operationen linear)
*/
public function getFlatReport(): array
{
$allOperations = [];
// Aktive Operationen
foreach ($this->activeOperations as $active) {
$allOperations[] = $active->toArray();
}
// Alle abgeschlossenen (inklusive Children)
foreach ($this->completedOperations as $completed) {
$allOperations[] = $completed->toArray();
$descendants = $completed->getAllDescendants();
foreach ($descendants as $descendant) {
$allOperations[] = $descendant->toArray();
}
}
return $allOperations;
}
/**
* Get Execution Tree als String (für Debug)
*/
public function getExecutionTree(): string
{
$tree = "=== Nested Performance Execution Tree ===\n";
if (! empty($this->callStack)) {
$tree .= "Active Call Stack:\n";
foreach ($this->callStack as $i => $operation) {
$tree .= sprintf(
"%d. %s%s [%s] (running)\n",
$i + 1,
str_repeat(' ', $operation->depth),
$operation->name,
$operation->category->value
);
}
$tree .= "\n";
}
if (! empty($this->completedOperations)) {
$tree .= "Completed Operations:\n";
foreach ($this->completedOperations as $completed) {
$tree .= $completed->getExecutionTree();
}
}
return $tree;
}
/**
* Get Performance-Zusammenfassung
*/
public function getSummary(): array
{
$totalOperations = count($this->activeOperations) + count($this->completedOperations);
$completedCount = count($this->completedOperations);
if ($completedCount === 0) {
return [
'total_operations' => $totalOperations,
'completed_operations' => 0,
'active_operations' => count($this->activeOperations),
'average_duration_ms' => 0,
'total_duration_ms' => 0,
'max_depth' => $this->getCallStackDepth(),
];
}
// Berechne Statistiken für abgeschlossene Operationen
$totalDuration = Duration::zero();
$maxDepth = $this->getCallStackDepth();
foreach ($this->completedOperations as $completed) {
if ($completed->duration) {
$totalDuration = $totalDuration->add($completed->duration);
}
$descendants = $completed->getAllDescendants();
foreach ($descendants as $descendant) {
$maxDepth = max($maxDepth, $descendant->depth + 1);
}
}
return [
'total_operations' => $totalOperations,
'completed_operations' => $completedCount,
'active_operations' => count($this->activeOperations),
'average_duration_ms' => $completedCount > 0 ?
$totalDuration->toMilliseconds() / $completedCount : 0,
'total_duration_ms' => $totalDuration->toMilliseconds(),
'max_depth' => $maxDepth,
];
}
/**
* Reset alle Messungen
*/
public function reset(): void
{
$this->callStack = [];
$this->activeOperations = [];
$this->completedOperations = [];
$this->operationCounter = 0;
$this->logger?->info('Nested performance tracker reset');
}
/**
* Enable/Disable Tracking
*/
public function setEnabled(bool $enabled): void
{
$this->enabled = $enabled;
}
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* Generiere eindeutige Operation ID
*/
private function generateOperationId(string $name): string
{
$this->operationCounter++;
$sanitizedName = preg_replace('/[^a-zA-Z0-9_]/', '_', $name);
return sprintf('%s_%d_%s', $sanitizedName, $this->operationCounter, substr(md5($name . microtime()), 0, 8));
}
/**
* Entferne Operation vom Call Stack
*/
private function removeFromCallStack(string $operationId): void
{
foreach ($this->callStack as $index => $operation) {
if ($operation->operationId === $operationId) {
array_splice($this->callStack, $index, 1);
break;
}
}
}
/**
* Finde Parent Operation in aktiven Operationen
*/
private function findParentOperation(?string $parentId): ?NestedMeasurement
{
if (! $parentId) {
return null;
}
return $this->activeOperations[$parentId] ?? null;
}
/**
* Aktualisiere aktive Operation
*/
private function updateActiveOperation(string $operationId, NestedMeasurement $updated): void
{
$this->activeOperations[$operationId] = $updated;
// Auch im Call Stack aktualisieren
foreach ($this->callStack as $index => $operation) {
if ($operation->operationId === $operationId) {
$this->callStack[$index] = $updated;
break;
}
}
}
/**
* Debug info
*/
public function getDebugInfo(): array
{
return [
'enabled' => $this->enabled,
'call_stack_depth' => count($this->callStack),
'active_operations_count' => count($this->activeOperations),
'completed_operations_count' => count($this->completedOperations),
'operation_counter' => $this->operationCounter,
'current_operation' => $this->getCurrentOperation()?->name,
];
}
}

View File

@@ -0,0 +1,379 @@
<?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';
}
}

View File

@@ -15,6 +15,9 @@ enum PerformanceCategory: string
case VIEW = 'view';
case API = 'api';
case FILESYSTEM = 'filesystem';
case SECURITY = 'security';
case BENCHMARK = 'benchmark';
case DISCOVERY = 'discovery';
case CUSTOM = 'custom';
public static function fromString(string $value): self

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance;
final readonly class PerformanceConfig
{
public function __construct(
public bool $enabled = true,
public bool $httpTracking = true,
public bool $databaseTracking = true,
public bool $cacheTracking = true,
public bool $viewTracking = true,
public bool $routingTracking = true,
public bool $controllerTracking = true,
public bool $filesystemTracking = true,
public bool $memoryTracking = true,
public bool $queryCountTracking = true,
public bool $slowQueryLogging = true,
public float $slowQueryThreshold = 100.0, // ms
public bool $detailedReports = true,
public bool $includeStackTrace = false,
public int $maxMetricsPerCategory = 1000,
public array $excludedPaths = ['/health', '/metrics'],
public array $enabledCategories = [],
public bool $persistMetrics = false,
public string $outputFormat = 'html', // html, json, text
public bool $useEnhancedCollector = true,
public array $thresholds = [],
) {
}
public function isTrackingEnabled(PerformanceCategory $category): bool
{
if (! $this->enabled) {
return false;
}
if (! empty($this->enabledCategories) && ! in_array($category->value, $this->enabledCategories)) {
return false;
}
return match ($category) {
PerformanceCategory::SYSTEM, PerformanceCategory::API => $this->httpTracking,
PerformanceCategory::DATABASE => $this->databaseTracking,
PerformanceCategory::CACHE => $this->cacheTracking,
PerformanceCategory::VIEW, PerformanceCategory::TEMPLATE => $this->viewTracking,
PerformanceCategory::ROUTING => $this->routingTracking,
PerformanceCategory::CONTROLLER => $this->controllerTracking,
PerformanceCategory::FILESYSTEM => $this->filesystemTracking,
PerformanceCategory::BENCHMARK => true,
PerformanceCategory::CUSTOM => true,
};
}
public function shouldExcludePath(string $path): bool
{
return array_any(
$this->excludedPaths,
fn ($excludedPath) => str_starts_with($path, $excludedPath)
);
}
public function isSlowQuery(float $duration): bool
{
return $this->slowQueryLogging && $duration > $this->slowQueryThreshold;
}
public function useEnhancedCollector(): bool
{
return $this->useEnhancedCollector;
}
public function isEnabled(): bool
{
return $this->enabled;
}
public function getThreshold(string $key, mixed $default = null): mixed
{
return $this->thresholds[$key] ?? $default;
}
}

View File

@@ -1,298 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance;
use App\Framework\Database\Connection;
use App\Framework\DateTime\Clock;
class PerformanceDatabaseWorker
{
private Connection $connection;
private Clock $clock;
private array $config;
public function __construct(Connection $connection, Clock $clock, array $config = [])
{
$this->connection = $connection;
$this->clock = $clock;
$this->config = array_merge([
'batch_size' => 100,
'create_tables' => true,
'table_prefix' => 'performance_',
], $config);
if ($this->config['create_tables']) {
$this->ensureTables();
}
}
public function processLogFile(string $logFile): void
{
if (!file_exists($logFile)) {
return;
}
$tempFile = $logFile . '.processing';
// Datei atomar umbenennen für Processing
if (!rename($logFile, $tempFile)) {
return;
}
try {
$this->processFile($tempFile);
unlink($tempFile); // Erfolgreich verarbeitet
} catch (\Exception $e) {
// Bei Fehler Datei zurück umbenennen
rename($tempFile, $logFile);
error_log("Performance log processing failed: " . $e->getMessage());
throw $e;
}
}
private function processFile(string $file): void
{
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (empty($lines)) {
return;
}
$logBatch = [];
$measurementBatch = [];
foreach ($lines as $line) {
$data = json_decode($line, true);
if (!$data) {
continue;
}
$logId = $this->prepareLogEntry($data, $logBatch);
$this->prepareMeasurementEntries($data, $logId, $measurementBatch);
// Batch-Verarbeitung
if (count($logBatch) >= $this->config['batch_size']) {
$this->insertBatches($logBatch, $measurementBatch);
$logBatch = [];
$measurementBatch = [];
}
}
// Rest verarbeiten
if (!empty($logBatch)) {
$this->insertBatches($logBatch, $measurementBatch);
}
}
private function prepareLogEntry(array $data, array &$logBatch): string
{
$logId = uniqid('log_', true);
$logBatch[] = [
'id' => $logId,
'timestamp' => $data['timestamp'],
'request_id' => $data['request_id'],
'url' => $data['url'],
'method' => $data['method'],
'total_time_ms' => $data['performance']['summary']['total_time_ms'],
'memory_mb' => $data['performance']['summary']['total_memory_mb'],
'memory_peak_mb' => $data['memory_peak_mb'],
'marker_count' => $data['performance']['summary']['marker_count'],
'user_agent' => $data['user_agent'] ?? null,
'data' => json_encode($data['performance']),
'context' => json_encode($data['context'] ?? []),
'server_data' => json_encode($data['server_data'] ?? []),
'created_at' => $this->clock->now()->format('Y-m-d H:i:s'),
];
return $logId;
}
private function prepareMeasurementEntries(array $data, string $logId, array &$measurementBatch): void
{
foreach ($data['performance']['measurements'] ?? [] as $label => $measurement) {
$measurementBatch[] = [
'log_id' => $logId,
'category' => $measurement['category'],
'label' => $label,
'count' => $measurement['count'],
'total_time_ms' => $measurement['total_time_ms'],
'avg_time_ms' => $measurement['avg_time_ms'],
'min_time_ms' => $measurement['min_time_ms'],
'max_time_ms' => $measurement['max_time_ms'],
'total_memory_mb' => $measurement['total_memory_mb'],
'avg_memory_mb' => $measurement['avg_memory_mb'],
];
}
}
private function insertBatches(array $logBatch, array $measurementBatch): void
{
$pdo = $this->connection->getPdo();
$pdo->beginTransaction();
try {
// Performance Logs einfügen
if (!empty($logBatch)) {
$this->insertPerformanceLogs($pdo, $logBatch);
}
// Measurements einfügen
if (!empty($measurementBatch)) {
$this->insertMeasurements($pdo, $measurementBatch);
}
$pdo->commit();
} catch (\Exception $e) {
$pdo->rollBack();
throw $e;
}
}
private function insertPerformanceLogs(\PDO $pdo, array $batch): void
{
$tableName = $this->config['table_prefix'] . 'logs';
$sql = "INSERT INTO {$tableName} (
id, timestamp, request_id, url, method, total_time_ms, memory_mb,
memory_peak_mb, marker_count, user_agent, data, context, server_data, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $pdo->prepare($sql);
foreach ($batch as $entry) {
$stmt->execute([
$entry['id'],
$entry['timestamp'],
$entry['request_id'],
$entry['url'],
$entry['method'],
$entry['total_time_ms'],
$entry['memory_mb'],
$entry['memory_peak_mb'],
$entry['marker_count'],
$entry['user_agent'],
$entry['data'],
$entry['context'],
$entry['server_data'],
$entry['created_at'],
]);
}
}
private function insertMeasurements(\PDO $pdo, array $batch): void
{
$tableName = $this->config['table_prefix'] . 'measurements';
$sql = "INSERT INTO {$tableName} (
log_id, category, label, count, total_time_ms, avg_time_ms,
min_time_ms, max_time_ms, total_memory_mb, avg_memory_mb
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $pdo->prepare($sql);
foreach ($batch as $entry) {
$stmt->execute([
$entry['log_id'],
$entry['category'],
$entry['label'],
$entry['count'],
$entry['total_time_ms'],
$entry['avg_time_ms'],
$entry['min_time_ms'],
$entry['max_time_ms'],
$entry['total_memory_mb'],
$entry['avg_memory_mb'],
]);
}
}
private function ensureTables(): void
{
$pdo = $this->connection->getPdo();
$tablePrefix = $this->config['table_prefix'];
// Performance Logs Tabelle
$pdo->exec("
CREATE TABLE IF NOT EXISTS {$tablePrefix}logs (
id VARCHAR(255) PRIMARY KEY,
timestamp DATETIME NOT NULL,
request_id VARCHAR(255) NOT NULL,
url VARCHAR(500) NOT NULL,
method VARCHAR(10) NOT NULL,
total_time_ms DECIMAL(10,2) NOT NULL,
memory_mb DECIMAL(10,2) NOT NULL,
memory_peak_mb DECIMAL(10,2) NOT NULL,
marker_count INT NOT NULL,
user_agent TEXT,
data JSON,
context JSON,
server_data JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_timestamp (timestamp),
INDEX idx_url (url(100)),
INDEX idx_method (method),
INDEX idx_total_time (total_time_ms),
INDEX idx_request_id (request_id),
INDEX idx_created_at (created_at)
)
");
// Measurements Tabelle
$pdo->exec("
CREATE TABLE IF NOT EXISTS {$tablePrefix}measurements (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
log_id VARCHAR(255) NOT NULL,
category VARCHAR(50) NOT NULL,
label VARCHAR(100) NOT NULL,
count INT NOT NULL,
total_time_ms DECIMAL(10,2) NOT NULL,
avg_time_ms DECIMAL(10,2) NOT NULL,
min_time_ms DECIMAL(10,2) NOT NULL,
max_time_ms DECIMAL(10,2) NOT NULL,
total_memory_mb DECIMAL(10,2) NOT NULL,
avg_memory_mb DECIMAL(10,2) NOT NULL,
FOREIGN KEY (log_id) REFERENCES {$tablePrefix}logs(id) ON DELETE CASCADE,
INDEX idx_log_id (log_id),
INDEX idx_category (category),
INDEX idx_label (label),
INDEX idx_avg_time (avg_time_ms)
)
");
}
public function getStatistics(): array
{
$pdo = $this->connection->getPdo();
$tablePrefix = $this->config['table_prefix'];
$stmt = $pdo->query("
SELECT
COUNT(*) as total_logs,
AVG(total_time_ms) as avg_response_time,
MAX(total_time_ms) as max_response_time,
AVG(memory_mb) as avg_memory,
MAX(memory_mb) as max_memory
FROM {$tablePrefix}logs
WHERE created_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)
");
return $stmt->fetch(\PDO::FETCH_ASSOC) ?: [];
}
public function cleanup(int $retentionDays = 30): void
{
$pdo = $this->connection->getPdo();
$tablePrefix = $this->config['table_prefix'];
$pdo->exec("
DELETE FROM {$tablePrefix}logs
WHERE created_at < DATE_SUB(NOW(), INTERVAL {$retentionDays} DAY)
");
}
}

View File

@@ -1,169 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance;
use App\Framework\Core\PathProvider;
use App\Framework\DateTime\Clock;
use App\Framework\Jobs\JobQueue;
class PerformanceFileLogger
{
private array $buffer = [];
private string $logFile;
private Clock $clock;
private ?JobQueue $jobQueue;
private array $config;
public function __construct(
PathProvider $pathProvider,
Clock $clock,
?JobQueue $jobQueue = null,
array $config = []
) {
$this->clock = $clock;
$this->jobQueue = $jobQueue;
$this->config = array_merge([
'buffer_size' => 50,
'flush_interval' => 300, // 5 Minuten
'min_threshold_ms' => 0,
'background_processing' => true,
'include_request_data' => true,
], $config);
$logDir = $pathProvider->getStoragePath('logs/performance');
$this->ensureLogDirectory($logDir);
$date = $this->clock->now()->format('Y-m-d');
$this->logFile = "{$logDir}/performance-{$date}.jsonl";
// Cleanup bei Script-Ende
register_shutdown_function([$this, 'shutdown']);
}
public function log(PerformanceMeter $meter, array $context = []): void
{
$report = $meter->generateReport();
// Schwellenwert prüfen
if ($report['summary']['total_time_ms'] < $this->config['min_threshold_ms']) {
return;
}
$data = [
'timestamp' => $this->clock->now()->format('c'),
'timestamp_float' => $this->clock->microtime(),
'request_id' => $this->generateRequestId(),
'url' => $_SERVER['REQUEST_URI'] ?? 'cli',
'method' => $_SERVER['REQUEST_METHOD'] ?? 'CLI',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'CLI',
'memory_peak_mb' => memory_get_peak_usage(true) / 1024 / 1024,
'performance' => $report,
'context' => $context,
];
if ($this->config['include_request_data']) {
$data['server_data'] = $this->getServerData();
}
// In Buffer schreiben (extrem schnell)
$this->buffer[] = $data;
// Bei Puffergröße verarbeiten
if (count($this->buffer) >= $this->config['buffer_size']) {
$this->flushBuffer();
}
}
private function flushBuffer(): void
{
if (empty($this->buffer)) {
return;
}
// Schnell in File schreiben
$content = '';
foreach ($this->buffer as $entry) {
$content .= json_encode($entry, JSON_UNESCAPED_SLASHES) . "\n";
}
file_put_contents($this->logFile, $content, FILE_APPEND | LOCK_EX);
// Background-Verarbeitung anstoßen
if ($this->config['background_processing'] && $this->jobQueue) {
$this->scheduleBackgroundProcessing();
}
$this->buffer = [];
}
private function scheduleBackgroundProcessing(): void
{
// Job für die Verarbeitung in die Queue einreihen
$this->jobQueue->dispatch(new ProcessPerformanceLogsJob($this->logFile));
}
private function getServerData(): array
{
return [
'php_version' => PHP_VERSION,
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'server_name' => $_SERVER['SERVER_NAME'] ?? 'unknown',
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
];
}
private function generateRequestId(): string
{
return uniqid('req_', true);
}
private function ensureLogDirectory(string $directory): void
{
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
}
public function shutdown(): void
{
$this->flushBuffer();
}
public function getLogFile(): string
{
return $this->logFile;
}
public function setEnabled(bool $enabled): void
{
$this->config['enabled'] = $enabled;
}
public function isEnabled(): bool
{
return $this->config['enabled'] ?? true;
}
/**
* Bereinigt alte Log-Dateien
*/
public function cleanup(int $retentionDays = 30): void
{
if ($retentionDays <= 0) {
return;
}
$cutoffTime = $this->clock->now()->getTimestamp() - ($retentionDays * 24 * 3600);
$logDir = dirname($this->logFile);
$files = glob($logDir . '/performance-*.jsonl');
foreach ($files as $file) {
if (filemtime($file) < $cutoffTime) {
unlink($file);
}
}
}
}

View File

@@ -1,39 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance;
readonly class PerformanceMarker
{
public float $time;
public int $memory;
public function __construct(
public string $label,
public PerformanceCategory $category,
) {
$this->time = microtime(true);
$this->memory = memory_get_usage();
}
public function getLabel(): string
{
return $this->label;
}
public function getCategory(): PerformanceCategory
{
return $this->category;
}
public function getTime(): float
{
return $this->time;
}
public function getMemory(): int
{
return $this->memory;
}
}

View File

@@ -1,82 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance;
class PerformanceMeasurement
{
public float $startTime;
public int $startMemory;
public ?float $endTime = null;
public ?int $endMemory = null;
public function __construct(
public readonly string $label,
public readonly PerformanceCategory $category,
) {
$this->startTime = microtime(true);
$this->startMemory = memory_get_usage();
}
public function end(): void
{
if ($this->endTime === null) {
$this->endTime = microtime(true);
$this->endMemory = memory_get_usage();
}
}
public function isCompleted(): bool
{
return $this->endTime !== null;
}
public function getLabel(): string
{
return $this->label;
}
public function getCategory(): PerformanceCategory
{
return $this->category;
}
public function getDurationMs(): float
{
if ($this->endTime === null) {
return 0.0;
}
return ($this->endTime - $this->startTime) * 1000;
}
public function getMemoryUsageMb(): float
{
if ($this->endMemory === null) {
return 0.0;
}
return ($this->endMemory - $this->startMemory) / 1024 / 1024;
}
public function getStartTime(): float
{
return $this->startTime;
}
public function getEndTime(): ?float
{
return $this->endTime;
}
public function getStartMemory(): int
{
return $this->startMemory;
}
public function getEndMemory(): ?int
{
return $this->endMemory;
}
}

View File

@@ -1,446 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance;
class PerformanceMeter
{
/** @var array<string, PerformanceMarker> */
private array $markers = [];
/** @var array<string, PerformanceMeasurement> */
private array $activeMeasurements = [];
/** @var array<string, array<int, PerformanceMeasurement>> */
private array $completedMeasurements = [];
private float $initTime;
private int $initMemory;
private bool $enabled;
public function __construct(bool $enabled = true)
{
$this->initTime = microtime(true);
$this->initMemory = memory_get_usage();
$this->enabled = $enabled;
// Automatisch den ersten Marker setzen
$this->mark('init', PerformanceCategory::SYSTEM);
}
/**
* Setzt einen Marker mit einem Label und einer Kategorie
*/
public function mark(string $label, PerformanceCategory $category = PerformanceCategory::CUSTOM): void
{
if (!$this->enabled) {
return;
}
$this->markers[$label] = new PerformanceMarker($label, $category);
}
/**
* Startet eine Zeitmessung für einen bestimmten Prozess
*/
public function startMeasure(string $label, PerformanceCategory $category = PerformanceCategory::CUSTOM): void
{
if (!$this->enabled) {
return;
}
$this->activeMeasurements[$label] = new PerformanceMeasurement($label, $category);
}
/**
* Beendet eine Zeitmessung und speichert das Ergebnis
*/
public function endMeasure(string $label): void
{
if (!$this->enabled || !isset($this->activeMeasurements[$label])) {
return;
}
$measurement = $this->activeMeasurements[$label];
$measurement->end();
$category = $measurement->getCategory()->value;
if (!isset($this->completedMeasurements[$category])) {
$this->completedMeasurements[$category] = [];
}
$this->completedMeasurements[$category][] = $measurement;
// Aktive Messung entfernen
unset($this->activeMeasurements[$label]);
// Marker zur einfachen zeitlichen Einordnung setzen
$this->mark("{$label}_end", $measurement->getCategory());
}
/**
* Führt eine Funktion aus und misst deren Performance
*/
public function measure(
string $label,
callable $callback,
PerformanceCategory $category = PerformanceCategory::CUSTOM
): mixed {
if (!$this->enabled) {
return $callback();
}
$this->startMeasure($label, $category);
$result = $callback();
$this->endMeasure($label);
return $result;
}
/**
* Gibt die Zeit zwischen zwei Markern zurück
*/
public function getTimeBetween(string $startLabel, string $endLabel): float
{
if (!isset($this->markers[$startLabel]) || !isset($this->markers[$endLabel])) {
return 0.0;
}
return ($this->markers[$endLabel]->getTime() - $this->markers[$startLabel]->getTime()) * 1000;
}
/**
* Gibt den Speicherverbrauch zwischen zwei Markern zurück
*/
public function getMemoryBetween(string $startLabel, string $endLabel): float
{
if (!isset($this->markers[$startLabel]) || !isset($this->markers[$endLabel])) {
return 0.0;
}
return ($this->markers[$endLabel]->getMemory() - $this->markers[$startLabel]->getMemory()) / 1024 / 1024;
}
/**
* Gibt alle Marker zurück, gefiltert nach Kategorie
*/
public function getMarkers(?PerformanceCategory $category = null): array
{
if ($category === null) {
return $this->markers;
}
return array_filter(
$this->markers,
fn (PerformanceMarker $marker): bool => $marker->getCategory() === $category
);
}
/**
* Gibt einen Bericht über alle Marker aus
*/
public function generateReport(): array
{
$report = [
'summary' => [
'total_time_ms' => $this->getTotalTime(),
'total_memory_mb' => $this->getTotalMemory(),
'marker_count' => count($this->markers),
],
'markers' => [],
'measurements' => [],
'categories' => [],
];
// Marker chronologisch sortieren
$sortedMarkers = $this->markers;
uasort($sortedMarkers, fn($a, $b) => $a->getTime() <=> $b->getTime());
// Marker-Bericht erstellen
$previousTime = $this->initTime;
foreach ($sortedMarkers as $label => $marker) {
$timeSinceStart = ($marker->getTime() - $this->initTime) * 1000;
$timeSincePrevious = ($marker->getTime() - $previousTime) * 1000;
$memorySinceStart = ($marker->getMemory() - $this->initMemory) / 1024 / 1024;
$report['markers'][$label] = [
'category' => $marker->getCategory()->value,
'time_since_start_ms' => round($timeSinceStart, 2),
'time_since_previous_ms' => round($timeSincePrevious, 2),
'memory_mb' => round($memorySinceStart, 2),
];
$previousTime = $marker->getTime();
}
// Messungen nach Kategorien zusammenfassen
foreach ($this->completedMeasurements as $categoryKey => $measurements) {
$report['categories'][$categoryKey] = [
'count' => count($measurements),
'measurements' => [],
];
foreach ($measurements as $measurement) {
$label = $measurement->getLabel();
if (!isset($report['measurements'][$label])) {
$report['measurements'][$label] = [
'category' => $measurement->getCategory()->value,
'count' => 0,
'total_time_ms' => 0,
'total_memory_mb' => 0,
'avg_time_ms' => 0,
'avg_memory_mb' => 0,
'min_time_ms' => PHP_FLOAT_MAX,
'max_time_ms' => 0,
];
}
$duration = $measurement->getDurationMs();
$memory = $measurement->getMemoryUsageMb();
$data = &$report['measurements'][$label];
$data['count']++;
$data['total_time_ms'] += $duration;
$data['total_memory_mb'] += $memory;
$data['min_time_ms'] = min($data['min_time_ms'], $duration);
$data['max_time_ms'] = max($data['max_time_ms'], $duration);
$data['avg_time_ms'] = $data['total_time_ms'] / $data['count'];
$data['avg_memory_mb'] = $data['total_memory_mb'] / $data['count'];
// Einzelmessungen pro Kategorie
$report['categories'][$categoryKey]['measurements'][] = [
'label' => $label,
'time_ms' => round($duration, 2),
'memory_mb' => round($memory, 2),
'start_time' => ($measurement->getStartTime() - $this->initTime) * 1000,
'end_time' => ($measurement->getEndTime() - $this->initTime) * 1000,
];
}
// Statistiken für die Kategorie berechnen
$categoryMeasurements = $report['categories'][$categoryKey]['measurements'];
if (!empty($categoryMeasurements)) {
$totalTime = array_sum(array_column($categoryMeasurements, 'time_ms'));
$totalMemory = array_sum(array_column($categoryMeasurements, 'memory_mb'));
$count = count($categoryMeasurements);
$report['categories'][$categoryKey]['total_time_ms'] = $totalTime;
$report['categories'][$categoryKey]['avg_time_ms'] = $totalTime / $count;
$report['categories'][$categoryKey]['total_memory_mb'] = $totalMemory;
$report['categories'][$categoryKey]['avg_memory_mb'] = $totalMemory / $count;
}
}
// Rundungen für alle numerischen Werte
foreach ($report['measurements'] as &$measurement) {
$measurement['avg_time_ms'] = round($measurement['avg_time_ms'], 2);
$measurement['avg_memory_mb'] = round($measurement['avg_memory_mb'], 2);
$measurement['total_time_ms'] = round($measurement['total_time_ms'], 2);
$measurement['total_memory_mb'] = round($measurement['total_memory_mb'], 2);
$measurement['min_time_ms'] = round($measurement['min_time_ms'], 2);
$measurement['max_time_ms'] = round($measurement['max_time_ms'], 2);
}
return $report;
}
/**
* Formatiert einen Bericht als HTML
*/
public function generateHtmlReport(): string
{
$report = $this->generateReport();
$html = '<div style="font-family: monospace; font-size: 13px; background: #f5f5f5; padding: 10px; border: 1px solid #ddd;">';
$html .= '<h3>Performance-Bericht</h3>';
$html .= '<p>';
$html .= sprintf('Gesamtzeit: <strong>%.2f ms</strong><br>', $report['summary']['total_time_ms']);
$html .= sprintf('Speicherverbrauch: <strong>%.2f MB</strong><br>', $report['summary']['total_memory_mb']);
$html .= sprintf('Anzahl Marker: <strong>%d</strong>', $report['summary']['marker_count']);
$html .= '</p>';
if (!empty($report['measurements'])) {
$html .= '<h4>Messungen</h4>';
$html .= '<table style="border-collapse: collapse; width: 100%;">';
$html .= '<tr style="background: #eee;"><th style="text-align: left; padding: 5px;">Label</th><th style="text-align: left; padding: 5px;">Kategorie</th><th style="text-align: right; padding: 5px;">Anzahl</th><th style="text-align: right; padding: 5px;">Avg. Zeit (ms)</th><th style="text-align: right; padding: 5px;">Min/Max (ms)</th><th style="text-align: right; padding: 5px;">Avg. Speicher (MB)</th></tr>';
foreach ($report['measurements'] as $label => $data) {
$html .= sprintf(
'<tr style="border-bottom: 1px solid #ddd;"><td style="padding: 5px;">%s</td><td style="padding: 5px;">%s</td><td style="text-align: right; padding: 5px;">%d</td><td style="text-align: right; padding: 5px;">%.2f</td><td style="text-align: right; padding: 5px;">%.2f / %.2f</td><td style="text-align: right; padding: 5px;">%.2f</td></tr>',
htmlspecialchars($label),
htmlspecialchars($data['category']),
$data['count'],
$data['avg_time_ms'],
$data['min_time_ms'],
$data['max_time_ms'],
$data['avg_memory_mb']
);
}
$html .= '</table>';
}
$html .= '<h4>Marker-Zeitleiste</h4>';
$html .= '<table style="border-collapse: collapse; width: 100%;">';
$html .= '<tr style="background: #eee;"><th style="text-align: left; padding: 5px;">Zeit (ms)</th><th style="text-align: left; padding: 5px;">Label</th><th style="text-align: left; padding: 5px;">Kategorie</th><th style="text-align: right; padding: 5px;">+Zeit (ms)</th><th style="text-align: right; padding: 5px;">Speicher (MB)</th></tr>';
foreach ($report['markers'] as $label => $data) {
$html .= sprintf(
'<tr style="border-bottom: 1px solid #ddd;"><td style="padding: 5px;">%.2f</td><td style="padding: 5px;">%s</td><td style="padding: 5px;">%s</td><td style="text-align: right; padding: 5px;">%.2f</td><td style="text-align: right; padding: 5px;">%.2f</td></tr>',
$data['time_since_start_ms'],
htmlspecialchars($label),
htmlspecialchars($data['category']),
$data['time_since_previous_ms'],
$data['memory_mb']
);
}
$html .= '</table>';
// Kategorieübersicht
if (!empty($report['categories'])) {
$html .= '<h4>Kategorien</h4>';
$html .= '<table style="border-collapse: collapse; width: 100%;">';
$html .= '<tr style="background: #eee;"><th style="text-align: left; padding: 5px;">Kategorie</th><th style="text-align: right; padding: 5px;">Anzahl</th><th style="text-align: right; padding: 5px;">Gesamtzeit (ms)</th><th style="text-align: right; padding: 5px;">Durchschnitt (ms)</th></tr>';
foreach ($report['categories'] as $category => $data) {
if (isset($data['total_time_ms'])) {
$html .= sprintf(
'<tr style="border-bottom: 1px solid #ddd;"><td style="padding: 5px;">%s</td><td style="text-align: right; padding: 5px;">%d</td><td style="text-align: right; padding: 5px;">%.2f</td><td style="text-align: right; padding: 5px;">%.2f</td></tr>',
htmlspecialchars($category),
$data['count'],
$data['total_time_ms'],
$data['avg_time_ms']
);
}
}
$html .= '</table>';
}
$html .= '</div>';
return $html;
}
/**
* Gibt einen Bericht als Text aus
*/
public function generateTextReport(): string
{
$report = $this->generateReport();
$text = "== Performance-Bericht ==\n";
$text .= sprintf("Gesamtzeit: %.2f ms\n", $report['summary']['total_time_ms']);
$text .= sprintf("Speicherverbrauch: %.2f MB\n", $report['summary']['total_memory_mb']);
$text .= sprintf("Anzahl Marker: %d\n\n", $report['summary']['marker_count']);
if (!empty($report['measurements'])) {
$text .= "== Messungen ==\n";
$text .= sprintf("%-30s %-15s %10s %15s %15s %15s\n",
"Label", "Kategorie", "Anzahl", "Avg. Zeit (ms)", "Min/Max (ms)", "Avg. Speicher (MB)");
$text .= str_repeat("-", 100) . "\n";
foreach ($report['measurements'] as $label => $data) {
$text .= sprintf(
"%-30s %-15s %10d %15.2f %15s %15.2f\n",
$label,
$data['category'],
$data['count'],
$data['avg_time_ms'],
sprintf("%.2f/%.2f", $data['min_time_ms'], $data['max_time_ms']),
$data['avg_memory_mb']
);
}
$text .= "\n";
}
$text .= "== Marker-Zeitleiste ==\n";
$text .= sprintf("%-10s %-30s %-15s %15s %15s\n",
"Zeit (ms)", "Label", "Kategorie", "+Zeit (ms)", "Speicher (MB)");
$text .= str_repeat("-", 90) . "\n";
foreach ($report['markers'] as $label => $data) {
$text .= sprintf(
"%-10.2f %-30s %-15s %15.2f %15.2f\n",
$data['time_since_start_ms'],
$label,
$data['category'],
$data['time_since_previous_ms'],
$data['memory_mb']
);
}
if (!empty($report['categories'])) {
$text .= "\n== Kategorien ==\n";
$text .= sprintf("%-15s %10s %20s %20s\n",
"Kategorie", "Anzahl", "Gesamtzeit (ms)", "Durchschnitt (ms)");
$text .= str_repeat("-", 70) . "\n";
foreach ($report['categories'] as $category => $data) {
if (isset($data['total_time_ms'])) {
$text .= sprintf(
"%-15s %10d %20.2f %20.2f\n",
$category,
$data['count'],
$data['total_time_ms'],
$data['avg_time_ms']
);
}
}
}
return $text;
}
/**
* Gibt die Gesamtausführungszeit zurück
*/
public function getTotalTime(): float
{
return (microtime(true) - $this->initTime) * 1000; // in ms
}
/**
* Gibt den Gesamtspeicherverbrauch zurück
*/
public function getTotalMemory(): float
{
return (memory_get_usage() - $this->initMemory) / 1024 / 1024; // in MB
}
/**
* Aktiviert oder deaktiviert den PerformanceMeter
*/
public function setEnabled(bool $enabled): void
{
$this->enabled = $enabled;
}
/**
* Gibt zurück, ob der PerformanceMeter aktiviert ist
*/
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* Setzt alle Messungen zurück
*/
public function reset(): void
{
$this->markers = [];
$this->activeMeasurements = [];
$this->completedMeasurements = [];
$this->initTime = microtime(true);
$this->initMemory = memory_get_usage();
// Automatisch den ersten Marker setzen
$this->mark('init', PerformanceCategory::SYSTEM);
}
}

View File

@@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Performance\ValueObjects\Measurement;
use App\Framework\Performance\ValueObjects\MeasurementCollection;
use App\Framework\Performance\ValueObjects\MetricContext;
final class PerformanceMetric
{
private MeasurementCollection $measurements;
/** @var array<float> */
private array $values = [];
private int $count = 0;
public function __construct(
private readonly string $key,
private readonly PerformanceCategory $category,
private readonly MetricContext $context
) {
$this->measurements = new MeasurementCollection();
}
public static function create(string $key, PerformanceCategory $category, array $context = []): self
{
return new self($key, $category, new MetricContext($context));
}
public function addMeasurementObject(Measurement $measurement): void
{
$this->measurements->add($measurement);
$this->count++;
}
public function addValue(float $value): void
{
$this->values[] = $value;
$this->count++;
}
public function increment(int $amount = 1): void
{
$this->count += $amount;
}
public function getKey(): string
{
return $this->key;
}
public function getCategory(): PerformanceCategory
{
return $this->category;
}
public function getContext(): array
{
return $this->context->toArray();
}
public function getContextObject(): MetricContext
{
return $this->context;
}
public function getCount(): int
{
return $this->count;
}
public function getMeasurements(): MeasurementCollection
{
return $this->measurements;
}
public function getTotalDuration(): float
{
return $this->measurements->getTotalDuration()->toMilliseconds();
}
public function getTotalDurationObject(): Duration
{
return $this->measurements->getTotalDuration();
}
public function getAverageDuration(): Duration
{
return $this->measurements->getAverageDuration();
}
public function getAverageDurationMilliseconds(): float
{
return $this->measurements->getAverageDuration()->toMilliseconds();
}
public function getMinDuration(): float
{
$min = $this->measurements->getMinDuration();
return $min ? $min->toMilliseconds() : 0.0;
}
public function getMinDurationObject(): ?Duration
{
return $this->measurements->getMinDuration();
}
public function getMaxDuration(): float
{
$max = $this->measurements->getMaxDuration();
return $max ? $max->toMilliseconds() : 0.0;
}
public function getMaxDurationObject(): ?Duration
{
return $this->measurements->getMaxDuration();
}
public function getTotalMemory(): int
{
return $this->measurements->getTotalMemory()->toBytes();
}
public function getTotalMemoryAsBytes(): Byte
{
return $this->measurements->getTotalMemory();
}
public function getAverageMemory(): float
{
return (float) $this->measurements->getAverageMemory()->toBytes();
}
public function getAverageMemoryAsBytes(): Byte
{
return $this->measurements->getAverageMemory();
}
public function getMinMemory(): int
{
$min = $this->measurements->getMinMemory();
return $min ? $min->toBytes() : 0;
}
public function getMinMemoryAsBytes(): Byte
{
return $this->measurements->getMinMemory() ?? Byte::zero();
}
public function getMaxMemory(): int
{
$max = $this->measurements->getMaxMemory();
return $max ? $max->toBytes() : 0;
}
public function getMaxMemoryAsBytes(): Byte
{
return $this->measurements->getMaxMemory() ?? Byte::zero();
}
public function getValues(): array
{
return $this->values;
}
public function getAverageValue(): float
{
if (empty($this->values)) {
return 0.0;
}
return array_sum($this->values) / count($this->values);
}
public function toArray(): array
{
return [
'key' => $this->key,
'category' => $this->category->value,
'context' => $this->context->toArray(),
'count' => $this->count,
'measurements' => [
'total_duration_ms' => round($this->getTotalDuration(), 2),
'avg_duration_ms' => round($this->getAverageDurationMilliseconds(), 2),
'min_duration_ms' => round($this->getMinDuration(), 2),
'max_duration_ms' => round($this->getMaxDuration(), 2),
'total_memory_bytes' => $this->getTotalMemory(),
'total_memory_human' => $this->getTotalMemoryAsBytes()->toHumanReadable(),
'avg_memory_bytes' => round($this->getAverageMemory(), 2),
'avg_memory_human' => $this->getAverageMemoryAsBytes()->toHumanReadable(),
'min_memory_bytes' => $this->getMinMemory(),
'min_memory_human' => $this->getMinMemoryAsBytes()->toHumanReadable(),
'max_memory_bytes' => $this->getMaxMemory(),
'max_memory_human' => $this->getMaxMemoryAsBytes()->toHumanReadable(),
],
'values' => [
'count' => count($this->values),
'total' => round(array_sum($this->values), 2),
'average' => round($this->getAverageValue(), 2),
],
];
}
}

View File

@@ -1,189 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance;
use App\Framework\DateTime\Clock;
class PerformanceMetricLogger
{
private \Redis $redis;
private Clock $clock;
private array $config;
public function __construct(\Redis $redis, Clock $clock, array $config = [])
{
$this->redis = $redis;
$this->clock = $clock;
$this->config = array_merge([
'enabled' => true,
'ttl_minutes' => 60,
'slow_threshold_ms' => 100,
'max_slow_endpoints' => 100,
'max_popular_endpoints' => 100,
], $config);
}
public function log(PerformanceMeter $meter, array $context = []): void
{
if (!$this->config['enabled']) {
return;
}
$report = $meter->generateReport();
$totalTime = $report['summary']['total_time_ms'];
$url = $_SERVER['REQUEST_URI'] ?? 'cli';
$method = $_SERVER['REQUEST_METHOD'] ?? 'CLI';
try {
$this->updateLiveMetrics($url, $method, $totalTime, $report, $context);
} catch (\Exception $e) {
// Redis-Fehler sollten nicht den Request crashen lassen
error_log("Redis metrics update failed: " . $e->getMessage());
}
}
private function updateLiveMetrics(string $url, string $method, float $duration, array $report, array $context): void
{
$now = $this->clock->now();
$minute = $now->format('Y-m-d H:i');
$hour = $now->format('Y-m-d H');
$ttl = $this->config['ttl_minutes'] * 60;
// Pipeline für bessere Performance
$pipe = $this->redis->pipeline();
// Requests pro Minute/Stunde
$pipe->incr("performance:requests_per_minute:$minute");
$pipe->expire("performance:requests_per_minute:$minute", $ttl);
$pipe->incr("performance:requests_per_hour:$hour");
$pipe->expire("performance:requests_per_hour:$hour", $ttl * 24);
// Response-Zeiten sammeln
$pipe->lpush("performance:response_times:$minute", $duration);
$pipe->ltrim("performance:response_times:$minute", 0, 999); // Max 1000 Werte
$pipe->expire("performance:response_times:$minute", $ttl);
// Speicherverbrauch
$memoryMb = $report['summary']['total_memory_mb'];
$pipe->lpush("performance:memory_usage:$minute", $memoryMb);
$pipe->ltrim("performance:memory_usage:$minute", 0, 999);
$pipe->expire("performance:memory_usage:$minute", $ttl);
// Langsame Endpoints tracken
if ($duration > $this->config['slow_threshold_ms']) {
$endpoint = "$method $url";
$pipe->zadd('performance:slow_endpoints', $duration, $endpoint);
$pipe->zremrangebyrank('performance:slow_endpoints', 0, -($this->config['max_slow_endpoints'] + 1));
}
// Populäre Endpoints
$endpoint = "$method $url";
$pipe->zincrby('performance:popular_endpoints', 1, $endpoint);
$pipe->zremrangebyrank('performance:popular_endpoints', 0, -($this->config['max_popular_endpoints'] + 1));
// Kategorien-Statistiken
foreach ($report['categories'] ?? [] as $category => $data) {
if (isset($data['total_time_ms'])) {
$pipe->lpush("performance:category:$category:$minute", $data['total_time_ms']);
$pipe->ltrim("performance:category:$category:$minute", 0, 99);
$pipe->expire("performance:category:$category:$minute", $ttl);
}
}
// Error-Rate tracking
if (isset($context['has_errors']) && $context['has_errors']) {
$pipe->incr("performance:errors_per_minute:$minute");
$pipe->expire("performance:errors_per_minute:$minute", $ttl);
}
$pipe->exec();
}
public function getLiveMetrics(): array
{
try {
$now = $this->clock->now();
$minute = $now->format('Y-m-d H:i');
$prevMinute = $now->modify('-1 minute')->format('Y-m-d H:i');
$hour = $now->format('Y-m-d H');
return [
'requests' => [
'current_minute' => (int) ($this->redis->get("performance:requests_per_minute:$minute") ?? 0),
'previous_minute' => (int) ($this->redis->get("performance:requests_per_minute:$prevMinute") ?? 0),
'current_hour' => (int) ($this->redis->get("performance:requests_per_hour:$hour") ?? 0),
],
'response_times' => [
'current_avg_ms' => $this->calculateAverage("performance:response_times:$minute"),
'previous_avg_ms' => $this->calculateAverage("performance:response_times:$prevMinute"),
],
'memory' => [
'current_avg_mb' => $this->calculateAverage("performance:memory_usage:$minute"),
'previous_avg_mb' => $this->calculateAverage("performance:memory_usage:$prevMinute"),
],
'slowest_endpoints' => $this->redis->zrevrange('performance:slow_endpoints', 0, 9, true),
'popular_endpoints' => $this->redis->zrevrange('performance:popular_endpoints', 0, 9, true),
'error_rate' => [
'current_minute' => (int) ($this->redis->get("performance:errors_per_minute:$minute") ?? 0),
'previous_minute' => (int) ($this->redis->get("performance:errors_per_minute:$prevMinute") ?? 0),
],
'categories' => $this->getCategoryMetrics($minute, $prevMinute),
];
} catch (\Exception $e) {
return ['error' => $e->getMessage()];
}
}
private function calculateAverage(string $key): float
{
$values = $this->redis->lrange($key, 0, -1);
if (empty($values)) {
return 0.0;
}
$sum = array_sum(array_map('floatval', $values));
return round($sum / count($values), 2);
}
private function getCategoryMetrics(string $minute, string $prevMinute): array
{
$categories = [];
$keys = $this->redis->keys("performance:category:*:$minute");
foreach ($keys as $key) {
if (preg_match('/performance:category:([^:]+):/', $key, $matches)) {
$category = $matches[1];
$prevKey = "performance:category:$category:$prevMinute";
$categories[$category] = [
'current_avg_ms' => $this->calculateAverage($key),
'previous_avg_ms' => $this->calculateAverage($prevKey),
];
}
}
return $categories;
}
public function clearMetrics(): void
{
$keys = $this->redis->keys('performance:*');
if (!empty($keys)) {
$this->redis->del($keys);
}
}
public function setEnabled(bool $enabled): void
{
$this->config['enabled'] = $enabled;
}
public function isEnabled(): bool
{
return $this->config['enabled'];
}
}

View File

@@ -1,80 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\Response;
use App\Framework\Config\Configuration;
use App\Framework\Http\ResponseManipulator;
readonly class PerformanceMiddleware implements XHttpMiddleware
{
public function __construct(
private PerformanceMeter $performanceMeter,
private Configuration $config
) {}
public function __invoke(MiddlewareContext $context, callable $next): MiddlewareContext
{
$request = $context->request;
// Marker für Anfrage-Start setzen
$this->performanceMeter->mark('request_start', PerformanceCategory::SYSTEM);
// Messung für Anfrageverarbeitung starten
$this->performanceMeter->startMeasure('request_processing', PerformanceCategory::SYSTEM);
// Anfrage weiterleiten
$response = $next($request);
// Messung beenden
$this->performanceMeter->endMeasure('request_processing');
$this->performanceMeter->mark('request_end', PerformanceCategory::SYSTEM);
// Performance-Bericht hinzufügen, wenn Debug-Modus aktiv
if ($this->config->get('debug', false) && !$request->isAjax()) {
// Nur für HTML-Antworten
$contentType = $response->headers->get('Content-Type', '');
if (str_contains($contentType, 'text/html') || empty($contentType)) {
$this->appendPerformanceReport($response);
} else {
$this->addPerformanceHeaders($response);
}
} elseif ($this->config->get('debug', false) && $request->isAjax()) {
// Für AJAX-Anfragen Header hinzufügen
$this->addPerformanceHeaders($response);
}
return $response;
}
private function appendPerformanceReport(Response $response): void
{
$body = $response->body;
// Suchen nach dem schließenden </body> Tag
$closingBodyPos = strripos($body, '</body>');
if ($closingBodyPos !== false) {
$report = $this->performanceMeter->generateHtmlReport();
// Report vor dem schließenden </body> Tag einfügen
$newBody = substr($body, 0, $closingBodyPos);
$newBody .= $report;
$newBody .= substr($body, $closingBodyPos);
$response = new ResponseManipulator()->withBody($response, $newBody);
//$response->setBody($newBody);
}
}
private function addPerformanceHeaders(Response $response): void
{
$report = $this->performanceMeter->generateReport();
$response->headers->set('X-Performance-Time', sprintf('%.2f ms', $report['summary']['total_time_ms']));
$response->headers->set('X-Performance-Memory', sprintf('%.2f MB', $report['summary']['total_memory_mb']));
}
}

View File

@@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\SystemClock;
use App\Framework\Filesystem\FilePath;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Performance\Contracts\PerformanceReporterInterface;
use App\Framework\Performance\ValueObjects\CategoryMetrics;
use App\Framework\Performance\ValueObjects\PerformanceReport;
use App\Framework\Performance\ValueObjects\PerformanceSummary;
use RuntimeException;
final readonly class PerformanceReporter implements PerformanceReporterInterface
{
private MemoryMonitor $memoryMonitor;
private Clock $clock;
public function __construct(
private PerformanceCollectorInterface $collector,
#private PerformanceConfig $config
?Clock $clock = null
) {
$this->memoryMonitor = new MemoryMonitor();
$this->clock = $clock ?? new SystemClock();
}
public function generateReport(string $format = 'array'): string|array
{
$report = $this->collectReportData();
return match ($format) {
'json' => json_encode($report->toArray(), JSON_PRETTY_PRINT) ?: '{}',
'html' => $this->generateHtmlReport($report),
'text' => $this->generateTextReport($report),
'array' => $report->toArray(),
default => throw new \InvalidArgumentException("Unsupported format: {$format}"),
};
}
private function collectReportData(): PerformanceReport
{
$metrics = $this->collector->getMetrics();
$memorySummary = $this->memoryMonitor->getSummary();
// Create performance summary
$summary = PerformanceSummary::fromRawValues(
totalRequestTimeMs: round($this->collector->getTotalRequestTime(), 2),
totalRequestMemoryBytes: $this->collector->getTotalRequestMemory(),
peakMemoryBytes: $this->collector->getPeakMemory(),
metricsCount: count($metrics),
memorySummary: $memorySummary
);
// Group metrics by category
$categorizedMetrics = [];
foreach ($metrics as $metric) {
$category = $metric->getCategory()->value;
$categorizedMetrics[$category][] = $metric;
}
// Process each category
$categories = [];
foreach ($categorizedMetrics as $categoryName => $categoryMetrics) {
$categories[$categoryName] = CategoryMetrics::fromMetrics($categoryMetrics);
}
// Add individual metrics
$metricsArray = [];
foreach ($metrics as $metric) {
$metricsArray[$metric->getKey()] = $metric->toArray();
}
return new PerformanceReport(
timestamp: $this->clock->time(),
summary: $summary,
categories: $categories,
metrics: $metricsArray
);
}
private function generateHtmlReport(PerformanceReport $report): string
{
$html = '<div style="font-family: monospace; font-size: 12px; background: #f8f9fa; padding: 15px; border-radius: 5px; max-width: 1200px;">';
// Header
$html .= '<h3 style="margin: 0 0 15px 0; color: #333;">🚀 Performance Report</h3>';
$html .= '<p style="margin: 0 0 15px 0; color: #666;">Generated at: ' . $report->timestamp->format('Y-m-d H:i:s') . '</p>';
// Summary
$summary = $report->summary;
$memorySummary = $summary->memorySummary;
$html .= '<div style="background: white; padding: 10px; margin-bottom: 15px; border-radius: 3px; border-left: 4px solid #007bff;">';
$html .= '<h4 style="margin: 0 0 10px 0;">📊 Summary</h4>';
$html .= '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px;">';
$html .= '<div><strong>Request Time:</strong> ' . $summary->getFormattedRequestTime() . '</div>';
$html .= '<div><strong>Memory Usage:</strong> ' . $memorySummary->getCurrentHumanReadable() . '</div>';
$html .= '<div><strong>Peak Memory:</strong> ' . $memorySummary->getPeakHumanReadable() . '</div>';
$html .= '<div><strong>Memory Limit:</strong> ' . $memorySummary->getLimitHumanReadable() . '</div>';
$html .= '<div><strong>Memory Available:</strong> ' . $memorySummary->getAvailableMemory()->toHumanReadable() . '</div>';
$html .= '<div><strong>Usage:</strong> ' . $memorySummary->getUsagePercentageFormatted() . '</div>';
$html .= '<div><strong>Metrics Count:</strong> ' . $summary->metricsCount . '</div>';
$html .= '</div></div>';
// Categories
if (! empty($report->categories)) {
$html .= '<div style="background: white; padding: 10px; margin-bottom: 15px; border-radius: 3px; border-left: 4px solid #28a745;">';
$html .= '<h4 style="margin: 0 0 10px 0;">📂 Categories</h4>';
$html .= '<table style="width: 100%; border-collapse: collapse;">';
$html .= '<thead><tr style="background: #f8f9fa;">';
$html .= '<th style="padding: 8px; text-align: left; border-bottom: 1px solid #dee2e6;">Category</th>';
$html .= '<th style="padding: 8px; text-align: right; border-bottom: 1px solid #dee2e6;">Metrics</th>';
$html .= '<th style="padding: 8px; text-align: right; border-bottom: 1px solid #dee2e6;">Total Time (ms)</th>';
$html .= '<th style="padding: 8px; text-align: right; border-bottom: 1px solid #dee2e6;">Total Calls</th>';
$html .= '<th style="padding: 8px; text-align: right; border-bottom: 1px solid #dee2e6;">Avg Time (ms)</th>';
$html .= '</tr></thead><tbody>';
foreach ($report->categories as $categoryName => $categoryData) {
$html .= '<tr>';
$html .= '<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . htmlspecialchars($categoryName) . '</td>';
$html .= '<td style="padding: 8px; text-align: right; border-bottom: 1px solid #dee2e6;">' . $categoryData->metricsCount . '</td>';
$html .= '<td style="padding: 8px; text-align: right; border-bottom: 1px solid #dee2e6;">' . round($categoryData->totalTime->toMilliseconds(), 2) . '</td>';
$html .= '<td style="padding: 8px; text-align: right; border-bottom: 1px solid #dee2e6;">' . $categoryData->totalCalls . '</td>';
$html .= '<td style="padding: 8px; text-align: right; border-bottom: 1px solid #dee2e6;">' . round($categoryData->averageTime->toMilliseconds(), 2) . '</td>';
$html .= '</tr>';
}
$html .= '</tbody></table></div>';
}
// Top metrics by time
$topMetrics = $report->getTopMetricsByTime(10);
if (! empty($topMetrics)) {
$html .= '<div style="background: white; padding: 10px; margin-bottom: 15px; border-radius: 3px; border-left: 4px solid #ffc107;">';
$html .= '<h4 style="margin: 0 0 10px 0;">⏱️ Slowest Operations</h4>';
$html .= '<table style="width: 100%; border-collapse: collapse;">';
$html .= '<thead><tr style="background: #f8f9fa;">';
$html .= '<th style="padding: 8px; text-align: left; border-bottom: 1px solid #dee2e6;">Metric</th>';
$html .= '<th style="padding: 8px; text-align: left; border-bottom: 1px solid #dee2e6;">Category</th>';
$html .= '<th style="padding: 8px; text-align: right; border-bottom: 1px solid #dee2e6;">Total Time (ms)</th>';
$html .= '<th style="padding: 8px; text-align: right; border-bottom: 1px solid #dee2e6;">Avg Time (ms)</th>';
$html .= '<th style="padding: 8px; text-align: right; border-bottom: 1px solid #dee2e6;">Count</th>';
$html .= '</tr></thead><tbody>';
foreach ($topMetrics as $metricData) {
$measurements = $metricData['measurements'];
$html .= '<tr>';
$html .= '<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . htmlspecialchars($metricData['key']) . '</td>';
$html .= '<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">' . htmlspecialchars($metricData['category']) . '</td>';
$html .= '<td style="padding: 8px; text-align: right; border-bottom: 1px solid #dee2e6;">' . $measurements['total_duration_ms'] . '</td>';
$html .= '<td style="padding: 8px; text-align: right; border-bottom: 1px solid #dee2e6;">' . $measurements['avg_duration_ms'] . '</td>';
$html .= '<td style="padding: 8px; text-align: right; border-bottom: 1px solid #dee2e6;">' . $metricData['count'] . '</td>';
$html .= '</tr>';
}
$html .= '</tbody></table></div>';
}
$html .= '</div>';
return $html;
}
private function generateTextReport(PerformanceReport $report): string
{
$text = "=== Performance Report ===\n";
$text .= "Generated at: {$report->timestamp->format('Y-m-d H:i:s')}\n\n";
// Summary
$summary = $report->summary;
$memorySummary = $summary->memorySummary;
$text .= "=== Summary ===\n";
$text .= sprintf("Request Time: %s\n", $summary->getFormattedRequestTime());
$text .= sprintf("Memory Usage: %s\n", $memorySummary->getCurrentHumanReadable());
$text .= sprintf("Peak Memory: %s\n", $memorySummary->getPeakHumanReadable());
$text .= sprintf("Memory Limit: %s\n", $memorySummary->getLimitHumanReadable());
$text .= sprintf("Memory Available: %s\n", $memorySummary->getAvailableMemory()->toHumanReadable());
$text .= sprintf("Memory Usage: %s\n", $memorySummary->getUsagePercentageFormatted());
$text .= sprintf("Metrics Count: %d\n\n", $summary->metricsCount);
// Categories
if (! empty($report->categories)) {
$text .= "=== Categories ===\n";
$text .= sprintf("%-20s %10s %15s %12s %15s\n", "Category", "Metrics", "Total Time(ms)", "Total Calls", "Avg Time(ms)");
$text .= str_repeat("-", 75) . "\n";
foreach ($report->categories as $categoryName => $categoryData) {
$text .= sprintf(
"%-20s %10d %15.2f %12d %15.2f\n",
$categoryName,
$categoryData->metricsCount,
$categoryData->totalTime->toMilliseconds(),
$categoryData->totalCalls,
$categoryData->averageTime->toMilliseconds()
);
}
$text .= "\n";
}
// Top metrics
$topMetrics = $report->getTopMetricsByTime(5);
if (! empty($topMetrics)) {
$text .= "=== Slowest Operations ===\n";
foreach ($topMetrics as $metricData) {
$measurements = $metricData['measurements'];
$text .= sprintf(
"%s (%s): %.2f ms total, %.2f ms avg, %d calls\n",
$metricData['key'],
$metricData['category'],
$measurements['total_duration_ms'],
$measurements['avg_duration_ms'],
$metricData['count']
);
}
}
return $text;
}
public function getSummary(): array
{
$report = $this->collectReportData();
return $report->summary->toArray();
}
public function getTopMetricsByTime(int $limit = 10): array
{
$report = $this->collectReportData();
return $report->getTopMetricsByTime($limit);
}
public function getTopMetricsByMemory(int $limit = 10): array
{
$report = $this->collectReportData();
return $report->getTopMetricsByMemory($limit);
}
public function getMetricsByCategory(): array
{
$report = $this->collectReportData();
$result = [];
foreach ($report->categories as $name => $category) {
$result[$name] = $category->toArray();
}
return $result;
}
public function exportToFile(FilePath $filepath, string $format = 'json'): void
{
$report = $this->generateReport($format);
if (is_array($report)) {
$report = json_encode($report, JSON_PRETTY_PRINT);
}
if (! is_string($report)) {
throw new RuntimeException('Report must be a string for file export');
}
$directory = $filepath->getDirectory();
if (! $directory->exists() && ! mkdir($directory->toString(), 0755, true)) {
throw new RuntimeException('Failed to create directory: ' . $directory);
}
if (file_put_contents($filepath->toString(), $report) === false) {
throw new RuntimeException('Failed to write report to file: ' . $filepath);
}
}
}

View File

@@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Performance\Contracts\PerformanceReporterInterface;
use App\Framework\Performance\Contracts\PerformanceServiceInterface;
final readonly class PerformanceService implements PerformanceServiceInterface
{
public function __construct(
private PerformanceCollectorInterface $collector,
private PerformanceConfig $config,
private ?PerformanceReporterInterface $reporter = null
) {
}
/**
* Quick method to measure a function's execution
*/
public function measure(string $key, callable $callback, PerformanceCategory $category = PerformanceCategory::CUSTOM, array $context = []): mixed
{
return $this->collector->measure($key, $category, $callback, $context);
}
/**
* Start timing an operation
*/
public function startTiming(string $key, PerformanceCategory $category = PerformanceCategory::CUSTOM, array $context = []): void
{
$this->collector->startTiming($key, $category, $context);
}
/**
* End timing an operation
*/
public function endTiming(string $key): void
{
$this->collector->endTiming($key);
}
/**
* Record a metric value
*/
public function recordMetric(string $key, float $value, PerformanceCategory $category = PerformanceCategory::CUSTOM, array $context = []): void
{
$this->collector->recordMetric($key, $category, $value, $context);
}
/**
* Increment a counter
*/
public function increment(string $key, int $amount = 1, PerformanceCategory $category = PerformanceCategory::CUSTOM, array $context = []): void
{
$this->collector->increment($key, $category, $amount, $context);
}
/**
* Get all metrics for a category
*/
public function getMetrics(?PerformanceCategory $category = null): array
{
return $this->collector->getMetrics($category);
}
/**
* Get a specific metric
*/
public function getMetric(string $key): ?PerformanceMetric
{
return $this->collector->getMetric($key);
}
/**
* Generate a performance report
*/
public function generateReport(string $format = 'array'): string|array
{
if ($this->reporter === null) {
return $format === 'array' ? [] : '';
}
return $this->reporter->generateReport($format);
}
/**
* Get current request statistics
*/
public function getRequestStats(): array
{
return [
'time_ms' => $this->collector->getTotalRequestTime(),
'memory_bytes' => $this->collector->getTotalRequestMemory(),
'peak_memory_bytes' => $this->collector->getPeakMemory(),
'metrics_count' => count($this->collector->getMetrics()),
];
}
/**
* Reset all metrics
*/
public function reset(): void
{
$this->collector->reset();
}
/**
* Check if performance tracking is enabled
*/
public function isEnabled(): bool
{
return $this->collector->isEnabled();
}
/**
* Enable or disable performance tracking
*/
public function setEnabled(bool $enabled): void
{
$this->collector->setEnabled($enabled);
}
/**
* Get configuration
*/
public function getConfig(): PerformanceConfig
{
return $this->config;
}
/**
* Convenience method for database query timing
*/
public function measureDatabaseQuery(string $queryType, callable $callback, array $context = []): mixed
{
return $this->measure("db_query_{$queryType}", $callback, PerformanceCategory::DATABASE, $context);
}
/**
* Convenience method for cache operation timing
*/
public function measureCacheOperation(string $operation, callable $callback, array $context = []): mixed
{
return $this->measure("cache_{$operation}", $callback, PerformanceCategory::CACHE, $context);
}
/**
* Convenience method for view rendering timing
*/
public function measureViewRender(string $view, callable $callback, array $context = []): mixed
{
return $this->measure("view_render_{$view}", $callback, PerformanceCategory::VIEW, $context);
}
/**
* Get performance summary as array
*/
public function getSummary(): array
{
$report = $this->generateReport('array');
return $report['summary'] ?? [];
}
/**
* Get slowest operations
*/
public function getSlowestOperations(int $limit = 10): array
{
$report = $this->generateReport('array');
$metrics = $report['metrics'] ?? [];
$metricsWithTime = array_filter($metrics, function ($metric) {
return $metric['measurements']['total_duration_ms'] > 0;
});
usort($metricsWithTime, function ($a, $b) {
return $b['measurements']['total_duration_ms'] <=> $a['measurements']['total_duration_ms'];
});
return array_slice($metricsWithTime, 0, $limit);
}
/**
* Export metrics to array for external processing
*/
public function exportMetrics(): array
{
$metrics = [];
foreach ($this->collector->getMetrics() as $metric) {
$metrics[] = $metric->toArray();
}
return $metrics;
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance;
use App\Framework\DI\Container;
use App\Framework\DI\Initializer;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Performance\Contracts\PerformanceReporterInterface;
use App\Framework\Performance\Contracts\PerformanceServiceInterface;
final readonly class PerformanceServiceInitializer
{
public function __construct(
private Container $container
) {
}
#[Initializer]
public function __invoke(): PerformanceServiceInterface
{
// Get the existing collector instance from container (registered in entry points)
$collector = $this->container->get(PerformanceCollectorInterface::class);
$config = new PerformanceConfig(
enabled: true,
useEnhancedCollector: true,
thresholds: [
'slow_query_ms' => 100,
'slow_request_ms' => 1000,
'high_memory_mb' => 50,
]
);
$reporter = new PerformanceReporter($collector);
// Register the reporter interface binding BEFORE creating PerformanceService
$this->container->singleton(PerformanceReporterInterface::class, $reporter);
$performanceService = new PerformanceService($collector, $config, $reporter);
// Register the concrete class for direct access
$this->container->singleton(PerformanceService::class, $performanceService);
return $performanceService;
}
}

View File

@@ -1,14 +1,370 @@
# Performance Monitoring Framework
# Performance Monitoring Module
Ein umfassendes Performance-Monitoring-System für PHP-Anwendungen mit File-Logging, Redis-Metriken und Database-Persistierung.
## Overview
The Performance Module provides comprehensive performance monitoring and profiling capabilities for the application. It uses a modular middleware-based architecture that allows fine-grained tracking of different application components.
## Architecture
### Core Components
- **PerformanceCollector** - Central collection point for all performance metrics
- **PerformanceMetric** - Individual metric with timing, memory, and statistical data
- **PerformanceConfig** - Configuration for enabling/disabling tracking per category
- **PerformanceReporter** - Generates reports in multiple formats (HTML, JSON, Text)
- **PerformanceService** - Simplified API for application developers
### Middleware Components
The module provides specialized middleware for different application layers:
#### HTTP Layer
- `RequestPerformanceMiddleware` - Tracks overall request/response performance
- `ControllerPerformanceMiddleware` - Monitors controller execution
- `RoutingPerformanceMiddleware` - Measures route resolution time
- `PerformanceDebugMiddleware` - Injects debug reports into HTML responses
#### Data Layer
- `DatabasePerformanceMiddleware` - Tracks database queries and slow query detection
- `CachePerformanceMiddleware` - Monitors cache operations and hit/miss ratios
## Features
- 🚀 **Minimaler Overhead** - Optimiert für Production-Einsatz
- 📊 **Live-Metriken** - Real-time Monitoring via Redis
- 💾 **Asynchrone Persistierung** - Background-Jobs für Database-Storage
- 🔍 **Detaillierte Berichte** - Kategorisierte Performance-Analyse
- 🛠️ **Flexible Integration** - Middleware, Event Handler, Manual Usage
- 📈 **Skalierbar** - Buffer-System und Batch-Processing
### Metrics Tracked
## Architektur
1. **Request Metrics**
- Total request time
- Memory usage (before/after/peak)
- Request success/failure counts
2. **Controller Metrics**
- Controller execution time
- Action-specific performance
- Controller invocation counts
3. **Database Metrics**
- Query execution time
- Query type distribution (SELECT, INSERT, UPDATE, etc.)
- Slow query detection
- Query success/failure rates
4. **Cache Metrics**
- Cache hit/miss ratios
- Operation performance (get, set, delete)
- Data size tracking
5. **Routing Metrics**
- Route resolution time
- Route pattern matching
- 404 detection
### Performance Categories
The system categorizes metrics into logical groups:
- `SYSTEM` - Overall system and request metrics
- `DATABASE` - Database-related operations
- `CACHE` - Caching operations
- `VIEW` - Template rendering
- `TEMPLATE` - Template processing
- `ROUTING` - Route resolution
- `CONTROLLER` - Controller execution
- `API` - API-specific metrics
- `FILESYSTEM` - File operations
- `CUSTOM` - User-defined metrics
## Configuration
### Basic Configuration
```php
$config = new PerformanceConfig(
enabled: true,
httpTracking: true,
databaseTracking: true,
cacheTracking: true,
slowQueryThreshold: 100.0, // ms
detailedReports: false,
excludedPaths: ['/health', '/metrics']
);
```
### Environment-based Configuration
```php
$config = new PerformanceConfig(
enabled: $_ENV['APP_DEBUG'] ?? false,
slowQueryThreshold: (float) ($_ENV['PERFORMANCE_SLOW_QUERY_THRESHOLD'] ?? 100.0),
detailedReports: $_ENV['PERFORMANCE_DETAILED_REPORTS'] === 'true'
);
```
## Usage
### Basic Usage with PerformanceService
```php
// Inject the service
public function __construct(
private PerformanceService $performance
) {}
// Measure a function
$result = $this->performance->measure('user_lookup', function() {
return $this->userRepository->findById($id);
}, PerformanceCategory::DATABASE);
// Manual timing
$this->performance->startTiming('complex_calculation');
$result = $this->performComplexCalculation();
$this->performance->endTiming('complex_calculation');
// Record metrics
$this->performance->recordMetric('cache_size', $size, PerformanceCategory::CACHE);
$this->performance->increment('api_calls', 1, PerformanceCategory::API);
```
### Convenience Methods
```php
// Database queries
$users = $this->performance->measureDatabaseQuery('user_select', function() {
return $this->db->select('SELECT * FROM users');
});
// Cache operations
$data = $this->performance->measureCacheOperation('get', function() {
return $this->cache->get('user_data');
});
// View rendering
$html = $this->performance->measureViewRender('user_profile', function() {
return $this->templateEngine->render('user/profile.html');
});
```
### Getting Performance Data
```php
// Get current request stats
$stats = $this->performance->getRequestStats();
// Returns: ['time_ms' => 150.5, 'memory_bytes' => 2048, ...]
// Get slowest operations
$slowest = $this->performance->getSlowestOperations(5);
// Generate reports
$htmlReport = $this->performance->generateReport('html');
$jsonReport = $this->performance->generateReport('json');
$textReport = $this->performance->generateReport('text');
```
## Middleware Integration
### HTTP Middleware Setup
Add the middleware to your HTTP middleware stack in order of priority:
```php
// High priority - should run first
$middlewareStack->add(RequestPerformanceMiddleware::class);
// After routing
$middlewareStack->add(RoutingPerformanceMiddleware::class);
// Before controller execution
$middlewareStack->add(ControllerPerformanceMiddleware::class);
// Low priority - should run last for debug output
$middlewareStack->add(PerformanceDebugMiddleware::class);
```
### Database Middleware Setup
```php
// Register with your database layer
$databaseLayer->addMiddleware(DatabasePerformanceMiddleware::class);
```
### Cache Middleware Setup
```php
// Register with your cache layer
$cacheLayer->addMiddleware(CachePerformanceMiddleware::class);
```
## Report Formats
### HTML Reports
HTML reports provide a comprehensive, visually appealing overview with:
- Summary statistics
- Category breakdown
- Slowest operations table
- Interactive toggle for debugging
### JSON Reports
Perfect for APIs and external monitoring tools:
```json
{
"timestamp": "2024-01-15 10:30:00",
"summary": {
"total_request_time_ms": 245.67,
"total_request_memory_bytes": 4096,
"peak_memory_bytes": 8192,
"metrics_count": 15
},
"categories": {
"database": {
"metrics_count": 3,
"total_time_ms": 89.45,
"total_calls": 12
}
},
"metrics": {
"http_request": {
"category": "system",
"measurements": {
"total_duration_ms": 245.67,
"avg_duration_ms": 245.67
}
}
}
}
```
### Text Reports
Console-friendly format for logs and CLI tools.
## Debug Features
### Debug Middleware
The `PerformanceDebugMiddleware` automatically injects performance reports into HTML responses when debug mode is enabled. It adds:
- A floating "Performance" button
- Detailed performance overlay
- Automatic insertion before `</body>` tag
- Performance headers for AJAX requests
### Performance Headers
When not injecting HTML reports, the system adds HTTP headers:
```
X-Performance-Time: 245.67 ms
X-Performance-Memory: 4.0 KB
X-Performance-Peak-Memory: 8.0 KB
X-Performance-Database: 89.45 ms (12 calls)
```
## Performance Considerations
### Overhead
The performance monitoring system is designed to have minimal overhead:
- Disabled tracking adds virtually no performance cost
- Metric collection uses efficient data structures
- Report generation only occurs when requested
### Memory Usage
- Metrics are stored in memory during request lifecycle
- Automatic cleanup after request completion
- Configurable limits for metrics per category
### Production Usage
For production environments:
```php
$config = new PerformanceConfig(
enabled: false, // Disable in production
// Or enable only critical tracking
enabled: true,
httpTracking: true,
databaseTracking: true,
cacheTracking: false,
detailedReports: false,
excludedPaths: ['/health', '/metrics', '/api/status']
);
```
## Examples
### Custom Metrics
```php
class UserService
{
public function __construct(
private PerformanceService $performance
) {}
public function processUserData(array $userData): User
{
return $this->performance->measure('user_processing', function() use ($userData) {
// Validate data
$this->performance->startTiming('user_validation');
$this->validateUserData($userData);
$this->performance->endTiming('user_validation');
// Transform data
$transformedData = $this->performance->measure('user_transformation',
fn() => $this->transformUserData($userData),
PerformanceCategory::CUSTOM
);
// Save to database
return $this->performance->measureDatabaseQuery('user_insert',
fn() => $this->userRepository->create($transformedData)
);
}, PerformanceCategory::CUSTOM);
}
}
```
### Conditional Tracking
```php
public function expensiveOperation(): Result
{
if ($this->performance->isEnabled()) {
return $this->performance->measure('expensive_op',
fn() => $this->doExpensiveOperation(),
PerformanceCategory::CUSTOM
);
}
return $this->doExpensiveOperation();
}
```
## Troubleshooting
### Common Issues
1. **Missing metrics**: Ensure middleware is properly registered
2. **Memory issues**: Check `maxMetricsPerCategory` configuration
3. **Slow performance**: Disable detailed reports in production
4. **Missing HTML reports**: Verify `PerformanceDebugMiddleware` is last in chain
### Debug Information
```php
// Check if tracking is enabled
if (!$performance->isEnabled()) {
echo "Performance tracking is disabled\n";
}
// Get current configuration
$config = $performance->getConfig();
echo "Database tracking: " . ($config->databaseTracking ? 'enabled' : 'disabled') . "\n";
// Check collected metrics
$metrics = $performance->getMetrics();
echo "Collected " . count($metrics) . " metrics\n";
```

View File

@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\ValueObjects;
use App\Framework\Core\ValueObjects\Duration;
/**
* Value Object representing metrics for a specific category
*/
final readonly class CategoryMetrics
{
/**
* @param int $metricsCount Number of metrics in this category
* @param Duration $totalTime Total execution time for all metrics
* @param int $totalCalls Total number of calls across all metrics
* @param Duration $averageTime Average time per call
* @param array<array<string, mixed>> $metrics Individual metric details
*/
public function __construct(
public int $metricsCount,
public Duration $totalTime,
public int $totalCalls,
public Duration $averageTime,
public array $metrics
) {
}
/**
* Create from raw values
* @param array<\App\Framework\Performance\PerformanceMetric> $metrics Array of PerformanceMetric objects
*/
public static function fromMetrics(array $metrics): self
{
$metricsCount = count($metrics);
$totalTimeMs = 0.0;
$totalCalls = 0;
$metricsArray = [];
foreach ($metrics as $metric) {
$metricData = $metric->toArray();
$metricsArray[] = $metricData;
$totalTimeMs += $metricData['measurements']['total_duration_ms'];
$totalCalls += $metric->getCount();
}
$averageTimeMs = $totalCalls > 0 ? $totalTimeMs / $totalCalls : 0.0;
return new self(
metricsCount: $metricsCount,
totalTime: Duration::fromMilliseconds($totalTimeMs),
totalCalls: (int) $totalCalls,
averageTime: Duration::fromMilliseconds($averageTimeMs),
metrics: $metricsArray
);
}
/**
* Convert to array for backward compatibility
* @return array{
* metrics_count: int,
* total_time_ms: float,
* total_calls: int,
* avg_time_ms: float,
* metrics: array<array<string, mixed>>
* }
*/
public function toArray(): array
{
return [
'metrics_count' => $this->metricsCount,
'total_time_ms' => round($this->totalTime->toMilliseconds(), 2),
'total_calls' => $this->totalCalls,
'avg_time_ms' => round($this->averageTime->toMilliseconds(), 2),
'metrics' => $this->metrics,
];
}
/**
* Get formatted total time
*/
public function getFormattedTotalTime(): string
{
return sprintf('%.2f ms', $this->totalTime->toMilliseconds());
}
/**
* Get formatted average time
*/
public function getFormattedAverageTime(): string
{
return sprintf('%.2f ms', $this->averageTime->toMilliseconds());
}
/**
* Check if this category has any metrics
*/
public function hasMetrics(): bool
{
return $this->metricsCount > 0;
}
/**
* Check if this category has any calls
*/
public function hasCalls(): bool
{
return $this->totalCalls > 0;
}
/**
* Get average calls per metric
*/
public function getAverageCallsPerMetric(): float
{
return $this->metricsCount > 0 ? $this->totalCalls / $this->metricsCount : 0.0;
}
/**
* Check if performance is acceptable for this category
*/
public function isPerformanceAcceptable(Duration $maxAverageTime, Duration $maxTotalTime): bool
{
return $this->averageTime->toMilliseconds() <= $maxAverageTime->toMilliseconds()
&& $this->totalTime->toMilliseconds() <= $maxTotalTime->toMilliseconds();
}
/**
* Get the slowest metric in this category
* @return array<string, mixed>|null
*/
public function getSlowestMetric(): ?array
{
if (empty($this->metrics)) {
return null;
}
$slowest = null;
$maxTime = 0.0;
foreach ($this->metrics as $metric) {
$totalTime = $metric['measurements']['total_duration_ms'] ?? 0.0;
if ($totalTime > $maxTime) {
$maxTime = $totalTime;
$slowest = $metric;
}
}
return $slowest;
}
/**
* Get metrics sorted by total time (descending)
* @return array<array<string, mixed>>
*/
public function getMetricsSortedByTime(): array
{
$metrics = $this->metrics;
usort($metrics, function ($a, $b) {
$aTime = $a['measurements']['total_duration_ms'] ?? 0.0;
$bTime = $b['measurements']['total_duration_ms'] ?? 0.0;
return $bTime <=> $aTime;
});
return $metrics;
}
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\ValueObjects;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\HighResolutionClock;
use App\Framework\Performance\MemoryMonitor;
/**
* Immutable value object representing a single performance measurement
*/
final readonly class Measurement
{
public function __construct(
private Duration $duration,
private Byte $memory,
private Timestamp $timestamp
) {
}
public static function create(Duration $duration, Byte $memory, Timestamp $timestamp): self
{
return new self($duration, $memory, $timestamp);
}
public static function fromMicrotime(
float $startTime,
float $endTime,
int $startMemory,
int $endMemory,
?Timestamp $timestamp = null
): self {
$duration = Duration::fromSeconds($endTime - $startTime);
$memoryDiff = $endMemory - $startMemory;
$memory = Byte::fromBytes((int) max(0, $memoryDiff));
$timestamp = $timestamp ?? Timestamp::fromFloat($endTime);
return new self($duration, $memory, $timestamp);
}
public static function startTiming(Clock $clock, MemoryMonitor $memoryMonitor): array
{
return [
'start_time' => $clock->time(),
'start_memory' => $memoryMonitor->getCurrentMemory()->toBytes(),
];
}
public static function endTiming(array $startData, Clock $clock, MemoryMonitor $memoryMonitor): self
{
$endTime = $clock->time();
$endMemory = $memoryMonitor->getCurrentMemory()->toBytes();
$duration = $endTime->diff($startData['start_time']);
$memoryDiff = $endMemory - $startData['start_memory'];
$memory = Byte::fromBytes((int) max(0, $memoryDiff));
return new self($duration, $memory, $endTime);
}
/**
* Create measurement from high-resolution nanosecond timing data
*/
public static function fromNanoseconds(
int $startTimeNanos,
int $endTimeNanos,
int $startMemory,
int $endMemory,
Timestamp $timestamp
): self {
$durationNanos = $endTimeNanos - $startTimeNanos;
$duration = Duration::fromNanoseconds($durationNanos);
$memoryDiff = $endMemory - $startMemory;
$memory = Byte::fromBytes((int) max(0, $memoryDiff));
return new self($duration, $memory, $timestamp);
}
/**
* End timing with high-resolution clock for nanosecond precision
*/
public static function endHighResTiming(array $startData, HighResolutionClock $highResClock, MemoryMonitor $memoryMonitor, Clock $clock): self
{
$endTime = $highResClock->hrtime();
$endMemory = $memoryMonitor->getCurrentMemory()->toBytes();
$duration = $endTime->subtract($startData['start_time']);
$memoryDiff = $endMemory - $startData['start_memory'];
$memory = Byte::fromBytes((int) max(0, $memoryDiff));
return new self($duration, $memory, $clock->time());
}
public function getDuration(): Duration
{
return $this->duration;
}
public function getMemory(): Byte
{
return $this->memory;
}
public function getTimestamp(): Timestamp
{
return $this->timestamp;
}
public function toArray(): array
{
return [
'duration' => $this->duration->toMilliseconds(),
'memory' => $this->memory->toBytes(),
'timestamp' => $this->timestamp->toFloat(),
];
}
public function equals(self $other): bool
{
return $this->duration->equals($other->duration)
&& $this->memory->equals($other->memory)
&& $this->timestamp->equals($other->timestamp);
}
}

View File

@@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\ValueObjects;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use Countable;
use InvalidArgumentException;
use IteratorAggregate;
use Traversable;
/**
* Type-safe collection of performance measurements
*/
final class MeasurementCollection implements Countable, IteratorAggregate
{
/** @var array<int, Measurement> */
private array $measurements = [];
public function __construct(array $measurements = [])
{
foreach ($measurements as $measurement) {
$this->add($measurement);
}
}
public function add(Measurement $measurement): void
{
$this->measurements[] = $measurement;
}
public function count(): int
{
return count($this->measurements);
}
public function isEmpty(): bool
{
return empty($this->measurements);
}
public function getIterator(): Traversable
{
yield from $this->measurements;
}
/**
* @return array<int, Measurement>
*/
public function toArray(): array
{
return $this->measurements;
}
public function getTotalDuration(): Duration
{
if ($this->isEmpty()) {
return Duration::zero();
}
$total = Duration::zero();
foreach ($this->measurements as $measurement) {
$total = $total->add($measurement->getDuration());
}
return $total;
}
public function getAverageDuration(): Duration
{
if ($this->isEmpty()) {
return Duration::zero();
}
return Duration::fromSeconds(
$this->getTotalDuration()->toSeconds() / $this->count()
);
}
public function getMinDuration(): ?Duration
{
if ($this->isEmpty()) {
return null;
}
$min = null;
foreach ($this->measurements as $measurement) {
$duration = $measurement->getDuration();
if ($min === null || $duration->lessThan($min)) {
$min = $duration;
}
}
return $min;
}
public function getMaxDuration(): ?Duration
{
if ($this->isEmpty()) {
return null;
}
$max = null;
foreach ($this->measurements as $measurement) {
$duration = $measurement->getDuration();
if ($max === null || $duration->greaterThan($max)) {
$max = $duration;
}
}
return $max;
}
public function getTotalMemory(): Byte
{
if ($this->isEmpty()) {
return Byte::zero();
}
$total = Byte::zero();
foreach ($this->measurements as $measurement) {
$total = $total->add($measurement->getMemory());
}
return $total;
}
public function getAverageMemory(): Byte
{
if ($this->isEmpty()) {
return Byte::zero();
}
return Byte::fromBytes(
(int) round($this->getTotalMemory()->toBytes() / $this->count())
);
}
public function getMinMemory(): ?Byte
{
if ($this->isEmpty()) {
return null;
}
$min = null;
foreach ($this->measurements as $measurement) {
$memory = $measurement->getMemory();
if ($min === null || $memory->lessThan($min)) {
$min = $memory;
}
}
return $min;
}
public function getMaxMemory(): ?Byte
{
if ($this->isEmpty()) {
return null;
}
$max = null;
foreach ($this->measurements as $measurement) {
$memory = $measurement->getMemory();
if ($max === null || $memory->greaterThan($max)) {
$max = $memory;
}
}
return $max;
}
public function getFirst(): ?Measurement
{
return $this->measurements[0] ?? null;
}
public function getLast(): ?Measurement
{
$count = $this->count();
return $count > 0 ? $this->measurements[$count - 1] : null;
}
/**
* Filter measurements by minimum duration
*/
public function filterByMinDuration(Duration $minDuration): self
{
$filtered = array_filter(
$this->measurements,
fn (Measurement $m) => $m->getDuration()->greaterThan($minDuration) || $m->getDuration()->equals($minDuration)
);
return new self($filtered);
}
/**
* Sort measurements by duration (slowest first)
*/
public function sortByDuration(bool $ascending = false): self
{
$sorted = $this->measurements;
usort($sorted, function (Measurement $a, Measurement $b) use ($ascending) {
$result = $a->getDuration()->greaterThan($b->getDuration()) ? 1 : -1;
return $ascending ? -$result : $result;
});
return new self($sorted);
}
/**
* Get top N measurements by duration
*/
public function getTopByDuration(int $limit): self
{
if ($limit <= 0) {
throw new InvalidArgumentException('Limit must be positive');
}
$sorted = $this->sortByDuration();
return new self(array_slice($sorted->toArray(), 0, $limit));
}
/**
* Export to array format
*/
public function export(): array
{
return [
'count' => $this->count(),
'total_duration_ms' => $this->getTotalDuration()->toMilliseconds(),
'avg_duration_ms' => $this->getAverageDuration()->toMilliseconds(),
'min_duration_ms' => $this->getMinDuration()?->toMilliseconds() ?? 0,
'max_duration_ms' => $this->getMaxDuration()?->toMilliseconds() ?? 0,
'total_memory_bytes' => $this->getTotalMemory()->toBytes(),
'avg_memory_bytes' => $this->getAverageMemory()->toBytes(),
'min_memory_bytes' => $this->getMinMemory()?->toBytes() ?? 0,
'max_memory_bytes' => $this->getMaxMemory()?->toBytes() ?? 0,
'measurements' => array_map(fn (Measurement $m) => $m->toArray(), $this->measurements),
];
}
}

View File

@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\ValueObjects;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Percentage;
/**
* Value Object für Memory-Zusammenfassung
*/
final readonly class MemorySummary
{
public function __construct(
public Byte $current,
public Byte $peak,
public Byte $limit,
public Percentage $usagePercentage,
public bool $isApproachingLimit
) {
}
/**
* Gibt die aktuelle Speichernutzung in Bytes zurück
*/
public function getCurrentBytes(): int
{
return $this->current->toBytes();
}
/**
* Gibt die Peak-Speichernutzung in Bytes zurück
*/
public function getPeakBytes(): int
{
return $this->peak->toBytes();
}
/**
* Gibt das Speicherlimit in Bytes zurück
*/
public function getLimitBytes(): int
{
return $this->limit->toBytes();
}
/**
* Gibt die aktuelle Speichernutzung human-readable zurück
*/
public function getCurrentHumanReadable(): string
{
return $this->current->toHumanReadable();
}
/**
* Gibt die Peak-Speichernutzung human-readable zurück
*/
public function getPeakHumanReadable(): string
{
return $this->peak->toHumanReadable();
}
/**
* Gibt das Speicherlimit human-readable zurück
*/
public function getLimitHumanReadable(): string
{
return $this->limit->toHumanReadable();
}
/**
* Gibt den Usage-Prozentsatz formatiert zurück
*/
public function getUsagePercentageFormatted(int $decimals = 2): string
{
return $this->usagePercentage->format($decimals);
}
/**
* Prüft ob der Speicher knapp wird (Standard: 80%)
*/
public function isMemoryLow(float $threshold = 80.0): bool
{
return $this->usagePercentage->greaterThanOrEqual(Percentage::from($threshold));
}
/**
* Prüft ob der Speicher kritisch knapp wird (Standard: 90%)
*/
public function isMemoryCritical(float $threshold = 90.0): bool
{
return $this->usagePercentage->greaterThanOrEqual(Percentage::from($threshold));
}
/**
* Gibt die verfügbare Speichermenge zurück
*/
public function getAvailableMemory(): Byte
{
$availableBytes = $this->limit->toBytes() - $this->current->toBytes();
return Byte::fromBytes(max(0, $availableBytes));
}
/**
* Konvertiert zu Array (für Backward Compatibility)
*/
public function toArray(): array
{
return [
'current' => [
'bytes' => $this->getCurrentBytes(),
'human' => $this->getCurrentHumanReadable(),
],
'peak' => [
'bytes' => $this->getPeakBytes(),
'human' => $this->getPeakHumanReadable(),
],
'limit' => [
'bytes' => $this->getLimitBytes(),
'human' => $this->getLimitHumanReadable(),
],
'available' => [
'bytes' => $this->getAvailableMemory()->toBytes(),
'human' => $this->getAvailableMemory()->toHumanReadable(),
],
'usage_percentage' => $this->getUsagePercentageFormatted(),
'is_approaching_limit' => $this->isApproachingLimit,
'is_memory_low' => $this->isMemoryLow(),
'is_memory_critical' => $this->isMemoryCritical(),
];
}
/**
* String-Repräsentation für Debug-Zwecke
*/
public function __toString(): string
{
return sprintf(
'Memory: %s / %s (%s) | Peak: %s | Available: %s',
$this->getCurrentHumanReadable(),
$this->getLimitHumanReadable(),
$this->getUsagePercentageFormatted(),
$this->getPeakHumanReadable(),
$this->getAvailableMemory()->toHumanReadable()
);
}
/**
* JSON-Serialisierung
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
}

View File

@@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\ValueObjects;
use InvalidArgumentException;
/**
* Immutable value object for metric context/metadata
*/
final readonly class MetricContext
{
/** @var array<string, mixed> */
private array $data;
public function __construct(array $data = [])
{
$this->validateData($data);
$this->data = $data;
}
private function validateData(array $data): void
{
foreach ($data as $key => $value) {
if (! is_string($key)) {
throw new InvalidArgumentException('Context keys must be strings');
}
if (! $this->isValidValue($value)) {
throw new InvalidArgumentException(
sprintf('Invalid context value type for key "%s"', $key)
);
}
}
}
private function isValidValue(mixed $value): bool
{
// Allow scalar values and null
if (is_scalar($value) || is_null($value)) {
return true;
}
// Allow objects that can be converted to string (have __toString method)
if (is_object($value)) {
return method_exists($value, '__toString') ||
$value instanceof \Stringable ||
$value instanceof \BackedEnum ||
$value instanceof \JsonSerializable;
}
// For arrays, check if it's a simple list of scalar values
if (is_array($value)) {
// Empty arrays are valid
if (empty($value)) {
return true;
}
// Check if it's a list (sequential numeric keys starting from 0)
if (array_is_list($value)) {
// All values must be scalar, null, or valid objects
return array_reduce(
$value,
fn (bool $carry, mixed $item) => $carry && $this->isValidValue($item),
true
);
}
// For associative arrays, recursively validate
return array_reduce(
array_keys($value),
fn (bool $carry, mixed $key) => $carry && is_string($key) && $this->isValidValue($value[$key]),
true
);
}
return false;
}
public function with(string $key, mixed $value): self
{
$newData = $this->data;
$newData[$key] = $value;
return new self($newData);
}
public function without(string $key): self
{
$newData = $this->data;
unset($newData[$key]);
return new self($newData);
}
public function merge(self $other): self
{
return new self(array_merge($this->data, $other->data));
}
public function get(string $key, mixed $default = null): mixed
{
return $this->data[$key] ?? $default;
}
public function has(string $key): bool
{
return array_key_exists($key, $this->data);
}
public function isEmpty(): bool
{
return empty($this->data);
}
public function toArray(): array
{
return $this->normalizeData($this->data);
}
private function normalizeData(array $data): array
{
$normalized = [];
foreach ($data as $key => $value) {
$normalized[$key] = $this->normalizeValue($value);
}
return $normalized;
}
private function normalizeValue(mixed $value): mixed
{
if (is_scalar($value) || is_null($value)) {
return $value;
}
if (is_object($value)) {
if ($value instanceof \JsonSerializable) {
return $value->jsonSerialize();
}
if ($value instanceof \BackedEnum) {
return $value->value;
}
if (method_exists($value, '__toString') || $value instanceof \Stringable) {
return (string) $value;
}
}
if (is_array($value)) {
return array_map([$this, 'normalizeValue'], $value);
}
return (string) $value;
}
public function equals(self $other): bool
{
return $this->data === $other->data;
}
// Common context factory methods
public static function empty(): self
{
return new self([]);
}
public static function fromRequest(\App\Framework\Http\Method $method, string $path, ?string $userId = null): self
{
$data = [
'request_method' => $method,
'request_path' => $path,
];
if ($userId !== null) {
$data['user_id'] = $userId;
}
return new self($data);
}
public static function fromDatabase(string $query, string $type, ?string $table = null): self
{
$data = [
'query' => $query,
'query_type' => $type,
];
if ($table !== null) {
$data['table'] = $table;
}
return new self($data);
}
public static function fromCache(string $operation, string $key, ?bool $hit = null): self
{
$data = [
'cache_operation' => $operation,
'cache_key' => $key,
];
if ($hit !== null) {
$data['cache_hit'] = $hit;
}
return new self($data);
}
}

View File

@@ -0,0 +1,297 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\ValueObjects;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Performance\PerformanceCategory;
/**
* Value Object für verschachtelte Performance-Messungen
*/
final readonly class NestedMeasurement
{
/**
* @param NestedMeasurement[] $children
*/
public function __construct(
public string $operationId,
public string $name,
public PerformanceCategory $category,
public Timestamp $startTime,
public ?Timestamp $endTime = null,
public ?Duration $duration = null,
public Byte $startMemory = new Byte(0),
public Byte $endMemory = new Byte(0),
public array $context = [],
public array $children = [],
public ?string $parentId = null,
public int $depth = 0,
public bool $completed = false
) {
}
/**
* Start a new nested measurement
*/
public static function start(
string $operationId,
string $name,
PerformanceCategory $category,
Timestamp $startTime,
Byte $startMemory,
array $context = [],
?string $parentId = null,
int $depth = 0
): self {
return new self(
operationId: $operationId,
name: $name,
category: $category,
startTime: $startTime,
startMemory: $startMemory,
context: $context,
parentId: $parentId,
depth: $depth
);
}
/**
* Complete the measurement
*/
public function complete(
Timestamp $endTime,
Byte $endMemory
): self {
$duration = Duration::fromSeconds(
$endTime->toFloat() - $this->startTime->toFloat()
);
return new self(
operationId: $this->operationId,
name: $this->name,
category: $this->category,
startTime: $this->startTime,
endTime: $endTime,
duration: $duration,
startMemory: $this->startMemory,
endMemory: $endMemory,
context: $this->context,
children: $this->children,
parentId: $this->parentId,
depth: $this->depth,
completed: true
);
}
/**
* Add a child measurement
*/
public function addChild(NestedMeasurement $child): self
{
return new self(
operationId: $this->operationId,
name: $this->name,
category: $this->category,
startTime: $this->startTime,
endTime: $this->endTime,
duration: $this->duration,
startMemory: $this->startMemory,
endMemory: $this->endMemory,
context: $this->context,
children: [...$this->children, $child],
parentId: $this->parentId,
depth: $this->depth,
completed: $this->completed
);
}
/**
* Get total time spent in child operations
*/
public function getChildrenTime(): Duration
{
$totalChildTime = Duration::zero();
foreach ($this->children as $child) {
if ($child->duration) {
$totalChildTime = $totalChildTime->add($child->duration);
}
}
return $totalChildTime;
}
/**
* Get self time (excluding children)
*/
public function getSelfTime(): Duration
{
if (! $this->duration) {
return Duration::zero();
}
$childTime = $this->getChildrenTime();
return $this->duration->subtract($childTime);
}
/**
* Get memory used (delta between start and end)
*/
public function getMemoryUsed(): Byte
{
if ($this->endMemory->toBytes() === 0) {
return Byte::fromBytes(0);
}
$delta = $this->endMemory->toBytes() - $this->startMemory->toBytes();
return Byte::fromBytes(max(0, $delta));
}
/**
* Check if this measurement overlaps with another
*/
public function overlapsWith(NestedMeasurement $other): bool
{
if (! $this->endTime || ! $other->endTime) {
return false;
}
$thisStart = $this->startTime->toFloat();
$thisEnd = $this->endTime->toFloat();
$otherStart = $other->startTime->toFloat();
$otherEnd = $other->endTime->toFloat();
return $thisStart < $otherEnd && $otherStart < $thisEnd;
}
/**
* Get performance percentage relative to total
*/
public function getPercentageOf(Duration $totalTime): float
{
if (! $this->duration || $totalTime->toSeconds() <= 0) {
return 0.0;
}
return ($this->duration->toSeconds() / $totalTime->toSeconds()) * 100;
}
/**
* Get depth string for display (indentation)
*/
public function getDepthString(string $indent = ' '): string
{
return str_repeat($indent, $this->depth);
}
/**
* Convert to hierarchical array
*/
public function toHierarchicalArray(): array
{
$childrenArray = [];
foreach ($this->children as $child) {
$childrenArray[] = $child->toHierarchicalArray();
}
return [
'operation_id' => $this->operationId,
'name' => $this->name,
'category' => $this->category->value,
'start_time' => $this->startTime->format('Y-m-d H:i:s.u'),
'end_time' => $this->endTime?->format('Y-m-d H:i:s.u'),
'duration_ms' => $this->duration?->toMilliseconds(),
'self_time_ms' => $this->getSelfTime()->toMilliseconds(),
'children_time_ms' => $this->getChildrenTime()->toMilliseconds(),
'memory_used_bytes' => $this->getMemoryUsed()->toBytes(),
'memory_used_human' => $this->getMemoryUsed()->toHumanReadable(),
'parent_id' => $this->parentId,
'depth' => $this->depth,
'completed' => $this->completed,
'context' => $this->context,
'children' => $childrenArray,
'children_count' => count($this->children),
];
}
/**
* Convert to flat array (useful for debugging)
*/
public function toArray(): array
{
return [
'operation_id' => $this->operationId,
'name' => $this->name,
'category' => $this->category->value,
'start_time' => $this->startTime->format('Y-m-d H:i:s.u'),
'end_time' => $this->endTime?->format('Y-m-d H:i:s.u'),
'duration_ms' => $this->duration?->toMilliseconds(),
'self_time_ms' => $this->getSelfTime()->toMilliseconds(),
'memory_used_bytes' => $this->getMemoryUsed()->toBytes(),
'parent_id' => $this->parentId,
'depth' => $this->depth,
'completed' => $this->completed,
'children_count' => count($this->children),
];
}
/**
* Get all descendants (children, grandchildren, etc.)
*/
public function getAllDescendants(): array
{
$descendants = [];
foreach ($this->children as $child) {
$descendants[] = $child;
$descendants = array_merge($descendants, $child->getAllDescendants());
}
return $descendants;
}
/**
* Find child by operation ID
*/
public function findChild(string $operationId): ?NestedMeasurement
{
foreach ($this->children as $child) {
if ($child->operationId === $operationId) {
return $child;
}
$found = $child->findChild($operationId);
if ($found) {
return $found;
}
}
return null;
}
/**
* Get execution tree as string for debugging
*/
public function getExecutionTree(): string
{
$tree = $this->getDepthString() . sprintf(
"%s [%s] %.2fms (self: %.2fms)\n",
$this->name,
$this->category->value,
$this->duration?->toMilliseconds() ?? 0,
$this->getSelfTime()->toMilliseconds()
);
foreach ($this->children as $child) {
$tree .= $child->getExecutionTree();
}
return $tree;
}
}

View File

@@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\ValueObjects;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Value Object representing a complete performance report
*/
final readonly class PerformanceReport
{
/**
* @param Timestamp $timestamp When the report was generated
* @param PerformanceSummary $summary Overall performance summary
* @param array<string, CategoryMetrics> $categories Metrics grouped by category
* @param array<string, array<string, mixed>> $metrics Individual metric details
*/
public function __construct(
public Timestamp $timestamp,
public PerformanceSummary $summary,
public array $categories,
public array $metrics
) {
}
/**
* Convert to array for backward compatibility and serialization
* @return array{
* timestamp: string,
* summary: array<string, mixed>,
* categories: array<string, array<string, mixed>>,
* metrics: array<string, array<string, mixed>>
* }
*/
public function toArray(): array
{
$categoriesArray = [];
foreach ($this->categories as $name => $category) {
$categoriesArray[$name] = $category->toArray();
}
return [
'timestamp' => $this->timestamp->format('Y-m-d H:i:s'),
'summary' => $this->summary->toArray(),
'categories' => $categoriesArray,
'metrics' => $this->metrics,
];
}
/**
* Get total request time in milliseconds
*/
public function getTotalRequestTime(): float
{
return $this->summary->totalRequestTime->toMilliseconds();
}
/**
* Get total memory usage in bytes
*/
public function getTotalMemoryUsage(): int
{
return $this->summary->totalRequestMemory->toBytes();
}
/**
* Get peak memory usage in bytes
*/
public function getPeakMemoryUsage(): int
{
return $this->summary->peakMemory->toBytes();
}
/**
* Get metrics for a specific category
*/
public function getCategoryMetrics(string $category): ?CategoryMetrics
{
return $this->categories[$category] ?? null;
}
/**
* Get all category names
* @return array<string>
*/
public function getCategoryNames(): array
{
return array_keys($this->categories);
}
/**
* Get total number of metrics
*/
public function getMetricsCount(): int
{
return $this->summary->metricsCount;
}
/**
* Check if report has any metrics
*/
public function hasMetrics(): bool
{
return $this->summary->metricsCount > 0;
}
/**
* Get memory summary
*/
public function getMemorySummary(): MemorySummary
{
return $this->summary->memorySummary;
}
/**
* Get top metrics by time
* @return array<array<string, mixed>>
*/
public function getTopMetricsByTime(int $limit = 10): array
{
$metricsWithTime = array_filter($this->metrics, function ($metric) {
return $metric['measurements']['total_duration_ms'] > 0;
});
usort($metricsWithTime, function ($a, $b) {
return $b['measurements']['total_duration_ms'] <=> $a['measurements']['total_duration_ms'];
});
return array_slice($metricsWithTime, 0, $limit);
}
/**
* Get top metrics by memory
* @return array<array<string, mixed>>
*/
public function getTopMetricsByMemory(int $limit = 10): array
{
$metricsWithMemory = array_filter($this->metrics, function ($metric) {
return $metric['measurements']['total_memory_bytes'] > 0;
});
usort($metricsWithMemory, function ($a, $b) {
return $b['measurements']['total_memory_bytes'] <=> $a['measurements']['total_memory_bytes'];
});
return array_slice($metricsWithMemory, 0, $limit);
}
}

View File

@@ -0,0 +1,242 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\ValueObjects;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Score;
use App\Framework\Core\ValueObjects\ScoreLevel;
/**
* Represents a performance score with performance-specific logic
*/
final readonly class PerformanceScore
{
public function __construct(
private Score $score,
private array $metrics = [],
private ?Duration $responseTime = null
) {
}
/**
* Create from response time (inverse relationship - higher time = lower score)
*/
public static function fromResponseTime(Duration $responseTime, ?Duration $baseline = null): self
{
$baseline = $baseline ?? Duration::fromMilliseconds(100); // 100ms baseline
// Calculate score inversely - faster response = higher score
$ratio = $baseline->toMilliseconds() / max(1, $responseTime->toMilliseconds());
$score = new Score(min(1.0, $ratio));
return new self($score, ['response_time' => $responseTime->toMilliseconds()], $responseTime);
}
/**
* Create from throughput (requests per second)
*/
public static function fromThroughput(float $currentThroughput, float $maxThroughput): self
{
if ($maxThroughput <= 0) {
return new self(Score::zero());
}
$score = new Score(min(1.0, $currentThroughput / $maxThroughput));
return new self($score, [
'current_throughput' => $currentThroughput,
'max_throughput' => $maxThroughput,
'utilization_percentage' => ($currentThroughput / $maxThroughput) * 100,
]);
}
/**
* Create from resource utilization (CPU, Memory, etc.)
*/
public static function fromResourceUtilization(float $utilization): self
{
// Invert utilization - lower utilization = better performance
$score = new Score(max(0.0, 1.0 - ($utilization / 100.0)));
return new self($score, ['resource_utilization' => $utilization]);
}
/**
* Create excellent performance score
*/
public static function excellent(): self
{
return new self(Score::max(), ['status' => 'excellent']);
}
/**
* Create poor performance score
*/
public static function poor(): self
{
return new self(Score::zero(), ['status' => 'poor']);
}
/**
* Get the underlying score
*/
public function getScore(): Score
{
return $this->score;
}
/**
* Get performance metrics
*/
public function getMetrics(): array
{
return $this->metrics;
}
/**
* Get response time if available
*/
public function getResponseTime(): ?Duration
{
return $this->responseTime;
}
/**
* Check if performance is acceptable (>= medium level)
*/
public function isAcceptable(): bool
{
return $this->score->isAtLevel(ScoreLevel::MEDIUM);
}
/**
* Check if optimization is required
*/
public function requiresOptimization(): bool
{
return $this->score->toLevel() === ScoreLevel::LOW;
}
/**
* Check if performance is excellent
*/
public function isExcellent(): bool
{
return $this->score->isAtLevel(ScoreLevel::HIGH);
}
/**
* Check if performance is critical (needs immediate attention)
*/
public function isCritical(): bool
{
return $this->score->isLow();
}
/**
* Get performance grade (A, B, C, D, F)
*/
public function getGrade(): string
{
return match ($this->score->toLevel()) {
ScoreLevel::CRITICAL => 'A', // High score = good performance
ScoreLevel::HIGH => 'B',
ScoreLevel::MEDIUM => 'C',
ScoreLevel::LOW => 'F'
};
}
/**
* Get recommended actions based on performance
*/
public function getRecommendations(): array
{
return match ($this->score->toLevel()) {
ScoreLevel::CRITICAL => ['maintain_current_performance', 'monitor_trends'],
ScoreLevel::HIGH => ['continue_monitoring', 'minor_optimizations'],
ScoreLevel::MEDIUM => ['investigate_bottlenecks', 'consider_optimizations'],
ScoreLevel::LOW => ['immediate_optimization_required', 'investigate_critical_issues']
};
}
/**
* Combine with another performance score
*/
public function combineWith(PerformanceScore $other, float $weight = 0.5): self
{
$combinedScore = $this->score->combine($other->score, $weight);
$combinedMetrics = array_merge($this->metrics, $other->metrics);
return new self($combinedScore, $combinedMetrics);
}
/**
* Compare with baseline performance
*/
public function compareWith(PerformanceScore $baseline): array
{
$improvement = $this->score->value() - $baseline->score->value();
return [
'current_score' => $this->score->value(),
'baseline_score' => $baseline->score->value(),
'improvement' => $improvement,
'improvement_percentage' => $baseline->score->value() > 0
? ($improvement / $baseline->score->value()) * 100
: 0,
'status' => $improvement > 0 ? 'improved' : ($improvement < 0 ? 'degraded' : 'unchanged'),
];
}
/**
* Get performance description
*/
public function getDescription(): string
{
$level = $this->score->toLevel();
$percentage = $this->score->toPercentage();
return sprintf(
'%s performance (%.1f%% score, Grade: %s)',
ucfirst($level->value),
$percentage->getValue(),
$this->getGrade()
);
}
/**
* Convert to array for serialization
*/
public function toArray(): array
{
return [
'score' => $this->score->toArray(),
'level' => $this->score->toLevel()->value,
'grade' => $this->getGrade(),
'percentage' => $this->score->toPercentage()->getValue(),
'metrics' => $this->metrics,
'response_time_ms' => $this->responseTime?->toMilliseconds(),
'is_acceptable' => $this->isAcceptable(),
'requires_optimization' => $this->requiresOptimization(),
'recommendations' => $this->getRecommendations(),
'description' => $this->getDescription(),
];
}
/**
* Create from array
*/
public static function fromArray(array $data): self
{
$score = isset($data['score']) ? Score::fromArray($data['score']) : new Score($data['value'] ?? 0.0);
$responseTime = isset($data['response_time_ms']) ? Duration::fromMilliseconds($data['response_time_ms']) : null;
return new self(
score: $score,
metrics: $data['metrics'] ?? [],
responseTime: $responseTime
);
}
}

View File

@@ -0,0 +1,444 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\ValueObjects;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Performance\PerformanceCategory;
/**
* Immutable snapshot of performance data during an operation
*
* Provides comprehensive performance tracking with domain context
* for detailed analysis and monitoring.
*/
final readonly class PerformanceSnapshot
{
public function __construct(
public string $operationId,
public PerformanceCategory $category,
public Timestamp $startTime,
public Byte $startMemory,
public Byte $peakMemory,
public ?Timestamp $endTime = null,
public ?Byte $endMemory = null,
public ?Duration $duration = null,
public ?Byte $memoryDelta = null,
public float $cpuUsage = 0.0,
public int $ioOperations = 0,
public int $cacheHits = 0,
public int $cacheMisses = 0,
public int $itemsProcessed = 0,
public int $errorsEncountered = 0,
public array $contextData = [],
public array $customMetrics = []
) {
}
/**
* Create a new snapshot for starting an operation
*/
public static function start(
string $operationId,
PerformanceCategory $category,
Timestamp $startTime,
Byte $startMemory,
array $contextData = []
): self {
return new self(
operationId: $operationId,
category: $category,
startTime: $startTime,
startMemory: $startMemory,
peakMemory: $startMemory,
contextData: $contextData
);
}
public function withEndTime(Timestamp $endTime): self
{
return new self(
$this->operationId,
$this->category,
$this->startTime,
$this->startMemory,
$this->peakMemory,
$endTime,
$this->endMemory,
$this->duration,
$this->memoryDelta,
$this->cpuUsage,
$this->ioOperations,
$this->cacheHits,
$this->cacheMisses,
$this->itemsProcessed,
$this->errorsEncountered,
$this->contextData,
$this->customMetrics
);
}
public function withEndMemory(Byte $endMemory): self
{
return new self(
$this->operationId,
$this->category,
$this->startTime,
$this->startMemory,
$this->peakMemory,
$this->endTime,
$endMemory,
$this->duration,
$this->memoryDelta,
$this->cpuUsage,
$this->ioOperations,
$this->cacheHits,
$this->cacheMisses,
$this->itemsProcessed,
$this->errorsEncountered,
$this->contextData,
$this->customMetrics
);
}
public function withDuration(Duration $duration): self
{
return new self(
$this->operationId,
$this->category,
$this->startTime,
$this->startMemory,
$this->peakMemory,
$this->endTime,
$this->endMemory,
$duration,
$this->memoryDelta,
$this->cpuUsage,
$this->ioOperations,
$this->cacheHits,
$this->cacheMisses,
$this->itemsProcessed,
$this->errorsEncountered,
$this->contextData,
$this->customMetrics
);
}
public function withMemoryDelta(Byte $memoryDelta): self
{
return new self(
$this->operationId,
$this->category,
$this->startTime,
$this->startMemory,
$this->peakMemory,
$this->endTime,
$this->endMemory,
$this->duration,
$memoryDelta,
$this->cpuUsage,
$this->ioOperations,
$this->cacheHits,
$this->cacheMisses,
$this->itemsProcessed,
$this->errorsEncountered,
$this->contextData,
$this->customMetrics
);
}
public function withPeakMemory(Byte $peakMemory): self
{
return new self(
$this->operationId,
$this->category,
$this->startTime,
$this->startMemory,
$peakMemory,
$this->endTime,
$this->endMemory,
$this->duration,
$this->memoryDelta,
$this->cpuUsage,
$this->ioOperations,
$this->cacheHits,
$this->cacheMisses,
$this->itemsProcessed,
$this->errorsEncountered,
$this->contextData,
$this->customMetrics
);
}
public function withCacheHits(int $cacheHits): self
{
return new self(
$this->operationId,
$this->category,
$this->startTime,
$this->startMemory,
$this->peakMemory,
$this->endTime,
$this->endMemory,
$this->duration,
$this->memoryDelta,
$this->cpuUsage,
$this->ioOperations,
$cacheHits,
$this->cacheMisses,
$this->itemsProcessed,
$this->errorsEncountered,
$this->contextData,
$this->customMetrics
);
}
public function withCacheMisses(int $cacheMisses): self
{
return new self(
$this->operationId,
$this->category,
$this->startTime,
$this->startMemory,
$this->peakMemory,
$this->endTime,
$this->endMemory,
$this->duration,
$this->memoryDelta,
$this->cpuUsage,
$this->ioOperations,
$this->cacheHits,
$cacheMisses,
$this->itemsProcessed,
$this->errorsEncountered,
$this->contextData,
$this->customMetrics
);
}
public function withItemsProcessed(int $itemsProcessed): self
{
return new self(
$this->operationId,
$this->category,
$this->startTime,
$this->startMemory,
$this->peakMemory,
$this->endTime,
$this->endMemory,
$this->duration,
$this->memoryDelta,
$this->cpuUsage,
$this->ioOperations,
$this->cacheHits,
$this->cacheMisses,
$itemsProcessed,
$this->errorsEncountered,
$this->contextData,
$this->customMetrics
);
}
public function withIoOperations(int $ioOperations): self
{
return new self(
$this->operationId,
$this->category,
$this->startTime,
$this->startMemory,
$this->peakMemory,
$this->endTime,
$this->endMemory,
$this->duration,
$this->memoryDelta,
$this->cpuUsage,
$ioOperations,
$this->cacheHits,
$this->cacheMisses,
$this->itemsProcessed,
$this->errorsEncountered,
$this->contextData,
$this->customMetrics
);
}
public function withErrorsEncountered(int $errorsEncountered): self
{
return new self(
$this->operationId,
$this->category,
$this->startTime,
$this->startMemory,
$this->peakMemory,
$this->endTime,
$this->endMemory,
$this->duration,
$this->memoryDelta,
$this->cpuUsage,
$this->ioOperations,
$this->cacheHits,
$this->cacheMisses,
$this->itemsProcessed,
$errorsEncountered,
$this->contextData,
$this->customMetrics
);
}
public function withCustomMetric(string $name, mixed $value): self
{
$customMetrics = $this->customMetrics;
$customMetrics[$name] = $value;
return new self(
$this->operationId,
$this->category,
$this->startTime,
$this->startMemory,
$this->peakMemory,
$this->endTime,
$this->endMemory,
$this->duration,
$this->memoryDelta,
$this->cpuUsage,
$this->ioOperations,
$this->cacheHits,
$this->cacheMisses,
$this->itemsProcessed,
$this->errorsEncountered,
$this->contextData,
$customMetrics
);
}
/**
* Check if the operation is completed
*/
public function isCompleted(): bool
{
return $this->endTime !== null && $this->duration !== null;
}
/**
* Get cache hit rate
*/
public function getCacheHitRate(): float
{
$total = $this->cacheHits + $this->cacheMisses;
return $total > 0 ? $this->cacheHits / $total : 0.0;
}
/**
* Get error rate
*/
public function getErrorRate(): float
{
return $this->itemsProcessed > 0
? $this->errorsEncountered / $this->itemsProcessed
: 0.0;
}
/**
* Get throughput (items per second)
*/
public function getThroughput(): float
{
if ($this->duration === null || $this->duration->toSeconds() === 0) {
return 0.0;
}
return $this->itemsProcessed / $this->duration->toSeconds();
}
/**
* Get memory efficiency (bytes per item)
*/
public function getMemoryEfficiency(): float
{
if ($this->itemsProcessed === 0 || $this->memoryDelta === null) {
return 0.0;
}
return $this->memoryDelta->toBytes() / $this->itemsProcessed;
}
/**
* Get memory pressure (peak memory vs typical usage)
*/
public function getMemoryPressure(): float
{
$memoryUsed = $this->peakMemory->subtract($this->startMemory);
$typicalMemoryLimit = Byte::fromMegabytes(128); // 128MB as baseline
return $memoryUsed->toBytes() / $typicalMemoryLimit->toBytes();
}
/**
* Convert to array for serialization
*/
public function toArray(): array
{
return [
'operation_id' => $this->operationId,
'category' => $this->category->value,
'start_time' => $this->startTime->toFloat(),
'end_time' => $this->endTime?->toFloat(),
'duration_ms' => $this->duration?->toMilliseconds(),
'start_memory_bytes' => $this->startMemory->toBytes(),
'end_memory_bytes' => $this->endMemory?->toBytes(),
'peak_memory_bytes' => $this->peakMemory->toBytes(),
'memory_delta_bytes' => $this->memoryDelta?->toBytes(),
'cpu_usage' => $this->cpuUsage,
'io_operations' => $this->ioOperations,
'cache_hits' => $this->cacheHits,
'cache_misses' => $this->cacheMisses,
'items_processed' => $this->itemsProcessed,
'errors_encountered' => $this->errorsEncountered,
'context_data' => $this->contextData,
'custom_metrics' => $this->customMetrics,
'computed_metrics' => [
'cache_hit_rate' => $this->getCacheHitRate(),
'error_rate' => $this->getErrorRate(),
'throughput' => $this->getThroughput(),
'memory_efficiency' => $this->getMemoryEfficiency(),
'memory_pressure' => $this->getMemoryPressure(),
'is_completed' => $this->isCompleted(),
],
];
}
/**
* Get telemetry data optimized for monitoring systems
*/
public function toTelemetryData(): array
{
$base = [
'operation_id' => $this->operationId,
'category' => $this->category->value,
'timestamp' => $this->endTime?->toFloat() ?? $this->startTime->toFloat(),
'duration_ms' => $this->duration?->toMilliseconds() ?? 0,
'memory_peak_mb' => $this->peakMemory->toMegabytes(),
'items_processed' => $this->itemsProcessed,
'throughput' => $this->getThroughput(),
'cache_hit_rate' => $this->getCacheHitRate(),
'error_rate' => $this->getErrorRate(),
'memory_pressure' => $this->getMemoryPressure(),
];
// Add context data with telemetry prefix
foreach ($this->contextData as $key => $value) {
$base["ctx_{$key}"] = $value;
}
// Add custom metrics with metric prefix
foreach ($this->customMetrics as $key => $value) {
$base["metric_{$key}"] = $value;
}
return $base;
}
}

View File

@@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\ValueObjects;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
/**
* Value Object representing performance summary statistics
*/
final readonly class PerformanceSummary
{
public function __construct(
public Duration $totalRequestTime,
public Byte $totalRequestMemory,
public Byte $peakMemory,
public int $metricsCount,
public MemorySummary $memorySummary
) {
}
/**
* Create from raw values (for backward compatibility)
*/
public static function fromRawValues(
float $totalRequestTimeMs,
int $totalRequestMemoryBytes,
int $peakMemoryBytes,
int $metricsCount,
MemorySummary $memorySummary
): self {
return new self(
totalRequestTime: Duration::fromSeconds($totalRequestTimeMs / 1000.0),
totalRequestMemory: Byte::fromBytes($totalRequestMemoryBytes),
peakMemory: Byte::fromBytes($peakMemoryBytes),
metricsCount: $metricsCount,
memorySummary: $memorySummary
);
}
/**
* Convert to array for backward compatibility
* @return array{
* total_request_time_ms: float,
* total_request_memory_bytes: int,
* peak_memory_bytes: int,
* metrics_count: int,
* memory_summary: MemorySummary
* }
*/
public function toArray(): array
{
return [
'total_request_time_ms' => $this->totalRequestTime->toMilliseconds(),
'total_request_memory_bytes' => $this->totalRequestMemory->toBytes(),
'peak_memory_bytes' => $this->peakMemory->toBytes(),
'metrics_count' => $this->metricsCount,
'memory_summary' => $this->memorySummary,
];
}
/**
* Get formatted request time
*/
public function getFormattedRequestTime(): string
{
return sprintf('%.2f ms', $this->totalRequestTime->toMilliseconds());
}
/**
* Get formatted memory usage
*/
public function getFormattedMemoryUsage(): string
{
return $this->totalRequestMemory->toHumanReadable();
}
/**
* Get formatted peak memory
*/
public function getFormattedPeakMemory(): string
{
return $this->peakMemory->toHumanReadable();
}
/**
* Check if performance is within acceptable bounds
*/
public function isPerformanceAcceptable(Duration $maxTime, Byte $maxMemory): bool
{
return $this->totalRequestTime->toMilliseconds() <= $maxTime->toMilliseconds()
&& $this->peakMemory->toBytes() <= $maxMemory->toBytes();
}
/**
* Get performance grade (A-F)
*/
public function getPerformanceGrade(): string
{
$timeMs = $this->totalRequestTime->toMilliseconds();
$memoryMb = $this->peakMemory->toMegabytes();
// Time-based grading (50% weight)
$timeGrade = match (true) {
$timeMs < 100 => 5, // A
$timeMs < 300 => 4, // B
$timeMs < 500 => 3, // C
$timeMs < 1000 => 2, // D
default => 1, // F
};
// Memory-based grading (50% weight)
$memoryGrade = match (true) {
$memoryMb < 32 => 5, // A
$memoryMb < 64 => 4, // B
$memoryMb < 128 => 3, // C
$memoryMb < 256 => 2, // D
default => 1, // F
};
$averageGrade = ($timeGrade + $memoryGrade) / 2;
return match (true) {
$averageGrade >= 4.5 => 'A',
$averageGrade >= 3.5 => 'B',
$averageGrade >= 2.5 => 'C',
$averageGrade >= 1.5 => 'D',
default => 'F',
};
}
/**
* Get performance as percentage (0-100)
*/
public function getPerformanceScore(): int
{
$timeMs = $this->totalRequestTime->toMilliseconds();
$memoryMb = $this->peakMemory->toMegabytes();
// Time score (50% weight) - inverse scale
$timeScore = match (true) {
$timeMs < 100 => 100,
$timeMs < 300 => 80,
$timeMs < 500 => 60,
$timeMs < 1000 => 40,
$timeMs < 2000 => 20,
default => 0,
};
// Memory score (50% weight) - inverse scale
$memoryScore = match (true) {
$memoryMb < 32 => 100,
$memoryMb < 64 => 80,
$memoryMb < 128 => 60,
$memoryMb < 256 => 40,
$memoryMb < 512 => 20,
default => 0,
};
return (int) round(($timeScore + $memoryScore) / 2);
}
}