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:
235
src/Framework/Performance/ARCHITECTURE.md
Normal file
235
src/Framework/Performance/ARCHITECTURE.md
Normal 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
|
||||
126
src/Framework/Performance/Analysis/BottleneckAnalysis.php
Normal file
126
src/Framework/Performance/Analysis/BottleneckAnalysis.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
502
src/Framework/Performance/Analysis/BottleneckAnalyzer.php
Normal file
502
src/Framework/Performance/Analysis/BottleneckAnalyzer.php
Normal 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';
|
||||
}
|
||||
}
|
||||
365
src/Framework/Performance/Analysis/TrendAnalysis.php
Normal file
365
src/Framework/Performance/Analysis/TrendAnalysis.php
Normal 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;
|
||||
}
|
||||
}
|
||||
618
src/Framework/Performance/Analysis/TrendAnalyzer.php
Normal file
618
src/Framework/Performance/Analysis/TrendAnalyzer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
557
src/Framework/Performance/EnhancedPerformanceCollector.php
Normal file
557
src/Framework/Performance/EnhancedPerformanceCollector.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
34
src/Framework/Performance/Events/OperationCompletedEvent.php
Normal file
34
src/Framework/Performance/Events/OperationCompletedEvent.php
Normal 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',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
38
src/Framework/Performance/Events/OperationFailedEvent.php
Normal file
38
src/Framework/Performance/Events/OperationFailedEvent.php
Normal 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(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
22
src/Framework/Performance/Events/OperationStartedEvent.php
Normal file
22
src/Framework/Performance/Events/OperationStartedEvent.php
Normal 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
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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**:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
80
src/Framework/Performance/MemoryMonitor.php
Normal file
80
src/Framework/Performance/MemoryMonitor.php
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(' ', $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(' ', $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;
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
427
src/Framework/Performance/NestedPerformanceTracker.php
Normal file
427
src/Framework/Performance/NestedPerformanceTracker.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
379
src/Framework/Performance/OperationTracker.php
Normal file
379
src/Framework/Performance/OperationTracker.php
Normal 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';
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
84
src/Framework/Performance/PerformanceConfig.php
Normal file
84
src/Framework/Performance/PerformanceConfig.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
212
src/Framework/Performance/PerformanceMetric.php
Normal file
212
src/Framework/Performance/PerformanceMetric.php
Normal 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),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -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']));
|
||||
}
|
||||
}
|
||||
278
src/Framework/Performance/PerformanceReporter.php
Normal file
278
src/Framework/Performance/PerformanceReporter.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
198
src/Framework/Performance/PerformanceService.php
Normal file
198
src/Framework/Performance/PerformanceService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
48
src/Framework/Performance/PerformanceServiceInitializer.php
Normal file
48
src/Framework/Performance/PerformanceServiceInitializer.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
```
|
||||
169
src/Framework/Performance/ValueObjects/CategoryMetrics.php
Normal file
169
src/Framework/Performance/ValueObjects/CategoryMetrics.php
Normal 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;
|
||||
}
|
||||
}
|
||||
129
src/Framework/Performance/ValueObjects/Measurement.php
Normal file
129
src/Framework/Performance/ValueObjects/Measurement.php
Normal 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);
|
||||
}
|
||||
}
|
||||
248
src/Framework/Performance/ValueObjects/MeasurementCollection.php
Normal file
248
src/Framework/Performance/ValueObjects/MeasurementCollection.php
Normal 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),
|
||||
];
|
||||
}
|
||||
}
|
||||
157
src/Framework/Performance/ValueObjects/MemorySummary.php
Normal file
157
src/Framework/Performance/ValueObjects/MemorySummary.php
Normal 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();
|
||||
}
|
||||
}
|
||||
210
src/Framework/Performance/ValueObjects/MetricContext.php
Normal file
210
src/Framework/Performance/ValueObjects/MetricContext.php
Normal 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);
|
||||
}
|
||||
}
|
||||
297
src/Framework/Performance/ValueObjects/NestedMeasurement.php
Normal file
297
src/Framework/Performance/ValueObjects/NestedMeasurement.php
Normal 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;
|
||||
}
|
||||
}
|
||||
150
src/Framework/Performance/ValueObjects/PerformanceReport.php
Normal file
150
src/Framework/Performance/ValueObjects/PerformanceReport.php
Normal 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);
|
||||
}
|
||||
}
|
||||
242
src/Framework/Performance/ValueObjects/PerformanceScore.php
Normal file
242
src/Framework/Performance/ValueObjects/PerformanceScore.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
444
src/Framework/Performance/ValueObjects/PerformanceSnapshot.php
Normal file
444
src/Framework/Performance/ValueObjects/PerformanceSnapshot.php
Normal 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;
|
||||
}
|
||||
}
|
||||
164
src/Framework/Performance/ValueObjects/PerformanceSummary.php
Normal file
164
src/Framework/Performance/ValueObjects/PerformanceSummary.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user