- 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.
319 lines
12 KiB
PHP
319 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\DateTime\SystemClock;
|
|
use App\Framework\DateTime\SystemHighResolutionClock;
|
|
use App\Framework\LiveComponents\Performance\ActionProfiler;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
|
use App\Framework\Performance\MemoryMonitor;
|
|
use App\Framework\Performance\NestedPerformanceTracker;
|
|
|
|
/**
|
|
* Unit Tests for ActionProfiler
|
|
*
|
|
* Tests ActionProfiler's capabilities for profiling LiveComponent actions:
|
|
* - CSRF validation profiling
|
|
* - Authorization check profiling
|
|
* - Rate limit check profiling
|
|
* - Parameter binding profiling
|
|
* - Idempotency check profiling
|
|
* - Action metrics aggregation
|
|
* - Component-level summaries
|
|
* - System-wide performance reports
|
|
*/
|
|
describe('ActionProfiler', function () {
|
|
beforeEach(function () {
|
|
$this->tracker = new NestedPerformanceTracker(
|
|
new SystemClock(),
|
|
new SystemHighResolutionClock(),
|
|
new MemoryMonitor()
|
|
);
|
|
|
|
$this->profiler = new ActionProfiler($this->tracker);
|
|
$this->componentId = ComponentId::create('test-component', 'instance-1');
|
|
});
|
|
|
|
it('profiles CSRF validation', function () {
|
|
$this->profiler->profileCsrfValidation(
|
|
$this->componentId,
|
|
'testAction',
|
|
function () {
|
|
usleep(1000); // 1ms
|
|
return true;
|
|
}
|
|
);
|
|
|
|
$timeline = $this->tracker->generateTimeline();
|
|
|
|
expect($timeline)->not->toBeEmpty();
|
|
|
|
$csrfEvents = array_filter($timeline, fn($e) => str_contains($e['name'], 'csrf'));
|
|
|
|
expect($csrfEvents)->not->toBeEmpty();
|
|
|
|
$event = array_values($csrfEvents)[0];
|
|
expect($event['duration_ms'])->toBeGreaterThan(0.5);
|
|
expect($event)->toHaveKey('context');
|
|
expect($event['context'])->toHaveKey('component');
|
|
expect($event['context']['component'])->toBe('test-component');
|
|
});
|
|
|
|
it('profiles authorization check', function () {
|
|
$this->profiler->profileAuthorizationCheck(
|
|
$this->componentId,
|
|
'testAction',
|
|
function () {
|
|
usleep(500); // 0.5ms
|
|
return true;
|
|
}
|
|
);
|
|
|
|
$timeline = $this->tracker->generateTimeline();
|
|
|
|
$authEvents = array_filter($timeline, fn($e) => str_contains($e['name'], 'authorization'));
|
|
|
|
expect($authEvents)->not->toBeEmpty();
|
|
|
|
$event = array_values($authEvents)[0];
|
|
expect($event['context'])->toHaveKey('action');
|
|
expect($event['context']['action'])->toBe('testAction');
|
|
});
|
|
|
|
it('profiles rate limit check', function () {
|
|
$this->profiler->profileRateLimitCheck(
|
|
$this->componentId,
|
|
'testAction',
|
|
function () {
|
|
usleep(300); // 0.3ms
|
|
return true;
|
|
}
|
|
);
|
|
|
|
$timeline = $this->tracker->generateTimeline();
|
|
|
|
$rateLimitEvents = array_filter($timeline, fn($e) => str_contains($e['name'], 'rateLimit'));
|
|
|
|
expect($rateLimitEvents)->not->toBeEmpty();
|
|
});
|
|
|
|
it('profiles parameter binding', function () {
|
|
$this->profiler->profileParameterBinding(
|
|
$this->componentId,
|
|
'testAction',
|
|
function () {
|
|
usleep(2000); // 2ms - reflection overhead
|
|
return ['param1' => 'value1'];
|
|
}
|
|
);
|
|
|
|
$timeline = $this->tracker->generateTimeline();
|
|
|
|
$bindingEvents = array_filter($timeline, fn($e) => str_contains($e['name'], 'binding'));
|
|
|
|
expect($bindingEvents)->not->toBeEmpty();
|
|
|
|
$event = array_values($bindingEvents)[0];
|
|
expect($event['duration_ms'])->toBeGreaterThan(1);
|
|
});
|
|
|
|
it('profiles idempotency check', function () {
|
|
$this->profiler->profileIdempotencyCheck(
|
|
$this->componentId,
|
|
'testAction',
|
|
function () {
|
|
usleep(400); // 0.4ms - cache lookup
|
|
return null; // No cached result
|
|
}
|
|
);
|
|
|
|
$timeline = $this->tracker->generateTimeline();
|
|
|
|
$idempotencyEvents = array_filter($timeline, fn($e) => str_contains($e['name'], 'idempotency'));
|
|
|
|
expect($idempotencyEvents)->not->toBeEmpty();
|
|
});
|
|
|
|
it('aggregates action metrics across multiple executions', function () {
|
|
$component1 = ComponentId::create('counter', 'inst-1');
|
|
|
|
// Execute multiple actions WITHOUT reset to accumulate metrics
|
|
for ($i = 0; $i < 3; $i++) {
|
|
$this->profiler->profileCsrfValidation($component1, 'increment', fn() => usleep(100));
|
|
$this->profiler->profileAuthorizationCheck($component1, 'increment', fn() => usleep(50));
|
|
}
|
|
|
|
// Get metrics for specific action
|
|
$counterMetrics = $this->profiler->getActionMetrics('counter', 'increment');
|
|
|
|
expect($counterMetrics)->toHaveKey('component');
|
|
expect($counterMetrics)->toHaveKey('action');
|
|
expect($counterMetrics)->toHaveKey('phase_timings');
|
|
|
|
expect($counterMetrics['component'])->toBe('counter');
|
|
expect($counterMetrics['action'])->toBe('increment');
|
|
|
|
// Verify we tracked multiple phases
|
|
expect($counterMetrics['phase_timings'])->not->toBeEmpty();
|
|
});
|
|
|
|
it('provides component-level performance summary', function () {
|
|
$component = ComponentId::create('user-profile', 'user-123');
|
|
|
|
// Multiple actions on same component WITHOUT reset
|
|
$this->profiler->profileCsrfValidation($component, 'update', fn() => usleep(100));
|
|
$this->profiler->profileParameterBinding($component, 'update', fn() => usleep(200));
|
|
$this->profiler->profileCsrfValidation($component, 'delete', fn() => usleep(100));
|
|
$this->profiler->profileAuthorizationCheck($component, 'delete', fn() => usleep(150));
|
|
|
|
$summary = $this->profiler->getComponentSummary('user-profile');
|
|
|
|
expect($summary)->toHaveKey('component');
|
|
expect($summary)->toHaveKey('total_operations');
|
|
expect($summary)->toHaveKey('total_time_ms');
|
|
expect($summary)->toHaveKey('operations');
|
|
|
|
expect($summary['component'])->toBe('user-profile');
|
|
expect($summary['total_operations'])->toBeGreaterThan(0);
|
|
expect($summary['operations'])->toBeArray();
|
|
});
|
|
|
|
it('generates system-wide performance report', function () {
|
|
$comp1 = ComponentId::create('component-a', 'a-1');
|
|
$comp2 = ComponentId::create('component-b', 'b-1');
|
|
$comp3 = ComponentId::create('component-c', 'c-1');
|
|
|
|
// Simulate multiple components and actions WITHOUT reset
|
|
$this->profiler->profileCsrfValidation($comp1, 'action1', fn() => usleep(100));
|
|
$this->profiler->profileAuthorizationCheck($comp2, 'action2', fn() => usleep(150));
|
|
$this->profiler->profileRateLimitCheck($comp3, 'action3', fn() => usleep(75));
|
|
|
|
$report = $this->profiler->generatePerformanceReport();
|
|
|
|
expect($report)->toHaveKey('generated_at');
|
|
expect($report)->toHaveKey('total_components');
|
|
expect($report)->toHaveKey('total_operations');
|
|
expect($report)->toHaveKey('components');
|
|
|
|
expect($report['total_components'])->toBe(3);
|
|
expect($report['components'])->toHaveKey('component-a');
|
|
expect($report['components'])->toHaveKey('component-b');
|
|
expect($report['components'])->toHaveKey('component-c');
|
|
});
|
|
|
|
it('tracks execution times for action phases', function () {
|
|
$component = ComponentId::create('fast-component', 'fast-1');
|
|
|
|
// Execute action multiple times
|
|
for ($i = 0; $i < 3; $i++) {
|
|
$this->profiler->profileCsrfValidation($component, 'quickAction', fn() => usleep(100));
|
|
}
|
|
|
|
$metrics = $this->profiler->getActionMetrics('fast-component', 'quickAction');
|
|
|
|
expect($metrics)->toHaveKey('phase_timings');
|
|
expect($metrics['phase_timings'])->not->toBeEmpty();
|
|
|
|
// Verify phase timing structure
|
|
$firstPhase = array_values($metrics['phase_timings'])[0];
|
|
expect($firstPhase)->toHaveKey('count');
|
|
expect($firstPhase)->toHaveKey('total_ms');
|
|
expect($firstPhase)->toHaveKey('avg_ms');
|
|
expect($firstPhase['count'])->toBe(3);
|
|
});
|
|
|
|
it('calculates average execution time for phases', function () {
|
|
$component = ComponentId::create('slow-component', 'slow-1');
|
|
|
|
// Execute multiple times
|
|
for ($i = 0; $i < 3; $i++) {
|
|
$this->profiler->profileParameterBinding($component, 'complexAction', fn() => usleep(1000));
|
|
}
|
|
|
|
$metrics = $this->profiler->getActionMetrics('slow-component', 'complexAction');
|
|
|
|
expect($metrics)->toHaveKey('phase_timings');
|
|
expect($metrics['phase_timings'])->not->toBeEmpty();
|
|
|
|
// Verify average is calculated correctly
|
|
$firstPhase = array_values($metrics['phase_timings'])[0];
|
|
expect($firstPhase['avg_ms'])->toBeGreaterThan(0);
|
|
expect($firstPhase['avg_ms'])->toBe($firstPhase['total_ms'] / $firstPhase['count']);
|
|
});
|
|
|
|
it('tracks total execution count per phase', function () {
|
|
$component = ComponentId::create('counted-component', 'count-1');
|
|
|
|
// Execute 5 times WITHOUT reset
|
|
for ($i = 0; $i < 5; $i++) {
|
|
$this->profiler->profileCsrfValidation($component, 'countedAction', fn() => usleep(100));
|
|
}
|
|
|
|
$metrics = $this->profiler->getActionMetrics('counted-component', 'countedAction');
|
|
|
|
expect($metrics)->toHaveKey('phase_timings');
|
|
expect($metrics['phase_timings'])->not->toBeEmpty();
|
|
|
|
// Verify count tracking
|
|
$firstPhase = array_values($metrics['phase_timings'])[0];
|
|
expect($firstPhase['count'])->toBe(5);
|
|
});
|
|
|
|
it('handles components with no profiled actions gracefully', function () {
|
|
$summary = $this->profiler->getComponentSummary('non-existent-component');
|
|
|
|
expect($summary['component'])->toBe('non-existent-component');
|
|
expect($summary['total_operations'])->toBe(0);
|
|
expect($summary['total_time_ms'])->toBe(0);
|
|
expect($summary['operations'])->toBe([]);
|
|
});
|
|
|
|
it('handles actions with no profiled phases gracefully', function () {
|
|
$metrics = $this->profiler->getActionMetrics('non-existent', 'action');
|
|
|
|
expect($metrics['component'])->toBe('non-existent');
|
|
expect($metrics['action'])->toBe('action');
|
|
expect($metrics['executions'])->toBe(0);
|
|
expect($metrics['metrics'])->toBe([]);
|
|
});
|
|
|
|
it('measures overhead of profiling system', function () {
|
|
$component = ComponentId::create('overhead-test', 'ov-1');
|
|
|
|
// Measure baseline (no profiling) - execute noop lambda
|
|
$baselineStart = microtime(true);
|
|
for ($i = 0; $i < 100; $i++) {
|
|
$noop = function () {}; // Minimal work
|
|
$noop();
|
|
}
|
|
$baselineDuration = (microtime(true) - $baselineStart) * 1000;
|
|
|
|
// Measure with profiling
|
|
$this->tracker->reset();
|
|
$profiledStart = microtime(true);
|
|
for ($i = 0; $i < 100; $i++) {
|
|
$this->profiler->profileCsrfValidation($component, 'noop', fn() => null);
|
|
}
|
|
$profiledDuration = (microtime(true) - $profiledStart) * 1000;
|
|
|
|
// Overhead should be reasonable (<5ms for 100 operations)
|
|
$overhead = $profiledDuration - $baselineDuration;
|
|
|
|
expect($overhead)->toBeLessThan(10); // Maximum 10ms overhead for 100 operations
|
|
});
|
|
|
|
it('includes context data in all profiled operations', function () {
|
|
$component = ComponentId::create('context-test', 'ctx-1');
|
|
|
|
$this->profiler->profileCsrfValidation($component, 'contextAction', fn() => usleep(100));
|
|
|
|
$timeline = $this->tracker->generateTimeline();
|
|
|
|
foreach ($timeline as $event) {
|
|
expect($event)->toHaveKey('context');
|
|
expect($event['context'])->toHaveKey('component');
|
|
expect($event['context'])->toHaveKey('action');
|
|
}
|
|
});
|
|
});
|