feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,296 @@
<?php
declare(strict_types=1);
use App\Framework\Performance\GarbageCollectionMonitor;
use App\Framework\Performance\ValueObjects\GcStatus;
use App\Framework\Performance\ValueObjects\GcResult;
describe('GarbageCollectionMonitor', function () {
beforeEach(function () {
$this->monitor = new GarbageCollectionMonitor();
});
it('returns current GC status', function () {
$status = $this->monitor->getStatus();
expect($status)->toBeInstanceOf(GcStatus::class);
expect($status->runs)->toBeInt();
expect($status->collected)->toBeInt();
expect($status->threshold)->toBeGreaterThan(0);
expect($status->roots)->toBeInt();
});
it('gets number of GC runs', function () {
$runs = $this->monitor->getRuns();
expect($runs)->toBeInt();
expect($runs)->toBeGreaterThanOrEqual(0);
});
it('gets number of collected cycles', function () {
$collected = $this->monitor->getCollected();
expect($collected)->toBeInt();
expect($collected)->toBeGreaterThanOrEqual(0);
});
it('gets GC threshold', function () {
$threshold = $this->monitor->getThreshold();
expect($threshold)->toBeInt();
expect($threshold)->toBeGreaterThan(0);
});
it('checks if GC is running', function () {
$isRunning = $this->monitor->isRunning();
expect($isRunning)->toBeBool();
});
it('checks if GC is enabled', function () {
$isEnabled = $this->monitor->isEnabled();
expect($isEnabled)->toBeBool();
});
it('can force garbage collection', function () {
// Create some circular references to collect
$obj1 = new stdClass();
$obj2 = new stdClass();
$obj1->ref = $obj2;
$obj2->ref = $obj1;
unset($obj1, $obj2);
$result = $this->monitor->forceCollection();
expect($result)->toBeInstanceOf(GcResult::class);
expect($result->collected)->toBeInt();
expect($result->duration)->toBeInstanceOf(App\Framework\Core\ValueObjects\Duration::class);
expect($result->memoryFreed)->toBeInstanceOf(App\Framework\Core\ValueObjects\Byte::class);
expect($result->statusBefore)->toBeInstanceOf(GcStatus::class);
expect($result->statusAfter)->toBeInstanceOf(GcStatus::class);
expect($result->timestamp)->toBeInstanceOf(App\Framework\Core\ValueObjects\Timestamp::class);
});
it('provides efficiency metrics', function () {
$metrics = $this->monitor->getEfficiencyMetrics();
expect($metrics)->toHaveKeys([
'collection_rate',
'threshold_ratio',
'is_healthy',
'runs',
'collected',
'roots',
'threshold',
]);
expect($metrics['collection_rate'])->toBeFloat();
expect($metrics['threshold_ratio'])->toBeFloat();
expect($metrics['is_healthy'])->toBeBool();
});
it('can take GC snapshot', function () {
$snapshot = $this->monitor->takeSnapshot('test_checkpoint');
expect($snapshot)->toHaveKeys(['label', 'timestamp', 'status', 'efficiency']);
expect($snapshot['label'])->toBe('test_checkpoint');
expect($snapshot['status'])->toBeArray();
expect($snapshot['efficiency'])->toBeArray();
});
it('can enable and disable GC', function () {
$this->monitor->disable();
expect(gc_enabled())->toBeFalse();
$this->monitor->enable();
expect(gc_enabled())->toBeTrue();
});
});
describe('GcStatus', function () {
it('creates from gc_status array', function () {
$status = GcStatus::fromGcStatus(gc_status());
expect($status)->toBeInstanceOf(GcStatus::class);
expect($status->runs)->toBeInt();
expect($status->collected)->toBeInt();
expect($status->threshold)->toBeInt();
expect($status->roots)->toBeInt();
});
it('creates current GC status', function () {
$status = GcStatus::current();
expect($status)->toBeInstanceOf(GcStatus::class);
});
it('calculates collection efficiency', function () {
$status = new GcStatus(runs: 10, collected: 50, threshold: 10001, roots: 100);
$efficiency = $status->getCollectionEfficiency();
expect($efficiency)->toBe(5.0); // 50 / 10
});
it('returns zero efficiency when no runs', function () {
$status = new GcStatus(runs: 0, collected: 0, threshold: 10001, roots: 0);
expect($status->getCollectionEfficiency())->toBe(0.0);
});
it('calculates threshold utilization', function () {
$status = new GcStatus(runs: 10, collected: 50, threshold: 1000, roots: 900);
$utilization = $status->getThresholdUtilization();
expect($utilization)->toBe(0.9); // 900 / 1000
});
it('checks if near threshold', function () {
$nearThreshold = new GcStatus(runs: 10, collected: 50, threshold: 1000, roots: 950);
$notNearThreshold = new GcStatus(runs: 10, collected: 50, threshold: 1000, roots: 500);
expect($nearThreshold->isNearThreshold(0.9))->toBeTrue();
expect($notNearThreshold->isNearThreshold(0.9))->toBeFalse();
});
it('determines if GC is healthy', function () {
$healthy = new GcStatus(runs: 10, collected: 50, threshold: 10000, roots: 100);
$unhealthy = new GcStatus(runs: 0, collected: 0, threshold: 1000, roots: 950);
expect($healthy->isHealthy())->toBeTrue();
expect($unhealthy->isHealthy())->toBeFalse();
});
it('converts to array', function () {
$status = new GcStatus(runs: 10, collected: 50, threshold: 10001, roots: 100);
$array = $status->toArray();
expect($array)->toHaveKeys([
'runs',
'collected',
'threshold',
'roots',
'running',
'collection_efficiency',
'threshold_utilization',
'is_healthy',
]);
});
});
describe('GcResult', function () {
it('determines if GC was effective', function () {
$effective = new GcResult(
collected: 10,
duration: App\Framework\Core\ValueObjects\Duration::fromMilliseconds(5.0),
memoryFreed: App\Framework\Core\ValueObjects\Byte::fromBytes(1024),
statusBefore: new GcStatus(runs: 1, collected: 0, threshold: 10001, roots: 100),
statusAfter: new GcStatus(runs: 2, collected: 10, threshold: 10001, roots: 90),
timestamp: App\Framework\Core\ValueObjects\Timestamp::now()
);
$ineffective = new GcResult(
collected: 0,
duration: App\Framework\Core\ValueObjects\Duration::fromMilliseconds(5.0),
memoryFreed: App\Framework\Core\ValueObjects\Byte::fromBytes(0),
statusBefore: new GcStatus(runs: 1, collected: 0, threshold: 10001, roots: 100),
statusAfter: new GcStatus(runs: 2, collected: 0, threshold: 10001, roots: 100),
timestamp: App\Framework\Core\ValueObjects\Timestamp::now()
);
expect($effective->wasEffective())->toBeTrue();
expect($ineffective->wasEffective())->toBeFalse();
});
it('gets GC overhead in milliseconds', function () {
$result = new GcResult(
collected: 5,
duration: App\Framework\Core\ValueObjects\Duration::fromMilliseconds(12.5),
memoryFreed: App\Framework\Core\ValueObjects\Byte::fromBytes(2048),
statusBefore: new GcStatus(runs: 1, collected: 0, threshold: 10001, roots: 100),
statusAfter: new GcStatus(runs: 2, collected: 5, threshold: 10001, roots: 95),
timestamp: App\Framework\Core\ValueObjects\Timestamp::now()
);
expect($result->getOverheadMs())->toBeFloat();
expect($result->getOverheadMs())->toBeGreaterThan(0);
});
it('gets memory freed in megabytes', function () {
$result = new GcResult(
collected: 5,
duration: App\Framework\Core\ValueObjects\Duration::fromMilliseconds(5.0),
memoryFreed: App\Framework\Core\ValueObjects\Byte::fromBytes(1048576), // 1 MB
statusBefore: new GcStatus(runs: 1, collected: 0, threshold: 10001, roots: 100),
statusAfter: new GcStatus(runs: 2, collected: 5, threshold: 10001, roots: 95),
timestamp: App\Framework\Core\ValueObjects\Timestamp::now()
);
expect($result->getMemoryFreedMB())->toBe(1.0);
});
it('calculates efficiency score', function () {
$result = new GcResult(
collected: 5,
duration: App\Framework\Core\ValueObjects\Duration::fromMilliseconds(5.0),
memoryFreed: App\Framework\Core\ValueObjects\Byte::fromBytes(1048576),
statusBefore: new GcStatus(runs: 1, collected: 0, threshold: 10001, roots: 100),
statusAfter: new GcStatus(runs: 2, collected: 5, threshold: 10001, roots: 95),
timestamp: App\Framework\Core\ValueObjects\Timestamp::now()
);
$score = $result->getEfficiencyScore();
expect($score)->toBeFloat();
expect($score)->toBeGreaterThanOrEqual(0);
expect($score)->toBeLessThanOrEqual(100);
});
it('gets status changes', function () {
$result = new GcResult(
collected: 5,
duration: App\Framework\Core\ValueObjects\Duration::fromMilliseconds(5.0),
memoryFreed: App\Framework\Core\ValueObjects\Byte::fromBytes(1024),
statusBefore: new GcStatus(runs: 1, collected: 10, threshold: 10001, roots: 100),
statusAfter: new GcStatus(runs: 2, collected: 15, threshold: 10001, roots: 95),
timestamp: App\Framework\Core\ValueObjects\Timestamp::now()
);
$changes = $result->getStatusChanges();
expect($changes)->toHaveKeys(['runs_delta', 'collected_delta', 'roots_delta']);
expect($changes['runs_delta'])->toBe(1);
expect($changes['collected_delta'])->toBe(5);
expect($changes['roots_delta'])->toBe(-5);
});
it('converts to array', function () {
$result = new GcResult(
collected: 5,
duration: App\Framework\Core\ValueObjects\Duration::fromMilliseconds(5.0),
memoryFreed: App\Framework\Core\ValueObjects\Byte::fromBytes(1024),
statusBefore: new GcStatus(runs: 1, collected: 0, threshold: 10001, roots: 100),
statusAfter: new GcStatus(runs: 2, collected: 5, threshold: 10001, roots: 95),
timestamp: App\Framework\Core\ValueObjects\Timestamp::now()
);
$array = $result->toArray();
expect($array)->toHaveKeys([
'collected',
'duration_ms',
'memory_freed_bytes',
'memory_freed_mb',
'timestamp',
'was_effective',
'efficiency_score',
'status_before',
'status_after',
'status_changes',
]);
});
});

