Files
michaelschiemer/tests/Integration/Framework/LiveComponents/LiveComponentProfilingIntegrationTest.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- 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.
2025-10-25 19:18:37 +02:00

853 lines
29 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\DateTime\SystemClock;
use App\Framework\DateTime\SystemHighResolutionClock;
use App\Framework\LiveComponents\Attributes\Action;
use App\Framework\LiveComponents\ComponentEventDispatcher;
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
use App\Framework\LiveComponents\LiveComponentHandler;
use App\Framework\LiveComponents\ParameterBinding\ParameterBinder;
use App\Framework\LiveComponents\Security\ActionAuthorizationChecker;
use App\Framework\LiveComponents\Services\LiveComponentRateLimiter;
use App\Framework\LiveComponents\Validation\SchemaCache;
use App\Framework\LiveComponents\ValueObjects\ActionParameters;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\LiveComponents\Performance\ActionProfiler;
use App\Framework\Performance\MemoryMonitor;
use App\Framework\Performance\NestedPerformanceTracker;
use App\Framework\Performance\PerformanceCategory;
/**
* Integration Tests for LiveComponent Performance Profiling
*
* Tests the integration between:
* - LiveComponentHandler
* - NestedPerformanceTracker
* - ActionProfiler
* - Component Lifecycle Profiling
*/
describe('LiveComponent Profiling Integration', function () {
beforeEach(function () {
// Initialize performance tracking infrastructure
$this->tracker = new NestedPerformanceTracker(
new SystemClock(),
new SystemHighResolutionClock(),
new MemoryMonitor()
);
$this->profiler = new ActionProfiler($this->tracker);
// Mock dependencies for LiveComponentHandler
$this->eventDispatcher = new ComponentEventDispatcher();
// Simple anonymous class mocks instead of Mockery (to avoid readonly property issues)
$this->session = new class implements \App\Framework\Http\Session\SessionInterface {
public readonly object $csrf;
public readonly object $validation;
public readonly object $form;
public function __construct()
{
$this->csrf = new class {
public function validateToken(string $formId, string $token): bool
{
return true;
}
};
$this->validation = new class {};
$this->form = new class {};
}
public function get(string $key, mixed $default = null): mixed { return $default; }
public function set(string $key, mixed $value): void {}
public function has(string $key): bool { return false; }
public function remove(string $key): void {}
public function clear(): void {}
public function all(): array { return []; }
public function getId(): \App\Framework\Http\Session\SessionId { return new \App\Framework\Http\Session\SessionId('test-session-id'); }
public static function fromArray(\App\Framework\Http\Session\SessionId $sessionId, \App\Framework\DateTime\Clock $clock, \App\Framework\Security\CsrfTokenGenerator $csrfTokenGenerator, array $data): self { return new self(); }
};
$this->authChecker = new class implements \App\Framework\LiveComponents\Security\ActionAuthorizationChecker {
public function isAuthorized($component, string $method, $permissionAttribute): bool
{
return true;
}
public function getUserPermissions(): array
{
return [];
}
public function hasPermission(string $permission): bool
{
return true;
}
public function isAuthenticated(): bool
{
return true;
}
};
$this->schemaCache = new SchemaCache();
$this->rateLimiter = new class {
public function checkActionLimit($component, string $method, string $clientId, $actionAttr): \App\Framework\LiveComponents\ValueObjects\RateLimitResult
{
return new \App\Framework\LiveComponents\ValueObjects\RateLimitResult(
allowed: true,
limit: 100,
current: 1,
retryAfter: null
);
}
};
$this->idempotency = new class {
public function execute(mixed $key, callable $operation, mixed $ttl): mixed
{
return $operation();
}
};
$this->parameterBinder = new class {
public function bindParameters(\ReflectionMethod $method, ActionParameters $params): array
{
return [];
}
};
$this->frameworkDispatcher = new class {
public function dispatch($event): void {}
};
// Create handler with performance tracker
$this->handler = new LiveComponentHandler(
$this->eventDispatcher,
$this->session,
$this->authChecker,
$this->schemaCache,
$this->rateLimiter,
$this->idempotency,
$this->parameterBinder,
$this->frameworkDispatcher,
$this->tracker // PerformanceTracker injected here
);
});
it('tracks performance for component action execution', function () {
// Create test component
$component = new class implements LiveComponentContract {
public ComponentId $id;
public object $state;
public function __construct()
{
$this->id = ComponentId::create('test-counter', 'profiling-1');
$this->state = new class {
public int $count = 0;
public function toArray(): array
{
return ['count' => $this->count];
}
};
}
#[Action]
public function increment(): object
{
// Simulate some work
usleep(5000); // 5ms
$newState = clone $this->state;
$newState->count++;
return $newState;
}
};
// Execute action
$params = ActionParameters::create([
'_csrf_token' => 'valid-token'
]);
$this->handler->handle($component, 'increment', $params);
// Verify performance tracking
$timeline = $this->tracker->generateTimeline();
expect($timeline)->toBeArray();
expect($timeline)->not->toBeEmpty();
// Find the main component action measurement
$componentMeasurements = array_filter($timeline, function ($event) {
return str_contains($event['name'], 'livecomponent.test-counter.increment');
});
expect($componentMeasurements)->not->toBeEmpty();
$mainMeasurement = array_values($componentMeasurements)[0];
expect($mainMeasurement['category'])->toBe('custom');
expect($mainMeasurement['duration_ms'])->toBeGreaterThan(4); // At least 5ms from usleep
});
it('tracks nested profiling for schema derivation', function () {
$component = new class implements LiveComponentContract {
public ComponentId $id;
public object $state;
public function __construct()
{
$this->id = ComponentId::create('schema-test', 'nested-1');
$this->state = new class {
public string $value = 'test';
public function toArray(): array
{
return ['value' => $this->value];
}
};
}
#[Action]
public function update(): object
{
return $this->state;
}
};
$params = ActionParameters::create(['_csrf_token' => 'valid-token']);
$this->handler->handle($component, 'update', $params);
$timeline = $this->tracker->generateTimeline();
// Should have nested measurements for:
// 1. Main component action
// 2. Schema derivation
// 3. Action execution
// 4. State validation
$schemaMeasurements = array_filter($timeline, fn($e) => str_contains($e['name'], 'schema.derive'));
$actionMeasurements = array_filter($timeline, fn($e) => str_contains($e['name'], 'action.execute'));
$validationMeasurements = array_filter($timeline, fn($e) => str_contains($e['name'], 'state.validate'));
expect($schemaMeasurements)->not->toBeEmpty();
expect($actionMeasurements)->not->toBeEmpty();
expect($validationMeasurements)->not->toBeEmpty();
});
it('tracks lifecycle hook performance (onMount)', function () {
$component = new class implements LiveComponentContract, \App\Framework\LiveComponents\Contracts\LifecycleAware {
public ComponentId $id;
public object $state;
public bool $mountCalled = false;
public function __construct()
{
$this->id = ComponentId::create('lifecycle-test', 'mount-1');
$this->state = new class {
public array $data = [];
public function toArray(): array
{
return $this->data;
}
};
}
public function onMount(): void
{
// Simulate initialization work
usleep(2000); // 2ms
$this->mountCalled = true;
}
public function onUpdate(): void
{
// Not tested here
}
#[Action]
public function test(): object
{
return $this->state;
}
};
// Call onMount via handler
$this->handler->callMountHook($component);
expect($component->mountCalled)->toBeTrue();
$timeline = $this->tracker->generateTimeline();
$mountMeasurements = array_filter($timeline, fn($e) => str_contains($e['name'], 'lifecycle.onMount'));
expect($mountMeasurements)->not->toBeEmpty();
$mountMeasurement = array_values($mountMeasurements)[0];
expect($mountMeasurement['duration_ms'])->toBeGreaterThan(1); // At least 2ms
expect($mountMeasurement['context'])->toHaveKey('component');
expect($mountMeasurement['context']['component'])->toBe('lifecycle-test');
});
it('tracks lifecycle hook performance (onUpdate)', function () {
$component = new class implements LiveComponentContract, \App\Framework\LiveComponents\Contracts\LifecycleAware {
public ComponentId $id;
public object $state;
public bool $updateCalled = false;
public function __construct()
{
$this->id = ComponentId::create('lifecycle-test', 'update-1');
$this->state = new class {
public int $counter = 0;
public function toArray(): array
{
return ['counter' => $this->counter];
}
};
}
public function onMount(): void
{
// Not tested here
}
public function onUpdate(): void
{
// Simulate update work
usleep(3000); // 3ms
$this->updateCalled = true;
}
#[Action]
public function increment(): object
{
$newState = clone $this->state;
$newState->counter++;
return $newState;
}
};
$params = ActionParameters::create(['_csrf_token' => 'valid-token']);
$this->handler->handle($component, 'increment', $params);
expect($component->updateCalled)->toBeTrue();
$timeline = $this->tracker->generateTimeline();
$updateMeasurements = array_filter($timeline, fn($e) => str_contains($e['name'], 'lifecycle.onUpdate'));
expect($updateMeasurements)->not->toBeEmpty();
$updateMeasurement = array_values($updateMeasurements)[0];
expect($updateMeasurement['duration_ms'])->toBeGreaterThan(2); // At least 3ms
});
it('provides context data for all profiled operations', function () {
$component = new class implements LiveComponentContract {
public ComponentId $id;
public object $state;
public function __construct()
{
$this->id = ComponentId::create('context-component', 'ctx-1');
$this->state = new class {
public string $name = 'test';
public function toArray(): array
{
return ['name' => $this->name];
}
};
}
#[Action]
public function doSomething(): object
{
usleep(1000);
return $this->state;
}
};
$params = ActionParameters::create(['_csrf_token' => 'valid-token']);
$this->handler->handle($component, 'doSomething', $params);
$timeline = $this->tracker->generateTimeline();
// Check that context is provided for measurements
foreach ($timeline as $event) {
if (str_contains($event['name'], 'livecomponent.context-component')) {
expect($event)->toHaveKey('context');
expect($event['context'])->toHaveKey('component');
expect($event['context']['component'])->toBe('context-component');
if (str_contains($event['name'], 'livecomponent.context-component.doSomething')) {
expect($event['context'])->toHaveKey('action');
expect($event['context']['action'])->toBe('doSomething');
}
}
}
});
it('tracks memory usage for component actions', function () {
$component = new class implements LiveComponentContract {
public ComponentId $id;
public object $state;
public function __construct()
{
$this->id = ComponentId::create('memory-test', 'mem-1');
$this->state = new class {
public array $data = [];
public function toArray(): array
{
return $this->data;
}
};
}
#[Action]
public function allocateMemory(): object
{
// Allocate some memory
$data = array_fill(0, 5000, str_repeat('x', 100)); // ~0.5MB
$newState = clone $this->state;
$newState->data = $data;
return $newState;
}
};
$params = ActionParameters::create(['_csrf_token' => 'valid-token']);
$this->handler->handle($component, 'allocateMemory', $params);
$timeline = $this->tracker->generateTimeline();
// Find component action measurement
$componentMeasurements = array_filter($timeline, function ($event) {
return str_contains($event['name'], 'livecomponent.memory-test.allocateMemory');
});
expect($componentMeasurements)->not->toBeEmpty();
$measurement = array_values($componentMeasurements)[0];
// Should have memory tracking
expect($measurement)->toHaveKey('memory_delta_mb');
expect($measurement['memory_delta_mb'])->toBeGreaterThan(0);
});
it('integrates with ActionProfiler for detailed metrics', function () {
$component = new class implements LiveComponentContract {
public ComponentId $id;
public object $state;
public function __construct()
{
$this->id = ComponentId::create('profiler-test', 'prof-1');
$this->state = new class {
public int $value = 0;
public function toArray(): array
{
return ['value' => $this->value];
}
};
}
#[Action]
public function process(): object
{
usleep(10000); // 10ms
return $this->state;
}
};
$params = ActionParameters::create(['_csrf_token' => 'valid-token']);
// Execute action multiple times
for ($i = 0; $i < 3; $i++) {
$this->handler->handle($component, 'process', $params);
$this->tracker->reset(); // Reset for next execution
}
// Note: ActionProfiler methods would need to be used here
// This test verifies the integration point exists
expect($this->profiler)->toBeInstanceOf(ActionProfiler::class);
});
it('handles fast actions without overhead', function () {
$component = new class implements LiveComponentContract {
public ComponentId $id;
public object $state;
public function __construct()
{
$this->id = ComponentId::create('fast-action', 'fast-1');
$this->state = new class {
public bool $flag = false;
public function toArray(): array
{
return ['flag' => $this->flag];
}
};
}
#[Action]
public function toggle(): object
{
// Very fast action - no usleep
$newState = clone $this->state;
$newState->flag = !$newState->flag;
return $newState;
}
};
$params = ActionParameters::create(['_csrf_token' => 'valid-token']);
$start = microtime(true);
$this->handler->handle($component, 'toggle', $params);
$duration = (microtime(true) - $start) * 1000;
// Performance tracking should add minimal overhead (<5ms for very fast actions)
expect($duration)->toBeLessThan(10);
$timeline = $this->tracker->generateTimeline();
expect($timeline)->not->toBeEmpty();
});
it('tracks performance categories correctly', function () {
$component = new class implements LiveComponentContract {
public ComponentId $id;
public object $state;
public function __construct()
{
$this->id = ComponentId::create('category-test', 'cat-1');
$this->state = new class {
public array $items = [];
public function toArray(): array
{
return ['items' => $this->items];
}
};
}
#[Action]
public function load(): object
{
return $this->state;
}
};
$params = ActionParameters::create(['_csrf_token' => 'valid-token']);
$this->handler->handle($component, 'load', $params);
$timeline = $this->tracker->generateTimeline();
// Verify categories
$categoryCounts = [];
foreach ($timeline as $event) {
$category = $event['category'];
$categoryCounts[$category] = ($categoryCounts[$category] ?? 0) + 1;
}
// Should have:
// - CUSTOM for main component action
// - CACHE for schema derivation
expect($categoryCounts)->toHaveKey('custom');
expect($categoryCounts)->toHaveKey('cache');
});
});
describe('LiveComponent Profiling - Error Scenarios', function () {
beforeEach(function () {
$this->tracker = new NestedPerformanceTracker(
new SystemClock(),
new SystemHighResolutionClock(),
new MemoryMonitor()
);
$this->profiler = new ActionProfiler($this->tracker);
$this->eventDispatcher = new ComponentEventDispatcher();
// Simple mocks (copy from first describe block)
$this->session = new class {
public readonly object $csrf;
public readonly object $validation;
public readonly object $form;
public function __construct()
{
$this->csrf = new class {
public function validateToken(string $formId, string $token): bool { return true; }
};
$this->validation = new class {};
$this->form = new class {};
}
};
$this->authChecker = new class implements \App\Framework\LiveComponents\Security\ActionAuthorizationChecker {
public function isAuthorized($component, string $method, $permissionAttribute): bool { return true; }
public function getUserPermissions(): array { return []; }
public function hasPermission(string $permission): bool { return true; }
public function isAuthenticated(): bool { return true; }
};
$this->schemaCache = new SchemaCache();
$this->rateLimiter = new class {
public function checkActionLimit($component, string $method, string $clientId, $actionAttr): \App\Framework\LiveComponents\ValueObjects\RateLimitResult {
return new \App\Framework\LiveComponents\ValueObjects\RateLimitResult(allowed: true, limit: 100, current: 1, retryAfter: null);
}
};
$this->idempotency = new class {
public function execute(mixed $key, callable $operation, mixed $ttl): mixed { return $operation(); }
};
$this->parameterBinder = new class {
public function bindParameters(\ReflectionMethod $method, ActionParameters $params): array { return []; }
};
$this->frameworkDispatcher = new class {
public function dispatch($event): void {}
};
$this->handler = new LiveComponentHandler(
$this->eventDispatcher,
$this->session,
$this->authChecker,
$this->schemaCache,
$this->rateLimiter,
$this->idempotency,
$this->parameterBinder,
$this->frameworkDispatcher,
$this->tracker
);
});
it('tracks performance even when action throws exception', function () {
$component = new class implements LiveComponentContract {
public ComponentId $id;
public object $state;
public function __construct()
{
$this->id = ComponentId::create('error-test', 'err-1');
$this->state = new class {
public function toArray(): array
{
return [];
}
};
}
#[Action]
public function failing(): object
{
usleep(3000); // 3ms before error
throw new \RuntimeException('Action failed');
}
};
$params = ActionParameters::create(['_csrf_token' => 'valid-token']);
try {
$this->handler->handle($component, 'failing', $params);
expect(true)->toBeFalse('Should have thrown exception');
} catch (\RuntimeException $e) {
expect($e->getMessage())->toBe('Action failed');
}
// Performance should still be tracked even though action failed
$timeline = $this->tracker->generateTimeline();
$actionMeasurements = array_filter($timeline, fn($e) => str_contains($e['name'], 'action.execute'));
expect($actionMeasurements)->not->toBeEmpty();
// The execution time should reflect the work done before the exception
$measurement = array_values($actionMeasurements)[0];
expect($measurement['duration_ms'])->toBeGreaterThan(2);
});
it('handles lifecycle hook errors gracefully while still tracking', function () {
$component = new class implements LiveComponentContract, \App\Framework\LiveComponents\Contracts\LifecycleAware {
public ComponentId $id;
public object $state;
public function __construct()
{
$this->id = ComponentId::create('lifecycle-error', 'err-2');
$this->state = new class {
public function toArray(): array
{
return [];
}
};
}
public function onMount(): void
{
usleep(1000);
throw new \Exception('Mount failed');
}
public function onUpdate(): void
{
// Not tested
}
#[Action]
public function test(): object
{
return $this->state;
}
};
// Should not throw - lifecycle hook errors are caught
$this->handler->callMountHook($component);
$timeline = $this->tracker->generateTimeline();
// Performance tracking should still work
$mountMeasurements = array_filter($timeline, fn($e) => str_contains($e['name'], 'lifecycle.onMount'));
expect($mountMeasurements)->not->toBeEmpty();
});
});
describe('LiveComponent Profiling - Performance Benchmarks', function () {
beforeEach(function () {
$this->tracker = new NestedPerformanceTracker(
new SystemClock(),
new SystemHighResolutionClock(),
new MemoryMonitor()
);
$this->profiler = new ActionProfiler($this->tracker);
$this->eventDispatcher = new ComponentEventDispatcher();
// Simple mocks (copy from first describe block)
$this->session = new class {
public readonly object $csrf;
public readonly object $validation;
public readonly object $form;
public function __construct()
{
$this->csrf = new class {
public function validateToken(string $formId, string $token): bool { return true; }
};
$this->validation = new class {};
$this->form = new class {};
}
};
$this->authChecker = new class implements \App\Framework\LiveComponents\Security\ActionAuthorizationChecker {
public function isAuthorized($component, string $method, $permissionAttribute): bool { return true; }
public function getUserPermissions(): array { return []; }
public function hasPermission(string $permission): bool { return true; }
public function isAuthenticated(): bool { return true; }
};
$this->schemaCache = new SchemaCache();
$this->rateLimiter = new class {
public function checkActionLimit($component, string $method, string $clientId, $actionAttr): \App\Framework\LiveComponents\ValueObjects\RateLimitResult {
return new \App\Framework\LiveComponents\ValueObjects\RateLimitResult(allowed: true, limit: 100, current: 1, retryAfter: null);
}
};
$this->idempotency = new class {
public function execute(mixed $key, callable $operation, mixed $ttl): mixed { return $operation(); }
};
$this->parameterBinder = new class {
public function bindParameters(\ReflectionMethod $method, ActionParameters $params): array { return []; }
};
$this->frameworkDispatcher = new class {
public function dispatch($event): void {}
};
$this->handler = new LiveComponentHandler(
$this->eventDispatcher,
$this->session,
$this->authChecker,
$this->schemaCache,
$this->rateLimiter,
$this->idempotency,
$this->parameterBinder,
$this->frameworkDispatcher,
$this->tracker
);
});
it('measures overhead of profiling system', function () {
$component = new class implements LiveComponentContract {
public ComponentId $id;
public object $state;
public function __construct()
{
$this->id = ComponentId::create('benchmark', 'bench-1');
$this->state = new class {
public int $counter = 0;
public function toArray(): array
{
return ['counter' => $this->counter];
}
};
}
#[Action]
public function noop(): object
{
// Minimal work - just to measure overhead
return $this->state;
}
};
$params = ActionParameters::create(['_csrf_token' => 'valid-token']);
// Execute multiple times to get average
$executions = 100;
$totalTime = 0;
for ($i = 0; $i < $executions; $i++) {
$start = microtime(true);
$this->handler->handle($component, 'noop', $params);
$totalTime += (microtime(true) - $start) * 1000;
$this->tracker->reset();
}
$avgTime = $totalTime / $executions;
// Average execution time should be reasonable (<5ms per action with profiling)
expect($avgTime)->toBeLessThan(5);
// Should be able to handle 200+ actions per second
$actionsPerSecond = 1000 / $avgTime;
expect($actionsPerSecond)->toBeGreaterThan(200);
});
});