- 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.
252 lines
8.7 KiB
PHP
252 lines
8.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\LiveComponents\Profiling\ProfileTimeline;
|
|
use App\Framework\LiveComponents\Profiling\ValueObjects\ProfileResult;
|
|
use App\Framework\LiveComponents\Profiling\ValueObjects\ProfileSessionId;
|
|
use App\Framework\LiveComponents\Profiling\ValueObjects\ProfilePhase;
|
|
use App\Framework\LiveComponents\Profiling\ValueObjects\MemorySnapshot;
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
use App\Framework\Core\ValueObjects\Byte;
|
|
use App\Framework\Core\ValueObjects\Timestamp;
|
|
|
|
describe('ProfileTimeline', function () {
|
|
beforeEach(function () {
|
|
$this->timeline = new ProfileTimeline();
|
|
|
|
// Create test ProfileResult
|
|
$this->result = new ProfileResult(
|
|
sessionId: ProfileSessionId::generate('UserCard'),
|
|
componentId: 'UserCard',
|
|
totalDuration: Duration::fromMilliseconds(50.0),
|
|
totalMemory: Byte::fromBytes(4096),
|
|
phases: [
|
|
ProfilePhase::create('resolve', 10.0, 1024, ['success' => true]),
|
|
ProfilePhase::create('render', 30.0, 2048, ['cached' => false, 'success' => true]),
|
|
ProfilePhase::create('action.submit', 10.0, 1024, ['success' => true]),
|
|
],
|
|
memorySnapshots: [
|
|
new MemorySnapshot(
|
|
label: 'after_resolve',
|
|
currentUsage: Byte::fromBytes(1048576),
|
|
peakUsage: Byte::fromBytes(1048576),
|
|
allocatedObjects: 100,
|
|
timestamp: Timestamp::now()
|
|
),
|
|
new MemorySnapshot(
|
|
label: 'after_render',
|
|
currentUsage: Byte::fromBytes(2097152),
|
|
peakUsage: Byte::fromBytes(2097152),
|
|
allocatedObjects: 150,
|
|
timestamp: Timestamp::now()
|
|
),
|
|
],
|
|
startTime: Timestamp::now(),
|
|
endTime: Timestamp::now()
|
|
);
|
|
});
|
|
|
|
it('generates DevTools timeline', function () {
|
|
$timeline = $this->timeline->generateDevToolsTimeline($this->result);
|
|
|
|
expect($timeline)->toBeArray();
|
|
expect($timeline)->not->toBeEmpty();
|
|
|
|
// Should have metadata event
|
|
$metadataEvent = $timeline[0];
|
|
expect($metadataEvent['ph'])->toBe('M'); // Metadata
|
|
expect($metadataEvent['name'])->toBe('thread_name');
|
|
|
|
// Should have complete events for phases
|
|
$completeEvents = array_filter($timeline, fn($e) => $e['ph'] === 'X');
|
|
expect($completeEvents)->toHaveCount(3); // 3 phases
|
|
|
|
// Should have instant events for memory snapshots
|
|
$instantEvents = array_filter($timeline, fn($e) => $e['ph'] === 'i');
|
|
expect($instantEvents)->toHaveCount(2); // 2 snapshots
|
|
});
|
|
|
|
it('generates simple timeline', function () {
|
|
$timeline = $this->timeline->generateSimpleTimeline($this->result);
|
|
|
|
expect($timeline)->toBeArray();
|
|
expect($timeline)->toHaveKeys([
|
|
'component_id',
|
|
'session_id',
|
|
'total_duration_ms',
|
|
'total_memory_mb',
|
|
'start_time',
|
|
'end_time',
|
|
'phases',
|
|
'memory_snapshots',
|
|
]);
|
|
|
|
expect($timeline['component_id'])->toBe('UserCard');
|
|
expect($timeline['phases'])->toHaveCount(3);
|
|
expect($timeline['memory_snapshots'])->toHaveCount(2);
|
|
|
|
// Verify phase structure
|
|
$firstPhase = $timeline['phases'][0];
|
|
expect($firstPhase)->toHaveKeys([
|
|
'name',
|
|
'start_ms',
|
|
'end_ms',
|
|
'duration_ms',
|
|
'memory_mb',
|
|
'success',
|
|
'error',
|
|
]);
|
|
});
|
|
|
|
it('generates flamegraph data', function () {
|
|
$flamegraph = $this->timeline->generateFlamegraph($this->result);
|
|
|
|
expect($flamegraph)->toBeString();
|
|
expect($flamegraph)->toContain('livecomponent.UserCard;resolve');
|
|
expect($flamegraph)->toContain('livecomponent.UserCard;render');
|
|
expect($flamegraph)->toContain('livecomponent.UserCard;action.submit');
|
|
|
|
// Should have stack format: "stack_name count"
|
|
$lines = explode("\n", $flamegraph);
|
|
foreach ($lines as $line) {
|
|
if (!empty($line)) {
|
|
expect($line)->toMatch('/^[^;]+;[^\s]+ \d+$/');
|
|
}
|
|
}
|
|
});
|
|
|
|
it('generates Gantt chart data', function () {
|
|
$gantt = $this->timeline->generateGanttChart($this->result);
|
|
|
|
expect($gantt)->toBeArray();
|
|
expect($gantt)->toHaveKeys(['component_id', 'total_duration_ms', 'tasks']);
|
|
expect($gantt['tasks'])->toHaveCount(3);
|
|
|
|
// Verify task structure
|
|
$firstTask = $gantt['tasks'][0];
|
|
expect($firstTask)->toHaveKeys([
|
|
'id',
|
|
'name',
|
|
'start',
|
|
'end',
|
|
'duration',
|
|
'memory',
|
|
'success',
|
|
'type',
|
|
]);
|
|
|
|
expect($firstTask['name'])->toBe('Resolve');
|
|
expect($firstTask['type'])->toBe('initialization');
|
|
});
|
|
|
|
it('generates waterfall diagram', function () {
|
|
$waterfall = $this->timeline->generateWaterfall($this->result);
|
|
|
|
expect($waterfall)->toBeArray();
|
|
expect($waterfall)->toHaveKeys(['component_id', 'total_time', 'entries']);
|
|
expect($waterfall['entries'])->toHaveCount(3);
|
|
|
|
// Verify entry structure
|
|
$firstEntry = $waterfall['entries'][0];
|
|
expect($firstEntry)->toHaveKeys([
|
|
'name',
|
|
'start_time',
|
|
'duration',
|
|
'memory_delta',
|
|
'timing',
|
|
'status',
|
|
]);
|
|
|
|
expect($firstEntry['timing'])->toHaveKeys([
|
|
'blocked',
|
|
'execution',
|
|
'total',
|
|
]);
|
|
});
|
|
|
|
it('categorizes phases by type', function () {
|
|
$gantt = $this->timeline->generateGanttChart($this->result);
|
|
|
|
$resolveTask = $gantt['tasks'][0];
|
|
$renderTask = $gantt['tasks'][1];
|
|
$actionTask = $gantt['tasks'][2];
|
|
|
|
expect($resolveTask['type'])->toBe('initialization');
|
|
expect($renderTask['type'])->toBe('rendering');
|
|
expect($actionTask['type'])->toBe('interaction');
|
|
});
|
|
|
|
it('exports as JSON in different formats', function () {
|
|
$formats = ['devtools', 'simple', 'gantt', 'waterfall'];
|
|
|
|
foreach ($formats as $format) {
|
|
$json = $this->timeline->exportAsJson($this->result, $format);
|
|
|
|
expect($json)->toBeString();
|
|
|
|
$decoded = json_decode($json, true);
|
|
expect($decoded)->not->toBeNull();
|
|
expect(json_last_error())->toBe(JSON_ERROR_NONE);
|
|
}
|
|
});
|
|
|
|
it('throws exception for unknown format', function () {
|
|
expect(fn() => $this->timeline->exportAsJson($this->result, 'unknown'))
|
|
->toThrow(\InvalidArgumentException::class);
|
|
});
|
|
|
|
it('includes error stacks in flamegraph for failed phases', function () {
|
|
$resultWithError = new ProfileResult(
|
|
sessionId: ProfileSessionId::generate('UserCard'),
|
|
componentId: 'UserCard',
|
|
totalDuration: Duration::fromMilliseconds(20.0),
|
|
totalMemory: Byte::fromBytes(2048),
|
|
phases: [
|
|
ProfilePhase::create('render', 20.0, 2048, [
|
|
'success' => false,
|
|
'error' => 'Render failed'
|
|
]),
|
|
],
|
|
memorySnapshots: [],
|
|
startTime: Timestamp::now(),
|
|
endTime: Timestamp::now()
|
|
);
|
|
|
|
$flamegraph = $this->timeline->generateFlamegraph($resultWithError);
|
|
|
|
expect($flamegraph)->toContain('livecomponent.UserCard;render');
|
|
expect($flamegraph)->toContain('livecomponent.UserCard;render;error');
|
|
});
|
|
|
|
it('generates counter events for memory tracking in DevTools format', function () {
|
|
$timeline = $this->timeline->generateDevToolsTimeline($this->result);
|
|
|
|
$counterEvents = array_filter($timeline, fn($e) => $e['ph'] === 'C');
|
|
expect(count($counterEvents))->toBeGreaterThan(0);
|
|
|
|
foreach ($counterEvents as $event) {
|
|
expect($event)->toHaveKey('args');
|
|
expect($event['args'])->toHaveKey('memory_mb');
|
|
}
|
|
});
|
|
|
|
it('preserves phase timing in simple timeline', function () {
|
|
$timeline = $this->timeline->generateSimpleTimeline($this->result);
|
|
|
|
$phases = $timeline['phases'];
|
|
|
|
// First phase starts at 0
|
|
expect($phases[0]['start_ms'])->toBe(0.0);
|
|
expect($phases[0]['end_ms'])->toBe(10.0);
|
|
|
|
// Second phase starts where first ended
|
|
expect($phases[1]['start_ms'])->toBe(10.0);
|
|
expect($phases[1]['end_ms'])->toBe(40.0);
|
|
|
|
// Third phase starts where second ended
|
|
expect($phases[2]['start_ms'])->toBe(40.0);
|
|
expect($phases[2]['end_ms'])->toBe(50.0);
|
|
});
|
|
});
|