- 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.
334 lines
12 KiB
PHP
334 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\LiveComponents\Profiling\LiveComponentProfiler;
|
|
use App\Framework\LiveComponents\Profiling\ProfileSession;
|
|
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\LiveComponents\Observability\ComponentMetricsCollector;
|
|
use App\Framework\Telemetry\UnifiedTelemetryService;
|
|
use App\Framework\Performance\MemoryMonitor;
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
use App\Framework\Core\ValueObjects\Byte;
|
|
use App\Framework\Core\ValueObjects\Timestamp;
|
|
use Tests\Unit\Framework\LiveComponents\Profiling\SimpleTelemetryService;
|
|
|
|
describe('LiveComponentProfiler', function () {
|
|
beforeEach(function () {
|
|
// Use simple test implementations
|
|
$randomGenerator = new \App\Framework\Random\SecureRandomGenerator();
|
|
$this->telemetryService = new SimpleTelemetryService($randomGenerator);
|
|
$this->metricsCollector = new ComponentMetricsCollector();
|
|
$this->memoryMonitor = new MemoryMonitor();
|
|
|
|
$this->profiler = new LiveComponentProfiler(
|
|
$this->telemetryService,
|
|
$this->metricsCollector,
|
|
$this->memoryMonitor
|
|
);
|
|
});
|
|
|
|
it('starts profiling session', function () {
|
|
$session = $this->profiler->startSession('UserCard');
|
|
|
|
expect($session)->toBeInstanceOf(ProfileSession::class);
|
|
expect($session->componentId)->toBe('UserCard');
|
|
expect($session->sessionId)->toBeInstanceOf(ProfileSessionId::class);
|
|
expect($session->startTime)->toBeInstanceOf(Timestamp::class);
|
|
expect($session->startMemory)->toBeInstanceOf(Byte::class);
|
|
expect($session->operation)->toBeInstanceOf(\App\Framework\Telemetry\OperationHandle::class);
|
|
});
|
|
|
|
it('profiles resolve phase', function () {
|
|
$session = $this->profiler->startSession('UserCard');
|
|
|
|
$result = $this->profiler->profileResolve($session, function () {
|
|
return ['resolved' => true];
|
|
});
|
|
|
|
expect($result)->toBe(['resolved' => true]);
|
|
expect($session->getPhases())->toHaveCount(1);
|
|
|
|
$resolvePhase = $session->getPhase('resolve');
|
|
expect($resolvePhase)->toBeInstanceOf(ProfilePhase::class);
|
|
expect($resolvePhase->name)->toBe('resolve');
|
|
expect($resolvePhase->isSuccessful())->toBeTrue();
|
|
});
|
|
|
|
it('profiles render phase with cache flag', function () {
|
|
$session = $this->profiler->startSession('UserCard');
|
|
|
|
$html = $this->profiler->profileRender($session, function () {
|
|
return '<div>User Card</div>';
|
|
}, cached: true);
|
|
|
|
expect($html)->toBe('<div>User Card</div>');
|
|
expect($session->getPhases())->toHaveCount(1);
|
|
|
|
$renderPhase = $session->getPhase('render');
|
|
expect($renderPhase)->toBeInstanceOf(ProfilePhase::class);
|
|
expect($renderPhase->attributes['cached'])->toBeTrue();
|
|
expect($renderPhase->isSuccessful())->toBeTrue();
|
|
});
|
|
|
|
it('profiles action execution', function () {
|
|
$session = $this->profiler->startSession('UserCard');
|
|
|
|
$result = $this->profiler->profileAction($session, 'submit', function () {
|
|
return ['success' => true];
|
|
});
|
|
|
|
expect($result)->toBe(['success' => true]);
|
|
expect($session->getPhases())->toHaveCount(1);
|
|
|
|
$actionPhase = $session->getPhase('action.submit');
|
|
expect($actionPhase)->toBeInstanceOf(ProfilePhase::class);
|
|
expect($actionPhase->name)->toBe('action.submit');
|
|
expect($actionPhase->isSuccessful())->toBeTrue();
|
|
});
|
|
|
|
it('handles action errors', function () {
|
|
$session = $this->profiler->startSession('UserCard');
|
|
|
|
expect(fn() => $this->profiler->profileAction($session, 'submit', function () {
|
|
throw new \RuntimeException('Action failed');
|
|
}))->toThrow(\RuntimeException::class);
|
|
|
|
$actionPhase = $session->getPhase('action.submit');
|
|
expect($actionPhase)->toBeInstanceOf(ProfilePhase::class);
|
|
expect($actionPhase->isSuccessful())->toBeFalse();
|
|
expect($actionPhase->getError())->toBe('Action failed');
|
|
});
|
|
|
|
it('profiles cache operations', function () {
|
|
$session = $this->profiler->startSession('UserCard');
|
|
|
|
$result = $this->profiler->profileCache($session, 'get', function () {
|
|
return ['cached_data' => true];
|
|
});
|
|
|
|
expect($result)->toBe(['cached_data' => true]);
|
|
expect($session->getPhases())->toHaveCount(1);
|
|
|
|
$cachePhase = $session->getPhase('cache.get');
|
|
expect($cachePhase)->toBeInstanceOf(ProfilePhase::class);
|
|
expect($cachePhase->name)->toBe('cache.get');
|
|
expect($cachePhase->attributes['hit'])->toBeTrue();
|
|
});
|
|
|
|
it('takes memory snapshots', function () {
|
|
$session = $this->profiler->startSession('UserCard');
|
|
|
|
$snapshot = $this->profiler->takeMemorySnapshot($session, 'after_render');
|
|
|
|
expect($snapshot)->toBeInstanceOf(MemorySnapshot::class);
|
|
expect($snapshot->label)->toBe('after_render');
|
|
expect($snapshot->timestamp)->toBeInstanceOf(Timestamp::class);
|
|
expect($session->getMemorySnapshots())->toHaveCount(1);
|
|
});
|
|
|
|
it('ends session and returns result', function () {
|
|
$session = $this->profiler->startSession('UserCard');
|
|
|
|
// Profile some phases
|
|
$this->profiler->profileResolve($session, fn() => ['resolved' => true]);
|
|
$this->profiler->profileRender($session, fn() => '<div>Test</div>');
|
|
|
|
$result = $this->profiler->endSession($session);
|
|
|
|
expect($result)->toBeInstanceOf(ProfileResult::class);
|
|
expect($result->componentId)->toBe('UserCard');
|
|
expect($result->sessionId)->toBeInstanceOf(ProfileSessionId::class);
|
|
expect($result->totalDuration)->toBeInstanceOf(Duration::class);
|
|
expect($result->totalMemory)->toBeInstanceOf(Byte::class);
|
|
expect($result->phases)->toHaveCount(2);
|
|
});
|
|
|
|
it('provides timeline access', function () {
|
|
$timeline = $this->profiler->getTimeline();
|
|
|
|
expect($timeline)->toBeInstanceOf(ProfileTimeline::class);
|
|
});
|
|
});
|
|
|
|
describe('ProfileSessionId', function () {
|
|
it('generates unique session IDs', function () {
|
|
$id1 = ProfileSessionId::generate('UserCard');
|
|
$id2 = ProfileSessionId::generate('UserCard');
|
|
|
|
expect($id1->toString())->not->toBe($id2->toString());
|
|
expect($id1->toString())->toStartWith('UserCard_');
|
|
});
|
|
|
|
it('converts to string', function () {
|
|
$id = ProfileSessionId::generate('UserCard');
|
|
|
|
expect($id->toString())->toBeString();
|
|
expect(strlen($id->toString()))->toBeGreaterThan(10);
|
|
});
|
|
});
|
|
|
|
describe('ProfilePhase', function () {
|
|
it('creates from raw values', function () {
|
|
$phase = ProfilePhase::create(
|
|
name: 'render',
|
|
durationMs: 15.5,
|
|
memoryBytes: 2048,
|
|
attributes: ['cached' => true]
|
|
);
|
|
|
|
expect($phase)->toBeInstanceOf(ProfilePhase::class);
|
|
expect($phase->name)->toBe('render');
|
|
expect($phase->duration)->toBeInstanceOf(Duration::class);
|
|
expect($phase->memoryDelta)->toBeInstanceOf(Byte::class);
|
|
expect($phase->getDurationMs())->toBeFloat();
|
|
expect($phase->getMemoryBytes())->toBe(2048);
|
|
expect($phase->attributes)->toBe(['cached' => true]);
|
|
});
|
|
|
|
it('gets memory in megabytes', function () {
|
|
$phase = ProfilePhase::create(
|
|
name: 'render',
|
|
durationMs: 10.0,
|
|
memoryBytes: 1048576 // 1 MB
|
|
);
|
|
|
|
expect($phase->getMemoryMB())->toBe(1.0);
|
|
});
|
|
|
|
it('checks if phase was successful', function () {
|
|
$success = ProfilePhase::create(
|
|
name: 'render',
|
|
durationMs: 10.0,
|
|
memoryBytes: 1024,
|
|
attributes: ['success' => true]
|
|
);
|
|
|
|
$failure = ProfilePhase::create(
|
|
name: 'render',
|
|
durationMs: 10.0,
|
|
memoryBytes: 1024,
|
|
attributes: ['success' => false, 'error' => 'Render failed']
|
|
);
|
|
|
|
expect($success->isSuccessful())->toBeTrue();
|
|
expect($failure->isSuccessful())->toBeFalse();
|
|
expect($failure->getError())->toBe('Render failed');
|
|
});
|
|
|
|
it('converts to array', function () {
|
|
$phase = ProfilePhase::create(
|
|
name: 'render',
|
|
durationMs: 15.5,
|
|
memoryBytes: 2048
|
|
);
|
|
|
|
$array = $phase->toArray();
|
|
|
|
expect($array)->toHaveKeys([
|
|
'name',
|
|
'duration_ms',
|
|
'memory_bytes',
|
|
'memory_mb',
|
|
'attributes',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('MemorySnapshot', function () {
|
|
it('creates from MemoryMonitor', function () {
|
|
$monitor = new MemoryMonitor();
|
|
$timestamp = Timestamp::now();
|
|
|
|
$snapshot = MemorySnapshot::fromMonitor('checkpoint', $monitor, $timestamp);
|
|
|
|
expect($snapshot)->toBeInstanceOf(MemorySnapshot::class);
|
|
expect($snapshot->label)->toBe('checkpoint');
|
|
expect($snapshot->currentUsage)->toBeInstanceOf(Byte::class);
|
|
expect($snapshot->peakUsage)->toBeInstanceOf(Byte::class);
|
|
expect($snapshot->timestamp)->toBe($timestamp);
|
|
});
|
|
|
|
it('takes snapshot now', function () {
|
|
$snapshot = MemorySnapshot::now('test');
|
|
|
|
expect($snapshot)->toBeInstanceOf(MemorySnapshot::class);
|
|
expect($snapshot->label)->toBe('test');
|
|
});
|
|
|
|
it('gets memory in MB', function () {
|
|
$snapshot = new MemorySnapshot(
|
|
label: 'test',
|
|
currentUsage: Byte::fromBytes(1048576), // 1 MB
|
|
peakUsage: Byte::fromBytes(2097152), // 2 MB
|
|
allocatedObjects: 100,
|
|
timestamp: Timestamp::now()
|
|
);
|
|
|
|
expect($snapshot->getCurrentMemoryMB())->toBe(1.0);
|
|
expect($snapshot->getPeakMemoryMB())->toBe(2.0);
|
|
});
|
|
|
|
it('calculates memory delta', function () {
|
|
$snapshot1 = new MemorySnapshot(
|
|
label: 'before',
|
|
currentUsage: Byte::fromBytes(1048576),
|
|
peakUsage: Byte::fromBytes(1048576),
|
|
allocatedObjects: 100,
|
|
timestamp: Timestamp::now()
|
|
);
|
|
|
|
$snapshot2 = new MemorySnapshot(
|
|
label: 'after',
|
|
currentUsage: Byte::fromBytes(2097152),
|
|
peakUsage: Byte::fromBytes(2097152),
|
|
allocatedObjects: 100,
|
|
timestamp: Timestamp::now()
|
|
);
|
|
|
|
$delta = $snapshot2->deltaFrom($snapshot1);
|
|
|
|
expect($delta->toBytes())->toBe(1048576); // 1 MB increase
|
|
});
|
|
|
|
it('checks if memory increased', function () {
|
|
$snapshot1 = new MemorySnapshot(
|
|
label: 'before',
|
|
currentUsage: Byte::fromBytes(1048576),
|
|
peakUsage: Byte::fromBytes(1048576),
|
|
allocatedObjects: 100,
|
|
timestamp: Timestamp::now()
|
|
);
|
|
|
|
$snapshot2 = new MemorySnapshot(
|
|
label: 'after',
|
|
currentUsage: Byte::fromBytes(2097152),
|
|
peakUsage: Byte::fromBytes(2097152),
|
|
allocatedObjects: 100,
|
|
timestamp: Timestamp::now()
|
|
);
|
|
|
|
expect($snapshot2->memoryIncreasedFrom($snapshot1))->toBeTrue();
|
|
expect($snapshot1->memoryIncreasedFrom($snapshot2))->toBeFalse();
|
|
});
|
|
|
|
it('converts to array', function () {
|
|
$snapshot = MemorySnapshot::now('test');
|
|
$array = $snapshot->toArray();
|
|
|
|
expect($array)->toHaveKeys([
|
|
'label',
|
|
'current_usage_bytes',
|
|
'current_usage_mb',
|
|
'peak_usage_bytes',
|
|
'peak_usage_mb',
|
|
'allocated_objects',
|
|
'timestamp',
|
|
]);
|
|
});
|
|
});
|