chore: complete update
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
18
src/Framework/Performance/Examples/README.md
Normal file
18
src/Framework/Performance/Examples/README.md
Normal 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**:
|
||||
30
src/Framework/Performance/Jobs/ProcessPerformanceLogsJob.php
Normal file
30
src/Framework/Performance/Jobs/ProcessPerformanceLogsJob.php
Normal 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;
|
||||
}
|
||||
}
|
||||
132
src/Framework/Performance/MemoryUsageTracker.php
Normal file
132
src/Framework/Performance/MemoryUsageTracker.php
Normal 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];
|
||||
}
|
||||
}
|
||||
24
src/Framework/Performance/PerformanceCategory.php
Normal file
24
src/Framework/Performance/PerformanceCategory.php
Normal 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;
|
||||
}
|
||||
}
|
||||
298
src/Framework/Performance/PerformanceDatabaseWorker.php
Normal file
298
src/Framework/Performance/PerformanceDatabaseWorker.php
Normal 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)
|
||||
");
|
||||
}
|
||||
}
|
||||
169
src/Framework/Performance/PerformanceFileLogger.php
Normal file
169
src/Framework/Performance/PerformanceFileLogger.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/Framework/Performance/PerformanceMarker.php
Normal file
39
src/Framework/Performance/PerformanceMarker.php
Normal 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;
|
||||
}
|
||||
}
|
||||
82
src/Framework/Performance/PerformanceMeasurement.php
Normal file
82
src/Framework/Performance/PerformanceMeasurement.php
Normal 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;
|
||||
}
|
||||
}
|
||||
446
src/Framework/Performance/PerformanceMeter.php
Normal file
446
src/Framework/Performance/PerformanceMeter.php
Normal 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);
|
||||
}
|
||||
}
|
||||
189
src/Framework/Performance/PerformanceMetricLogger.php
Normal file
189
src/Framework/Performance/PerformanceMetricLogger.php
Normal 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'];
|
||||
}
|
||||
}
|
||||
80
src/Framework/Performance/PerformanceMiddleware.php
Normal file
80
src/Framework/Performance/PerformanceMiddleware.php
Normal 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']));
|
||||
}
|
||||
}
|
||||
14
src/Framework/Performance/README.md
Normal file
14
src/Framework/Performance/README.md
Normal 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
|
||||
Reference in New Issue
Block a user