View File

@@ -0,0 +1,824 @@
<?php
declare(strict_types=1);
use App\Framework\Performance\NestedPerformanceTracker;
use App\Framework\Performance\MemoryProfiler;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\DateTime\SystemClock;
use App\Framework\DateTime\SystemHighResolutionClock;
use App\Framework\Performance\MemoryMonitor;
describe('MemoryProfiler - Leak Detection', function () {
beforeEach(function () {
$this->tracker = new NestedPerformanceTracker(
new SystemClock(),
new SystemHighResolutionClock(),
new MemoryMonitor()
);
$this->profiler = new MemoryProfiler($this->tracker);
});
it('detects no leaks when memory usage is low', function () {
$this->tracker->measure(
'small.operation',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 100, 'x');
usleep(1000);
}
);
$result = $this->profiler->detectMemoryLeaks(1.0);
expect($result)->toHaveKeys([
'potential_leaks',
'total_memory_growth_mb',
'leak_count',
'threshold_mb'
]);
expect($result['leak_count'])->toBe(0);
expect($result['potential_leaks'])->toBeArray();
expect($result['potential_leaks'])->toHaveCount(0);
expect($result['threshold_mb'])->toBe(1.0);
});
it('uses default threshold of 5MB', function () {
$result = $this->profiler->detectMemoryLeaks();
expect($result['threshold_mb'])->toBe(5.0);
});
it('tracks cumulative memory growth', function () {
// Simulate multiple operations
for ($i = 0; $i < 3; $i++) {
$this->tracker->measure(
"operation.{$i}",
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 1000, str_repeat('x', 100));
usleep(500);
}
);
}
$result = $this->profiler->detectMemoryLeaks(0.1);
expect($result['total_memory_growth_mb'])->toBeFloat();
expect($result)->toHaveKey('leak_count');
});
it('includes context in leak reports', function () {
$this->tracker->measure(
'large.allocation',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 10000, str_repeat('x', 100));
usleep(1000);
},
['test_context' => 'value']
);
$result = $this->profiler->detectMemoryLeaks(0.01);
if ($result['leak_count'] > 0) {
$leak = $result['potential_leaks'][0];
expect($leak)->toHaveKeys([
'operation',
'memory_allocated_mb',
'cumulative_memory_mb',
'category',
'context'
]);
}
});
});
describe('MemoryProfiler - Hotspot Identification', function () {
beforeEach(function () {
$this->tracker = new NestedPerformanceTracker(
new SystemClock(),
new SystemHighResolutionClock(),
new MemoryMonitor()
);
$this->profiler = new MemoryProfiler($this->tracker);
});
it('returns empty array when no operations measured', function () {
$hotspots = $this->profiler->getMemoryHotspots();
expect($hotspots)->toBeArray();
expect($hotspots)->toHaveCount(0);
});
it('returns hotspots sorted by memory usage', function () {
// Small allocation
$this->tracker->measure(
'small',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 100, 'x');
usleep(1000);
}
);
// Medium allocation
$this->tracker->measure(
'medium',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 1000, 'x');
usleep(1000);
}
);
// Large allocation
$this->tracker->measure(
'large',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 5000, 'x');
usleep(1000);
}
);
$hotspots = $this->profiler->getMemoryHotspots();
expect($hotspots)->toBeArray();
expect($hotspots)->toHaveCount(3);
// Verify structure
foreach ($hotspots as $hotspot) {
expect($hotspot)->toHaveKeys([
'operation',
'memory_mb',
'duration_ms',
'memory_per_ms',
'category',
'depth'
]);
}
});
it('respects limit parameter', function () {
for ($i = 0; $i < 5; $i++) {
$this->tracker->measure(
"operation.{$i}",
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 1000, 'x');
usleep(500);
}
);
}
$hotspots = $this->profiler->getMemoryHotspots(3);
expect($hotspots)->toBeArray();
expect($hotspots)->toHaveCount(3);
});
it('calculates memory per millisecond correctly', function () {
$this->tracker->measure(
'test.operation',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 1000, 'x');
usleep(1000); // ~1ms
}
);
$hotspots = $this->profiler->getMemoryHotspots(1);
expect($hotspots)->toHaveCount(1);
expect($hotspots[0]['memory_per_ms'])->toBeFloat();
expect($hotspots[0]['memory_per_ms'])->toBeGreaterThanOrEqual(0);
});
it('handles zero duration operations', function () {
$this->tracker->measure(
'instant.operation',
PerformanceCategory::CUSTOM,
function () {
// Very fast operation
$x = 1 + 1;
}
);
$hotspots = $this->profiler->getMemoryHotspots(1);
expect($hotspots)->toHaveCount(1);
// Should not cause division by zero - can be 0 (int) or 0.0 (float)
expect($hotspots[0]['memory_per_ms'])->toBeGreaterThanOrEqual(0);
});
});
describe('MemoryProfiler - Efficiency Scoring', function () {
beforeEach(function () {
$this->tracker = new NestedPerformanceTracker(
new SystemClock(),
new SystemHighResolutionClock(),
new MemoryMonitor()
);
$this->profiler = new MemoryProfiler($this->tracker);
});
it('returns perfect score for empty tracker', function () {
$efficiency = $this->profiler->calculateEfficiencyScore();
expect($efficiency)->toHaveKeys([
'score',
'rating',
'total_memory_mb',
'total_time_ms'
]);
expect($efficiency['score'])->toBe(100);
expect($efficiency['rating'])->toBe('N/A');
expect($efficiency['total_memory_mb'])->toBe(0);
expect($efficiency['total_time_ms'])->toBe(0);
});
it('scores efficient operations highly', function () {
$this->tracker->measure(
'efficient.operation',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 10, 'x'); // Minimal memory
usleep(1000); // ~1ms
}
);
$efficiency = $this->profiler->calculateEfficiencyScore();
expect($efficiency['score'])->toBeGreaterThan(80);
expect($efficiency['rating'])->toBeIn(['Excellent', 'Good']);
});
it('includes memory per millisecond metric', function () {
$this->tracker->measure(
'test.operation',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 1000, 'x');
usleep(2000);
}
);
$efficiency = $this->profiler->calculateEfficiencyScore();
expect($efficiency)->toHaveKey('memory_per_ms');
expect($efficiency['memory_per_ms'])->toBeFloat();
expect($efficiency['memory_per_ms'])->toBeGreaterThanOrEqual(0);
});
it('provides correct rating categories', function () {
$efficiency = $this->profiler->calculateEfficiencyScore();
expect($efficiency['rating'])->toBeIn([
'Excellent',
'Good',
'Fair',
'Poor',
'Critical',
'N/A'
]);
});
it('calculates total memory correctly', function () {
$this->tracker->measure(
'operation.1',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 1000, 'x');
usleep(500);
}
);
$this->tracker->measure(
'operation.2',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 500, 'x');
usleep(500);
}
);
$efficiency = $this->profiler->calculateEfficiencyScore();
expect($efficiency['total_memory_mb'])->toBeFloat();
expect($efficiency['total_time_ms'])->toBeFloat();
expect($efficiency['total_time_ms'])->toBeGreaterThan(0);
});
});
describe('MemoryProfiler - Memory Report Generation', function () {
beforeEach(function () {
$this->tracker = new NestedPerformanceTracker(
new SystemClock(),
new SystemHighResolutionClock(),
new MemoryMonitor()
);
$this->profiler = new MemoryProfiler($this->tracker);
});
it('generates empty report for no operations', function () {
$report = $this->profiler->generateMemoryReport();
expect($report)->toHaveKeys([
'summary',
'by_category',
'hotspots',
'efficiency',
'leaks'
]);
expect($report['summary'])->toHaveKeys([
'total_operations',
'total_memory_mb',
'avg_memory_per_operation_mb',
'peak_memory_mb'
]);
expect($report['summary']['total_operations'])->toBe(0);
expect($report['by_category'])->toBeArray();
expect($report['hotspots'])->toBeArray();
});
it('generates comprehensive report with operations', function () {
$this->tracker->measure(
'database.query',
PerformanceCategory::DATABASE,
function () {
$data = array_fill(0, 1000, 'x');
usleep(1000);
}
);
$this->tracker->measure(
'cache.load',
PerformanceCategory::CACHE,
function () {
$data = array_fill(0, 500, 'x');
usleep(500);
}
);
$report = $this->profiler->generateMemoryReport();
expect($report['summary']['total_operations'])->toBe(2);
expect($report['summary']['total_memory_mb'])->toBeFloat();
expect($report['summary']['avg_memory_per_operation_mb'])->toBeFloat();
expect($report['summary']['peak_memory_mb'])->toBeFloat();
});
it('groups operations by category', function () {
$this->tracker->measure(
'db.query.1',
PerformanceCategory::DATABASE,
function () {
$data = array_fill(0, 1000, 'x');
usleep(500);
}
);
$this->tracker->measure(
'db.query.2',
PerformanceCategory::DATABASE,
function () {
$data = array_fill(0, 1000, 'x');
usleep(500);
}
);
$this->tracker->measure(
'cache.get',
PerformanceCategory::CACHE,
function () {
$data = array_fill(0, 500, 'x');
usleep(300);
}
);
$report = $this->profiler->generateMemoryReport();
expect($report['by_category'])->toBeArray();
expect($report['by_category'])->toHaveKey('database');
expect($report['by_category'])->toHaveKey('cache');
$dbStats = $report['by_category']['database'];
expect($dbStats)->toHaveKeys([
'operations',
'total_memory_mb',
'avg_memory_mb',
'peak_memory_mb'
]);
expect($dbStats['operations'])->toBe(2);
});
it('includes top hotspots in report', function () {
for ($i = 0; $i < 10; $i++) {
$this->tracker->measure(
"operation.{$i}",
PerformanceCategory::CUSTOM,
function () use ($i) {
$data = array_fill(0, ($i + 1) * 100, 'x');
usleep(500);
}
);
}
$report = $this->profiler->generateMemoryReport();
expect($report['hotspots'])->toBeArray();
expect($report['hotspots'])->toHaveCount(5); // Top 5
});
it('includes efficiency and leak detection', function () {
$this->tracker->measure(
'test.operation',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 1000, 'x');
usleep(1000);
}
);
$report = $this->profiler->generateMemoryReport();
expect($report['efficiency'])->toHaveKeys([
'score',
'rating',
'total_memory_mb',
'total_time_ms'
]);
expect($report['leaks'])->toHaveKeys([
'potential_leaks',
'leak_count',
'threshold_mb'
]);
});
});
describe('MemoryProfiler - Memory Tracking Over Time', function () {
beforeEach(function () {
$this->tracker = new NestedPerformanceTracker(
new SystemClock(),
new SystemHighResolutionClock(),
new MemoryMonitor()
);
$this->profiler = new MemoryProfiler($this->tracker);
});
it('returns empty tracking for no operations', function () {
$tracking = $this->profiler->trackMemoryOverTime();
expect($tracking)->toHaveKeys([
'tracking_points',
'final_cumulative_mb',
'trend'
]);
expect($tracking['tracking_points'])->toBeArray();
expect($tracking['tracking_points'])->toHaveCount(0);
expect($tracking['final_cumulative_mb'])->toBe(0.0);
expect($tracking['trend'])->toBe('insufficient_data');
});
it('tracks cumulative memory over operations', function () {
for ($i = 0; $i < 3; $i++) {
$this->tracker->measure(
"operation.{$i}",
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 1000, 'x');
usleep(500);
}
);
}
$tracking = $this->profiler->trackMemoryOverTime();
expect($tracking['tracking_points'])->toHaveCount(3);
foreach ($tracking['tracking_points'] as $point) {
expect($point)->toHaveKeys([
'timestamp',
'operation',
'delta_mb',
'cumulative_mb'
]);
expect($point['timestamp'])->toBeFloat();
expect($point['cumulative_mb'])->toBeFloat();
}
});
it('provides trend analysis', function () {
for ($i = 0; $i < 5; $i++) {
$this->tracker->measure(
"operation.{$i}",
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 1000, 'x');
usleep(300);
}
);
}
$tracking = $this->profiler->trackMemoryOverTime();
expect($tracking['trend'])->toBeIn([
'rapidly_growing',
'growing',
'stable',
'decreasing',
'rapidly_decreasing',
'insufficient_data'
]);
});
it('calculates cumulative memory correctly', function () {
$this->tracker->measure(
'operation.1',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 1000, 'x');
usleep(500);
}
);
$this->tracker->measure(
'operation.2',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 500, 'x');
usleep(500);
}
);
$tracking = $this->profiler->trackMemoryOverTime();
expect($tracking['tracking_points'])->toHaveCount(2);
$firstPoint = $tracking['tracking_points'][0];
$secondPoint = $tracking['tracking_points'][1];
// Cumulative should increase or stay same
expect($secondPoint['cumulative_mb'])->toBeGreaterThanOrEqual($firstPoint['cumulative_mb']);
});
});
describe('MemoryProfiler - Budget Validation', function () {
beforeEach(function () {
$this->tracker = new NestedPerformanceTracker(
new SystemClock(),
new SystemHighResolutionClock(),
new MemoryMonitor()
);
$this->profiler = new MemoryProfiler($this->tracker);
});
it('returns no violations for empty operations', function () {
$budgets = [
'test.operation' => 1.0
];
$violations = $this->profiler->getMemoryBudgetViolations($budgets);
expect($violations)->toHaveKeys([
'violations',
'violation_count',
'budgets_checked'
]);
expect($violations['violation_count'])->toBe(0);
expect($violations['budgets_checked'])->toBe(1);
});
it('detects budget violations', function () {
$this->tracker->measure(
'api.request',
PerformanceCategory::API,
function () {
$data = array_fill(0, 10000, str_repeat('x', 100)); // Large allocation
usleep(1000);
}
);
$budgets = [
'api.request' => 0.01 // Very strict budget
];
$violations = $this->profiler->getMemoryBudgetViolations($budgets);
// Violation detection depends on actual memory delta
expect($violations['budgets_checked'])->toBe(1);
expect($violations['violations'])->toBeArray();
});
it('includes violation details', function () {
$this->tracker->measure(
'database.query',
PerformanceCategory::DATABASE,
function () {
$data = array_fill(0, 10000, str_repeat('x', 100));
usleep(1000);
}
);
$budgets = [
'database.query' => 0.001
];
$violations = $this->profiler->getMemoryBudgetViolations($budgets);
if ($violations['violation_count'] > 0) {
$violation = $violations['violations'][0];
expect($violation)->toHaveKeys([
'operation',
'memory_mb',
'budget_mb',
'exceeded_by_mb',
'percentage'
]);
expect($violation['budget_mb'])->toBe(0.001);
}
});
it('supports pattern matching in budgets', function () {
$this->tracker->measure(
'api.request.user',
PerformanceCategory::API,
function () {
$data = array_fill(0, 1000, 'x');
usleep(500);
}
);
$this->tracker->measure(
'api.request.product',
PerformanceCategory::API,
function () {
$data = array_fill(0, 1000, 'x');
usleep(500);
}
);
$budgets = [
'api.request' => 0.001 // Matches both operations
];
$violations = $this->profiler->getMemoryBudgetViolations($budgets);
expect($violations['budgets_checked'])->toBe(1);
});
});
describe('MemoryProfiler - Operation Comparison', function () {
beforeEach(function () {
$this->tracker = new NestedPerformanceTracker(
new SystemClock(),
new SystemHighResolutionClock(),
new MemoryMonitor()
);
$this->profiler = new MemoryProfiler($this->tracker);
});
it('compares memory usage between operations', function () {
$this->tracker->measure(
'implementation.v1',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 5000, 'x');
usleep(1000);
}
);
$this->tracker->measure(
'implementation.v2',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 2500, 'x');
usleep(1000);
}
);
$comparison = $this->profiler->compareOperations('implementation.v1', 'implementation.v2');
expect($comparison)->toHaveKeys([
'operation_1',
'operation_2',
'comparison'
]);
expect($comparison['operation_1'])->toHaveKeys([
'name',
'memory_mb',
'executions'
]);
expect($comparison['operation_2'])->toHaveKeys([
'name',
'memory_mb',
'executions'
]);
expect($comparison['comparison'])->toHaveKeys([
'difference_mb',
'percentage_diff',
'winner'
]);
});
it('identifies winner correctly', function () {
$this->tracker->measure(
'heavy.operation',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 10000, 'x');
usleep(1000);
}
);
$this->tracker->measure(
'light.operation',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 100, 'x');
usleep(1000);
}
);
$comparison = $this->profiler->compareOperations('heavy.operation', 'light.operation');
expect($comparison['comparison']['winner'])->toBeIn(['heavy.operation', 'light.operation']);
});
it('handles operations with multiple executions', function () {
// Execute v1 twice
for ($i = 0; $i < 2; $i++) {
$this->tracker->measure(
'version.1',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 1000, 'x');
usleep(500);
}
);
}
// Execute v2 once
$this->tracker->measure(
'version.2',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 500, 'x');
usleep(500);
}
);
$comparison = $this->profiler->compareOperations('version.1', 'version.2');
expect($comparison['operation_1']['executions'])->toBe(2);
expect($comparison['operation_2']['executions'])->toBe(1);
});
it('calculates percentage difference correctly', function () {
$this->tracker->measure(
'op.a',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 1000, 'x');
usleep(500);
}
);
$this->tracker->measure(
'op.b',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 1000, 'x');
usleep(500);
}
);
$comparison = $this->profiler->compareOperations('op.a', 'op.b');
expect($comparison['comparison']['percentage_diff'])->toBeFloat();
});
it('handles non-existent operations gracefully', function () {
$this->tracker->measure(
'existing.operation',
PerformanceCategory::CUSTOM,
function () {
$data = array_fill(0, 1000, 'x');
usleep(500);
}
);
$comparison = $this->profiler->compareOperations('existing.operation', 'non.existent');
expect($comparison['operation_1']['memory_mb'])->toBeFloat();
expect($comparison['operation_2']['memory_mb'])->toBe(0.0);
expect($comparison['operation_2']['executions'])->toBe(0);
});
});

