feat(database): 添加用户角色权限系统及相关监控功能

- 创建用户表(users)包含基本信息和认证字段
- 创建角色表(roles)用于权限控制
- 创建权限表(permissions)定义系统权限
- 创建用户角色关联表(user_roles)建立用户与角色关系
- 创建角色权限关联表(role_permissions)建立角色与权限关系
- 创建迁移记录表(migrations)追踪数据库变更
- 添加AdminController提供管理员面板功能
- 实现系统监控、配置管理、缓存清理等功能
- 添加AOP切面编程支持的各种通知类型
- 实现告警管理AlertManager支持多渠道告警
- 添加文档注解接口规范
This commit is contained in:
Lawson
2026-04-08 17:00:28 +08:00
commit 2782d765fb
270 changed files with 107192 additions and 0 deletions

View File

@@ -0,0 +1,566 @@
<?php
declare(strict_types=1);
namespace Fendx\Debug;
use Fendx\Debug\Output\DebugOutputInterface;
use Fendx\Debug\Output\ConsoleOutput;
use Fendx\Debug\Collector\DataCollectorInterface;
use Fendx\Debug\Collector\RequestCollector;
use Fendx\Debug\Collector\MemoryCollector;
use Fendx\Debug\Collector\TimeCollector;
use Fendx\Debug\Collector\QueryCollector;
use Fendx\Debug\Formatter\DebugFormatter;
class Debugger
{
protected static ?self $instance = null;
protected bool $enabled = false;
protected array $collectors = [];
protected DebugOutputInterface $output;
protected DebugFormatter $formatter;
protected array $config = [];
protected float $startTime;
protected int $startMemory;
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->output = new ConsoleOutput();
$this->formatter = new DebugFormatter();
$this->startTime = microtime(true);
$this->startMemory = memory_get_usage(true);
$this->initializeCollectors();
}
/**
* Get debugger instance.
*/
public static function getInstance(array $config = []): self
{
if (self::$instance === null) {
self::$instance = new self($config);
}
return self::$instance;
}
/**
* Enable debugger.
*/
public function enable(): void
{
$this->enabled = true;
foreach ($this->collectors as $collector) {
$collector->enable();
}
// Register shutdown function
register_shutdown_function([$this, 'shutdown']);
}
/**
* Disable debugger.
*/
public function disable(): void
{
$this->enabled = false;
foreach ($this->collectors as $collector) {
$collector->disable();
}
}
/**
* Check if debugger is enabled.
*/
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* Add data collector.
*/
public function addCollector(DataCollectorInterface $collector): void
{
$this->collectors[$collector->getName()] = $collector;
if ($this->enabled) {
$collector->enable();
}
}
/**
* Get data collector.
*/
public function getCollector(string $name): ?DataCollectorInterface
{
return $this->collectors[$name] ?? null;
}
/**
* Get all collectors.
*/
public function getCollectors(): array
{
return $this->collectors;
}
/**
* Log debug information.
*/
public function log(string $message, array $context = [], string $level = 'info'): void
{
if (!$this->enabled) {
return;
}
$data = [
'message' => $message,
'context' => $context,
'level' => $level,
'timestamp' => microtime(true),
'memory' => memory_get_usage(true),
'file' => $this->getCallerFile(),
'line' => $this->getCallerLine()
];
$this->output->write($data);
}
/**
* Log variable dump.
*/
public function dump(mixed $variable, string $label = ''): void
{
if (!$this->enabled) {
return;
}
$data = [
'type' => 'dump',
'label' => $label,
'variable' => $variable,
'backtrace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5),
'timestamp' => microtime(true),
'memory' => memory_get_usage(true)
];
$this->output->write($data);
}
/**
* Measure execution time.
*/
public function measure(string $label, callable $callback): mixed
{
if (!$this->enabled) {
return $callback();
}
$startTime = microtime(true);
$startMemory = memory_get_usage(true);
try {
$result = $callback();
$endTime = microtime(true);
$endMemory = memory_get_usage(true);
$this->log("Performance: {$label}", [
'execution_time' => ($endTime - $startTime) * 1000, // ms
'memory_usage' => $endMemory - $startMemory,
'peak_memory' => memory_get_peak_usage(true)
]);
return $result;
} catch (\Exception $e) {
$this->log("Error in measurement: {$label}", [
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
], 'error');
throw $e;
}
}
/**
* Start timer.
*/
public function startTimer(string $name): void
{
if (!$this->enabled) {
return;
}
$timeCollector = $this->getCollector('time');
if ($timeCollector) {
$timeCollector->startTimer($name);
}
}
/**
* End timer.
*/
public function endTimer(string $name): float
{
if (!$this->enabled) {
return 0;
}
$timeCollector = $this->getCollector('time');
if ($timeCollector) {
return $timeCollector->endTimer($name);
}
return 0;
}
/**
* Add checkpoint.
*/
public function checkpoint(string $name): void
{
if (!$this->enabled) {
return;
}
$data = [
'type' => 'checkpoint',
'name' => $name,
'timestamp' => microtime(true),
'memory' => memory_get_usage(true),
'peak_memory' => memory_get_peak_usage(true),
'execution_time' => (microtime(true) - $this->startTime) * 1000
];
$this->output->write($data);
}
/**
* Get debug summary.
*/
public function getSummary(): array
{
$summary = [
'enabled' => $this->enabled,
'start_time' => $this->startTime,
'total_time' => (microtime(true) - $this->startTime) * 1000,
'start_memory' => $this->startMemory,
'current_memory' => memory_get_usage(true),
'peak_memory' => memory_get_peak_usage(true),
'memory_used' => memory_get_usage(true) - $this->startMemory,
'collectors' => []
];
foreach ($this->collectors as $name => $collector) {
$summary['collectors'][$name] = $collector->collect();
}
return $summary;
}
/**
* Generate debug report.
*/
public function generateReport(): string
{
$summary = $this->getSummary();
return $this->formatter->formatReport($summary);
}
/**
* Save debug report to file.
*/
public function saveReport(string $filename): bool
{
$report = $this->generateReport();
return file_put_contents($filename, $report) !== false;
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Set output handler.
*/
public function setOutput(DebugOutputInterface $output): void
{
$this->output = $output;
}
/**
* Set formatter.
*/
public function setFormatter(DebugFormatter $formatter): void
{
$this->formatter = $formatter;
}
/**
* Clear all collected data.
*/
public function clear(): void
{
foreach ($this->collectors as $collector) {
$collector->clear();
}
}
/**
* Reset debugger.
*/
public function reset(): void
{
$this->clear();
$this->startTime = microtime(true);
$this->startMemory = memory_get_usage(true);
}
/**
* Shutdown handler.
*/
public function shutdown(): void
{
if (!$this->enabled) {
return;
}
// Collect final data
foreach ($this->collectors as $collector) {
$collector->collect();
}
// Generate and output report
if ($this->config['auto_report']) {
$this->output->write([
'type' => 'report',
'summary' => $this->getSummary(),
'formatted' => $this->generateReport()
]);
}
// Save report if configured
if ($this->config['save_report']) {
$filename = $this->config['report_file'] ?? 'debug_report_' . date('Y-m-d_H-i-s') . '.log';
$this->saveReport($filename);
}
}
/**
* Initialize default collectors.
*/
protected function initializeCollectors(): void
{
$this->addCollector(new RequestCollector());
$this->addCollector(new MemoryCollector());
$this->addCollector(new TimeCollector());
$this->addCollector(new QueryCollector());
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'enabled' => false,
'auto_report' => true,
'save_report' => false,
'report_file' => null,
'max_depth' => 10,
'max_string_length' => 1000,
'collect_backtrace' => true,
'collect_server_vars' => true,
'collect_session_vars' => false,
'collect_cookie_vars' => false,
'collect_post_vars' => true,
'collect_get_vars' => true,
'collect_files_vars' => true,
'collect_headers' => true,
'collect_environment' => false,
'log_slow_queries' => true,
'slow_query_threshold' => 100, // ms
'log_memory_usage' => true,
'memory_threshold' => 50 * 1024 * 1024, // 50MB
'log_execution_time' => true,
'execution_time_threshold' => 1000, // ms
];
}
/**
* Get caller file.
*/
protected function getCallerFile(): string
{
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 4);
foreach ($backtrace as $trace) {
if (isset($trace['file']) &&
!str_contains($trace['file'], 'Debugger.php') &&
!str_contains($trace['file'], 'vendor')) {
return $trace['file'];
}
}
return 'unknown';
}
/**
* Get caller line.
*/
protected function getCallerLine(): int
{
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 4);
foreach ($backtrace as $trace) {
if (isset($trace['file']) &&
!str_contains($trace['file'], 'Debugger.php') &&
!str_contains($trace['file'], 'vendor')) {
return $trace['line'] ?? 0;
}
}
return 0;
}
/**
* Check if debugger should collect based on configuration.
*/
protected function shouldCollect(string $type): bool
{
$configKey = "collect_{$type}_vars";
return $this->config[$configKey] ?? true;
}
/**
* Format bytes to human readable format.
*/
protected function formatBytes(int $bytes): 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, 2) . ' ' . $units[$pow];
}
/**
* Get current memory usage formatted.
*/
public function getMemoryUsage(): string
{
return $this->formatBytes(memory_get_usage(true));
}
/**
* Get peak memory usage formatted.
*/
public function getPeakMemoryUsage(): string
{
return $this->formatBytes(memory_get_peak_usage(true));
}
/**
* Get execution time formatted.
*/
public function getExecutionTime(): string
{
$time = (microtime(true) - $this->startTime) * 1000;
return round($time, 2) . ' ms';
}
/**
* Check if memory limit is exceeded.
*/
public function isMemoryLimitExceeded(): bool
{
$current = memory_get_usage(true);
$limit = $this->config['memory_threshold'];
return $current > $limit;
}
/**
* Check if execution time limit is exceeded.
*/
public function isExecutionTimeExceeded(): bool
{
$current = (microtime(true) - $this->startTime) * 1000;
$limit = $this->config['execution_time_threshold'];
return $current > $limit;
}
/**
* Get system information.
*/
public function getSystemInfo(): array
{
return [
'php_version' => PHP_VERSION,
'php_sapi' => PHP_SAPI,
'os' => PHP_OS,
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'upload_max_filesize' => ini_get('upload_max_filesize'),
'post_max_size' => ini_get('post_max_size'),
'timezone' => date_default_timezone_get(),
'server_time' => date('Y-m-d H:i:s'),
'load_average' => function_exists('sys_getloadavg') ? sys_getloadavg() : null,
];
}
/**
* Get request information.
*/
public function getRequestInfo(): array
{
$requestCollector = $this->getCollector('request');
return $requestCollector ? $requestCollector->collect() : [];
}
/**
* Get query information.
*/
public function getQueryInfo(): array
{
$queryCollector = $this->getCollector('query');
return $queryCollector ? $queryCollector->collect() : [];
}
/**
* Get performance metrics.
*/
public function getPerformanceMetrics(): array
{
return [
'execution_time' => $this->getExecutionTime(),
'memory_usage' => $this->getMemoryUsage(),
'peak_memory' => $this->getPeakMemoryUsage(),
'memory_used' => $this->formatBytes(memory_get_usage(true) - $this->startMemory),
'queries_count' => count($this->getQueryInfo()['queries'] ?? []),
'slow_queries' => array_filter($this->getQueryInfo()['queries'] ?? [], fn($q) => $q['time'] > $this->config['slow_query_threshold']),
];
}
}

