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