- 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.
853 lines
29 KiB
PHP
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);
|
|
});
|
|
});
|