Files
michaelschiemer/tests/Unit/Framework/Performance/MemoryProfilerTest.php
Michael Schiemer fc3d7e6357 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.
2025-10-25 19:18:37 +02:00

825 lines
24 KiB
PHP

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