View File

@@ -0,0 +1,669 @@
<?php
declare(strict_types=1);
namespace Fendx\Debug;
use Fendx\Debug\Memory\MemorySnapshot;
use Fendx\Debug\Memory\MemoryLeakDetector;
use Fendx\Debug\Memory\MemoryUsageTracker;
class MemoryAnalyzer
{
protected static ?self $instance = null;
protected bool $enabled = false;
protected array $snapshots = [];
protected MemoryUsageTracker $tracker;
protected MemoryLeakDetector $leakDetector;
protected array $config = [];
protected int $baselineMemory;
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->tracker = new MemoryUsageTracker();
$this->leakDetector = new MemoryLeakDetector();
$this->baselineMemory = memory_get_usage(true);
}
/**
* Get memory analyzer instance.
*/
public static function getInstance(array $config = []): self
{
if (self::$instance === null) {
self::$instance = new self($config);
}
return self::$instance;
}
/**
* Enable memory analyzer.
*/
public function enable(): void
{
$this->enabled = true;
$this->tracker->enable();
$this->leakDetector->enable();
}
/**
* Disable memory analyzer.
*/
public function disable(): void
{
$this->enabled = false;
$this->tracker->disable();
$this->leakDetector->disable();
}
/**
* Check if analyzer is enabled.
*/
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* Take memory snapshot.
*/
public function snapshot(string $name, array $context = []): MemorySnapshot
{
if (!$this->enabled) {
throw new \RuntimeException('Memory analyzer is not enabled');
}
$snapshot = new MemorySnapshot($name, $context);
$snapshot->capture();
$this->snapshots[$name] = $snapshot;
return $snapshot;
}
/**
* Get memory snapshot by name.
*/
public function getSnapshot(string $name): ?MemorySnapshot
{
return $this->snapshots[$name] ?? null;
}
/**
* Get all snapshots.
*/
public function getSnapshots(): array
{
return $this->snapshots;
}
/**
* Compare two snapshots.
*/
public function compare(string $from, string $to): array
{
$fromSnapshot = $this->getSnapshot($from);
$toSnapshot = $this->getSnapshot($to);
if (!$fromSnapshot || !$toSnapshot) {
throw new \InvalidArgumentException('Both snapshots must exist');
}
return [
'from' => $fromSnapshot->getName(),
'to' => $toSnapshot->getName(),
'memory_diff' => $toSnapshot->getMemoryUsage() - $fromSnapshot->getMemoryUsage(),
'memory_diff_percent' => $this->calculatePercentageDiff(
$fromSnapshot->getMemoryUsage(),
$toSnapshot->getMemoryUsage()
),
'peak_memory_diff' => $toSnapshot->getPeakMemory() - $fromSnapshot->getPeakMemory(),
'objects_diff' => $toSnapshot->getObjectCount() - $fromSnapshot->getObjectCount(),
'time_diff' => $toSnapshot->getTimestamp() - $fromSnapshot->getTimestamp()
];
}
/**
* Get memory usage trend.
*/
public function getTrend(): array
{
if (empty($this->snapshots)) {
return [];
}
$trend = [];
$previousMemory = $this->baselineMemory;
foreach ($this->snapshots as $name => $snapshot) {
$currentMemory = $snapshot->getMemoryUsage();
$diff = $currentMemory - $previousMemory;
$trend[] = [
'name' => $name,
'timestamp' => $snapshot->getTimestamp(),
'memory_usage' => $currentMemory,
'memory_diff' => $diff,
'memory_diff_percent' => $this->calculatePercentageDiff($previousMemory, $currentMemory),
'peak_memory' => $snapshot->getPeakMemory(),
'object_count' => $snapshot->getObjectCount()
];
$previousMemory = $currentMemory;
}
return $trend;
}
/**
* Detect memory leaks.
*/
public function detectLeaks(): array
{
if (!$this->enabled) {
return [];
}
return $this->leakDetector->detect($this->snapshots);
}
/**
* Get memory usage statistics.
*/
public function getStatistics(): array
{
if (empty($this->snapshots)) {
return [
'current' => memory_get_usage(true),
'peak' => memory_get_peak_usage(true),
'baseline' => $this->baselineMemory,
'growth' => memory_get_usage(true) - $this->baselineMemory,
'snapshots_count' => 0
];
}
$current = memory_get_usage(true);
$peak = memory_get_peak_usage(true);
$growth = $current - $this->baselineMemory;
$memoryUsages = array_map(fn($s) => $s->getMemoryUsage(), $this->snapshots);
$objectCounts = array_map(fn($s) => $s->getObjectCount(), $this->snapshots);
return [
'current' => $current,
'peak' => $peak,
'baseline' => $this->baselineMemory,
'growth' => $growth,
'growth_percent' => $this->calculatePercentageDiff($this->baselineMemory, $current),
'snapshots_count' => count($this->snapshots),
'min_memory' => min($memoryUsages),
'max_memory' => max($memoryUsages),
'avg_memory' => array_sum($memoryUsages) / count($memoryUsages),
'min_objects' => min($objectCounts),
'max_objects' => max($objectCounts),
'avg_objects' => array_sum($objectCounts) / count($objectCounts),
'memory_efficiency' => $this->calculateMemoryEfficiency(),
'leak_detected' => !empty($this->detectLeaks())
];
}
/**
* Get memory usage by type.
*/
public function getUsageByType(): array
{
if (empty($this->snapshots)) {
return [];
}
$latest = end($this->snapshots);
return $latest->getUsageByType();
}
/**
* Get largest memory consumers.
*/
public function getLargestConsumers(int $limit = 10): array
{
if (empty($this->snapshots)) {
return [];
}
$latest = end($this->snapshots);
return $latest->getLargestConsumers($limit);
}
/**
* Generate memory analysis report.
*/
public function generateReport(): array
{
$report = [
'summary' => $this->getStatistics(),
'trend' => $this->getTrend(),
'snapshots' => [],
'leaks' => $this->detectLeaks(),
'usage_by_type' => $this->getUsageByType(),
'largest_consumers' => $this->getLargestConsumers(),
'recommendations' => $this->generateRecommendations(),
'generated_at' => time()
];
foreach ($this->snapshots as $name => $snapshot) {
$report['snapshots'][$name] = $snapshot->toArray();
}
return $report;
}
/**
* Save memory analysis report.
*/
public function saveReport(string $filename): bool
{
$report = $this->generateReport();
$json = json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
return file_put_contents($filename, $json) !== false;
}
/**
* Export memory data.
*/
public function export(string $format = 'json'): string
{
$report = $this->generateReport();
switch (strtolower($format)) {
case 'json':
return json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
case 'csv':
return $this->exportToCsv($report);
case 'html':
return $this->exportToHtml($report);
default:
throw new \InvalidArgumentException("Unsupported export format: {$format}");
}
}
/**
* Export to CSV format.
*/
protected function exportToCsv(array $report): string
{
$csv = "Snapshot,Timestamp,Memory Usage (bytes),Peak Memory (bytes),Object Count\n";
foreach ($report['snapshots'] as $name => $snapshot) {
$csv .= sprintf(
"%s,%d,%d,%d,%d\n",
$name,
$snapshot['timestamp'],
$snapshot['memory_usage'],
$snapshot['peak_memory'],
$snapshot['object_count']
);
}
return $csv;
}
/**
* Export to HTML format.
*/
protected function exportToHtml(array $report): string
{
$html = '<!DOCTYPE html><html><head><title>Memory Analysis Report</title>';
$html .= '<style>body{font-family:Arial,sans-serif;margin:20px;}';
$html .= 'table{border-collapse:collapse;width:100%;margin-bottom:20px;}';
$html .= 'th,td{border:1px solid #ddd;padding:8px;text-align:left;}';
$html .= 'th{background-color:#f2f2f2;}';
$html .= '.warning{color:#f39c12;}.error{color:#e74c3c;}.success{color:#27ae60;}</style>';
$html .= '</head><body>';
$html .= '<h1>Memory Analysis Report</h1>';
// Summary section
$html .= '<h2>Summary</h2>';
$html .= '<table><tr><th>Metric</th><th>Value</th></tr>';
foreach ($report['summary'] as $key => $value) {
if (is_array($value)) {
$value = json_encode($value);
} elseif (is_bool($value)) {
$value = $value ? 'Yes' : 'No';
}
$class = '';
if (str_contains($key, 'leak') && $value) {
$class = 'error';
} elseif (str_contains($key, 'growth') && $value > 0) {
$class = 'warning';
}
$html .= "<tr><td>{$key}</td><td class='{$class}'>{$value}</td></tr>";
}
$html .= '</table>';
// Snapshots section
$html .= '<h2>Memory Snapshots</h2>';
$html .= '<table><tr><th>Name</th><th>Memory (MB)</th><th>Peak (MB)</th><th>Objects</th></tr>';
foreach ($report['snapshots'] as $name => $snapshot) {
$html .= sprintf(
"<tr><td>%s</td><td>%.2f</td><td>%.2f</td><td>%d</td></tr>",
$name,
$snapshot['memory_usage'] / 1024 / 1024,
$snapshot['peak_memory'] / 1024 / 1024,
$snapshot['object_count']
);
}
$html .= '</table>';
// Recommendations section
if (!empty($report['recommendations'])) {
$html .= '<h2>Recommendations</h2>';
$html .= '<ul>';
foreach ($report['recommendations'] as $rec) {
$class = $rec['severity'] === 'error' ? 'error' :
($rec['severity'] === 'warning' ? 'warning' : '');
$html .= "<li class='{$class}'>{$rec['message']}</li>";
}
$html .= '</ul>';
}
$html .= '</body></html>';
return $html;
}
/**
* Clear all snapshots.
*/
public function clear(): void
{
$this->snapshots = [];
$this->tracker->clear();
$this->leakDetector->clear();
}
/**
* Reset analyzer.
*/
public function reset(): void
{
$this->clear();
$this->baselineMemory = memory_get_usage(true);
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Get memory usage tracker.
*/
public function getTracker(): MemoryUsageTracker
{
return $this->tracker;
}
/**
* Get memory leak detector.
*/
public function getLeakDetector(): MemoryLeakDetector
{
return $this->leakDetector;
}
/**
* Calculate percentage difference.
*/
protected function calculatePercentageDiff(int $from, int $to): float
{
if ($from === 0) {
return $to > 0 ? 100 : 0;
}
return (($to - $from) / $from) * 100;
}
/**
* Calculate memory efficiency.
*/
protected function calculateMemoryEfficiency(): float
{
if (empty($this->snapshots)) {
return 100;
}
$totalMemory = array_sum(array_map(fn($s) => $s->getMemoryUsage(), $this->snapshots));
$avgMemory = $totalMemory / count($this->snapshots);
$peakMemory = memory_get_peak_usage(true);
if ($peakMemory === 0) {
return 100;
}
return ($avgMemory / $peakMemory) * 100;
}
/**
* Generate memory recommendations.
*/
protected function generateRecommendations(): array
{
$recommendations = [];
$stats = $this->getStatistics();
// Check for memory growth
if ($stats['growth'] > $this->config['growth_threshold']) {
$recommendations[] = [
'type' => 'memory',
'severity' => 'warning',
'message' => sprintf(
'High memory growth detected: %s (%.1f%%)',
$this->formatBytes($stats['growth']),
$stats['growth_percent']
),
'suggestion' => 'Review memory allocation patterns and consider optimization'
];
}
// Check for memory leaks
if ($stats['leak_detected']) {
$recommendations[] = [
'type' => 'leak',
'severity' => 'error',
'message' => 'Memory leaks detected',
'suggestion' => 'Investigate object references and ensure proper cleanup'
];
}
// Check memory efficiency
if ($stats['memory_efficiency'] < $this->config['efficiency_threshold']) {
$recommendations[] = [
'type' => 'efficiency',
'severity' => 'info',
'message' => sprintf(
'Low memory efficiency: %.1f%%',
$stats['memory_efficiency']
),
'suggestion' => 'Consider memory optimization techniques'
];
}
// Check peak memory usage
if ($stats['peak'] > $this->config['peak_threshold']) {
$recommendations[] = [
'type' => 'peak',
'severity' => 'warning',
'message' => sprintf(
'High peak memory usage: %s',
$this->formatBytes($stats['peak'])
),
'suggestion' => 'Monitor memory usage patterns and optimize peak consumption'
];
}
return $recommendations;
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'growth_threshold' => 50 * 1024 * 1024, // 50MB
'peak_threshold' => 100 * 1024 * 1024, // 100MB
'efficiency_threshold' => 70, // 70%
'auto_snapshot' => true,
'snapshot_interval' => 1000, // ms
'max_snapshots' => 100,
'track_objects' => true,
'track_types' => true,
'detect_leaks' => true
];
}
/**
* Format bytes to human readable format.
*/
protected function formatBytes(int $bytes): 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, 2) . ' ' . $units[$pow];
}
/**
* Monitor memory usage continuously.
*/
public function startMonitoring(): void
{
if (!$this->enabled || !$this->config['auto_snapshot']) {
return;
}
$interval = $this->config['snapshot_interval'] * 1000; // Convert to microseconds
while ($this->enabled) {
$this->snapshot('auto_' . time());
usleep($interval);
// Limit number of snapshots
if (count($this->snapshots) > $this->config['max_snapshots']) {
array_shift($this->snapshots);
}
}
}
/**
* Get memory heatmap data.
*/
public function getHeatmap(): array
{
if (empty($this->snapshots)) {
return [];
}
$heatmap = [];
$maxMemory = max(array_map(fn($s) => $s->getMemoryUsage(), $this->snapshots));
foreach ($this->snapshots as $name => $snapshot) {
$memory = $snapshot->getMemoryUsage();
$intensity = $maxMemory > 0 ? ($memory / $maxMemory) : 0;
$heatmap[] = [
'name' => $name,
'timestamp' => $snapshot->getTimestamp(),
'memory' => $memory,
'intensity' => $intensity,
'color' => $this->getHeatmapColor($intensity)
];
}
return $heatmap;
}
/**
* Get heatmap color based on intensity.
*/
protected function getHeatmapColor(float $intensity): string
{
if ($intensity < 0.3) {
return '#27ae60'; // Green
} elseif ($intensity < 0.7) {
return '#f39c12'; // Orange
} else {
return '#e74c3c'; // Red
}
}
/**
* Analyze memory patterns.
*/
public function analyzePatterns(): array
{
if (empty($this->snapshots)) {
return [];
}
$patterns = [];
$memoryUsages = array_map(fn($s) => $s->getMemoryUsage(), $this->snapshots);
// Detect growth pattern
$isGrowing = $memoryUsages[count($memoryUsages) - 1] > $memoryUsages[0];
$patterns['growth_trend'] = $isGrowing ? 'increasing' : 'stable';
// Detect volatility
$avg = array_sum($memoryUsages) / count($memoryUsages);
$variance = array_sum(array_map(fn($m) => pow($m - $avg, 2), $memoryUsages)) / count($memoryUsages);
$stdDev = sqrt($variance);
$patterns['volatility'] = $stdDev / $avg; // Coefficient of variation
// Detect spikes
$spikes = [];
for ($i = 1; $i < count($memoryUsages) - 1; $i++) {
$prev = $memoryUsages[$i - 1];
$current = $memoryUsages[$i];
$next = $memoryUsages[$i + 1];
if ($current > $prev && $current > $next && $current > $avg + $stdDev) {
$spikes[] = [
'index' => $i,
'memory' => $current,
'timestamp' => $this->snapshots[$i]->getTimestamp()
];
}
}
$patterns['spikes'] = $spikes;
return $patterns;
}
}

