mirror of
https://devops.lemonos.cn/lawson/FendxPHP.git
synced 2026-06-15 23:12:49 +08:00
feat(database): 添加用户角色权限系统及相关监控功能
- 创建用户表(users)包含基本信息和认证字段 - 创建角色表(roles)用于权限控制 - 创建权限表(permissions)定义系统权限 - 创建用户角色关联表(user_roles)建立用户与角色关系 - 创建角色权限关联表(role_permissions)建立角色与权限关系 - 创建迁移记录表(migrations)追踪数据库变更 - 添加AdminController提供管理员面板功能 - 实现系统监控、配置管理、缓存清理等功能 - 添加AOP切面编程支持的各种通知类型 - 实现告警管理AlertManager支持多渠道告警 - 添加文档注解接口规范
This commit is contained in:
566
fendx-framework/fendx-debug/src/Debugger.php
Normal file
566
fendx-framework/fendx-debug/src/Debugger.php
Normal 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']),
|
||||
];
|
||||
}
|
||||
}
|
||||
669
fendx-framework/fendx-debug/src/MemoryAnalyzer.php
Normal file
669
fendx-framework/fendx-debug/src/MemoryAnalyzer.php
Normal 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;
|
||||
}
|
||||
}
|
||||
615
fendx-framework/fendx-debug/src/Profiler.php
Normal file
615
fendx-framework/fendx-debug/src/Profiler.php
Normal 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;
|
||||
}
|
||||
}
|
||||
714
fendx-framework/fendx-debug/src/QueryMonitor.php
Normal file
714
fendx-framework/fendx-debug/src/QueryMonitor.php
Normal 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)
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user