View File

@@ -0,0 +1,504 @@
<?php
declare(strict_types=1);
use App\Framework\Performance\NestedPerformanceTracker;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\DateTime\SystemClock;
use App\Framework\DateTime\SystemHighResolutionClock;
use App\Framework\Performance\MemoryMonitor;
use App\Framework\Core\ValueObjects\Duration;
describe('NestedPerformanceTracker - Flamegraph Generation', function () {
beforeEach(function () {
$this->clock = new SystemClock();
$this->highResClock = new SystemHighResolutionClock();
$this->memoryMonitor = new MemoryMonitor();
$this->tracker = new NestedPerformanceTracker(
$this->clock,
$this->highResClock,
$this->memoryMonitor
);
});
it('generates flamegraph data for single operation', function () {
$this->tracker->measure(
'database.query',
PerformanceCategory::DATABASE,
function () {
usleep(10000); // 10ms
}
);
$flamegraph = $this->tracker->generateFlamegraph();
expect($flamegraph)->toBeArray();
expect($flamegraph)->toHaveCount(1);
expect($flamegraph[0])->toHaveKeys(['stack_trace', 'samples']);
expect($flamegraph[0]['stack_trace'])->toBe('database.query');
expect($flamegraph[0]['samples'])->toBeGreaterThan(0);
});
it('generates flamegraph data for nested operations', function () {
$this->tracker->measure(
'http.request',
PerformanceCategory::CONTROLLER,
function () {
usleep(5000); // 5ms
$this->tracker->measure(
'database.query',
PerformanceCategory::DATABASE,
function () {
usleep(3000); // 3ms
}
);
usleep(2000); // 2ms
}
);
$flamegraph = $this->tracker->generateFlamegraph();
expect($flamegraph)->toBeArray();
expect($flamegraph)->toHaveCount(2);
// Find parent operation
$parentStack = array_values(array_filter(
$flamegraph,
fn($entry) => $entry['stack_trace'] === 'http.request'
));
expect($parentStack)->toHaveCount(1);
expect($parentStack[0]['samples'])->toBeGreaterThan(0);
// Find child operation
$childStack = array_values(array_filter(
$flamegraph,
fn($entry) => $entry['stack_trace'] === 'http.request;database.query'
));
expect($childStack)->toHaveCount(1);
expect($childStack[0]['samples'])->toBeGreaterThan(0);
});
it('generates flamegraph for deeply nested operations', function () {
$this->tracker->measure(
'controller.action',
PerformanceCategory::CONTROLLER,
function () {
usleep(2000);
$this->tracker->measure(
'service.process',
PerformanceCategory::CUSTOM,
function () {
usleep(2000);
$this->tracker->measure(
'repository.find',
PerformanceCategory::DATABASE,
function () {
usleep(2000);
}
);
}
);
}
);
$flamegraph = $this->tracker->generateFlamegraph();
expect($flamegraph)->toBeArray();
expect($flamegraph)->toHaveCount(3);
// Verify stack traces
$stacks = array_column($flamegraph, 'stack_trace');
expect($stacks)->toContain('controller.action');
expect($stacks)->toContain('controller.action;service.process');
expect($stacks)->toContain('controller.action;service.process;repository.find');
});
it('excludes operations with zero self-time', function () {
$this->tracker->measure(
'parent',
PerformanceCategory::CUSTOM,
function () {
// No self-time, only child time
$this->tracker->measure(
'child',
PerformanceCategory::CUSTOM,
function () {
usleep(5000);
}
);
}
);
$flamegraph = $this->tracker->generateFlamegraph();
expect($flamegraph)->toBeArray();
// Should only have child (parent has 0 self-time)
expect($flamegraph)->toHaveCount(1);
expect($flamegraph[0]['stack_trace'])->toBe('parent;child');
});
it('handles multiple parallel operations', function () {
// First operation
$this->tracker->measure(
'operation.a',
PerformanceCategory::CUSTOM,
function () {
usleep(3000);
}
);
// Second operation
$this->tracker->measure(
'operation.b',
PerformanceCategory::CUSTOM,
function () {
usleep(2000);
}
);
$flamegraph = $this->tracker->generateFlamegraph();
expect($flamegraph)->toBeArray();
expect($flamegraph)->toHaveCount(2);
$stacks = array_column($flamegraph, 'stack_trace');
expect($stacks)->toContain('operation.a');
expect($stacks)->toContain('operation.b');
});
it('returns empty array when no operations completed', function () {
$flamegraph = $this->tracker->generateFlamegraph();
expect($flamegraph)->toBeArray();
expect($flamegraph)->toHaveCount(0);
});
it('generates flamegraph with realistic component scenario', function () {
$this->tracker->measure(
'livecomponent.lifecycle',
PerformanceCategory::CUSTOM,
function () {
usleep(1000);
$this->tracker->measure(
'livecomponent.resolve',
PerformanceCategory::CUSTOM,
function () {
usleep(2000);
}
);
$this->tracker->measure(
'livecomponent.render',
PerformanceCategory::CUSTOM,
function () {
usleep(3000);
$this->tracker->measure(
'template.process',
PerformanceCategory::VIEW,
function () {
usleep(1500);
}
);
}
);
usleep(500);
}
);
$flamegraph = $this->tracker->generateFlamegraph();
expect($flamegraph)->toBeArray();
expect($flamegraph)->toHaveCount(4);
$stacks = array_column($flamegraph, 'stack_trace');
expect($stacks)->toContain('livecomponent.lifecycle');
expect($stacks)->toContain('livecomponent.lifecycle;livecomponent.resolve');
expect($stacks)->toContain('livecomponent.lifecycle;livecomponent.render');
expect($stacks)->toContain('livecomponent.lifecycle;livecomponent.render;template.process');
});
it('samples reflect actual duration', function () {
$this->tracker->measure(
'fast.operation',
PerformanceCategory::CUSTOM,
function () {
usleep(1000); // ~1ms
}
);
$this->tracker->measure(
'slow.operation',
PerformanceCategory::CUSTOM,
function () {
usleep(10000); // ~10ms
}
);
$flamegraph = $this->tracker->generateFlamegraph();
expect($flamegraph)->toHaveCount(2);
$fast = array_values(array_filter(
$flamegraph,
fn($entry) => $entry['stack_trace'] === 'fast.operation'
))[0];
$slow = array_values(array_filter(
$flamegraph,
fn($entry) => $entry['stack_trace'] === 'slow.operation'
))[0];
// Slow operation should have more samples
expect($slow['samples'])->toBeGreaterThan($fast['samples']);
});
});
describe('NestedPerformanceTracker - Timeline Generation', function () {
beforeEach(function () {
$this->clock = new SystemClock();
$this->highResClock = new SystemHighResolutionClock();
$this->memoryMonitor = new MemoryMonitor();
$this->tracker = new NestedPerformanceTracker(
$this->clock,
$this->highResClock,
$this->memoryMonitor
);
});
it('generates timeline for single operation', function () {
$this->tracker->measure(
'test.operation',
PerformanceCategory::CUSTOM,
function () {
usleep(5000);
}
);
$timeline = $this->tracker->generateTimeline();
expect($timeline)->toBeArray();
expect($timeline)->toHaveCount(1);
$event = $timeline[0];
expect($event)->toHaveKeys([
'name',
'category',
'start_time',
'end_time',
'duration_ms',
'depth',
'operation_id',
'parent_id',
'self_time_ms',
'memory_delta_mb',
'context'
]);
expect($event['name'])->toBe('test.operation');
expect($event['category'])->toBe('custom');
expect($event['depth'])->toBe(0);
expect($event['parent_id'])->toBeNull();
expect($event['duration_ms'])->toBeGreaterThan(0);
});
it('generates timeline for nested operations', function () {
$this->tracker->measure(
'parent',
PerformanceCategory::CONTROLLER,
function () {
usleep(2000);
$this->tracker->measure(
'child',
PerformanceCategory::DATABASE,
function () {
usleep(3000);
}
);
}
);
$timeline = $this->tracker->generateTimeline();
expect($timeline)->toBeArray();
expect($timeline)->toHaveCount(2);
// Check parent event
$parent = $timeline[0];
expect($parent['name'])->toBe('parent');
expect($parent['depth'])->toBe(0);
expect($parent['parent_id'])->toBeNull();
// Check child event
$child = $timeline[1];
expect($child['name'])->toBe('child');
expect($child['depth'])->toBe(1);
expect($child['parent_id'])->not->toBeNull();
expect($child['parent_id'])->toBe($parent['operation_id']);
});
it('sorts timeline events by start time', function () {
// Create operations in specific order
$this->tracker->measure(
'operation.1',
PerformanceCategory::CUSTOM,
function () {
usleep(1000);
}
);
usleep(500); // Small gap
$this->tracker->measure(
'operation.2',
PerformanceCategory::CUSTOM,
function () {
usleep(1000);
}
);
$timeline = $this->tracker->generateTimeline();
expect($timeline)->toHaveCount(2);
// Verify chronological order
expect($timeline[0]['name'])->toBe('operation.1');
expect($timeline[1]['name'])->toBe('operation.2');
expect($timeline[0]['start_time'])->toBeLessThan($timeline[1]['start_time']);
});
it('includes memory delta in timeline', function () {
$this->tracker->measure(
'memory.test',
PerformanceCategory::CUSTOM,
function () {
// Allocate some memory
$data = array_fill(0, 10000, 'test');
usleep(1000);
}
);
$timeline = $this->tracker->generateTimeline();
expect($timeline)->toHaveCount(1);
expect($timeline[0]['memory_delta_mb'])->toBeFloat();
});
it('includes operation context in timeline', function () {
$this->tracker->measure(
'contextual.operation',
PerformanceCategory::CUSTOM,
function () {
usleep(1000);
},
['user_id' => 123, 'action' => 'test']
);
$timeline = $this->tracker->generateTimeline();
expect($timeline)->toHaveCount(1);
expect($timeline[0]['context'])->toHaveKeys(['user_id', 'action']);
expect($timeline[0]['context']['user_id'])->toBe(123);
expect($timeline[0]['context']['action'])->toBe('test');
});
it('calculates self-time correctly', function () {
$this->tracker->measure(
'parent',
PerformanceCategory::CUSTOM,
function () {
usleep(5000); // Self time: ~5ms
$this->tracker->measure(
'child',
PerformanceCategory::CUSTOM,
function () {
usleep(3000); // Child time: ~3ms
}
);
}
);
$timeline = $this->tracker->generateTimeline();
$parent = $timeline[0];
$child = $timeline[1];
// Parent total duration should be > child duration
expect($parent['duration_ms'])->toBeGreaterThan($child['duration_ms']);
// Parent self-time should be less than total duration
expect($parent['self_time_ms'])->toBeLessThan($parent['duration_ms']);
// Child self-time should equal its duration (no sub-operations)
expect($child['self_time_ms'])->toBe($child['duration_ms']);
});
it('handles empty tracker gracefully', function () {
$timeline = $this->tracker->generateTimeline();
expect($timeline)->toBeArray();
expect($timeline)->toHaveCount(0);
});
it('generates complete timeline for component lifecycle', function () {
$this->tracker->measure(
'livecomponent.lifecycle',
PerformanceCategory::CUSTOM,
function () {
$this->tracker->measure(
'livecomponent.resolve',
PerformanceCategory::CUSTOM,
function () {
usleep(2000);
},
['phase' => 'initialization']
);
$this->tracker->measure(
'livecomponent.render',
PerformanceCategory::CUSTOM,
function () {
usleep(3000);
},
['cached' => false]
);
$this->tracker->measure(
'livecomponent.handle',
PerformanceCategory::CUSTOM,
function () {
usleep(1500);
},
['action' => 'submit']
);
},
['component_id' => 'user-profile']
);
$timeline = $this->tracker->generateTimeline();
expect($timeline)->toHaveCount(4);
// Verify all phases present
$names = array_column($timeline, 'name');
expect($names)->toContain('livecomponent.lifecycle');
expect($names)->toContain('livecomponent.resolve');
expect($names)->toContain('livecomponent.render');
expect($names)->toContain('livecomponent.handle');
// Verify context propagation
$lifecycle = $timeline[0];
expect($lifecycle['context'])->toHaveKey('component_id');
expect($lifecycle['context']['component_id'])->toBe('user-profile');
});
});