View File

@@ -0,0 +1,615 @@
<?php
declare(strict_types=1);
namespace Fendx\Debug;
use Fendx\Debug\Profiler\Profile;
use Fendx\Debug\Profiler\CallStack;
use Fendx\Debug\Profiler\MemoryProfile;
use Fendx\Debug\Profiler\TimeProfile;
class Profiler
{
protected static ?self $instance = null;
protected bool $enabled = false;
protected array $profiles = [];
protected ?CallStack $currentCallStack = null;
protected array $config = [];
protected float $startTime;
protected int $startMemory;
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->startTime = microtime(true);
$this->startMemory = memory_get_usage(true);
}
/**
* Get profiler instance.
*/
public static function getInstance(array $config = []): self
{
if (self::$instance === null) {
self::$instance = new self($config);
}
return self::$instance;
}
/**
* Enable profiler.
*/
public function enable(): void
{
$this->enabled = true;
if ($this->config['auto_start']) {
$this->start('main');
}
}
/**
* Disable profiler.
*/
public function disable(): void
{
$this->enabled = false;
if ($this->currentCallStack) {
$this->end('main');
}
}
/**
* Check if profiler is enabled.
*/
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* Start profiling.
*/
public function start(string $name, array $context = []): void
{
if (!$this->enabled) {
return;
}
$profile = new Profile($name, $context);
$profile->start();
if ($this->currentCallStack === null) {
$this->currentCallStack = new CallStack();
}
$this->currentCallStack->push($profile);
$this->profiles[$name] = $profile;
}
/**
* End profiling.
*/
public function end(string $name): ?Profile
{
if (!$this->enabled) {
return null;
}
$profile = $this->profiles[$name] ?? null;
if ($profile && !$profile->isEnded()) {
$profile->end();
if ($this->currentCallStack) {
$this->currentCallStack->pop();
}
}
return $profile;
}
/**
* Profile a function call.
*/
public function profile(string $name, callable $callback, array $context = []): mixed
{
if (!$this->enabled) {
return $callback();
}
$this->start($name, $context);
try {
$result = $callback();
$this->end($name);
return $result;
} catch (\Exception $e) {
$this->end($name);
throw $e;
}
}
/**
* Add memory checkpoint.
*/
public function memoryCheckpoint(string $name): void
{
if (!$this->enabled) {
return;
}
$memoryProfile = new MemoryProfile($name);
$memoryProfile->capture();
$this->profiles["memory_{$name}"] = $memoryProfile;
}
/**
* Add time checkpoint.
*/
public function timeCheckpoint(string $name): void
{
if (!$this->enabled) {
return;
}
$timeProfile = new TimeProfile($name);
$timeProfile->capture();
$this->profiles["time_{$name}"] = $timeProfile;
}
/**
* Get profile by name.
*/
public function getProfile(string $name): ?Profile
{
return $this->profiles[$name] ?? null;
}
/**
* Get all profiles.
*/
public function getProfiles(): array
{
return $this->profiles;
}
/**
* Get profiles by type.
*/
public function getProfilesByType(string $type): array
{
return array_filter($this->profiles, function ($profile) use ($type) {
return $profile instanceof $type;
});
}
/**
* Get execution time profiles.
*/
public function getTimeProfiles(): array
{
return $this->getProfilesByType(TimeProfile::class);
}
/**
* Get memory profiles.
*/
public function getMemoryProfiles(): array
{
return $this->getProfilesByType(MemoryProfile::class);
}
/**
* Get slow profiles.
*/
public function getSlowProfiles(float $threshold = null): array
{
$threshold = $threshold ?? $this->config['slow_threshold'];
return array_filter($this->profiles, function ($profile) use ($threshold) {
return $profile->getDuration() > $threshold;
});
}
/**
* Get memory intensive profiles.
*/
public function getMemoryIntensiveProfiles(int $threshold = null): array
{
$threshold = $threshold ?? $this->config['memory_threshold'];
return array_filter($this->profiles, function ($profile) use ($threshold) {
return $profile->getMemoryUsage() > $threshold;
});
}
/**
* Get profile summary.
*/
public function getSummary(): array
{
$summary = [
'enabled' => $this->enabled,
'total_profiles' => count($this->profiles),
'total_time' => 0,
'total_memory' => 0,
'peak_memory' => 0,
'slow_profiles' => [],
'memory_intensive' => [],
'call_stack_depth' => $this->currentCallStack ? $this->currentCallStack->getDepth() : 0,
'profiles_by_type' => []
];
foreach ($this->profiles as $name => $profile) {
$summary['total_time'] += $profile->getDuration();
$summary['total_memory'] += $profile->getMemoryUsage();
$summary['peak_memory'] = max($summary['peak_memory'], $profile->getPeakMemory());
$type = get_class($profile);
if (!isset($summary['profiles_by_type'][$type])) {
$summary['profiles_by_type'][$type] = 0;
}
$summary['profiles_by_type'][$type]++;
}
$summary['slow_profiles'] = $this->getSlowProfiles();
$summary['memory_intensive'] = $this->getMemoryIntensiveProfiles();
return $summary;
}
/**
* Generate performance report.
*/
public function generateReport(): array
{
$summary = $this->getSummary();
$report = [
'summary' => $summary,
'profiles' => [],
'call_stack' => $this->currentCallStack ? $this->currentCallStack->toArray() : [],
'timeline' => $this->generateTimeline(),
'recommendations' => $this->generateRecommendations($summary)
];
foreach ($this->profiles as $name => $profile) {
$report['profiles'][$name] = $profile->toArray();
}
return $report;
}
/**
* Generate timeline.
*/
protected function generateTimeline(): array
{
$timeline = [];
foreach ($this->profiles as $name => $profile) {
if ($profile->getStartTime() > 0) {
$timeline[] = [
'name' => $name,
'start' => $profile->getStartTime(),
'end' => $profile->getEndTime(),
'duration' => $profile->getDuration(),
'memory' => $profile->getMemoryUsage()
];
}
}
// Sort by start time
usort($timeline, function ($a, $b) {
return $a['start'] <=> $b['start'];
});
return $timeline;
}
/**
* Generate performance recommendations.
*/
protected function generateRecommendations(array $summary): array
{
$recommendations = [];
// Check for slow profiles
if (!empty($summary['slow_profiles'])) {
$recommendations[] = [
'type' => 'performance',
'severity' => 'warning',
'message' => count($summary['slow_profiles']) . ' slow operations detected',
'details' => array_map(function ($profile) {
return [
'name' => $profile->getName(),
'duration' => $profile->getDuration() . 'ms'
];
}, $summary['slow_profiles'])
];
}
// Check for memory intensive operations
if (!empty($summary['memory_intensive'])) {
$recommendations[] = [
'type' => 'memory',
'severity' => 'warning',
'message' => count($summary['memory_intensive']) . ' memory intensive operations detected',
'details' => array_map(function ($profile) {
return [
'name' => $profile->getName(),
'memory' => $this->formatBytes($profile->getMemoryUsage())
];
}, $summary['memory_intensive'])
];
}
// Check call stack depth
if ($summary['call_stack_depth'] > $this->config['max_call_depth']) {
$recommendations[] = [
'type' => 'complexity',
'severity' => 'info',
'message' => 'Deep call stack detected: ' . $summary['call_stack_depth'] . ' levels',
'details' => 'Consider refactoring to reduce complexity'
];
}
// Check total execution time
if ($summary['total_time'] > $this->config['total_time_threshold']) {
$recommendations[] = [
'type' => 'performance',
'severity' => 'error',
'message' => 'Total execution time exceeded threshold: ' . round($summary['total_time'], 2) . 'ms',
'details' => 'Optimize critical path operations'
];
}
return $recommendations;
}
/**
* Save profile report.
*/
public function saveReport(string $filename): bool
{
$report = $this->generateReport();
$json = json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
return file_put_contents($filename, $json) !== false;
}
/**
* Export profile data.
*/
public function export(string $format = 'json'): string
{
$report = $this->generateReport();
switch (strtolower($format)) {
case 'json':
return json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
case 'csv':
return $this->exportToCsv($report);
case 'html':
return $this->exportToHtml($report);
default:
throw new \InvalidArgumentException("Unsupported export format: {$format}");
}
}
/**
* Export to CSV format.
*/
protected function exportToCsv(array $report): string
{
$csv = "Name,Type,Duration (ms),Memory (bytes),Start Time,End Time\n";
foreach ($report['profiles'] as $name => $profile) {
$csv .= sprintf(
"%s,%s,%.2f,%d,%.4f,%.4f\n",
$name,
get_class($profile),
$profile['duration'],
$profile['memory_usage'],
$profile['start_time'],
$profile['end_time']
);
}
return $csv;
}
/**
* Export to HTML format.
*/
protected function exportToHtml(array $report): string
{
$html = '<!DOCTYPE html><html><head><title>Profile Report</title>';
$html .= '<style>body{font-family:Arial,sans-serif;margin:20px;}';
$html .= 'table{border-collapse:collapse;width:100%;}';
$html .= 'th,td{border:1px solid #ddd;padding:8px;text-align:left;}';
$html .= 'th{background-color:#f2f2f2;}</style></head><body>';
$html .= '<h1>Profile Report</h1>';
$html .= '<h2>Summary</h2>';
$html .= '<table><tr><th>Metric</th><th>Value</th></tr>';
foreach ($report['summary'] as $key => $value) {
if (is_array($value)) {
$value = json_encode($value);
}
$html .= "<tr><td>{$key}</td><td>{$value}</td></tr>";
}
$html .= '</table>';
$html .= '<h2>Profiles</h2>';
$html .= '<table><tr><th>Name</th><th>Duration (ms)</th><th>Memory (bytes)</th></tr>';
foreach ($report['profiles'] as $name => $profile) {
$html .= sprintf(
"<tr><td>%s</td><td>%.2f</td><td>%d</td></tr>",
$name,
$profile['duration'],
$profile['memory_usage']
);
}
$html .= '</table></body></html>';
return $html;
}
/**
* Clear all profiles.
*/
public function clear(): void
{
$this->profiles = [];
$this->currentCallStack = null;
}
/**
* Reset profiler.
*/
public function reset(): void
{
$this->clear();
$this->startTime = microtime(true);
$this->startMemory = memory_get_usage(true);
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'auto_start' => true,
'slow_threshold' => 100, // ms
'memory_threshold' => 10 * 1024 * 1024, // 10MB
'max_call_depth' => 50,
'total_time_threshold' => 5000, // ms
'collect_memory' => true,
'collect_call_stack' => true,
'collect_timeline' => true,
'save_on_shutdown' => false,
'export_format' => 'json'
];
}
/**
* Format bytes to human readable format.
*/
protected function formatBytes(int $bytes): 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, 2) . ' ' . $units[$pow];
}
/**
* Get current call stack.
*/
public function getCurrentCallStack(): ?CallStack
{
return $this->currentCallStack;
}
/**
* Get call stack depth.
*/
public function getCallStackDepth(): int
{
return $this->currentCallStack ? $this->currentCallStack->getDepth() : 0;
}
/**
* Check if profile is running.
*/
public function isProfiling(string $name): bool
{
$profile = $this->profiles[$name] ?? null;
return $profile && !$profile->isEnded();
}
/**
* Get running profiles.
*/
public function getRunningProfiles(): array
{
return array_filter($this->profiles, function ($profile) {
return !$profile->isEnded();
});
}
/**
* Get profile statistics.
*/
public function getStatistics(): array
{
$stats = [
'total_profiles' => count($this->profiles),
'running_profiles' => count($this->getRunningProfiles()),
'completed_profiles' => count($this->profiles) - count($this->getRunningProfiles()),
'average_duration' => 0,
'min_duration' => PHP_FLOAT_MAX,
'max_duration' => 0,
'average_memory' => 0,
'min_memory' => PHP_INT_MAX,
'max_memory' => 0
];
$durations = [];
$memories = [];
foreach ($this->profiles as $profile) {
if ($profile->isEnded()) {
$durations[] = $profile->getDuration();
$memories[] = $profile->getMemoryUsage();
}
}
if (!empty($durations)) {
$stats['average_duration'] = array_sum($durations) / count($durations);
$stats['min_duration'] = min($durations);
$stats['max_duration'] = max($durations);
}
if (!empty($memories)) {
$stats['average_memory'] = array_sum($memories) / count($memories);
$stats['min_memory'] = min($memories);
$stats['max_memory'] = max($memories);
}
return $stats;
}
}

