Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Performance\Middleware;
|
||||
|
||||
use App\Framework\Http\HttpMiddleware;
|
||||
use App\Framework\Http\MiddlewareContext;
|
||||
use App\Framework\Http\MiddlewarePriority;
|
||||
use App\Framework\Http\MiddlewarePriorityAttribute;
|
||||
use App\Framework\Http\Next;
|
||||
use App\Framework\Http\RequestStateManager;
|
||||
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
|
||||
use App\Framework\Performance\PerformanceCategory;
|
||||
use App\Framework\Performance\PerformanceConfig;
|
||||
|
||||
#[MiddlewarePriorityAttribute(MiddlewarePriority::CONTROLLER, -10)]
|
||||
final readonly class ControllerPerformanceMiddleware implements HttpMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
private PerformanceCollectorInterface $collector,
|
||||
private PerformanceConfig $config
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
|
||||
{
|
||||
if (! $this->config->isTrackingEnabled(PerformanceCategory::CONTROLLER)) {
|
||||
return $next($context);
|
||||
}
|
||||
|
||||
$request = $context->request;
|
||||
|
||||
// Try to get controller info from state manager or request
|
||||
$controllerInfo = $this->extractControllerInfo($request, $stateManager);
|
||||
|
||||
$controllerKey = $controllerInfo['controller'] ?? 'unknown_controller';
|
||||
$actionKey = $controllerInfo['action'] ?? 'unknown_action';
|
||||
|
||||
$performanceContext = [
|
||||
'controller' => $controllerKey,
|
||||
'action' => $actionKey,
|
||||
'method' => $request->method,
|
||||
'path' => $request->path,
|
||||
];
|
||||
|
||||
$timingKey = "controller_{$controllerKey}_{$actionKey}";
|
||||
|
||||
$this->collector->startTiming($timingKey, PerformanceCategory::CONTROLLER, $performanceContext);
|
||||
|
||||
// Count controller invocations
|
||||
$this->collector->increment(
|
||||
"controller_calls_{$controllerKey}",
|
||||
PerformanceCategory::CONTROLLER,
|
||||
1,
|
||||
$performanceContext
|
||||
);
|
||||
|
||||
try {
|
||||
$result = $next($context);
|
||||
|
||||
// Record successful controller execution
|
||||
$this->collector->increment(
|
||||
"controller_success_{$controllerKey}",
|
||||
PerformanceCategory::CONTROLLER,
|
||||
1,
|
||||
$performanceContext
|
||||
);
|
||||
|
||||
return $result;
|
||||
} catch (\Throwable $e) {
|
||||
// Record controller error
|
||||
$this->collector->increment(
|
||||
"controller_errors_{$controllerKey}",
|
||||
PerformanceCategory::CONTROLLER,
|
||||
1,
|
||||
array_merge($performanceContext, ['error' => $e::class])
|
||||
);
|
||||
|
||||
throw $e;
|
||||
} finally {
|
||||
$this->collector->endTiming($timingKey);
|
||||
}
|
||||
}
|
||||
|
||||
private function extractControllerInfo(mixed $request, RequestStateManager $stateManager): array
|
||||
{
|
||||
// Try to get controller info from various sources
|
||||
|
||||
// Method 1: From state manager
|
||||
$controllerData = $stateManager->get('controller_info');
|
||||
if ($controllerData) {
|
||||
return $controllerData;
|
||||
}
|
||||
|
||||
// Method 2: From request attributes/route
|
||||
if (isset($request->route)) {
|
||||
$handler = $request->route['handler'] ?? null;
|
||||
if ($handler && is_string($handler)) {
|
||||
return $this->parseControllerString($handler);
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: From request path (fallback)
|
||||
$path = $request->path ?? '/';
|
||||
$segments = explode('/', trim($path, '/'));
|
||||
|
||||
return [
|
||||
'controller' => $segments[0] ?? 'home',
|
||||
'action' => $segments[1] ?? 'index',
|
||||
];
|
||||
}
|
||||
|
||||
private function parseControllerString(string $handler): array
|
||||
{
|
||||
// Parse strings like "App\Controller\HomeController@index"
|
||||
if (str_contains($handler, '@')) {
|
||||
[$controller, $action] = explode('@', $handler, 2);
|
||||
$controller = basename(str_replace('\\', '/', $controller));
|
||||
|
||||
return ['controller' => $controller, 'action' => $action];
|
||||
}
|
||||
|
||||
// Parse class names
|
||||
if (str_contains($handler, '\\')) {
|
||||
$controller = basename(str_replace('\\', '/', $handler));
|
||||
|
||||
return ['controller' => $controller, 'action' => '__invoke'];
|
||||
}
|
||||
|
||||
return ['controller' => $handler, 'action' => 'unknown'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Performance\Middleware;
|
||||
|
||||
use App\Framework\Database\Middleware\QueryContext;
|
||||
use App\Framework\Database\Middleware\QueryMiddleware;
|
||||
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
|
||||
use App\Framework\Performance\PerformanceCategory;
|
||||
use App\Framework\Performance\PerformanceConfig;
|
||||
|
||||
final readonly class DatabasePerformanceMiddleware implements QueryMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
private PerformanceCollectorInterface $collector,
|
||||
private PerformanceConfig $config
|
||||
) {
|
||||
}
|
||||
|
||||
public function process(QueryContext $context, callable $next): mixed
|
||||
{
|
||||
if (! $this->config->isTrackingEnabled(PerformanceCategory::DATABASE)) {
|
||||
return $next();
|
||||
}
|
||||
|
||||
$query = $context->sql;
|
||||
$bindings = $context->parameters;
|
||||
$queryType = $this->getQueryType($query);
|
||||
$queryKey = "db_query_{$queryType}";
|
||||
|
||||
$performanceContext = [
|
||||
'query_type' => $queryType,
|
||||
'bindings_count' => count($bindings),
|
||||
'query_hash' => md5($query),
|
||||
];
|
||||
|
||||
// Add query text for detailed reports (careful with sensitive data)
|
||||
if ($this->config->detailedReports) {
|
||||
$performanceContext['query'] = $this->sanitizeQuery($query);
|
||||
$performanceContext['bindings'] = $this->sanitizeBindings($bindings);
|
||||
}
|
||||
|
||||
$this->collector->startTiming($queryKey, PerformanceCategory::DATABASE, $performanceContext);
|
||||
|
||||
// Count database queries
|
||||
$this->collector->increment('database_queries_total', PerformanceCategory::DATABASE, 1, $performanceContext);
|
||||
$this->collector->increment("database_queries_{$queryType}", PerformanceCategory::DATABASE, 1, $performanceContext);
|
||||
|
||||
try {
|
||||
$result = $next();
|
||||
|
||||
// Record successful query
|
||||
$this->collector->increment('database_queries_success', PerformanceCategory::DATABASE, 1, $performanceContext);
|
||||
|
||||
return $result;
|
||||
} catch (\Throwable $e) {
|
||||
// Record failed query
|
||||
$this->collector->increment(
|
||||
'database_queries_failed',
|
||||
PerformanceCategory::DATABASE,
|
||||
1,
|
||||
array_merge($performanceContext, ['error' => $e::class])
|
||||
);
|
||||
|
||||
throw $e;
|
||||
} finally {
|
||||
$duration = $this->collector->endTiming($queryKey);
|
||||
|
||||
// Check for slow queries
|
||||
if ($this->config->isSlowQuery($duration)) {
|
||||
$this->collector->increment(
|
||||
'database_slow_queries',
|
||||
PerformanceCategory::DATABASE,
|
||||
1,
|
||||
array_merge($performanceContext, ['duration_ms' => $duration])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 100; // Performance middleware should run with moderate priority
|
||||
}
|
||||
|
||||
private function getQueryType(string $query): string
|
||||
{
|
||||
$query = trim(strtoupper($query));
|
||||
|
||||
if (str_starts_with($query, 'SELECT')) {
|
||||
return 'select';
|
||||
}
|
||||
if (str_starts_with($query, 'INSERT')) {
|
||||
return 'insert';
|
||||
}
|
||||
if (str_starts_with($query, 'UPDATE')) {
|
||||
return 'update';
|
||||
}
|
||||
if (str_starts_with($query, 'DELETE')) {
|
||||
return 'delete';
|
||||
}
|
||||
if (str_starts_with($query, 'CREATE')) {
|
||||
return 'create';
|
||||
}
|
||||
if (str_starts_with($query, 'DROP')) {
|
||||
return 'drop';
|
||||
}
|
||||
if (str_starts_with($query, 'ALTER')) {
|
||||
return 'alter';
|
||||
}
|
||||
if (str_starts_with($query, 'SHOW')) {
|
||||
return 'show';
|
||||
}
|
||||
if (str_starts_with($query, 'DESCRIBE') || str_starts_with($query, 'DESC')) {
|
||||
return 'describe';
|
||||
}
|
||||
|
||||
return 'other';
|
||||
}
|
||||
|
||||
private function sanitizeQuery(string $query): string
|
||||
{
|
||||
// Remove potential sensitive data from queries for logging
|
||||
$query = preg_replace('/\b\d{4}[-\s]\d{4}[-\s]\d{4}[-\s]\d{4}\b/', '****-****-****-****', $query);
|
||||
$query = preg_replace('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', '***@***.***', $query);
|
||||
|
||||
// Truncate very long queries
|
||||
if (strlen($query) > 500) {
|
||||
$query = substr($query, 0, 500) . '...';
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function sanitizeBindings(array $bindings): array
|
||||
{
|
||||
// Sanitize binding values to prevent sensitive data exposure
|
||||
return array_map(function ($value) {
|
||||
if (is_string($value)) {
|
||||
// Hide potential sensitive strings
|
||||
if (strlen($value) > 50) {
|
||||
return '[LONG_STRING:' . strlen($value) . '_chars]';
|
||||
}
|
||||
if (preg_match('/^\d{4}[-\s]\d{4}[-\s]\d{4}[-\s]\d{4}$/', $value)) {
|
||||
return '[CREDIT_CARD]';
|
||||
}
|
||||
if (filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
return '[EMAIL]';
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}, $bindings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,696 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Performance\Middleware;
|
||||
|
||||
use App\Framework\Http\Headers;
|
||||
use App\Framework\Http\HttpMiddleware;
|
||||
use App\Framework\Http\HttpResponse;
|
||||
use App\Framework\Http\MiddlewareContext;
|
||||
use App\Framework\Http\MiddlewarePriority;
|
||||
use App\Framework\Http\MiddlewarePriorityAttribute;
|
||||
use App\Framework\Http\Next;
|
||||
use App\Framework\Http\RequestStateManager;
|
||||
use App\Framework\Http\Session\SessionInterface;
|
||||
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
|
||||
use App\Framework\Performance\EnhancedPerformanceCollector;
|
||||
use App\Framework\Performance\PerformanceConfig;
|
||||
use App\Framework\Performance\PerformanceReporter;
|
||||
|
||||
#[MiddlewarePriorityAttribute(MiddlewarePriority::LAST)]
|
||||
final readonly class PerformanceDebugMiddleware implements HttpMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
private PerformanceCollectorInterface $collector,
|
||||
private PerformanceConfig $config,
|
||||
private PerformanceReporter $reporter
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
|
||||
{
|
||||
// Always execute the request first
|
||||
$context = $next($context);
|
||||
|
||||
// Handle performance output and headers
|
||||
return $this->handlePerformanceOutput($context, $stateManager);
|
||||
}
|
||||
|
||||
private function handlePerformanceOutput(MiddlewareContext $context, RequestStateManager $stateManager): MiddlewareContext
|
||||
{
|
||||
// Check if performance tracking is enabled
|
||||
if (! $this->config->enabled) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
$request = $context->request;
|
||||
$response = $context->response ?? null;
|
||||
|
||||
// Skip if no response
|
||||
if (! $response) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
// Skip for excluded paths
|
||||
$path = $request->path ?? '/';
|
||||
if ($this->config->shouldExcludePath($path)) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
// Check content type - only add to HTML responses
|
||||
$contentType = $response->headers->get('Content-Type', '');
|
||||
if (is_string($contentType) && ! empty($contentType) && ! str_contains($contentType, 'text/html')) {
|
||||
// Add performance headers for non-HTML responses
|
||||
$headersWithPerformance = $this->addPerformanceHeaders($response->headers);
|
||||
$newResponse = new HttpResponse(
|
||||
status: $response->status,
|
||||
headers: $headersWithPerformance,
|
||||
body: $response->body
|
||||
);
|
||||
|
||||
|
||||
|
||||
return $context->withResponse($newResponse);
|
||||
}
|
||||
|
||||
// Skip for AJAX requests unless specifically enabled
|
||||
if ($this->isAjaxRequest($request) && ! $this->config->detailedReports) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
if (! $context->response instanceof HttpResponse) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
// Add full performance output to HTML responses
|
||||
return $this->addPerformanceOutput($context, $stateManager);
|
||||
}
|
||||
|
||||
private function isAjaxRequest(mixed $request): bool
|
||||
{
|
||||
$xhr = $request->headers->get('X-Requested-With', '');
|
||||
|
||||
return is_string($xhr) && strtolower($xhr) === 'xmlhttprequest';
|
||||
}
|
||||
|
||||
private function addPerformanceOutput(MiddlewareContext $context, RequestStateManager $stateManager): MiddlewareContext
|
||||
{
|
||||
$response = $context->response;
|
||||
|
||||
if ($context->response->headers->getFirst('Content-Type') === 'application/json') {
|
||||
return $context;
|
||||
}
|
||||
|
||||
if (! $response || ! isset($response->body)) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
$body = $response->body;
|
||||
|
||||
// Generate performance report
|
||||
$reportHtml = $this->reporter->generateReport('html');
|
||||
assert(is_string($reportHtml), 'HTML format should return string');
|
||||
|
||||
// Add hierarchical performance report if using EnhancedPerformanceCollector
|
||||
if ($this->collector instanceof EnhancedPerformanceCollector) {
|
||||
$hierarchicalHtml = $this->generateHierarchicalReport();
|
||||
$reportHtml .= $hierarchicalHtml;
|
||||
}
|
||||
|
||||
// Always add session debug information as collapsible details
|
||||
if ($this->config->detailedReports) {
|
||||
$sessionDebugHtml = $this->generateSessionDebugInfo($stateManager);
|
||||
$reportHtml .= $sessionDebugHtml;
|
||||
}
|
||||
|
||||
// Try to find closing body tag
|
||||
$closingBodyPos = strripos($body, '</body>');
|
||||
|
||||
if ($closingBodyPos !== false) {
|
||||
// Insert before closing body tag
|
||||
$newBody = substr($body, 0, $closingBodyPos);
|
||||
$newBody .= $this->wrapReportForInsertion($reportHtml);
|
||||
$newBody .= substr($body, $closingBodyPos);
|
||||
} else {
|
||||
// Append to end if no body tag found
|
||||
$newBody = $body . $this->wrapReportForInsertion($reportHtml);
|
||||
}
|
||||
|
||||
// Add performance headers to the headers
|
||||
$headersWithPerformance = $this->addPerformanceHeaders($response->headers);
|
||||
|
||||
// Create new response with modified body and headers
|
||||
$newResponse = new HttpResponse(
|
||||
status: $response->status,
|
||||
headers: $headersWithPerformance,
|
||||
body: $newBody
|
||||
);
|
||||
|
||||
// Return new context with new response
|
||||
return $context->withResponse($newResponse);
|
||||
}
|
||||
|
||||
private function wrapReportForInsertion(string $reportHtml): string
|
||||
{
|
||||
return "
|
||||
<!-- Performance Debug Report -->
|
||||
<style>
|
||||
.perf-debug-theme-light {
|
||||
--perf-bg-primary: #ffffff;
|
||||
--perf-bg-secondary: #f8f9fa;
|
||||
--perf-bg-tertiary: #e9ecef;
|
||||
--perf-text-primary: #212529;
|
||||
--perf-text-secondary: #6c757d;
|
||||
--perf-border: #dee2e6;
|
||||
--perf-accent: #007bff;
|
||||
--perf-success: #28a745;
|
||||
--perf-warning: #ffc107;
|
||||
--perf-danger: #dc3545;
|
||||
}
|
||||
.perf-debug-theme-dark {
|
||||
--perf-bg-primary: #1a1a1a;
|
||||
--perf-bg-secondary: #2d2d2d;
|
||||
--perf-bg-tertiary: #404040;
|
||||
--perf-text-primary: #ffffff;
|
||||
--perf-text-secondary: #b0b0b0;
|
||||
--perf-border: #404040;
|
||||
--perf-accent: #0d6efd;
|
||||
--perf-success: #198754;
|
||||
--perf-warning: #fd7e14;
|
||||
--perf-danger: #dc3545;
|
||||
}
|
||||
.perf-debug-container {
|
||||
color-scheme: light dark;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
color: var(--perf-text-primary, black);
|
||||
}
|
||||
.perf-debug-container * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.perf-debug-toggle {
|
||||
background: var(--perf-accent);
|
||||
color: var(--perf-bg-primary);
|
||||
border: none;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.perf-debug-toggle:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.2);
|
||||
}
|
||||
.perf-debug-modal {
|
||||
background: var(--perf-bg-primary);
|
||||
border: 1px solid var(--perf-border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.15);
|
||||
color: var(--perf-text-primary);
|
||||
backdrop-filter: blur(10px);
|
||||
min-width: 400px;
|
||||
max-width: 80vw;
|
||||
}
|
||||
.perf-debug-header {
|
||||
background: var(--perf-bg-secondary);
|
||||
padding: 16px 20px;
|
||||
border-radius: 12px 12px 0 0;
|
||||
border-bottom: 1px solid var(--perf-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.perf-debug-title {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.perf-debug-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.perf-debug-theme-toggle {
|
||||
background: var(--perf-bg-tertiary);
|
||||
color: var(--perf-text-primary);
|
||||
border: 1px solid var(--perf-border);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.perf-debug-theme-toggle:hover {
|
||||
background: var(--perf-accent);
|
||||
color: var(--perf-bg-primary);
|
||||
}
|
||||
.perf-debug-close {
|
||||
background: var(--perf-danger);
|
||||
color: white;
|
||||
border: none;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.perf-debug-close:hover {
|
||||
background: #c82333;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.perf-debug-content {
|
||||
color: var(--perf-text-primary);
|
||||
}
|
||||
.perf-debug-content table {
|
||||
color: var(--perf-text-primary);
|
||||
background: var(--perf-bg-primary);
|
||||
}
|
||||
.perf-debug-content th {
|
||||
background: var(--perf-bg-secondary) !important;
|
||||
color: var(--perf-text-primary) !important;
|
||||
border-color: var(--perf-border) !important;
|
||||
}
|
||||
.perf-debug-content td {
|
||||
background: var(--perf-bg-primary) !important;
|
||||
color: var(--perf-text-primary) !important;
|
||||
border-color: var(--perf-border) !important;
|
||||
}
|
||||
.perf-debug-content tr:nth-child(even) td {
|
||||
background: var(--perf-bg-secondary) !important;
|
||||
}
|
||||
</style>
|
||||
<div id=\"performance-debug-report\" class=\"perf-debug-container perf-debug-theme-light\" style=\"position: relative; z-index: 999999;\">
|
||||
<div style=\"position: fixed; bottom: 20px; right: 20px; z-index: 1000000;\">
|
||||
<button onclick=\"togglePerformanceReport()\" class=\"perf-debug-toggle\">
|
||||
<span>📊</span>
|
||||
<span>Performance</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id=\"performance-report-content\" class=\"perf-debug-modal\" style=\"display: none; position: fixed; top: 20px; right: 20px; z-index: 1000000; max-height: 80vh; overflow-y: auto;\">
|
||||
<div class=\"perf-debug-header\">
|
||||
<h3 class=\"perf-debug-title\">
|
||||
<span>⚡</span>
|
||||
<span>Performance Debug</span>
|
||||
</h3>
|
||||
<div class=\"perf-debug-controls\">
|
||||
<button onclick=\"toggleTheme()\" class=\"perf-debug-theme-toggle\" title=\"Toggle Dark/Light Mode\">
|
||||
<span id=\"theme-icon\">🌙</span>
|
||||
</button>
|
||||
<button onclick=\"closePerformanceReport()\" class=\"perf-debug-close\">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\"perf-debug-content\">
|
||||
{$reportHtml}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function togglePerformanceReport() {
|
||||
const content = document.getElementById('performance-report-content');
|
||||
content.style.display = content.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function closePerformanceReport() {
|
||||
document.getElementById('performance-report-content').style.display = 'none';
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const container = document.getElementById('performance-debug-report');
|
||||
const themeIcon = document.getElementById('theme-icon');
|
||||
const isDark = container.classList.contains('perf-debug-theme-dark');
|
||||
|
||||
if (isDark) {
|
||||
container.classList.remove('perf-debug-theme-dark');
|
||||
container.classList.add('perf-debug-theme-light');
|
||||
themeIcon.textContent = '🌙';
|
||||
localStorage.setItem('perf-debug-theme', 'light');
|
||||
} else {
|
||||
container.classList.remove('perf-debug-theme-light');
|
||||
container.classList.add('perf-debug-theme-dark');
|
||||
themeIcon.textContent = '☀️';
|
||||
localStorage.setItem('perf-debug-theme', 'dark');
|
||||
}
|
||||
}
|
||||
|
||||
// Load saved theme preference
|
||||
(function() {
|
||||
const savedTheme = localStorage.getItem('perf-debug-theme');
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const container = document.getElementById('performance-debug-report');
|
||||
const themeIcon = document.getElementById('theme-icon');
|
||||
|
||||
const shouldUseDark = savedTheme === 'dark' || (!savedTheme && prefersDark);
|
||||
|
||||
if (shouldUseDark) {
|
||||
container.classList.remove('perf-debug-theme-light');
|
||||
container.classList.add('perf-debug-theme-dark');
|
||||
themeIcon.textContent = '☀️';
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<!-- End Performance Debug Report -->
|
||||
";
|
||||
}
|
||||
|
||||
private function addPerformanceHeaders(Headers $headers): Headers
|
||||
{
|
||||
$data = $this->reporter->generateReport('array');
|
||||
$summary = $data['summary'];
|
||||
|
||||
$newHeaders = $headers
|
||||
->with('X-Performance-Time', sprintf('%.2f ms', $summary['total_request_time_ms']))
|
||||
->with('X-Performance-Memory', $this->formatBytes($summary['total_request_memory_bytes']))
|
||||
->with('X-Performance-Peak-Memory', $this->formatBytes($summary['peak_memory_bytes']))
|
||||
->with('X-Performance-Metrics-Count', (string) $summary['metrics_count']);
|
||||
|
||||
// Add category summaries
|
||||
foreach ($data['categories'] as $categoryName => $categoryData) {
|
||||
$headerName = 'X-Performance-' . ucfirst($categoryName);
|
||||
$headerValue = sprintf('%.2f ms (%d calls)', $categoryData['total_time_ms'], $categoryData['total_calls']);
|
||||
$newHeaders = $newHeaders->with($headerName, $headerValue);
|
||||
}
|
||||
|
||||
return $newHeaders;
|
||||
}
|
||||
|
||||
private function formatBytes(int $bytes): string
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB'];
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
|
||||
$bytes /= (1 << (10 * $pow));
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$pow];
|
||||
}
|
||||
|
||||
private function generateSessionDebugInfo(RequestStateManager $stateManager): string
|
||||
{
|
||||
$session = $stateManager->get('session');
|
||||
|
||||
if (! $session instanceof SessionInterface) {
|
||||
return '<details style="margin: 20px;">
|
||||
<summary style="cursor: pointer; padding: 12px 16px; background: var(--perf-bg-secondary); border: 1px solid var(--perf-border); border-radius: 8px; font-weight: 600; color: var(--perf-text-primary);">
|
||||
<span style="margin-right: 8px;">🔐</span>Session Debug Information
|
||||
</summary>
|
||||
<div style="padding: 20px; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; border-radius: 4px; margin-top: 10px;">
|
||||
<strong>⚠️ Session Debug:</strong> No session found in RequestStateManager
|
||||
</div>
|
||||
</details>';
|
||||
}
|
||||
|
||||
$sessionData = $session->all();
|
||||
$sessionId = $session->id->toString();
|
||||
|
||||
// Count form data and validation errors
|
||||
$formDataCount = isset($sessionData['__form']) ? count($sessionData['__form']) : 0;
|
||||
$validationErrorCount = isset($sessionData['__validation']) ? count($sessionData['__validation']) : 0;
|
||||
|
||||
// Build summary text
|
||||
$summaryText = '🔐 Session Debug';
|
||||
if ($formDataCount > 0) {
|
||||
$summaryText .= ' • 📝 ' . $formDataCount . ' form(s)';
|
||||
}
|
||||
if ($validationErrorCount > 0) {
|
||||
$summaryText .= ' • ❌ ' . $validationErrorCount . ' error(s)';
|
||||
}
|
||||
|
||||
// Format session data as HTML
|
||||
$html = '<details style="margin: 20px;">
|
||||
<summary style="cursor: pointer; padding: 12px 16px; background: var(--perf-bg-secondary); border: 1px solid var(--perf-border); border-radius: 8px; font-weight: 600; color: var(--perf-text-primary);">
|
||||
' . $summaryText . '
|
||||
</summary>
|
||||
<div style="padding: 20px; background: var(--perf-bg-secondary); border: 1px solid var(--perf-border); border-radius: 0 0 8px 8px; margin-top: -1px;">
|
||||
<table style="width: 100%; border-collapse: collapse; font-family: monospace; font-size: 12px;">
|
||||
<tr>
|
||||
<td style="padding: 8px; background: var(--perf-bg-tertiary); font-weight: bold; width: 200px;">Session ID</td>
|
||||
<td style="padding: 8px; background: var(--perf-bg-primary); word-break: break-all;">' . htmlspecialchars($sessionId) . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; background: var(--perf-bg-tertiary); font-weight: bold; vertical-align: top;">Session Data</td>
|
||||
<td style="padding: 8px; background: var(--perf-bg-primary);">
|
||||
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">' . htmlspecialchars(json_encode($sessionData, JSON_PRETTY_PRINT) ?: 'Unable to encode') . '</pre>
|
||||
</td>
|
||||
</tr>';
|
||||
|
||||
// Check for form data specifically
|
||||
if (isset($sessionData['__form'])) {
|
||||
$html .= '<tr>
|
||||
<td style="padding: 8px; background: var(--perf-bg-tertiary); font-weight: bold; vertical-align: top;">📝 Form Data</td>
|
||||
<td style="padding: 8px; background: var(--perf-bg-primary);">
|
||||
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">' . htmlspecialchars(json_encode($sessionData['__form'], JSON_PRETTY_PRINT) ?: 'Unable to encode') . '</pre>
|
||||
</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
// Check for validation errors
|
||||
if (isset($sessionData['__validation'])) {
|
||||
$html .= '<tr>
|
||||
<td style="padding: 8px; background: var(--perf-bg-tertiary); font-weight: bold; vertical-align: top;">❌ Validation Errors</td>
|
||||
<td style="padding: 8px; background: var(--perf-bg-primary);">
|
||||
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">' . htmlspecialchars(json_encode($sessionData['__validation'], JSON_PRETTY_PRINT) ?: 'Unable to encode') . '</pre>
|
||||
</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
$html .= '</table>
|
||||
</div>
|
||||
</details>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate hierarchical performance report HTML
|
||||
*/
|
||||
private function generateHierarchicalReport(): string
|
||||
{
|
||||
if (! $this->collector instanceof EnhancedPerformanceCollector) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$hierarchicalData = $this->collector->getHierarchicalReport();
|
||||
$executionTree = $this->collector->getExecutionTree();
|
||||
$summary = $this->collector->getNestedSummary();
|
||||
|
||||
$html = '
|
||||
<details style="margin: 20px;" open>
|
||||
<summary style="cursor: pointer; padding: 12px 16px; background: var(--perf-bg-secondary); border: 1px solid var(--perf-border); border-radius: 8px; font-weight: 600; color: var(--perf-text-primary);">
|
||||
<span style="margin-right: 8px;">🌳</span>Performance Execution Tree
|
||||
<span style="font-size: 11px; opacity: 0.7; margin-left: 8px;">
|
||||
• Max Depth: ' . $summary['max_depth'] . '
|
||||
• Total: ' . $summary['total_operations'] . ' ops
|
||||
• Active: ' . $summary['active_operations'] . ' ops
|
||||
</span>
|
||||
</summary>
|
||||
<div style="padding: 20px; background: var(--perf-bg-secondary); border: 1px solid var(--perf-border); border-radius: 0 0 8px 8px; margin-top: -1px;">
|
||||
|
||||
<!-- Execution Tree -->
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h4 style="margin: 0 0 10px 0; color: var(--perf-text-primary); font-size: 13px; font-weight: 600;">
|
||||
📊 Execution Tree (Text View)
|
||||
</h4>
|
||||
<pre style="
|
||||
background: var(--perf-bg-primary);
|
||||
border: 1px solid var(--perf-border);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
font-family: \'SF Mono\', Monaco, \'Cascadia Code\', \'Roboto Mono\', Consolas, \'Courier New\', monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: var(--perf-text-primary);
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
">' . htmlspecialchars($executionTree) . '</pre>
|
||||
</div>
|
||||
|
||||
<!-- Hierarchical Table -->
|
||||
<div>
|
||||
<h4 style="margin: 0 0 10px 0; color: var(--perf-text-primary); font-size: 13px; font-weight: 600;">
|
||||
📋 Hierarchical Operations (Table View)
|
||||
</h4>
|
||||
<div style="overflow-x: auto;">
|
||||
' . $this->buildHierarchicalTable($hierarchicalData['operations'] ?? []) . '
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Call Stack Info -->
|
||||
' . $this->buildCallStackInfo() . '
|
||||
|
||||
</div>
|
||||
</details>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build hierarchical table from operations data
|
||||
*/
|
||||
private function buildHierarchicalTable(array $operations): string
|
||||
{
|
||||
if (empty($operations)) {
|
||||
return '<p style="color: var(--perf-text-secondary); font-style: italic;">No hierarchical operations recorded.</p>';
|
||||
}
|
||||
|
||||
$html = '
|
||||
<table style="
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: \'SF Mono\', Monaco, \'Cascadia Code\', \'Roboto Mono\', Consolas, \'Courier New\', monospace;
|
||||
font-size: 11px;
|
||||
background: var(--perf-bg-primary);
|
||||
border: 1px solid var(--perf-border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
">
|
||||
<thead>
|
||||
<tr style="background: var(--perf-bg-tertiary);">
|
||||
<th style="padding: 10px 12px; text-align: left; font-weight: 600; color: var(--perf-text-primary); border-bottom: 1px solid var(--perf-border);">Operation</th>
|
||||
<th style="padding: 10px 12px; text-align: right; font-weight: 600; color: var(--perf-text-primary); border-bottom: 1px solid var(--perf-border);">Total Time</th>
|
||||
<th style="padding: 10px 12px; text-align: right; font-weight: 600; color: var(--perf-text-primary); border-bottom: 1px solid var(--perf-border);">Self Time</th>
|
||||
<th style="padding: 10px 12px; text-align: center; font-weight: 600; color: var(--perf-text-primary); border-bottom: 1px solid var(--perf-border);">Children</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>';
|
||||
|
||||
$html .= $this->buildHierarchicalRows($operations);
|
||||
|
||||
$html .= '
|
||||
</tbody>
|
||||
</table>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build hierarchical table rows recursively
|
||||
*/
|
||||
private function buildHierarchicalRows(array $operations, int $depth = 0): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
foreach ($operations as $operation) {
|
||||
$indent = str_repeat(' ', $depth * 2);
|
||||
$depthIndicator = $depth > 0 ? str_repeat('┗', 1) . '━ ' : '';
|
||||
|
||||
$totalTime = $operation['total_time_ms'] ?? 0;
|
||||
$selfTime = $operation['self_time_ms'] ?? 0;
|
||||
$childrenCount = count($operation['children'] ?? []);
|
||||
|
||||
// Color coding for performance
|
||||
$timeColor = $this->getTimeColor($totalTime);
|
||||
$selfTimeColor = $this->getTimeColor($selfTime);
|
||||
|
||||
$html .= '
|
||||
<tr style="' . ($depth % 2 === 1 ? 'background: var(--perf-bg-secondary);' : 'background: var(--perf-bg-primary);') . '">
|
||||
<td style="padding: 8px 12px; color: var(--perf-text-primary); border-bottom: 1px solid var(--perf-border);">
|
||||
<span style="font-family: monospace;">' . $indent . $depthIndicator . '</span>
|
||||
<strong style="color: var(--perf-text-primary);">' . htmlspecialchars($operation['key']) . '</strong>
|
||||
' . ($depth > 0 ? '<span style="opacity: 0.6; font-size: 10px; margin-left: 4px;">(depth: ' . $depth . ')</span>' : '') . '
|
||||
</td>
|
||||
<td style="padding: 8px 12px; text-align: right; color: ' . $timeColor . '; font-weight: 600; border-bottom: 1px solid var(--perf-border);">
|
||||
' . ($totalTime > 0 ? number_format($totalTime, 2) . 'ms' : '—') . '
|
||||
</td>
|
||||
<td style="padding: 8px 12px; text-align: right; color: ' . $selfTimeColor . '; font-weight: 600; border-bottom: 1px solid var(--perf-border);">
|
||||
' . ($selfTime > 0 ? number_format($selfTime, 2) . 'ms' : '—') . '
|
||||
</td>
|
||||
<td style="padding: 8px 12px; text-align: center; color: var(--perf-text-secondary); border-bottom: 1px solid var(--perf-border);">
|
||||
' . ($childrenCount > 0 ? $childrenCount : '—') . '
|
||||
</td>
|
||||
</tr>';
|
||||
|
||||
// Recursively add children
|
||||
if (! empty($operation['children'])) {
|
||||
$html .= $this->buildHierarchicalRows($operation['children'], $depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for time values based on performance thresholds
|
||||
*/
|
||||
private function getTimeColor(float $timeMs): string
|
||||
{
|
||||
if ($timeMs >= 1000) {
|
||||
return 'var(--perf-danger)'; // Red for >= 1s
|
||||
} elseif ($timeMs >= 500) {
|
||||
return 'var(--perf-warning)'; // Orange for >= 500ms
|
||||
} elseif ($timeMs >= 100) {
|
||||
return '#fd7e14'; // Yellow for >= 100ms
|
||||
} else {
|
||||
return 'var(--perf-success)'; // Green for < 100ms
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build call stack information
|
||||
*/
|
||||
private function buildCallStackInfo(): string
|
||||
{
|
||||
if (! $this->collector instanceof EnhancedPerformanceCollector) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$callStack = $this->collector->getCallStack();
|
||||
$currentOp = $this->collector->getCurrentOperation();
|
||||
$depth = $this->collector->getCallStackDepth();
|
||||
|
||||
if (empty($callStack)) {
|
||||
return '
|
||||
<div style="margin-top: 20px; padding: 12px 16px; background: var(--perf-bg-tertiary); border: 1px solid var(--perf-border); border-radius: 6px;">
|
||||
<h4 style="margin: 0 0 8px 0; color: var(--perf-text-primary); font-size: 12px; font-weight: 600;">
|
||||
📚 Call Stack Status
|
||||
</h4>
|
||||
<p style="margin: 0; color: var(--perf-success); font-size: 11px;">
|
||||
✅ All operations completed (stack is empty)
|
||||
</p>
|
||||
</div>';
|
||||
}
|
||||
|
||||
$stackHtml = '
|
||||
<div style="margin-top: 20px; padding: 12px 16px; background: var(--perf-bg-tertiary); border: 1px solid var(--perf-border); border-radius: 6px;">
|
||||
<h4 style="margin: 0 0 8px 0; color: var(--perf-text-primary); font-size: 12px; font-weight: 600;">
|
||||
📚 Active Call Stack (Depth: ' . $depth . ')
|
||||
</h4>
|
||||
<div style="font-family: \'SF Mono\', Monaco, \'Cascadia Code\', \'Roboto Mono\', Consolas, \'Courier New\', monospace; font-size: 11px; line-height: 1.4;">';
|
||||
|
||||
foreach ($callStack as $i => $operation) {
|
||||
$indent = str_repeat(' ', $i);
|
||||
$isCurrentOp = $operation === $currentOp;
|
||||
$stackHtml .= '
|
||||
<div style="margin: 4px 0; color: var(--perf-text-primary);">
|
||||
' . $indent . '
|
||||
<span style="color: var(--perf-text-secondary);">' . ($i + 1) . '.</span>
|
||||
<strong style="color: ' . ($isCurrentOp ? 'var(--perf-accent)' : 'var(--perf-text-primary)') . ';">' . htmlspecialchars($operation) . '</strong>
|
||||
' . ($isCurrentOp ? '<span style="color: var(--perf-accent); font-size: 10px;"> ← CURRENT</span>' : '') . '
|
||||
<span style="opacity: 0.6; font-size: 10px; margin-left: 8px;">(active)</span>
|
||||
</div>';
|
||||
}
|
||||
|
||||
$stackHtml .= '</div></div>';
|
||||
|
||||
return $stackHtml;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Performance\Middleware;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
use App\Framework\Http\HttpMiddleware;
|
||||
use App\Framework\Http\MiddlewareContext;
|
||||
use App\Framework\Http\MiddlewarePriority;
|
||||
use App\Framework\Http\MiddlewarePriorityAttribute;
|
||||
use App\Framework\Http\Next;
|
||||
use App\Framework\Http\RequestStateManager;
|
||||
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
|
||||
use App\Framework\Performance\MemoryMonitor;
|
||||
use App\Framework\Performance\PerformanceCategory;
|
||||
use App\Framework\Performance\PerformanceConfig;
|
||||
|
||||
#[MiddlewarePriorityAttribute(MiddlewarePriority::FIRST)]
|
||||
final readonly class RequestPerformanceMiddleware implements HttpMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
private PerformanceCollectorInterface $collector,
|
||||
private PerformanceConfig $config
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
|
||||
{
|
||||
if (! $this->config->isTrackingEnabled(PerformanceCategory::SYSTEM)) {
|
||||
return $next($context);
|
||||
}
|
||||
|
||||
$request = $context->request;
|
||||
$path = $request->path ?? '/';
|
||||
|
||||
if ($this->config->shouldExcludePath($path)) {
|
||||
return $next($context);
|
||||
}
|
||||
|
||||
$requestKey = 'http_request';
|
||||
$performanceContext = [
|
||||
'method' => $request->method,
|
||||
'path' => $path,
|
||||
'user_agent' => $request->headers->getFirst('User-Agent', ''),
|
||||
'ip' => $request->server->getClientIp()->value,
|
||||
];
|
||||
|
||||
$this->collector->startTiming($requestKey, PerformanceCategory::SYSTEM, $performanceContext);
|
||||
|
||||
// Initialize memory monitoring
|
||||
$memoryMonitor = new MemoryMonitor();
|
||||
|
||||
// Memory usage before request
|
||||
$memoryBefore = $memoryMonitor->getCurrentMemory();
|
||||
$this->collector->recordMetric(
|
||||
'memory_before_request',
|
||||
PerformanceCategory::SYSTEM,
|
||||
$memoryBefore->toBytes(),
|
||||
array_merge($performanceContext, [
|
||||
'memory_human' => $memoryBefore->toHumanReadable(),
|
||||
'memory_percentage' => $memoryMonitor->getMemoryUsagePercentage(),
|
||||
])
|
||||
);
|
||||
|
||||
try {
|
||||
$result = $next($context);
|
||||
|
||||
// Record successful request
|
||||
$this->collector->increment('requests_total', PerformanceCategory::SYSTEM, 1, $performanceContext);
|
||||
$this->collector->increment('requests_success', PerformanceCategory::SYSTEM, 1, $performanceContext);
|
||||
|
||||
return $result;
|
||||
} catch (\Throwable $e) {
|
||||
// Record failed request
|
||||
$this->collector->increment('requests_total', PerformanceCategory::SYSTEM, 1, $performanceContext);
|
||||
$this->collector->increment('requests_failed', PerformanceCategory::SYSTEM, 1, $performanceContext);
|
||||
$this->collector->recordMetric(
|
||||
'request_error',
|
||||
PerformanceCategory::SYSTEM,
|
||||
1,
|
||||
array_merge($performanceContext, ['error' => $e::class])
|
||||
);
|
||||
|
||||
throw $e;
|
||||
} finally {
|
||||
$this->collector->endTiming($requestKey);
|
||||
|
||||
// Memory monitoring after request
|
||||
$memoryAfter = $memoryMonitor->getCurrentMemory();
|
||||
$memoryPeak = $memoryMonitor->getPeakMemory();
|
||||
$memoryDiff = $memoryAfter->greaterThan($memoryBefore)
|
||||
? $memoryAfter->subtract($memoryBefore)
|
||||
: Byte::zero();
|
||||
|
||||
$this->collector->recordMetric(
|
||||
'memory_after_request',
|
||||
PerformanceCategory::SYSTEM,
|
||||
$memoryAfter->toBytes(),
|
||||
array_merge($performanceContext, [
|
||||
'memory_human' => $memoryAfter->toHumanReadable(),
|
||||
'memory_percentage' => $memoryMonitor->getMemoryUsagePercentage(),
|
||||
])
|
||||
);
|
||||
|
||||
if ($memoryDiff->isNotEmpty()) {
|
||||
$this->collector->recordMetric(
|
||||
'memory_usage_request',
|
||||
PerformanceCategory::SYSTEM,
|
||||
$memoryDiff->toBytes(),
|
||||
array_merge($performanceContext, [
|
||||
'memory_diff_human' => $memoryDiff->toHumanReadable(),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
// Peak memory
|
||||
$this->collector->recordMetric(
|
||||
'memory_peak',
|
||||
PerformanceCategory::SYSTEM,
|
||||
$memoryPeak->toBytes(),
|
||||
array_merge($performanceContext, [
|
||||
'memory_peak_human' => $memoryPeak->toHumanReadable(),
|
||||
'is_approaching_limit' => $memoryMonitor->isMemoryLimitApproaching(),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Performance\Middleware;
|
||||
|
||||
use App\Framework\Http\HttpMiddleware;
|
||||
use App\Framework\Http\MiddlewareContext;
|
||||
use App\Framework\Http\MiddlewarePriority;
|
||||
use App\Framework\Http\MiddlewarePriorityAttribute;
|
||||
use App\Framework\Http\Next;
|
||||
use App\Framework\Http\RequestStateManager;
|
||||
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
|
||||
use App\Framework\Performance\PerformanceCategory;
|
||||
use App\Framework\Performance\PerformanceConfig;
|
||||
|
||||
#[MiddlewarePriorityAttribute(MiddlewarePriority::ROUTING, 10)]
|
||||
final readonly class RoutingPerformanceMiddleware implements HttpMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
private PerformanceCollectorInterface $collector,
|
||||
private PerformanceConfig $config
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
|
||||
{
|
||||
if (! $this->config->isTrackingEnabled(PerformanceCategory::ROUTING)) {
|
||||
return $next($context);
|
||||
}
|
||||
|
||||
$request = $context->request;
|
||||
$path = $request->path ?? '/';
|
||||
$method = $request->method;
|
||||
|
||||
$routingContext = [
|
||||
'method' => $method,
|
||||
'path' => $path,
|
||||
];
|
||||
|
||||
$routingKey = 'route_resolution';
|
||||
|
||||
$this->collector->startTiming($routingKey, PerformanceCategory::ROUTING, $routingContext);
|
||||
|
||||
// Count routing attempts
|
||||
$this->collector->increment('routing_attempts', PerformanceCategory::ROUTING, 1, $routingContext);
|
||||
|
||||
try {
|
||||
$result = $next($context);
|
||||
|
||||
// Check if route was found (this is framework-specific logic)
|
||||
$routeFound = $this->isRouteFound($result, $stateManager);
|
||||
|
||||
if ($routeFound) {
|
||||
$this->collector->increment('routes_found', PerformanceCategory::ROUTING, 1, $routingContext);
|
||||
|
||||
// Get route pattern if available
|
||||
$routePattern = $this->getRoutePattern($result, $stateManager);
|
||||
if ($routePattern) {
|
||||
$this->collector->increment(
|
||||
"route_pattern_" . md5($routePattern),
|
||||
PerformanceCategory::ROUTING,
|
||||
1,
|
||||
array_merge($routingContext, ['pattern' => $routePattern])
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$this->collector->increment('routes_not_found', PerformanceCategory::ROUTING, 1, $routingContext);
|
||||
}
|
||||
|
||||
return $result;
|
||||
} catch (\Throwable $e) {
|
||||
$this->collector->increment(
|
||||
'routing_errors',
|
||||
PerformanceCategory::ROUTING,
|
||||
1,
|
||||
array_merge($routingContext, ['error' => $e::class])
|
||||
);
|
||||
|
||||
throw $e;
|
||||
} finally {
|
||||
$this->collector->endTiming($routingKey);
|
||||
}
|
||||
}
|
||||
|
||||
private function isRouteFound(MiddlewareContext $context, RequestStateManager $stateManager): bool
|
||||
{
|
||||
// Check various indicators that a route was found
|
||||
|
||||
// Method 1: Check state manager
|
||||
if ($stateManager->has('route_found')) {
|
||||
return $stateManager->get('route_found');
|
||||
}
|
||||
|
||||
// Method 2: Check if controller info is available
|
||||
if ($stateManager->has('controller_info')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Method 3: Check response status (if not 404)
|
||||
if (isset($context->response) && $context->response->status->value !== 404) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Default: assume found if we got here without exceptions
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getRoutePattern(MiddlewareContext $context, RequestStateManager $stateManager): ?string
|
||||
{
|
||||
// Try to get route pattern from various sources
|
||||
|
||||
// Method 1: From state manager
|
||||
$routeData = $stateManager->get('route_data');
|
||||
if ($routeData && isset($routeData['pattern'])) {
|
||||
return $routeData['pattern'];
|
||||
}
|
||||
|
||||
// Method 2: From request route
|
||||
if (isset($context->request->route['pattern'])) {
|
||||
return $context->request->route['pattern'];
|
||||
}
|
||||
|
||||
// Method 3: From matched route
|
||||
$matchedRoute = $stateManager->get('matched_route');
|
||||
if ($matchedRoute && isset($matchedRoute['pattern'])) {
|
||||
return $matchedRoute['pattern'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user