chore: complete update

This commit is contained in:
2025-07-17 16:24:20 +02:00
parent 899227b0a4
commit 64a7051137
1300 changed files with 85570 additions and 2756 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance;
enum PerformanceCategory: string
{
case SYSTEM = 'system';
case DATABASE = 'database';
case CACHE = 'cache';
case TEMPLATE = 'template';
case ROUTING = 'routing';
case CONTROLLER = 'controller';
case VIEW = 'view';
case API = 'api';
case FILESYSTEM = 'filesystem';
case CUSTOM = 'custom';
public static function fromString(string $value): self
{
return self::tryFrom($value) ?? self::CUSTOM;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
# Performance Monitoring Framework
Ein umfassendes Performance-Monitoring-System für PHP-Anwendungen mit File-Logging, Redis-Metriken und Database-Persistierung.
## 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
## Architektur