View File

@@ -0,0 +1,714 @@
<?php
declare(strict_types=1);
namespace Fendx\Debug;
use Fendx\Debug\Query\QueryLog;
use Fendx\Debug\Query\QueryAnalyzer;
use Fendx\Debug\Query\QueryPerformanceAnalyzer;
class QueryMonitor
{
protected static ?self $instance = null;
protected bool $enabled = false;
protected array $queries = [];
protected QueryAnalyzer $analyzer;
protected QueryPerformanceAnalyzer $performanceAnalyzer;
protected array $config = [];
protected float $startTime;
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->analyzer = new QueryAnalyzer();
$this->performanceAnalyzer = new QueryPerformanceAnalyzer();
$this->startTime = microtime(true);
}
/**
* Get query monitor instance.
*/
public static function getInstance(array $config = []): self
{
if (self::$instance === null) {
self::$instance = new self($config);
}
return self::$instance;
}
/**
* Enable query monitor.
*/
public function enable(): void
{
$this->enabled = true;
$this->analyzer->enable();
$this->performanceAnalyzer->enable();
}
/**
* Disable query monitor.
*/
public function disable(): void
{
$this->enabled = false;
$this->analyzer->disable();
$this->performanceAnalyzer->disable();
}
/**
* Check if monitor is enabled.
*/
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* Log a query.
*/
public function logQuery(string $sql, array $params = [], float $executionTime = 0, array $context = []): QueryLog
{
if (!$this->enabled) {
throw new \RuntimeException('Query monitor is not enabled');
}
$query = new QueryLog($sql, $params, $executionTime, $context);
$query->setConnectionId($context['connection_id'] ?? 'default');
$query->setTimestamp(microtime(true));
$this->queries[] = $query;
// Analyze query
$this->analyzer->analyze($query);
// Check performance
if ($executionTime > $this->config['slow_query_threshold']) {
$this->performanceAnalyzer->analyzeSlowQuery($query);
}
return $query;
}
/**
* Start query execution.
*/
public function startQuery(string $sql, array $params = [], array $context = []): string
{
if (!$this->enabled) {
return '';
}
$queryId = uniqid('query_', true);
$context['query_id'] = $queryId;
$context['start_time'] = microtime(true);
$this->logQuery($sql, $params, 0, $context);
return $queryId;
}
/**
* End query execution.
*/
public function endQuery(string $queryId, float $executionTime = null): ?QueryLog
{
if (!$this->enabled || !$queryId) {
return null;
}
$query = $this->findQueryById($queryId);
if ($query) {
$executionTime = $executionTime ?? (microtime(true) - $query->getContext('start_time'));
$query->setExecutionTime($executionTime);
$query->setEndTime(microtime(true));
// Re-analyze with actual execution time
$this->analyzer->analyze($query);
if ($executionTime > $this->config['slow_query_threshold']) {
$this->performanceAnalyzer->analyzeSlowQuery($query);
}
}
return $query;
}
/**
* Find query by ID.
*/
protected function findQueryById(string $queryId): ?QueryLog
{
foreach ($this->queries as $query) {
if ($query->getContext('query_id') === $queryId) {
return $query;
}
}
return null;
}
/**
* Get all queries.
*/
public function getQueries(): array
{
return $this->queries;
}
/**
* Get queries by type.
*/
public function getQueriesByType(string $type): array
{
return array_filter($this->queries, function ($query) use ($type) {
return $query->getType() === $type;
});
}
/**
* Get slow queries.
*/
public function getSlowQueries(float $threshold = null): array
{
$threshold = $threshold ?? $this->config['slow_query_threshold'];
return array_filter($this->queries, function ($query) use ($threshold) {
return $query->getExecutionTime() > $threshold;
});
}
/**
* Get failed queries.
*/
public function getFailedQueries(): array
{
return array_filter($this->queries, function ($query) {
return $query->hasError();
});
}
/**
* Get duplicate queries.
*/
public function getDuplicateQueries(): array
{
$duplicates = [];
$seen = [];
foreach ($this->queries as $query) {
$hash = $query->getHash();
if (!isset($seen[$hash])) {
$seen[$hash] = [];
}
$seen[$hash][] = $query;
}
foreach ($seen as $hash => $queries) {
if (count($queries) > 1) {
$duplicates[$hash] = $queries;
}
}
return $duplicates;
}
/**
* Get query statistics.
*/
public function getStatistics(): array
{
if (empty($this->queries)) {
return [
'total_queries' => 0,
'total_time' => 0,
'average_time' => 0,
'slow_queries' => 0,
'failed_queries' => 0,
'duplicate_queries' => 0
];
}
$totalTime = array_sum(array_map(fn($q) => $q->getExecutionTime(), $this->queries));
$slowQueries = $this->getSlowQueries();
$failedQueries = $this->getFailedQueries();
$duplicateQueries = $this->getDuplicateQueries();
$times = array_map(fn($q) => $q->getExecutionTime(), $this->queries);
return [
'total_queries' => count($this->queries),
'total_time' => $totalTime,
'average_time' => $totalTime / count($this->queries),
'min_time' => min($times),
'max_time' => max($times),
'median_time' => $this->calculateMedian($times),
'slow_queries' => count($slowQueries),
'failed_queries' => count($failedQueries),
'duplicate_queries' => count($duplicateQueries),
'queries_per_second' => $this->calculateQueriesPerSecond(),
'query_types' => $this->getQueryTypeDistribution(),
'connections' => $this->getConnectionDistribution()
];
}
/**
* Get query type distribution.
*/
protected function getQueryTypeDistribution(): array
{
$types = [];
foreach ($this->queries as $query) {
$type = $query->getType();
if (!isset($types[$type])) {
$types[$type] = 0;
}
$types[$type]++;
}
return $types;
}
/**
* Get connection distribution.
*/
protected function getConnectionDistribution(): array
{
$connections = [];
foreach ($this->queries as $query) {
$connection = $query->getConnectionId();
if (!isset($connections[$connection])) {
$connections[$connection] = 0;
}
$connections[$connection]++;
}
return $connections;
}
/**
* Calculate median value.
*/
protected function calculateMedian(array $values): float
{
sort($values);
$count = count($values);
if ($count === 0) {
return 0;
}
$middle = floor($count / 2);
if ($count % 2 === 0) {
return ($values[$middle - 1] + $values[$middle]) / 2;
}
return $values[$middle];
}
/**
* Calculate queries per second.
*/
protected function calculateQueriesPerSecond(): float
{
$elapsed = microtime(true) - $this->startTime;
if ($elapsed === 0) {
return 0;
}
return count($this->queries) / $elapsed;
}
/**
* Generate query analysis report.
*/
public function generateReport(): array
{
$report = [
'summary' => $this->getStatistics(),
'queries' => [],
'slow_queries' => [],
'failed_queries' => [],
'duplicate_queries' => [],
'analysis' => [],
'recommendations' => [],
'generated_at' => time()
];
// Include all queries (limited by config)
$maxQueries = $this->config['max_queries_in_report'];
$querySlice = array_slice($this->queries, 0, $maxQueries);
foreach ($querySlice as $query) {
$report['queries'][] = $query->toArray();
}
// Slow queries
foreach ($this->getSlowQueries() as $query) {
$report['slow_queries'][] = $query->toArray();
}
// Failed queries
foreach ($this->getFailedQueries() as $query) {
$report['failed_queries'][] = $query->toArray();
}
// Duplicate queries
foreach ($this->getDuplicateQueries() as $hash => $queries) {
$report['duplicate_queries'][$hash] = array_map(fn($q) => $q->toArray(), $queries);
}
// Analysis results
$report['analysis'] = [
'performance' => $this->performanceAnalyzer->getAnalysis(),
'patterns' => $this->analyzer->getPatterns(),
'optimization_opportunities' => $this->analyzer->getOptimizationOpportunities()
];
// Recommendations
$report['recommendations'] = $this->generateRecommendations();
return $report;
}
/**
* Generate query recommendations.
*/
protected function generateRecommendations(): array
{
$recommendations = [];
$stats = $this->getStatistics();
// Check slow queries
if ($stats['slow_queries'] > 0) {
$recommendations[] = [
'type' => 'performance',
'severity' => 'warning',
'message' => "{$stats['slow_queries']} slow queries detected",
'suggestion' => 'Review slow queries and consider adding indexes or optimization'
];
}
// Check failed queries
if ($stats['failed_queries'] > 0) {
$recommendations[] = [
'type' => 'reliability',
'severity' => 'error',
'message' => "{$stats['failed_queries']} failed queries detected",
'suggestion' => 'Investigate and fix query errors'
];
}
// Check duplicate queries
if ($stats['duplicate_queries'] > 0) {
$recommendations[] = [
'type' => 'efficiency',
'severity' => 'info',
'message' => "{$stats['duplicate_queries']} duplicate query groups detected",
'suggestion' => 'Consider caching or query optimization to reduce duplicates'
];
}
// Check average execution time
if ($stats['average_time'] > $this->config['average_time_threshold']) {
$recommendations[] = [
'type' => 'performance',
'severity' => 'warning',
'message' => sprintf(
'High average query time: %.2fms',
$stats['average_time']
),
'suggestion' => 'Review overall query performance and optimization strategies'
];
}
// Check query frequency
if ($stats['queries_per_second'] > $this->config['queries_per_second_threshold']) {
$recommendations[] = [
'type' => 'load',
'severity' => 'warning',
'message' => sprintf(
'High query frequency: %.2f queries/second',
$stats['queries_per_second']
),
'suggestion' => 'Consider query optimization, caching, or connection pooling'
];
}
return $recommendations;
}
/**
* Save query report.
*/
public function saveReport(string $filename): bool
{
$report = $this->generateReport();
$json = json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
return file_put_contents($filename, $json) !== false;
}
/**
* Export query data.
*/
public function export(string $format = 'json'): string
{
$report = $this->generateReport();
switch (strtolower($format)) {
case 'json':
return json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
case 'csv':
return $this->exportToCsv($report);
case 'sql':
return $this->exportToSql($report);
default:
throw new \InvalidArgumentException("Unsupported export format: {$format}");
}
}
/**
* Export to CSV format.
*/
protected function exportToCsv(array $report): string
{
$csv = "Timestamp,Query,Type,Execution Time (ms),Parameters,Error\n";
foreach ($report['queries'] as $query) {
$csv .= sprintf(
"%s,\"%s\",%s,%.2f,\"%s\",\"%s\"\n",
date('Y-m-d H:i:s', $query['timestamp']),
str_replace('"', '""', $query['sql']),
$query['type'],
$query['execution_time'],
json_encode($query['params']),
$query['error'] ?? ''
);
}
return $csv;
}
/**
* Export to SQL format.
*/
protected function exportToSql(array $report): string
{
$sql = "-- Query Log Export\n";
$sql .= "-- Generated: " . date('Y-m-d H:i:s') . "\n\n";
foreach ($report['queries'] as $query) {
$sql .= "-- Query ID: {$query['id']}\n";
$sql .= "-- Execution Time: {$query['execution_time']}ms\n";
$sql .= "-- Type: {$query['type']}\n";
if (!empty($query['error'])) {
$sql .= "-- Error: {$query['error']}\n";
}
$sql .= $query['sql'] . ";\n\n";
}
return $sql;
}
/**
* Clear all queries.
*/
public function clear(): void
{
$this->queries = [];
$this->analyzer->clear();
$this->performanceAnalyzer->clear();
}
/**
* Reset monitor.
*/
public function reset(): void
{
$this->clear();
$this->startTime = microtime(true);
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Get query analyzer.
*/
public function getAnalyzer(): QueryAnalyzer
{
return $this->analyzer;
}
/**
* Get performance analyzer.
*/
public function getPerformanceAnalyzer(): QueryPerformanceAnalyzer
{
return $this->performanceAnalyzer;
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'slow_query_threshold' => 100, // ms
'average_time_threshold' => 50, // ms
'queries_per_second_threshold' => 100,
'max_queries_in_report' => 1000,
'log_params' => true,
'log_backtrace' => false,
'explain_queries' => false,
'track_duplicates' => true,
'track_patterns' => true,
'auto_save_report' => false,
'report_file' => null
];
}
/**
* Get query execution timeline.
*/
public function getTimeline(): array
{
$timeline = [];
foreach ($this->queries as $query) {
$timeline[] = [
'timestamp' => $query->getTimestamp(),
'execution_time' => $query->getExecutionTime(),
'type' => $query->getType(),
'sql' => $query->getSql(),
'connection' => $query->getConnectionId()
];
}
// Sort by timestamp
usort($timeline, function ($a, $b) {
return $a['timestamp'] <=> $b['timestamp'];
});
return $timeline;
}
/**
* Get query performance heatmap.
*/
public function getPerformanceHeatmap(): array
{
$heatmap = [];
$maxTime = 0;
foreach ($this->queries as $query) {
$maxTime = max($maxTime, $query->getExecutionTime());
}
foreach ($this->queries as $query) {
$time = $query->getExecutionTime();
$intensity = $maxTime > 0 ? ($time / $maxTime) : 0;
$heatmap[] = [
'timestamp' => $query->getTimestamp(),
'execution_time' => $time,
'intensity' => $intensity,
'color' => $this->getHeatmapColor($intensity),
'type' => $query->getType()
];
}
return $heatmap;
}
/**
* Get heatmap color based on intensity.
*/
protected function getHeatmapColor(float $intensity): string
{
if ($intensity < 0.3) {
return '#27ae60'; // Green
} elseif ($intensity < 0.7) {
return '#f39c12'; // Orange
} else {
return '#e74c3c'; // Red
}
}
/**
* Analyze query patterns.
*/
public function analyzePatterns(): array
{
return $this->analyzer->getPatterns();
}
/**
* Get optimization opportunities.
*/
public function getOptimizationOpportunities(): array
{
return $this->analyzer->getOptimizationOpportunities();
}
/**
* Benchmark query.
*/
public function benchmarkQuery(string $sql, array $params = [], int $iterations = 10): array
{
$times = [];
for ($i = 0; $i < $iterations; $i++) {
$startTime = microtime(true);
// Execute query (this would need to be implemented based on your database layer)
// $result = $this->executeQuery($sql, $params);
$endTime = microtime(true);
$times[] = ($endTime - $startTime) * 1000; // Convert to ms
}
sort($times);
$count = count($times);
return [
'sql' => $sql,
'params' => $params,
'iterations' => $iterations,
'min_time' => min($times),
'max_time' => max($times),
'avg_time' => array_sum($times) / $count,
'median_time' => $count % 2 === 0 ?
($times[$count / 2 - 1] + $times[$count / 2]) / 2 :
$times[floor($count / 2)],
'std_dev' => sqrt(array_sum(array_map(fn($t) => pow($t - array_sum($times) / $count, 2), $times)) / $count)
];
}
}