Files
FendxPHP/fendx-framework/fendx-debug/src/MemoryAnalyzer.php

670 lines
19 KiB
PHP
Raw Normal View History

<?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;
}
}