- 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.
505 lines
16 KiB
PHP
505 lines
16 KiB
PHP
<?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');
|
|
});
|
|
});
|