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