docs: consolidate documentation into organized structure

- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
This commit is contained in:
2025-10-05 11:05:04 +02:00
parent 887847dde6
commit 5050c7d73a
36686 changed files with 196456 additions and 12398919 deletions

View File

@@ -0,0 +1,622 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Queue\Performance;
use App\Framework\Database\DatabaseManager;
use App\Framework\Queue\Distribution\JobDistributionService;
use App\Framework\Queue\Jobs\JobStatus;
use App\Framework\Queue\Workers\WorkerRegistry;
use App\Framework\Queue\Workers\WorkerStatus;
use PHPUnit\Framework\TestCase;
final class DatabasePerformanceTest extends TestCase
{
private DatabaseManager $database;
private WorkerRegistry $workerRegistry;
private JobDistributionService $distributionService;
protected function setUp(): void
{
$this->database = $this->createTestDatabase();
$this->workerRegistry = new WorkerRegistry($this->database);
$this->distributionService = new JobDistributionService(
$this->database,
$this->workerRegistry
);
$this->cleanupTestData();
PerformanceTestHelper::warmupDatabase($this->database->getConnection());
}
protected function tearDown(): void
{
$this->cleanupTestData();
}
public function testWorkerQueryPerformance(): void
{
// Create workers with different statuses and capacities
$workers = array_merge(
$this->createWorkers(50, 20, WorkerStatus::AVAILABLE),
$this->createWorkers(20, 15, WorkerStatus::BUSY),
$this->createWorkers(10, 25, WorkerStatus::FAILED)
);
$this->registerWorkers($workers);
echo "\nWorker Query Performance Test:\n";
echo "Total workers: " . count($workers) . "\n";
// Test worker lookup by ID
$workerLookupTimes = [];
for ($i = 0; $i < 100; $i++) {
$randomWorker = $workers[array_rand($workers)];
$workerId = $randomWorker->id->toString();
$time = PerformanceTestHelper::measureTime(function() use ($workerId) {
return $this->workerRegistry->getWorker($workerId);
});
$workerLookupTimes[] = $time;
}
$lookupStats = PerformanceTestHelper::calculateStatistics($workerLookupTimes);
echo "Worker lookup by ID: " . PerformanceTestHelper::formatStatistics($lookupStats) . "\n";
// Test getting available workers
$availableWorkerTimes = [];
for ($i = 0; $i < 100; $i++) {
$time = PerformanceTestHelper::measureTime(function() {
return $this->workerRegistry->getAvailableWorkers();
});
$availableWorkerTimes[] = $time;
}
$availableStats = PerformanceTestHelper::calculateStatistics($availableWorkerTimes);
echo "Get available workers: " . PerformanceTestHelper::formatStatistics($availableStats) . "\n";
// Test worker status updates
$updateTimes = [];
for ($i = 0; $i < 100; $i++) {
$randomWorker = $workers[array_rand($workers)];
$workerId = $randomWorker->id->toString();
$time = PerformanceTestHelper::measureTime(function() use ($workerId) {
$this->updateWorkerStatus($workerId, WorkerStatus::BUSY);
$this->updateWorkerStatus($workerId, WorkerStatus::AVAILABLE);
});
$updateTimes[] = $time;
}
$updateStats = PerformanceTestHelper::calculateStatistics($updateTimes);
echo "Worker status updates: " . PerformanceTestHelper::formatStatistics($updateStats) . "\n";
// Validate performance benchmarks
$this->assertLessThan(1.0, $lookupStats['avg'], 'Worker lookup average time exceeds 1ms');
$this->assertLessThan(2.0, $availableStats['avg'], 'Available workers query average time exceeds 2ms');
$this->assertLessThan(5.0, $updateStats['avg'], 'Worker update average time exceeds 5ms');
PerformanceTestHelper::assertPerformance($workerLookupTimes, 1.0, 2.0, 'Worker lookup');
PerformanceTestHelper::assertPerformance($availableWorkerTimes, 2.0, 5.0, 'Available workers query');
}
public function testJobQueryPerformance(): void
{
$workers = $this->createWorkers(20, 25);
$this->registerWorkers($workers);
// Create jobs with different statuses and priorities
$jobs = array_merge(
PerformanceTestHelper::createBulkJobs(500),
PerformanceTestHelper::createBulkJobs(300, \App\Framework\Queue\Jobs\JobPriority::HIGH),
PerformanceTestHelper::createBulkJobs(200, \App\Framework\Queue\Jobs\JobPriority::CRITICAL)
);
// Distribute and update job statuses
foreach ($jobs as $index => $job) {
$this->distributionService->distributeJob($job);
// Simulate some completed jobs
if ($index % 3 === 0) {
$this->updateJobStatus($job->id, JobStatus::COMPLETED);
} elseif ($index % 5 === 0) {
$this->updateJobStatus($job->id, JobStatus::FAILED);
}
}
echo "\nJob Query Performance Test:\n";
echo "Total jobs: " . count($jobs) . "\n";
// Test job lookup by ID
$jobLookupTimes = [];
for ($i = 0; $i < 100; $i++) {
$randomJob = $jobs[array_rand($jobs)];
$jobId = $randomJob->id;
$time = PerformanceTestHelper::measureTime(function() use ($jobId) {
return $this->getJobById($jobId);
});
$jobLookupTimes[] = $time;
}
$lookupStats = PerformanceTestHelper::calculateStatistics($jobLookupTimes);
echo "Job lookup by ID: " . PerformanceTestHelper::formatStatistics($lookupStats) . "\n";
// Test getting jobs by status
$statusQueryTimes = [];
$statuses = [JobStatus::PENDING, JobStatus::PROCESSING, JobStatus::COMPLETED, JobStatus::FAILED];
foreach ($statuses as $status) {
for ($i = 0; $i < 25; $i++) {
$time = PerformanceTestHelper::measureTime(function() use ($status) {
return $this->getJobsByStatus($status);
});
$statusQueryTimes[] = $time;
}
}
$statusStats = PerformanceTestHelper::calculateStatistics($statusQueryTimes);
echo "Jobs by status query: " . PerformanceTestHelper::formatStatistics($statusStats) . "\n";
// Test getting jobs by worker
$workerJobTimes = [];
for ($i = 0; $i < 50; $i++) {
$randomWorker = $workers[array_rand($workers)];
$workerId = $randomWorker->id->toString();
$time = PerformanceTestHelper::measureTime(function() use ($workerId) {
return $this->getJobsByWorker($workerId);
});
$workerJobTimes[] = $time;
}
$workerJobStats = PerformanceTestHelper::calculateStatistics($workerJobTimes);
echo "Jobs by worker query: " . PerformanceTestHelper::formatStatistics($workerJobStats) . "\n";
// Validate performance benchmarks
$this->assertLessThan(1.0, $lookupStats['avg'], 'Job lookup average time exceeds 1ms');
$this->assertLessThan(5.0, $statusStats['avg'], 'Job status query average time exceeds 5ms');
$this->assertLessThan(3.0, $workerJobStats['avg'], 'Jobs by worker query average time exceeds 3ms');
PerformanceTestHelper::assertPerformance($jobLookupTimes, 1.0, 2.0, 'Job lookup');
PerformanceTestHelper::assertPerformance($statusQueryTimes, 5.0, 10.0, 'Job status queries');
}
public function testBatchOperationPerformance(): void
{
echo "\nBatch Operation Performance Test:\n";
$batchSizes = [10, 50, 100, 500, 1000];
foreach ($batchSizes as $batchSize) {
echo "Testing batch size: {$batchSize}\n";
// Test batch worker registration
$workers = $this->createWorkers($batchSize, 20);
$batchRegisterTime = PerformanceTestHelper::measureTime(function() use ($workers) {
$this->registerWorkers($workers);
});
// Test batch job distribution
$jobs = PerformanceTestHelper::createBulkJobs($batchSize);
$batchDistributeTime = PerformanceTestHelper::measureTime(function() use ($jobs) {
foreach ($jobs as $job) {
$this->distributionService->distributeJob($job);
}
});
// Test batch job status updates
$batchUpdateTime = PerformanceTestHelper::measureTime(function() use ($jobs) {
foreach ($jobs as $job) {
$this->updateJobStatus($job->id, JobStatus::COMPLETED);
}
});
$perItemRegister = $batchRegisterTime / $batchSize;
$perItemDistribute = $batchDistributeTime / $batchSize;
$perItemUpdate = $batchUpdateTime / $batchSize;
echo sprintf(
" Register: %6.1fms total (%4.2fms/item), Distribute: %6.1fms (%4.2fms/item), Update: %6.1fms (%4.2fms/item)\n",
$batchRegisterTime,
$perItemRegister,
$batchDistributeTime,
$perItemDistribute,
$batchUpdateTime,
$perItemUpdate
);
// Batch operations should be efficient
$this->assertLessThan(2.0, $perItemRegister, "Worker registration too slow for batch size {$batchSize}");
$this->assertLessThan(5.0, $perItemDistribute, "Job distribution too slow for batch size {$batchSize}");
$this->assertLessThan(1.0, $perItemUpdate, "Job update too slow for batch size {$batchSize}");
$this->cleanupTestData();
}
}
public function testIndexEfficiency(): void
{
echo "\nIndex Efficiency Test:\n";
// Create large dataset to test index effectiveness
$workerCount = 1000;
$jobCount = 5000;
$workers = $this->createWorkers($workerCount, 20);
$this->registerWorkers($workers);
$jobs = PerformanceTestHelper::createBulkJobs($jobCount);
foreach ($jobs as $job) {
$this->distributionService->distributeJob($job);
}
echo "Dataset: {$workerCount} workers, {$jobCount} jobs\n";
// Test indexed queries performance
$indexTests = [
'worker_by_id' => function() use ($workers) {
$randomWorker = $workers[array_rand($workers)];
return $this->workerRegistry->getWorker($randomWorker->id->toString());
},
'workers_by_status' => function() {
return $this->workerRegistry->getWorkersByStatus(WorkerStatus::AVAILABLE);
},
'jobs_by_status' => function() {
return $this->getJobsByStatus(JobStatus::PENDING);
},
'jobs_by_priority' => function() {
return $this->getJobsByPriority(\App\Framework\Queue\Jobs\JobPriority::HIGH);
},
'jobs_by_queue' => function() {
return $this->getJobsByQueue('test_queue');
}
];
foreach ($indexTests as $testName => $testFunction) {
$times = [];
for ($i = 0; $i < 100; $i++) {
$time = PerformanceTestHelper::measureTime($testFunction);
$times[] = $time;
}
$stats = PerformanceTestHelper::calculateStatistics($times);
echo "{$testName}: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// All indexed queries should be fast even with large dataset
$this->assertLessThan(10.0, $stats['avg'], "Index query {$testName} too slow with large dataset");
$this->assertLessThan(25.0, $stats['p95'], "Index query {$testName} P95 too slow with large dataset");
}
}
public function testConnectionPoolingPerformance(): void
{
echo "\nConnection Pooling Performance Test:\n";
// Simulate multiple concurrent database operations
$operationTypes = [
'worker_lookup' => function() {
$workerId = 'worker_' . rand(1, 100);
return $this->workerRegistry->getWorker($workerId);
},
'job_insertion' => function() {
$job = PerformanceTestHelper::createTestJob('pool_test_' . uniqid());
return $this->distributionService->distributeJob($job);
},
'status_query' => function() {
return $this->getJobsByStatus(JobStatus::PENDING);
}
];
$concurrencyLevels = [1, 5, 10, 20];
foreach ($concurrencyLevels as $concurrency) {
echo "Testing concurrency level: {$concurrency}\n";
$operationTimes = [];
$operationsPerType = 20;
foreach ($operationTypes as $typeName => $operation) {
$typeTimes = [];
// Simulate concurrent operations (simplified for single-threaded PHP)
for ($i = 0; $i < $operationsPerType * $concurrency; $i++) {
$time = PerformanceTestHelper::measureTime($operation);
$typeTimes[] = $time;
$operationTimes[] = $time;
}
$typeStats = PerformanceTestHelper::calculateStatistics($typeTimes);
echo " {$typeName}: " . PerformanceTestHelper::formatStatistics($typeStats) . "\n";
}
$overallStats = PerformanceTestHelper::calculateStatistics($operationTimes);
echo " Overall: " . PerformanceTestHelper::formatStatistics($overallStats) . "\n";
// Performance should not degrade significantly with concurrency
$this->assertLessThan(50.0, $overallStats['avg'], "Database operations too slow at concurrency {$concurrency}");
}
}
public function testQueryOptimizationEffectiveness(): void
{
echo "\nQuery Optimization Effectiveness Test:\n";
// Create test data with specific patterns to test optimization
$workers = $this->createWorkers(500, 20);
$this->registerWorkers($workers);
$jobs = PerformanceTestHelper::createBulkJobs(2000);
foreach ($jobs as $job) {
$this->distributionService->distributeJob($job);
}
// Test complex queries that should benefit from optimization
$complexQueries = [
'workers_with_capacity_filter' => function() {
return $this->getWorkersByCapacityRange(15, 25);
},
'jobs_with_multiple_filters' => function() {
return $this->getJobsWithFilters(JobStatus::PENDING, \App\Framework\Queue\Jobs\JobPriority::NORMAL);
},
'job_count_aggregation' => function() {
return $this->getJobCountsByStatus();
},
'worker_utilization_stats' => function() {
return $this->getWorkerUtilizationStats();
}
];
foreach ($complexQueries as $queryName => $queryFunction) {
$times = [];
for ($i = 0; $i < 50; $i++) {
$time = PerformanceTestHelper::measureTime($queryFunction);
$times[] = $time;
}
$stats = PerformanceTestHelper::calculateStatistics($times);
echo "{$queryName}: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// Complex queries should still be reasonably fast
$this->assertLessThan(20.0, $stats['avg'], "Complex query {$queryName} not optimized enough");
$this->assertLessThan(50.0, $stats['p95'], "Complex query {$queryName} P95 not optimized enough");
}
}
public function testTransactionPerformance(): void
{
echo "\nTransaction Performance Test:\n";
$workers = $this->createWorkers(10, 20);
$this->registerWorkers($workers);
// Test transaction overhead
$transactionSizes = [1, 5, 10, 50, 100];
foreach ($transactionSizes as $size) {
$transactionTimes = [];
for ($iteration = 0; $iteration < 20; $iteration++) {
$time = PerformanceTestHelper::measureTime(function() use ($size, $iteration) {
$pdo = $this->database->getConnection();
try {
$pdo->beginTransaction();
for ($i = 0; $i < $size; $i++) {
$job = PerformanceTestHelper::createTestJob("tx_job_{$iteration}_{$i}");
$this->distributionService->distributeJob($job);
}
$pdo->commit();
} catch (\Exception $e) {
$pdo->rollBack();
throw $e;
}
});
$transactionTimes[] = $time;
}
$stats = PerformanceTestHelper::calculateStatistics($transactionTimes);
$timePerOperation = $stats['avg'] / $size;
echo sprintf(
"Transaction size %3d: %6.1fms total (%5.2fms/operation)\n",
$size,
$stats['avg'],
$timePerOperation
);
// Transaction overhead should be reasonable
$this->assertLessThan(200.0, $stats['avg'], "Transaction time too high for size {$size}");
$this->cleanupJobs();
}
}
private function createWorkers(int $count, int $capacity, WorkerStatus $status = WorkerStatus::AVAILABLE): array
{
$workers = [];
for ($i = 1; $i <= $count; $i++) {
$workers[] = PerformanceTestHelper::createTestWorker(
"db_perf_worker_{$i}",
$capacity,
$status
);
}
return $workers;
}
private function registerWorkers(array $workers): void
{
foreach ($workers as $worker) {
$this->workerRegistry->registerWorker($worker);
}
}
private function updateWorkerStatus(string $workerId, WorkerStatus $status): void
{
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('UPDATE workers SET status = ? WHERE id = ?');
$stmt->execute([$status->value, $workerId]);
}
private function updateJobStatus(string $jobId, JobStatus $status): void
{
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('UPDATE jobs SET status = ? WHERE id = ?');
$stmt->execute([$status->value, $jobId]);
}
private function getJobById(string $jobId): ?array
{
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT * FROM jobs WHERE id = ?');
$stmt->execute([$jobId]);
return $stmt->fetch(\PDO::FETCH_ASSOC) ?: null;
}
private function getJobsByStatus(JobStatus $status): array
{
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT * FROM jobs WHERE status = ? LIMIT 100');
$stmt->execute([$status->value]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
private function getJobsByWorker(string $workerId): array
{
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT * FROM jobs WHERE worker_id = ? LIMIT 100');
$stmt->execute([$workerId]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
private function getJobsByPriority(\App\Framework\Queue\Jobs\JobPriority $priority): array
{
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT * FROM jobs WHERE priority = ? LIMIT 100');
$stmt->execute([$priority->value]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
private function getJobsByQueue(string $queueName): array
{
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT * FROM jobs WHERE queue_name = ? LIMIT 100');
$stmt->execute([$queueName]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
private function getWorkersByCapacityRange(int $minCapacity, int $maxCapacity): array
{
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT * FROM workers WHERE capacity BETWEEN ? AND ?');
$stmt->execute([$minCapacity, $maxCapacity]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
private function getJobsWithFilters(JobStatus $status, \App\Framework\Queue\Jobs\JobPriority $priority): array
{
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT * FROM jobs WHERE status = ? AND priority = ? LIMIT 100');
$stmt->execute([$status->value, $priority->value]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
private function getJobCountsByStatus(): array
{
$pdo = $this->database->getConnection();
$stmt = $pdo->query('SELECT status, COUNT(*) as count FROM jobs GROUP BY status');
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
private function getWorkerUtilizationStats(): array
{
$pdo = $this->database->getConnection();
$stmt = $pdo->query('
SELECT
w.status,
AVG(w.capacity) as avg_capacity,
COUNT(*) as worker_count,
COUNT(j.id) as job_count
FROM workers w
LEFT JOIN jobs j ON w.id = j.worker_id
GROUP BY w.status
');
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
private function cleanupJobs(): void
{
$pdo = $this->database->getConnection();
$pdo->exec('DELETE FROM jobs');
}
private function createTestDatabase(): DatabaseManager
{
$pdo = new \PDO('sqlite::memory:');
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$pdo->exec('
CREATE TABLE workers (
id TEXT PRIMARY KEY,
queue_names TEXT NOT NULL,
capacity INTEGER NOT NULL,
status TEXT NOT NULL,
last_heartbeat TEXT NOT NULL,
metadata TEXT
)
');
$pdo->exec('
CREATE TABLE jobs (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
payload TEXT NOT NULL,
queue_name TEXT NOT NULL,
priority INTEGER NOT NULL,
status TEXT NOT NULL,
worker_id TEXT,
created_at TEXT NOT NULL,
started_at TEXT,
completed_at TEXT,
attempts INTEGER DEFAULT 0,
error_message TEXT
)
');
// Comprehensive indexes for performance testing
$pdo->exec('CREATE INDEX idx_workers_id ON workers(id)');
$pdo->exec('CREATE INDEX idx_workers_status ON workers(status)');
$pdo->exec('CREATE INDEX idx_workers_capacity ON workers(capacity)');
$pdo->exec('CREATE INDEX idx_workers_status_capacity ON workers(status, capacity)');
$pdo->exec('CREATE INDEX idx_jobs_id ON jobs(id)');
$pdo->exec('CREATE INDEX idx_jobs_status ON jobs(status)');
$pdo->exec('CREATE INDEX idx_jobs_priority ON jobs(priority)');
$pdo->exec('CREATE INDEX idx_jobs_queue ON jobs(queue_name)');
$pdo->exec('CREATE INDEX idx_jobs_worker ON jobs(worker_id)');
$pdo->exec('CREATE INDEX idx_jobs_status_priority ON jobs(status, priority)');
$pdo->exec('CREATE INDEX idx_jobs_created ON jobs(created_at)');
return new DatabaseManager($pdo);
}
private function cleanupTestData(): void
{
$pdo = $this->database->getConnection();
$pdo->exec('DELETE FROM workers');
$pdo->exec('DELETE FROM jobs');
}
}

View File

@@ -0,0 +1,435 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Queue\Performance;
use App\Framework\Database\DatabaseManager;
use App\Framework\Queue\Locks\DatabaseDistributedLock;
use App\Framework\Queue\Locks\LockKey;
use App\Framework\Queue\Locks\LockOwner;
use PHPUnit\Framework\TestCase;
final class DistributedLockPerformanceTest extends TestCase
{
private DatabaseManager $database;
private DatabaseDistributedLock $lockService;
protected function setUp(): void
{
$this->database = $this->createTestDatabase();
$this->lockService = new DatabaseDistributedLock($this->database);
$this->cleanupTestData();
PerformanceTestHelper::warmupDatabase($this->database->getConnection());
}
protected function tearDown(): void
{
$this->cleanupTestData();
}
public function testLockAcquisitionLatency(): void
{
$acquisitionTimes = [];
$iterations = 1000;
for ($i = 0; $i < $iterations; $i++) {
$lockKey = new LockKey("test_lock_{$i}");
$owner = new LockOwner("owner_{$i}");
$time = PerformanceTestHelper::measureTime(function() use ($lockKey, $owner) {
$acquired = $this->lockService->acquire($lockKey, $owner, 30);
if ($acquired) {
$this->lockService->release($lockKey, $owner);
}
return $acquired;
});
$acquisitionTimes[] = $time;
}
$stats = PerformanceTestHelper::calculateStatistics($acquisitionTimes);
// Validate performance benchmarks
$this->assertLessThan(2.0, $stats['avg'], 'Average lock acquisition time exceeds 2ms');
$this->assertLessThan(5.0, $stats['p95'], 'P95 lock acquisition time exceeds 5ms');
$this->assertLessThan(10.0, $stats['p99'], 'P99 lock acquisition time exceeds 10ms');
echo "\nLock Acquisition Latency Results:\n";
echo "Iterations: {$iterations}\n";
echo "Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
PerformanceTestHelper::assertPerformance(
$acquisitionTimes,
2.0,
5.0,
'Lock acquisition'
);
}
public function testLockReleaseLatency(): void
{
$releaseTimes = [];
$iterations = 1000;
// Pre-acquire locks
$locks = [];
for ($i = 0; $i < $iterations; $i++) {
$lockKey = new LockKey("release_lock_{$i}");
$owner = new LockOwner("owner_{$i}");
$acquired = $this->lockService->acquire($lockKey, $owner, 60);
if ($acquired) {
$locks[] = ['key' => $lockKey, 'owner' => $owner];
}
}
// Measure release times
foreach ($locks as $lock) {
$time = PerformanceTestHelper::measureTime(function() use ($lock) {
return $this->lockService->release($lock['key'], $lock['owner']);
});
$releaseTimes[] = $time;
}
$stats = PerformanceTestHelper::calculateStatistics($releaseTimes);
// Validate performance benchmarks
$this->assertLessThan(1.5, $stats['avg'], 'Average lock release time exceeds 1.5ms');
$this->assertLessThan(3.0, $stats['p95'], 'P95 lock release time exceeds 3ms');
$this->assertLessThan(8.0, $stats['p99'], 'P99 lock release time exceeds 8ms');
echo "\nLock Release Latency Results:\n";
echo "Locks released: " . count($releaseTimes) . "\n";
echo "Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
PerformanceTestHelper::assertPerformance(
$releaseTimes,
1.5,
3.0,
'Lock release'
);
}
public function testLockContentionPerformance(): void
{
$lockKey = new LockKey('contended_lock');
$concurrentAttempts = 20;
$attemptsPerWorker = 50;
$allResults = [];
$successCounts = [];
$failureCounts = [];
// Simulate concurrent lock acquisition attempts
for ($worker = 0; $worker < $concurrentAttempts; $worker++) {
$owner = new LockOwner("worker_{$worker}");
$workerResults = [];
$successes = 0;
$failures = 0;
for ($attempt = 0; $attempt < $attemptsPerWorker; $attempt++) {
$result = PerformanceTestHelper::measureTimeWithResult(function() use ($lockKey, $owner) {
$acquired = $this->lockService->acquire($lockKey, $owner, 1); // 1 second timeout
if ($acquired) {
// Hold lock briefly then release
usleep(100); // 0.1ms
$this->lockService->release($lockKey, $owner);
return true;
}
return false;
});
$workerResults[] = $result['time_ms'];
if ($result['result']) {
$successes++;
} else {
$failures++;
}
// Brief pause between attempts
usleep(50); // 0.05ms
}
$allResults = array_merge($allResults, $workerResults);
$successCounts[$worker] = $successes;
$failureCounts[$worker] = $failures;
}
$stats = PerformanceTestHelper::calculateStatistics($allResults);
$totalSuccesses = array_sum($successCounts);
$totalFailures = array_sum($failureCounts);
$successRate = $totalSuccesses / ($totalSuccesses + $totalFailures) * 100;
echo "\nLock Contention Performance Results:\n";
echo "Concurrent workers: {$concurrentAttempts}\n";
echo "Total attempts: " . ($totalSuccesses + $totalFailures) . "\n";
echo "Successful acquisitions: {$totalSuccesses}\n";
echo "Failed acquisitions: {$totalFailures}\n";
echo "Success rate: {$successRate}%\n";
echo "Attempt performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// Under contention, some failures are expected but most should succeed
$this->assertGreaterThan(50.0, $successRate, 'Lock success rate too low under contention');
// Performance should degrade gracefully under contention
$this->assertLessThan(50.0, $stats['avg'], 'Average lock time too high under contention');
$this->assertLessThan(200.0, $stats['p95'], 'P95 lock time too high under contention');
}
public function testLockTimeoutPerformance(): void
{
$lockKey = new LockKey('timeout_lock');
$owner1 = new LockOwner('owner_1');
$owner2 = new LockOwner('owner_2');
// First owner acquires the lock
$acquired = $this->lockService->acquire($lockKey, $owner1, 60);
$this->assertTrue($acquired, 'Initial lock acquisition should succeed');
$timeoutResults = [];
$iterations = 100;
// Second owner repeatedly tries to acquire with short timeouts
for ($i = 0; $i < $iterations; $i++) {
$result = PerformanceTestHelper::measureTimeWithResult(function() use ($lockKey, $owner2) {
return $this->lockService->acquire($lockKey, $owner2, 0.1); // 100ms timeout
});
$timeoutResults[] = $result['time_ms'];
$this->assertFalse($result['result'], 'Lock acquisition should fail due to timeout');
}
// Release the lock
$this->lockService->release($lockKey, $owner1);
$stats = PerformanceTestHelper::calculateStatistics($timeoutResults);
echo "\nLock Timeout Performance Results:\n";
echo "Timeout attempts: {$iterations}\n";
echo "Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// Timeout should be close to requested timeout (100ms) but not much longer
$this->assertGreaterThan(90.0, $stats['avg'], 'Timeout too fast - should wait approximately 100ms');
$this->assertLessThan(150.0, $stats['avg'], 'Timeout too slow - should be close to 100ms');
// Timeouts should be consistent
$this->assertLessThan(30.0, $stats['stddev'], 'Timeout timing too inconsistent');
}
public function testLockCleanupPerformance(): void
{
$expiredLockCount = 500;
$validLockCount = 100;
// Create expired locks
$pdo = $this->database->getConnection();
$expiredTime = (new \DateTimeImmutable())->modify('-1 hour');
for ($i = 0; $i < $expiredLockCount; $i++) {
$pdo->exec(sprintf(
"INSERT INTO distributed_locks (lock_key, owner_id, acquired_at, expires_at) VALUES ('expired_%d', 'owner_%d', '%s', '%s')",
$i,
$i,
$expiredTime->format('Y-m-d H:i:s'),
$expiredTime->format('Y-m-d H:i:s')
));
}
// Create valid locks
$validTime = (new \DateTimeImmutable())->modify('+1 hour');
for ($i = 0; $i < $validLockCount; $i++) {
$pdo->exec(sprintf(
"INSERT INTO distributed_locks (lock_key, owner_id, acquired_at, expires_at) VALUES ('valid_%d', 'owner_%d', '%s', '%s')",
$i,
$i,
$validTime->format('Y-m-d H:i:s'),
$validTime->format('Y-m-d H:i:s')
));
}
// Measure cleanup performance
$cleanupTime = PerformanceTestHelper::measureTime(function() {
$this->lockService->cleanupExpiredLocks();
});
// Verify cleanup results
$remaining = $pdo->query('SELECT COUNT(*) FROM distributed_locks')->fetchColumn();
echo "\nLock Cleanup Performance Results:\n";
echo "Expired locks created: {$expiredLockCount}\n";
echo "Valid locks created: {$validLockCount}\n";
echo "Locks remaining after cleanup: {$remaining}\n";
echo "Cleanup time: {$cleanupTime}ms\n";
$this->assertEquals($validLockCount, $remaining, 'Should only clean up expired locks');
$this->assertLessThan(100.0, $cleanupTime, 'Lock cleanup should complete within 100ms');
// Test cleanup performance with larger dataset
$this->cleanupTestData();
// Create many more expired locks
$largeExpiredCount = 5000;
for ($i = 0; $i < $largeExpiredCount; $i++) {
$pdo->exec(sprintf(
"INSERT INTO distributed_locks (lock_key, owner_id, acquired_at, expires_at) VALUES ('large_expired_%d', 'owner_%d', '%s', '%s')",
$i,
$i,
$expiredTime->format('Y-m-d H:i:s'),
$expiredTime->format('Y-m-d H:i:s')
));
}
$largeCleanupTime = PerformanceTestHelper::measureTime(function() {
$this->lockService->cleanupExpiredLocks();
});
echo "Large cleanup ({$largeExpiredCount} locks): {$largeCleanupTime}ms\n";
$this->assertLessThan(500.0, $largeCleanupTime, 'Large cleanup should complete within 500ms');
}
public function testHighThroughputLockOperations(): void
{
$operationsPerSecond = 500;
$testDuration = 10; // seconds
$totalOperations = $operationsPerSecond * $testDuration;
echo "\nHigh Throughput Lock Operations Test:\n";
echo "Target: {$operationsPerSecond} operations/second for {$testDuration} seconds\n";
$loadResult = PerformanceTestHelper::simulateLoad(
function($index) {
$lockKey = new LockKey("throughput_lock_{$index}");
$owner = new LockOwner("owner_{$index}");
// Acquire and immediately release
$acquired = $this->lockService->acquire($lockKey, $owner, 5);
if ($acquired) {
$this->lockService->release($lockKey, $owner);
return true;
}
return false;
},
$totalOperations,
25, // Moderate concurrency
$testDuration
);
$actualThroughput = $loadResult['throughput_ops_per_sec'];
$operationTimes = array_column($loadResult['results'], 'time_ms');
$stats = PerformanceTestHelper::calculateStatistics($operationTimes);
$successfulOperations = count(array_filter(
array_column($loadResult['results'], 'result'),
fn($result) => $result['result'] === true
));
echo "Actual Throughput: {$actualThroughput} operations/second\n";
echo "Successful operations: {$successfulOperations}\n";
echo "Operation Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// Should achieve at least 70% of target throughput
$this->assertGreaterThan(
$operationsPerSecond * 0.7,
$actualThroughput,
'High throughput lock operations below 70% of target'
);
// Most operations should succeed
$successRate = $successfulOperations / $loadResult['operations_completed'] * 100;
$this->assertGreaterThan(95.0, $successRate, 'Lock operation success rate too low');
// Operation times should remain reasonable
$this->assertLessThan(20.0, $stats['avg'], 'Average operation time too high under load');
$this->assertLessThan(100.0, $stats['p95'], 'P95 operation time too high under load');
}
public function testLockRetryPerformance(): void
{
$lockKey = new LockKey('retry_lock');
$owner1 = new LockOwner('blocking_owner');
$owner2 = new LockOwner('retry_owner');
// First owner acquires lock for a short time
$this->lockService->acquire($lockKey, $owner1, 60);
// Schedule lock release after 200ms
$releaseTime = microtime(true) + 0.2;
$retryAttempts = [];
$maxRetries = 50;
$retryDelay = 50; // 50ms between retries
for ($i = 0; $i < $maxRetries; $i++) {
$startTime = microtime(true);
// Release the lock when it's time
if ($startTime >= $releaseTime && $this->lockService->isLocked($lockKey)) {
$this->lockService->release($lockKey, $owner1);
}
$result = PerformanceTestHelper::measureTimeWithResult(function() use ($lockKey, $owner2) {
return $this->lockService->acquire($lockKey, $owner2, 0.01); // 10ms timeout
});
$retryAttempts[] = [
'time_ms' => $result['time_ms'],
'success' => $result['result']
];
if ($result['result']) {
// Successfully acquired, release it and stop
$this->lockService->release($lockKey, $owner2);
break;
}
usleep($retryDelay * 1000); // Convert to microseconds
}
$retryTimes = array_column($retryAttempts, 'time_ms');
$stats = PerformanceTestHelper::calculateStatistics($retryTimes);
$successfulAttempt = array_search(true, array_column($retryAttempts, 'success'));
$attemptsUntilSuccess = $successfulAttempt !== false ? $successfulAttempt + 1 : $maxRetries;
echo "\nLock Retry Performance Results:\n";
echo "Attempts until success: {$attemptsUntilSuccess}\n";
echo "Retry performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// Should eventually succeed
$this->assertNotFalse($successfulAttempt, 'Lock retry should eventually succeed');
// Retry attempts should be fast (mostly just timeout delays)
$this->assertLessThan(20.0, $stats['avg'], 'Retry attempts taking too long');
}
private function createTestDatabase(): DatabaseManager
{
$pdo = new \PDO('sqlite::memory:');
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$pdo->exec('
CREATE TABLE distributed_locks (
lock_key TEXT PRIMARY KEY,
owner_id TEXT NOT NULL,
acquired_at TEXT NOT NULL,
expires_at TEXT NOT NULL
)
');
// Performance-optimized indexes
$pdo->exec('CREATE INDEX idx_locks_expires ON distributed_locks(expires_at)');
$pdo->exec('CREATE INDEX idx_locks_owner ON distributed_locks(owner_id)');
return new DatabaseManager($pdo);
}
private function cleanupTestData(): void
{
$pdo = $this->database->getConnection();
$pdo->exec('DELETE FROM distributed_locks');
}
}

View File

@@ -0,0 +1,524 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Queue\Performance;
use App\Framework\Database\DatabaseManager;
use App\Framework\Queue\Distribution\JobDistributionService;
use App\Framework\Queue\Failover\FailoverRecoveryService;
use App\Framework\Queue\Health\WorkerHealthCheckService;
use App\Framework\Queue\Jobs\JobStatus;
use App\Framework\Queue\Workers\WorkerRegistry;
use App\Framework\Queue\Workers\WorkerStatus;
use PHPUnit\Framework\TestCase;
final class FailoverPerformanceTest extends TestCase
{
private DatabaseManager $database;
private WorkerRegistry $workerRegistry;
private WorkerHealthCheckService $healthCheckService;
private FailoverRecoveryService $failoverService;
private JobDistributionService $distributionService;
protected function setUp(): void
{
$this->database = $this->createTestDatabase();
$this->workerRegistry = new WorkerRegistry($this->database);
$this->healthCheckService = new WorkerHealthCheckService(
$this->database,
$this->workerRegistry
);
$this->failoverService = new FailoverRecoveryService(
$this->database,
$this->workerRegistry
);
$this->distributionService = new JobDistributionService(
$this->database,
$this->workerRegistry
);
$this->cleanupTestData();
PerformanceTestHelper::warmupDatabase($this->database->getConnection());
}
protected function tearDown(): void
{
$this->cleanupTestData();
}
public function testWorkerFailureDetectionTime(): void
{
// Create workers with recent heartbeats
$workers = $this->createHealthyWorkers(10);
$this->registerWorkers($workers);
// Simulate worker failures by setting old heartbeats
$failedWorkerIds = [];
$currentTime = new \DateTimeImmutable();
// Make 3 workers appear failed (no heartbeat for 2 minutes)
for ($i = 0; $i < 3; $i++) {
$workerId = $workers[$i]->id->toString();
$failedWorkerIds[] = $workerId;
$this->updateWorkerHeartbeat($workerId, $currentTime->modify('-2 minutes'));
}
// Measure failure detection time
$detectionTimes = [];
$iterations = 10;
for ($i = 0; $i < $iterations; $i++) {
$time = PerformanceTestHelper::measureTime(function() {
return $this->healthCheckService->checkAllWorkers();
});
$detectionTimes[] = $time;
// Brief pause between checks
usleep(10000); // 10ms
}
$stats = PerformanceTestHelper::calculateStatistics($detectionTimes);
// Verify failed workers were detected
$failedWorkers = $this->workerRegistry->getWorkersByStatus(WorkerStatus::FAILED);
$detectedFailures = array_map(fn($w) => $w->id->toString(), $failedWorkers);
echo "\nWorker Failure Detection Results:\n";
echo "Workers created: " . count($workers) . "\n";
echo "Workers failed: " . count($failedWorkerIds) . "\n";
echo "Failures detected: " . count($detectedFailures) . "\n";
echo "Detection performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// All failed workers should be detected
foreach ($failedWorkerIds as $failedId) {
$this->assertContains($failedId, $detectedFailures, "Failed worker {$failedId} not detected");
}
// Detection should be fast
$this->assertLessThan(50.0, $stats['avg'], 'Average failure detection time too slow');
$this->assertLessThan(100.0, $stats['p95'], 'P95 failure detection time too slow');
PerformanceTestHelper::assertPerformance(
$detectionTimes,
50.0,
100.0,
'Worker failure detection'
);
}
public function testJobReassignmentSpeed(): void
{
// Create workers and assign jobs
$workers = $this->createHealthyWorkers(5);
$this->registerWorkers($workers);
// Distribute jobs to workers
$jobs = PerformanceTestHelper::createBulkJobs(50);
$assignedJobs = [];
foreach ($jobs as $job) {
$assignedWorker = $this->distributionService->distributeJob($job);
if ($assignedWorker) {
$assignedJobs[] = [
'job' => $job,
'worker_id' => $assignedWorker->id->toString()
];
}
}
// Simulate worker failure
$failedWorkerId = $workers[0]->id->toString();
$this->updateWorkerStatus($failedWorkerId, WorkerStatus::FAILED);
// Find jobs assigned to failed worker
$jobsToReassign = array_filter(
$assignedJobs,
fn($item) => $item['worker_id'] === $failedWorkerId
);
echo "\nJob Reassignment Test:\n";
echo "Total jobs: " . count($assignedJobs) . "\n";
echo "Jobs to reassign: " . count($jobsToReassign) . "\n";
// Measure job reassignment performance
$reassignmentTime = PerformanceTestHelper::measureTime(function() use ($failedWorkerId) {
return $this->failoverService->reassignFailedWorkerJobs($failedWorkerId);
});
echo "Reassignment time: {$reassignmentTime}ms\n";
// Verify jobs were reassigned
$reassignedCount = $this->countJobsReassignedFrom($failedWorkerId);
echo "Jobs successfully reassigned: {$reassignedCount}\n";
// Job reassignment should be fast
$this->assertLessThan(200.0, $reassignmentTime, 'Job reassignment took too long');
// All jobs should be reassigned
$this->assertEquals(
count($jobsToReassign),
$reassignedCount,
'Not all jobs were reassigned'
);
// Performance should scale reasonably with job count
$averageTimePerJob = $reassignmentTime / max(1, count($jobsToReassign));
$this->assertLessThan(5.0, $averageTimePerJob, 'Reassignment time per job too high');
}
public function testSystemRecoveryTime(): void
{
// Create a system with multiple workers and jobs
$workers = $this->createHealthyWorkers(8);
$this->registerWorkers($workers);
// Distribute many jobs
$jobs = PerformanceTestHelper::createBulkJobs(200);
foreach ($jobs as $job) {
$this->distributionService->distributeJob($job);
}
// Simulate multiple worker failures
$failedWorkerIds = [
$workers[0]->id->toString(),
$workers[1]->id->toString(),
$workers[2]->id->toString()
];
foreach ($failedWorkerIds as $workerId) {
$this->updateWorkerStatus($workerId, WorkerStatus::FAILED);
}
echo "\nSystem Recovery Test:\n";
echo "Total workers: " . count($workers) . "\n";
echo "Failed workers: " . count($failedWorkerIds) . "\n";
echo "Total jobs: " . count($jobs) . "\n";
// Measure full system recovery time
$recoveryTime = PerformanceTestHelper::measureTime(function() {
return $this->failoverService->performFullSystemRecovery();
});
echo "Full recovery time: {$recoveryTime}ms\n";
// Verify system state after recovery
$activeWorkers = $this->workerRegistry->getAvailableWorkers();
$pendingJobs = $this->countJobsByStatus(JobStatus::PENDING);
$processingJobs = $this->countJobsByStatus(JobStatus::PROCESSING);
echo "Active workers after recovery: " . count($activeWorkers) . "\n";
echo "Pending jobs: {$pendingJobs}\n";
echo "Processing jobs: {$processingJobs}\n";
// Recovery should complete within reasonable time
$this->assertLessThan(5000.0, $recoveryTime, 'System recovery took too long (>5 seconds)');
// Should have remaining active workers
$this->assertGreaterThan(0, count($activeWorkers), 'No workers available after recovery');
// Jobs should be properly redistributed
$this->assertGreaterThan(0, $pendingJobs + $processingJobs, 'No jobs available after recovery');
}
public function testPartialFailureGracefulDegradation(): void
{
// Create system with mixed capacity workers
$workers = [
PerformanceTestHelper::createTestWorker('high_capacity_1', 50),
PerformanceTestHelper::createTestWorker('high_capacity_2', 50),
PerformanceTestHelper::createTestWorker('medium_capacity_1', 20),
PerformanceTestHelper::createTestWorker('medium_capacity_2', 20),
PerformanceTestHelper::createTestWorker('low_capacity_1', 10),
PerformanceTestHelper::createTestWorker('low_capacity_2', 10)
];
$this->registerWorkers($workers);
// Measure baseline throughput
$baselineThroughput = $this->measureDistributionThroughput(100, 'baseline');
// Fail high capacity workers
$this->updateWorkerStatus('high_capacity_1', WorkerStatus::FAILED);
$this->updateWorkerStatus('high_capacity_2', WorkerStatus::FAILED);
$degradedThroughput = $this->measureDistributionThroughput(100, 'degraded');
// Fail medium capacity workers too
$this->updateWorkerStatus('medium_capacity_1', WorkerStatus::FAILED);
$this->updateWorkerStatus('medium_capacity_2', WorkerStatus::FAILED);
$severeDegradationThroughput = $this->measureDistributionThroughput(100, 'severe');
echo "\nGraceful Degradation Results:\n";
echo "Baseline throughput: {$baselineThroughput} jobs/sec\n";
echo "After high-capacity failure: {$degradedThroughput} jobs/sec\n";
echo "After medium-capacity failure: {$severeDegradationThroughput} jobs/sec\n";
$degradationRatio1 = $degradedThroughput / $baselineThroughput;
$degradationRatio2 = $severeDegradationThroughput / $baselineThroughput;
echo "First degradation ratio: " . round($degradationRatio1 * 100, 1) . "%\n";
echo "Severe degradation ratio: " . round($degradationRatio2 * 100, 1) . "%\n";
// System should degrade gracefully
$this->assertGreaterThan(0.3, $degradationRatio1, 'Degradation too severe after high-capacity failure');
$this->assertGreaterThan(0.1, $degradationRatio2, 'System should still function with low-capacity workers');
// Should maintain some reasonable performance
$this->assertGreaterThan(10, $severeDegradationThroughput, 'Minimum throughput too low');
}
public function testFailoverUnderHighLoad(): void
{
// Create workers under high load
$workers = $this->createHealthyWorkers(6);
$this->registerWorkers($workers);
// Start high load job distribution
$jobsDistributed = 0;
$distributionErrors = 0;
$startTime = microtime(true);
$testDuration = 20; // 20 seconds
$endTime = $startTime + $testDuration;
$distributionTimes = [];
// Simulate ongoing load
while (microtime(true) < $endTime) {
$job = PerformanceTestHelper::createTestJob("load_job_{$jobsDistributed}");
$result = PerformanceTestHelper::measureTimeWithResult(function() use ($job) {
try {
return $this->distributionService->distributeJob($job);
} catch (\Exception $e) {
return null;
}
});
$distributionTimes[] = $result['time_ms'];
if ($result['result'] !== null) {
$jobsDistributed++;
} else {
$distributionErrors++;
}
// Simulate worker failure at 1/3 of test duration
if (microtime(true) > $startTime + ($testDuration / 3) &&
microtime(true) < $startTime + ($testDuration / 3) + 1) {
// Fail 2 workers during high load
$this->updateWorkerStatus($workers[0]->id->toString(), WorkerStatus::FAILED);
$this->updateWorkerStatus($workers[1]->id->toString(), WorkerStatus::FAILED);
// Trigger recovery
$this->failoverService->performFullSystemRecovery();
}
usleep(5000); // 5ms between jobs
}
$actualDuration = microtime(true) - $startTime;
$throughput = $jobsDistributed / $actualDuration;
$errorRate = $distributionErrors / ($jobsDistributed + $distributionErrors) * 100;
$stats = PerformanceTestHelper::calculateStatistics($distributionTimes);
echo "\nFailover Under High Load Results:\n";
echo "Test duration: {$actualDuration} seconds\n";
echo "Jobs distributed: {$jobsDistributed}\n";
echo "Distribution errors: {$distributionErrors}\n";
echo "Throughput: {$throughput} jobs/sec\n";
echo "Error rate: {$errorRate}%\n";
echo "Distribution performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// System should maintain reasonable performance during failover
$this->assertGreaterThan(20, $throughput, 'Throughput too low during failover');
$this->assertLessThan(10.0, $errorRate, 'Error rate too high during failover');
// Distribution times may be higher during failover but should recover
$this->assertLessThan(100.0, $stats['avg'], 'Average distribution time too high during failover');
}
public function testWorkerRecoveryPerformance(): void
{
// Create workers, some initially failed
$workers = $this->createHealthyWorkers(8);
$this->registerWorkers($workers);
// Mark some workers as failed
$failedWorkerIds = [
$workers[0]->id->toString(),
$workers[1]->id->toString(),
$workers[2]->id->toString()
];
foreach ($failedWorkerIds as $workerId) {
$this->updateWorkerStatus($workerId, WorkerStatus::FAILED);
}
echo "\nWorker Recovery Performance Test:\n";
echo "Failed workers: " . count($failedWorkerIds) . "\n";
// Simulate workers coming back online
$recoveryTimes = [];
foreach ($failedWorkerIds as $workerId) {
// Update heartbeat to simulate worker recovery
$this->updateWorkerHeartbeat($workerId, new \DateTimeImmutable());
$recoveryTime = PerformanceTestHelper::measureTime(function() use ($workerId) {
// Simulate health check detecting recovery
$this->healthCheckService->checkWorker($workerId);
// Trigger recovery process
return $this->failoverService->recoverWorker($workerId);
});
$recoveryTimes[] = $recoveryTime;
}
$stats = PerformanceTestHelper::calculateStatistics($recoveryTimes);
echo "Worker recovery performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// Verify workers are back online
$availableWorkers = $this->workerRegistry->getAvailableWorkers();
$availableCount = count($availableWorkers);
echo "Workers available after recovery: {$availableCount}\n";
// Recovery should be fast
$this->assertLessThan(100.0, $stats['avg'], 'Average worker recovery time too slow');
$this->assertLessThan(200.0, $stats['p95'], 'P95 worker recovery time too slow');
// All workers should be recovered
$this->assertGreaterThanOrEqual(
count($workers),
$availableCount,
'Not all workers recovered successfully'
);
PerformanceTestHelper::assertPerformance(
$recoveryTimes,
100.0,
200.0,
'Worker recovery'
);
}
private function measureDistributionThroughput(int $jobCount, string $label): float
{
$jobs = PerformanceTestHelper::createBulkJobs($jobCount);
$startTime = microtime(true);
foreach ($jobs as $job) {
$this->distributionService->distributeJob($job);
}
$endTime = microtime(true);
$duration = $endTime - $startTime;
return round($jobCount / $duration, 1);
}
private function createHealthyWorkers(int $count): array
{
$workers = [];
for ($i = 1; $i <= $count; $i++) {
$workers[] = PerformanceTestHelper::createTestWorker(
"healthy_worker_{$i}",
20,
WorkerStatus::AVAILABLE
);
}
return $workers;
}
private function registerWorkers(array $workers): void
{
foreach ($workers as $worker) {
$this->workerRegistry->registerWorker($worker);
}
}
private function updateWorkerHeartbeat(string $workerId, \DateTimeImmutable $heartbeat): void
{
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('UPDATE workers SET last_heartbeat = ? WHERE id = ?');
$stmt->execute([$heartbeat->format('Y-m-d H:i:s'), $workerId]);
}
private function updateWorkerStatus(string $workerId, WorkerStatus $status): void
{
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('UPDATE workers SET status = ? WHERE id = ?');
$stmt->execute([$status->value, $workerId]);
}
private function countJobsReassignedFrom(string $failedWorkerId): int
{
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT COUNT(*) FROM jobs WHERE worker_id != ? AND worker_id IS NOT NULL');
$stmt->execute([$failedWorkerId]);
return (int) $stmt->fetchColumn();
}
private function countJobsByStatus(JobStatus $status): int
{
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT COUNT(*) FROM jobs WHERE status = ?');
$stmt->execute([$status->value]);
return (int) $stmt->fetchColumn();
}
private function createTestDatabase(): DatabaseManager
{
$pdo = new \PDO('sqlite::memory:');
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$pdo->exec('
CREATE TABLE workers (
id TEXT PRIMARY KEY,
queue_names TEXT NOT NULL,
capacity INTEGER NOT NULL,
status TEXT NOT NULL,
last_heartbeat TEXT NOT NULL,
metadata TEXT
)
');
$pdo->exec('
CREATE TABLE jobs (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
payload TEXT NOT NULL,
queue_name TEXT NOT NULL,
priority INTEGER NOT NULL,
status TEXT NOT NULL,
worker_id TEXT,
created_at TEXT NOT NULL,
started_at TEXT,
completed_at TEXT,
attempts INTEGER DEFAULT 0,
error_message TEXT
)
');
// Performance indexes
$pdo->exec('CREATE INDEX idx_workers_status_heartbeat ON workers(status, last_heartbeat)');
$pdo->exec('CREATE INDEX idx_jobs_worker_status ON jobs(worker_id, status)');
$pdo->exec('CREATE INDEX idx_jobs_status_created ON jobs(status, created_at)');
return new DatabaseManager($pdo);
}
private function cleanupTestData(): void
{
$pdo = $this->database->getConnection();
$pdo->exec('DELETE FROM workers');
$pdo->exec('DELETE FROM jobs');
}
}

View File

@@ -0,0 +1,411 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Queue\Performance;
use App\Framework\Database\DatabaseManager;
use App\Framework\Queue\Distribution\JobDistributionService;
use App\Framework\Queue\Distribution\LoadBalancer;
use App\Framework\Queue\Jobs\JobPriority;
use App\Framework\Queue\Queue\QueueName;
use App\Framework\Queue\Workers\WorkerRegistry;
use App\Framework\Queue\Workers\WorkerStatus;
use PHPUnit\Framework\TestCase;
final class LoadBalancingPerformanceTest extends TestCase
{
private DatabaseManager $database;
private WorkerRegistry $workerRegistry;
private JobDistributionService $distributionService;
private LoadBalancer $loadBalancer;
protected function setUp(): void
{
$this->database = $this->createTestDatabase();
$this->workerRegistry = new WorkerRegistry($this->database);
$this->loadBalancer = new LoadBalancer($this->workerRegistry);
$this->distributionService = new JobDistributionService(
$this->database,
$this->workerRegistry
);
$this->cleanupTestData();
PerformanceTestHelper::warmupDatabase($this->database->getConnection());
}
protected function tearDown(): void
{
$this->cleanupTestData();
}
public function testWorkerSelectionLatency(): void
{
// Create workers with different loads
$workers = [
PerformanceTestHelper::createTestWorker('worker_1', 20),
PerformanceTestHelper::createTestWorker('worker_2', 15),
PerformanceTestHelper::createTestWorker('worker_3', 10),
PerformanceTestHelper::createTestWorker('worker_4', 25),
PerformanceTestHelper::createTestWorker('worker_5', 30)
];
$this->registerWorkers($workers);
$selectionTimes = [];
$iterations = 1000;
for ($i = 0; $i < $iterations; $i++) {
$time = PerformanceTestHelper::measureTime(function() {
$this->loadBalancer->selectWorker(new QueueName('test_queue'));
});
$selectionTimes[] = $time;
}
$stats = PerformanceTestHelper::calculateStatistics($selectionTimes);
// Validate performance benchmarks
$this->assertLessThan(5.0, $stats['avg'], 'Average worker selection time exceeds 5ms');
$this->assertLessThan(10.0, $stats['p95'], 'P95 worker selection time exceeds 10ms');
$this->assertLessThan(20.0, $stats['p99'], 'P99 worker selection time exceeds 20ms');
echo "\nWorker Selection Latency Results:\n";
echo "Iterations: {$iterations}\n";
echo "Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
PerformanceTestHelper::assertPerformance(
$selectionTimes,
5.0,
10.0,
'Worker selection'
);
}
public function testJobDistributionLatency(): void
{
$workers = $this->createBalancedWorkers(10, 20);
$this->registerWorkers($workers);
$distributionTimes = [];
$iterations = 500;
for ($i = 0; $i < $iterations; $i++) {
$job = PerformanceTestHelper::createTestJob("dist_job_{$i}");
$time = PerformanceTestHelper::measureTime(function() use ($job) {
$this->distributionService->distributeJob($job);
});
$distributionTimes[] = $time;
}
$stats = PerformanceTestHelper::calculateStatistics($distributionTimes);
// Validate performance benchmarks
$this->assertLessThan(10.0, $stats['avg'], 'Average job distribution time exceeds 10ms');
$this->assertLessThan(20.0, $stats['p95'], 'P95 job distribution time exceeds 20ms');
$this->assertLessThan(50.0, $stats['p99'], 'P99 job distribution time exceeds 50ms');
echo "\nJob Distribution Latency Results:\n";
echo "Iterations: {$iterations}\n";
echo "Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
PerformanceTestHelper::assertPerformance(
$distributionTimes,
10.0,
20.0,
'Job distribution'
);
}
public function testHighThroughputDistribution(): void
{
$workers = $this->createBalancedWorkers(15, 25);
$this->registerWorkers($workers);
$jobsPerSecond = 1000;
$testDuration = 10; // seconds
$totalJobs = $jobsPerSecond * $testDuration;
echo "\nHigh Throughput Distribution Test:\n";
echo "Target: {$jobsPerSecond} jobs/second for {$testDuration} seconds\n";
$loadResult = PerformanceTestHelper::simulateLoad(
function($index) {
$job = PerformanceTestHelper::createTestJob("load_job_{$index}");
return $this->distributionService->distributeJob($job);
},
$totalJobs,
50, // High concurrency
$testDuration
);
$actualThroughput = $loadResult['throughput_ops_per_sec'];
$distributionTimes = array_column($loadResult['results'], 'time_ms');
$stats = PerformanceTestHelper::calculateStatistics($distributionTimes);
echo "Actual Throughput: {$actualThroughput} jobs/second\n";
echo "Distribution Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// Should achieve at least 80% of target throughput
$this->assertGreaterThan(
$jobsPerSecond * 0.8,
$actualThroughput,
'High throughput distribution below 80% of target'
);
// Distribution times should remain reasonable under high load
$this->assertLessThan(30.0, $stats['avg'], 'Average distribution time too high under load');
$this->assertLessThan(100.0, $stats['p95'], 'P95 distribution time too high under load');
}
public function testFairDistributionUnderLoad(): void
{
$workers = [
PerformanceTestHelper::createTestWorker('worker_1', 10),
PerformanceTestHelper::createTestWorker('worker_2', 20),
PerformanceTestHelper::createTestWorker('worker_3', 30),
PerformanceTestHelper::createTestWorker('worker_4', 15),
PerformanceTestHelper::createTestWorker('worker_5', 25)
];
$this->registerWorkers($workers);
$jobCount = 1000;
$workerAssignments = [];
for ($i = 0; $i < $jobCount; $i++) {
$job = PerformanceTestHelper::createTestJob("fair_job_{$i}");
$selectedWorker = $this->distributionService->distributeJob($job);
if ($selectedWorker) {
$workerId = $selectedWorker->id->toString();
$workerAssignments[$workerId] = ($workerAssignments[$workerId] ?? 0) + 1;
}
}
echo "\nFair Distribution Results:\n";
$totalCapacity = array_sum(array_map(fn($w) => $w->capacity->value, $workers));
foreach ($workers as $worker) {
$workerId = $worker->id->toString();
$assignments = $workerAssignments[$workerId] ?? 0;
$expectedRatio = $worker->capacity->value / $totalCapacity;
$actualRatio = $assignments / $jobCount;
$efficiency = ($actualRatio / $expectedRatio) * 100;
echo sprintf(
"Worker %s: Capacity=%d, Assignments=%d, Expected=%.1f%%, Actual=%.1f%%, Efficiency=%.1f%%\n",
$workerId,
$worker->capacity->value,
$assignments,
$expectedRatio * 100,
$actualRatio * 100,
$efficiency
);
// Each worker should get jobs roughly proportional to their capacity
// Allow 20% variance for fair distribution
$this->assertGreaterThan(
80.0,
$efficiency,
"Worker {$workerId} received fewer jobs than expected (efficiency: {$efficiency}%)"
);
$this->assertLessThan(
120.0,
$efficiency,
"Worker {$workerId} received more jobs than expected (efficiency: {$efficiency}%)"
);
}
}
public function testMixedCapacityLoadBalancing(): void
{
// Create workers with very different capacities
$workers = [
PerformanceTestHelper::createTestWorker('small_worker', 5),
PerformanceTestHelper::createTestWorker('medium_worker_1', 15),
PerformanceTestHelper::createTestWorker('medium_worker_2', 20),
PerformanceTestHelper::createTestWorker('large_worker', 50),
PerformanceTestHelper::createTestWorker('xlarge_worker', 100)
];
$this->registerWorkers($workers);
$selectionTimes = [];
$iterations = 500;
for ($i = 0; $i < $iterations; $i++) {
$time = PerformanceTestHelper::measureTime(function() {
$this->loadBalancer->selectWorker(new QueueName('test_queue'));
});
$selectionTimes[] = $time;
}
$stats = PerformanceTestHelper::calculateStatistics($selectionTimes);
echo "\nMixed Capacity Load Balancing Results:\n";
echo "Worker capacities: 5, 15, 20, 50, 100\n";
echo "Selection performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// Selection should still be fast even with mixed capacities
$this->assertLessThan(8.0, $stats['avg'], 'Mixed capacity selection average time too high');
$this->assertLessThan(15.0, $stats['p95'], 'Mixed capacity selection P95 time too high');
}
public function testPriorityJobDistribution(): void
{
$workers = $this->createBalancedWorkers(8, 15);
$this->registerWorkers($workers);
$priorities = [JobPriority::LOW, JobPriority::NORMAL, JobPriority::HIGH, JobPriority::CRITICAL];
$distributionTimes = [];
foreach ($priorities as $priority) {
$iterationsPerPriority = 100;
for ($i = 0; $i < $iterationsPerPriority; $i++) {
$job = PerformanceTestHelper::createTestJob(
"priority_job_{$priority->value}_{$i}",
$priority
);
$time = PerformanceTestHelper::measureTime(function() use ($job) {
$this->distributionService->distributeJob($job);
});
$distributionTimes[$priority->value][] = $time;
}
}
echo "\nPriority Job Distribution Results:\n";
foreach ($priorities as $priority) {
$stats = PerformanceTestHelper::calculateStatistics($distributionTimes[$priority->value]);
echo "Priority {$priority->value}: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// All priorities should have similar distribution performance
$this->assertLessThan(15.0, $stats['avg'], "Priority {$priority->value} distribution too slow");
}
}
public function testWorkerOverloadHandling(): void
{
// Create workers that will quickly become overloaded
$workers = [
PerformanceTestHelper::createTestWorker('worker_1', 2),
PerformanceTestHelper::createTestWorker('worker_2', 3),
PerformanceTestHelper::createTestWorker('worker_3', 2)
];
$this->registerWorkers($workers);
$distributionTimes = [];
$successfulDistributions = 0;
$failedDistributions = 0;
// Try to distribute more jobs than total worker capacity
$jobCount = 20; // Total capacity is only 7
for ($i = 0; $i < $jobCount; $i++) {
$job = PerformanceTestHelper::createTestJob("overload_job_{$i}");
$result = PerformanceTestHelper::measureTimeWithResult(function() use ($job) {
return $this->distributionService->distributeJob($job);
});
$distributionTimes[] = $result['time_ms'];
if ($result['result'] !== null) {
$successfulDistributions++;
} else {
$failedDistributions++;
}
}
$stats = PerformanceTestHelper::calculateStatistics($distributionTimes);
echo "\nWorker Overload Handling Results:\n";
echo "Total jobs: {$jobCount}\n";
echo "Successful distributions: {$successfulDistributions}\n";
echo "Failed distributions: {$failedDistributions}\n";
echo "Distribution performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// Should successfully distribute up to total capacity
$this->assertGreaterThanOrEqual(7, $successfulDistributions, 'Should distribute at least 7 jobs');
// Distribution times should remain reasonable even when workers are overloaded
$this->assertLessThan(20.0, $stats['avg'], 'Distribution time too high during overload');
}
private function createBalancedWorkers(int $count, int $capacity): array
{
$workers = [];
for ($i = 1; $i <= $count; $i++) {
$workers[] = PerformanceTestHelper::createTestWorker(
"balanced_worker_{$i}",
$capacity,
WorkerStatus::AVAILABLE
);
}
return $workers;
}
private function registerWorkers(array $workers): void
{
foreach ($workers as $worker) {
$this->workerRegistry->registerWorker($worker);
}
}
private function createTestDatabase(): DatabaseManager
{
$pdo = new \PDO('sqlite::memory:');
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// Create required tables with indexes for performance
$pdo->exec('
CREATE TABLE workers (
id TEXT PRIMARY KEY,
queue_names TEXT NOT NULL,
capacity INTEGER NOT NULL,
status TEXT NOT NULL,
last_heartbeat TEXT NOT NULL,
metadata TEXT
)
');
$pdo->exec('
CREATE TABLE jobs (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
payload TEXT NOT NULL,
queue_name TEXT NOT NULL,
priority INTEGER NOT NULL,
status TEXT NOT NULL,
worker_id TEXT,
created_at TEXT NOT NULL,
started_at TEXT,
completed_at TEXT,
attempts INTEGER DEFAULT 0,
error_message TEXT
)
');
// Performance-optimized indexes
$pdo->exec('CREATE INDEX idx_workers_status_capacity ON workers(status, capacity DESC)');
$pdo->exec('CREATE INDEX idx_workers_queue_status ON workers(queue_names, status)');
$pdo->exec('CREATE INDEX idx_jobs_worker_status ON jobs(worker_id, status)');
$pdo->exec('CREATE INDEX idx_jobs_priority_created ON jobs(priority DESC, created_at)');
return new DatabaseManager($pdo);
}
private function cleanupTestData(): void
{
$pdo = $this->database->getConnection();
$pdo->exec('DELETE FROM workers');
$pdo->exec('DELETE FROM jobs');
}
}

View File

@@ -0,0 +1,386 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Queue\Performance;
use App\Framework\Database\DatabaseManager;
use App\Framework\Queue\Distribution\JobDistributionService;
use App\Framework\Queue\Jobs\Job;
use App\Framework\Queue\Jobs\JobPriority;
use App\Framework\Queue\Jobs\JobStatus;
use App\Framework\Queue\Queue\QueueName;
use App\Framework\Queue\Workers\Worker;
use App\Framework\Queue\Workers\WorkerCapacity;
use App\Framework\Queue\Workers\WorkerId;
use App\Framework\Queue\Workers\WorkerRegistry;
use App\Framework\Queue\Workers\WorkerStatus;
use PHPUnit\Framework\TestCase;
final class MultiWorkerThroughputTest extends TestCase
{
private DatabaseManager $database;
private WorkerRegistry $workerRegistry;
private JobDistributionService $distributionService;
protected function setUp(): void
{
$this->database = $this->createTestDatabase();
$this->workerRegistry = new WorkerRegistry($this->database);
$this->distributionService = new JobDistributionService(
$this->database,
$this->workerRegistry
);
// Clean up any existing test data
$this->cleanupTestData();
// Warm up database connections
PerformanceTestHelper::warmupDatabase($this->database->getConnection());
}
protected function tearDown(): void
{
$this->cleanupTestData();
}
public function testSingleWorkerThroughput(): void
{
$workerCount = 1;
$jobCount = 100;
$workers = $this->createWorkers($workerCount, 50); // High capacity worker
$result = $this->measureThroughput($workers, $jobCount);
$this->assertGreaterThan(
50, // At least 50 jobs/second with single worker
$result['throughput'],
'Single worker throughput below expected minimum'
);
echo "\nSingle Worker Throughput Results:\n";
echo "Throughput: {$result['throughput']} jobs/second\n";
echo "Total Time: {$result['total_time_ms']}ms\n";
echo "Average Job Distribution Time: " .
PerformanceTestHelper::formatStatistics($result['distribution_stats']) . "\n";
}
public function testFiveWorkerThroughput(): void
{
$workerCount = 5;
$jobCount = 500;
$workers = $this->createWorkers($workerCount, 20);
$result = $this->measureThroughput($workers, $jobCount);
$this->assertGreaterThan(
200, // Should achieve at least 200 jobs/second with 5 workers
$result['throughput'],
'Five worker throughput below expected minimum'
);
echo "\nFive Worker Throughput Results:\n";
echo "Throughput: {$result['throughput']} jobs/second\n";
echo "Total Time: {$result['total_time_ms']}ms\n";
echo "Average Job Distribution Time: " .
PerformanceTestHelper::formatStatistics($result['distribution_stats']) . "\n";
}
public function testTenWorkerThroughput(): void
{
$workerCount = 10;
$jobCount = 1000;
$workers = $this->createWorkers($workerCount, 15);
$result = $this->measureThroughput($workers, $jobCount);
$this->assertGreaterThan(
350, // Should achieve at least 350 jobs/second with 10 workers
$result['throughput'],
'Ten worker throughput below expected minimum'
);
echo "\nTen Worker Throughput Results:\n";
echo "Throughput: {$result['throughput']} jobs/second\n";
echo "Total Time: {$result['total_time_ms']}ms\n";
echo "Average Job Distribution Time: " .
PerformanceTestHelper::formatStatistics($result['distribution_stats']) . "\n";
}
public function testTwentyWorkerThroughput(): void
{
$workerCount = 20;
$jobCount = 2000;
$workers = $this->createWorkers($workerCount, 10);
$result = $this->measureThroughput($workers, $jobCount);
$this->assertGreaterThan(
600, // Should achieve at least 600 jobs/second with 20 workers
$result['throughput'],
'Twenty worker throughput below expected minimum'
);
echo "\nTwenty Worker Throughput Results:\n";
echo "Throughput: {$result['throughput']} jobs/second\n";
echo "Total Time: {$result['total_time_ms']}ms\n";
echo "Average Job Distribution Time: " .
PerformanceTestHelper::formatStatistics($result['distribution_stats']) . "\n";
}
public function testThroughputScaling(): void
{
$testCases = [
['workers' => 1, 'jobs' => 50, 'capacity' => 50],
['workers' => 5, 'jobs' => 250, 'capacity' => 20],
['workers' => 10, 'jobs' => 500, 'capacity' => 15],
['workers' => 20, 'jobs' => 1000, 'capacity' => 10]
];
$results = [];
foreach ($testCases as $case) {
$workers = $this->createWorkers($case['workers'], $case['capacity']);
$result = $this->measureThroughput($workers, $case['jobs']);
$results[] = [
'worker_count' => $case['workers'],
'throughput' => $result['throughput'],
'efficiency' => $result['throughput'] / $case['workers'] // Jobs per worker per second
];
$this->cleanupJobs();
}
// Validate scaling efficiency
for ($i = 1; $i < count($results); $i++) {
$prev = $results[$i - 1];
$curr = $results[$i];
$scalingFactor = $curr['worker_count'] / $prev['worker_count'];
$throughputIncrease = $curr['throughput'] / $prev['throughput'];
// Throughput should increase with more workers (allow for some overhead)
$this->assertGreaterThan(
$scalingFactor * 0.7, // Allow 30% overhead
$throughputIncrease,
sprintf(
'Throughput scaling below expected: %dx workers should achieve at least %.1fx throughput',
$scalingFactor,
$scalingFactor * 0.7
)
);
}
echo "\nThroughput Scaling Results:\n";
foreach ($results as $result) {
echo sprintf(
"Workers: %2d, Throughput: %6.1f jobs/sec, Efficiency: %5.1f jobs/worker/sec\n",
$result['worker_count'],
$result['throughput'],
$result['efficiency']
);
}
}
public function testMixedCapacityThroughput(): void
{
$workers = [
PerformanceTestHelper::createTestWorker('worker_1', 50),
PerformanceTestHelper::createTestWorker('worker_2', 30),
PerformanceTestHelper::createTestWorker('worker_3', 20),
PerformanceTestHelper::createTestWorker('worker_4', 10),
PerformanceTestHelper::createTestWorker('worker_5', 5)
];
$this->registerWorkers($workers);
$jobCount = 500;
$result = $this->measureThroughput($workers, $jobCount);
// Mixed capacity should still achieve good throughput
$this->assertGreaterThan(
200, // Reasonable expectation for mixed capacity workers
$result['throughput'],
'Mixed capacity worker throughput below expected minimum'
);
echo "\nMixed Capacity Worker Results:\n";
echo "Worker Capacities: 50, 30, 20, 10, 5\n";
echo "Throughput: {$result['throughput']} jobs/second\n";
echo "Average Job Distribution Time: " .
PerformanceTestHelper::formatStatistics($result['distribution_stats']) . "\n";
}
public function testSustainedLoadThroughput(): void
{
$workers = $this->createWorkers(10, 20);
$duration = 30; // 30 second sustained load test
$batchSize = 50;
$startTime = microtime(true);
$endTime = $startTime + $duration;
$totalJobs = 0;
$distributionTimes = [];
while (microtime(true) < $endTime) {
$jobs = PerformanceTestHelper::createBulkJobs($batchSize);
$batchStartTime = microtime(true);
foreach ($jobs as $job) {
$measureResult = PerformanceTestHelper::measureTimeWithResult(
fn() => $this->distributionService->distributeJob($job)
);
$distributionTimes[] = $measureResult['time_ms'];
}
$batchEndTime = microtime(true);
$totalJobs += count($jobs);
// Clean up completed jobs to prevent memory issues
if ($totalJobs % 200 === 0) {
$this->cleanupJobs();
}
// Brief pause to prevent overwhelming
usleep(10000); // 10ms
}
$actualDuration = microtime(true) - $startTime;
$sustainedThroughput = $totalJobs / $actualDuration;
$this->assertGreaterThan(
100, // Should maintain at least 100 jobs/second under sustained load
$sustainedThroughput,
'Sustained load throughput below minimum'
);
$distributionStats = PerformanceTestHelper::calculateStatistics($distributionTimes);
echo "\nSustained Load Test Results:\n";
echo "Duration: {$actualDuration} seconds\n";
echo "Total Jobs: {$totalJobs}\n";
echo "Sustained Throughput: {$sustainedThroughput} jobs/second\n";
echo "Distribution Times: " . PerformanceTestHelper::formatStatistics($distributionStats) . "\n";
// Distribution times should remain reasonable under sustained load
$this->assertLessThan(50, $distributionStats['avg'], 'Average distribution time too high under sustained load');
$this->assertLessThan(100, $distributionStats['p95'], 'P95 distribution time too high under sustained load');
}
private function measureThroughput(array $workers, int $jobCount): array
{
$this->registerWorkers($workers);
$jobs = PerformanceTestHelper::createBulkJobs($jobCount);
$distributionTimes = [];
$startTime = microtime(true);
foreach ($jobs as $job) {
$measureResult = PerformanceTestHelper::measureTimeWithResult(
fn() => $this->distributionService->distributeJob($job)
);
$distributionTimes[] = $measureResult['time_ms'];
}
$endTime = microtime(true);
$totalTimeMs = ($endTime - $startTime) * 1000;
$throughput = $jobCount / ($endTime - $startTime);
return [
'throughput' => round($throughput, 1),
'total_time_ms' => round($totalTimeMs, 1),
'distribution_stats' => PerformanceTestHelper::calculateStatistics($distributionTimes),
'jobs_processed' => $jobCount
];
}
private function createWorkers(int $count, int $capacity): array
{
$workers = [];
for ($i = 1; $i <= $count; $i++) {
$workers[] = PerformanceTestHelper::createTestWorker(
"perf_worker_{$i}",
$capacity,
WorkerStatus::AVAILABLE
);
}
return $workers;
}
private function registerWorkers(array $workers): void
{
foreach ($workers as $worker) {
$this->workerRegistry->registerWorker($worker);
}
}
private function createTestDatabase(): DatabaseManager
{
// Use in-memory SQLite for performance tests
$pdo = new \PDO('sqlite::memory:');
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// Create required tables
$pdo->exec('
CREATE TABLE workers (
id TEXT PRIMARY KEY,
queue_names TEXT NOT NULL,
capacity INTEGER NOT NULL,
status TEXT NOT NULL,
last_heartbeat TEXT NOT NULL,
metadata TEXT
)
');
$pdo->exec('
CREATE TABLE jobs (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
payload TEXT NOT NULL,
queue_name TEXT NOT NULL,
priority INTEGER NOT NULL,
status TEXT NOT NULL,
worker_id TEXT,
created_at TEXT NOT NULL,
started_at TEXT,
completed_at TEXT,
attempts INTEGER DEFAULT 0,
error_message TEXT
)
');
$pdo->exec('
CREATE TABLE distributed_locks (
lock_key TEXT PRIMARY KEY,
owner_id TEXT NOT NULL,
acquired_at TEXT NOT NULL,
expires_at TEXT NOT NULL
)
');
// Create indexes for performance
$pdo->exec('CREATE INDEX idx_workers_status ON workers(status)');
$pdo->exec('CREATE INDEX idx_jobs_status ON jobs(status)');
$pdo->exec('CREATE INDEX idx_jobs_queue ON jobs(queue_name)');
$pdo->exec('CREATE INDEX idx_jobs_priority ON jobs(priority)');
$pdo->exec('CREATE INDEX idx_locks_expires ON distributed_locks(expires_at)');
return new DatabaseManager($pdo);
}
private function cleanupTestData(): void
{
$pdo = $this->database->getConnection();
$pdo->exec('DELETE FROM workers WHERE id LIKE "perf_worker_%"');
$pdo->exec('DELETE FROM jobs WHERE id LIKE "job_%"');
$pdo->exec('DELETE FROM distributed_locks');
}
private function cleanupJobs(): void
{
$pdo = $this->database->getConnection();
$pdo->exec('DELETE FROM jobs WHERE status IN ("COMPLETED", "FAILED")');
}
}

View File

@@ -0,0 +1,295 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Queue\Performance;
use App\Framework\Queue\Jobs\Job;
use App\Framework\Queue\Jobs\JobRequest;
use App\Framework\Queue\Jobs\JobResult;
use App\Framework\Queue\Jobs\JobStatus;
use App\Framework\Queue\Workers\Worker;
use App\Framework\Queue\Workers\WorkerCapacity;
use App\Framework\Queue\Workers\WorkerId;
use App\Framework\Queue\Workers\WorkerStatus;
use App\Framework\Queue\Queue\QueueName;
use App\Framework\Queue\Queue\JobPriority;
final readonly class PerformanceTestHelper
{
public static function createTestWorker(
string $id = null,
int $capacity = 10,
WorkerStatus $status = WorkerStatus::AVAILABLE
): Worker {
return new Worker(
id: new WorkerId($id ?? uniqid('worker_')),
queueNames: [new QueueName('test_queue')],
capacity: new WorkerCapacity($capacity),
status: $status,
lastHeartbeat: new \DateTimeImmutable(),
metadata: []
);
}
public static function createTestJob(
string $id = null,
JobPriority $priority = JobPriority::NORMAL,
array $payload = []
): Job {
return new Job(
id: $id ?? uniqid('job_'),
request: new JobRequest(
type: 'test_job',
payload: $payload ?: ['test' => 'data'],
queue: new QueueName('test_queue'),
priority: $priority
),
status: JobStatus::PENDING,
createdAt: new \DateTimeImmutable(),
attempts: 0
);
}
public static function createBulkJobs(int $count, JobPriority $priority = JobPriority::NORMAL): array
{
$jobs = [];
for ($i = 0; $i < $count; $i++) {
$jobs[] = self::createTestJob(
id: "job_{$i}",
priority: $priority,
payload: ['batch_id' => $i, 'data' => str_repeat('x', 100)]
);
}
return $jobs;
}
public static function measureTime(callable $operation): float
{
$start = microtime(true);
$operation();
$end = microtime(true);
return ($end - $start) * 1000; // Return milliseconds
}
public static function measureTimeWithResult(callable $operation): array
{
$start = microtime(true);
$result = $operation();
$end = microtime(true);
return [
'result' => $result,
'time_ms' => ($end - $start) * 1000
];
}
public static function calculateStatistics(array $measurements): array
{
if (empty($measurements)) {
return [
'count' => 0,
'min' => 0,
'max' => 0,
'avg' => 0,
'median' => 0,
'p95' => 0,
'p99' => 0,
'stddev' => 0
];
}
sort($measurements);
$count = count($measurements);
$min = $measurements[0];
$max = $measurements[$count - 1];
$avg = array_sum($measurements) / $count;
$median = $count % 2 === 0
? ($measurements[$count / 2 - 1] + $measurements[$count / 2]) / 2
: $measurements[intval($count / 2)];
$p95Index = intval($count * 0.95) - 1;
$p99Index = intval($count * 0.99) - 1;
$p95 = $measurements[max(0, $p95Index)];
$p99 = $measurements[max(0, $p99Index)];
// Calculate standard deviation
$sumSquaredDiff = 0;
foreach ($measurements as $value) {
$sumSquaredDiff += pow($value - $avg, 2);
}
$stddev = sqrt($sumSquaredDiff / $count);
return [
'count' => $count,
'min' => round($min, 3),
'max' => round($max, 3),
'avg' => round($avg, 3),
'median' => round($median, 3),
'p95' => round($p95, 3),
'p99' => round($p99, 3),
'stddev' => round($stddev, 3)
];
}
public static function formatStatistics(array $stats, string $unit = 'ms'): string
{
return sprintf(
"Count: %d, Min: %.3f%s, Max: %.3f%s, Avg: %.3f%s, P95: %.3f%s, P99: %.3f%s, StdDev: %.3f%s",
$stats['count'],
$stats['min'], $unit,
$stats['max'], $unit,
$stats['avg'], $unit,
$stats['p95'], $unit,
$stats['p99'], $unit,
$stats['stddev'], $unit
);
}
public static function assertPerformance(
array $measurements,
float $expectedAvg,
float $expectedP95,
string $operation
): void {
$stats = self::calculateStatistics($measurements);
if ($stats['avg'] > $expectedAvg) {
throw new \AssertionError(
sprintf(
"%s average performance exceeded: expected ≤%.3fms, got %.3fms",
$operation,
$expectedAvg,
$stats['avg']
)
);
}
if ($stats['p95'] > $expectedP95) {
throw new \AssertionError(
sprintf(
"%s P95 performance exceeded: expected ≤%.3fms, got %.3fms",
$operation,
$expectedP95,
$stats['p95']
)
);
}
}
public static function getMemoryUsage(): array
{
return [
'current_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
'peak_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
'current_real_mb' => round(memory_get_usage(false) / 1024 / 1024, 2),
'peak_real_mb' => round(memory_get_peak_usage(false) / 1024 / 1024, 2)
];
}
public static function warmupDatabase(\PDO $pdo): void
{
// Execute simple queries to warm up connections
$pdo->query('SELECT 1');
$pdo->query('SELECT COUNT(*) FROM workers');
$pdo->query('SELECT COUNT(*) FROM jobs');
}
public static function createConcurrentOperation(callable $operation, int $concurrency): \Generator
{
$operations = [];
for ($i = 0; $i < $concurrency; $i++) {
$operations[] = function() use ($operation, $i) {
return $operation($i);
};
}
foreach ($operations as $op) {
yield $op;
}
}
public static function simulateLoad(
callable $operation,
int $totalOperations,
int $concurrency,
float $durationSeconds = null
): array {
$results = [];
$startTime = microtime(true);
$endTime = $durationSeconds ? $startTime + $durationSeconds : PHP_FLOAT_MAX;
$operationsCompleted = 0;
$batch = 0;
while ($operationsCompleted < $totalOperations && microtime(true) < $endTime) {
$batchSize = min($concurrency, $totalOperations - $operationsCompleted);
$batchResults = [];
// Execute concurrent operations
for ($i = 0; $i < $batchSize; $i++) {
$result = self::measureTimeWithResult(function() use ($operation, $batch, $i) {
return $operation($batch * $concurrency + $i);
});
$batchResults[] = $result;
}
$results = array_merge($results, $batchResults);
$operationsCompleted += $batchSize;
$batch++;
// Small delay to prevent overwhelming the system
if (microtime(true) < $endTime) {
usleep(1000); // 1ms
}
}
return [
'results' => $results,
'operations_completed' => $operationsCompleted,
'duration_seconds' => microtime(true) - $startTime,
'throughput_ops_per_sec' => $operationsCompleted / (microtime(true) - $startTime)
];
}
public static function generatePerformanceReport(array $testResults): string
{
$report = "\n" . str_repeat("=", 80) . "\n";
$report .= "PERFORMANCE TEST REPORT\n";
$report .= str_repeat("=", 80) . "\n\n";
foreach ($testResults as $testName => $results) {
$report .= "Test: {$testName}\n";
$report .= str_repeat("-", 40) . "\n";
if (isset($results['statistics'])) {
$report .= "Statistics: " . self::formatStatistics($results['statistics']) . "\n";
}
if (isset($results['throughput'])) {
$report .= "Throughput: {$results['throughput']} ops/sec\n";
}
if (isset($results['memory'])) {
$report .= sprintf(
"Memory: Current: %.2fMB, Peak: %.2fMB\n",
$results['memory']['current_mb'],
$results['memory']['peak_mb']
);
}
if (isset($results['notes'])) {
$report .= "Notes: {$results['notes']}\n";
}
$report .= "\n";
}
return $report;
}
}

View File

@@ -0,0 +1,720 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Queue\Performance;
use App\Framework\Database\DatabaseManager;
use App\Framework\Queue\Distribution\JobDistributionService;
use App\Framework\Queue\Failover\FailoverRecoveryService;
use App\Framework\Queue\Health\WorkerHealthCheckService;
use App\Framework\Queue\Jobs\JobPriority;
use App\Framework\Queue\Jobs\JobStatus;
use App\Framework\Queue\Workers\WorkerRegistry;
use App\Framework\Queue\Workers\WorkerStatus;
use PHPUnit\Framework\TestCase;
final class RealisticLoadScenariosTest extends TestCase
{
private DatabaseManager $database;
private WorkerRegistry $workerRegistry;
private JobDistributionService $distributionService;
private WorkerHealthCheckService $healthCheckService;
private FailoverRecoveryService $failoverService;
protected function setUp(): void
{
$this->database = $this->createTestDatabase();
$this->workerRegistry = new WorkerRegistry($this->database);
$this->distributionService = new JobDistributionService(
$this->database,
$this->workerRegistry
);
$this->healthCheckService = new WorkerHealthCheckService(
$this->database,
$this->workerRegistry
);
$this->failoverService = new FailoverRecoveryService(
$this->database,
$this->workerRegistry
);
$this->cleanupTestData();
PerformanceTestHelper::warmupDatabase($this->database->getConnection());
}
protected function tearDown(): void
{
$this->cleanupTestData();
}
public function testEcommercePeakTrafficScenario(): void
{
echo "\nE-commerce Peak Traffic Scenario:\n";
echo "Simulating Black Friday / Cyber Monday traffic patterns\n";
// Setup: Mixed capacity workers for different job types
$workers = [
// High-capacity workers for order processing
...$this->createWorkers(8, 50, 'order_processor'),
// Medium-capacity workers for inventory updates
...$this->createWorkers(12, 30, 'inventory_worker'),
// Lower-capacity workers for email notifications
...$this->createWorkers(20, 15, 'notification_worker')
];
$this->registerWorkers($workers);
// Simulate peak traffic: 1000+ jobs per minute for 5 minutes
$scenarioDuration = 300; // 5 minutes
$peakJobsPerMinute = 1200;
$jobsPerSecond = $peakJobsPerMinute / 60;
echo "Target load: {$peakJobsPerMinute} jobs/minute ({$jobsPerSecond} jobs/second)\n";
echo "Duration: {$scenarioDuration} seconds\n";
$results = $this->simulateRealisticLoad(
$scenarioDuration,
$jobsPerSecond,
$this->createEcommerceJobMix()
);
echo "\nE-commerce Peak Results:\n";
echo "Actual throughput: {$results['throughput']} jobs/second\n";
echo "Success rate: {$results['success_rate']}%\n";
echo "Average response time: {$results['avg_response_time']}ms\n";
echo "P95 response time: {$results['p95_response_time']}ms\n";
echo "Memory usage: {$results['memory_usage']}MB\n";
// Validate e-commerce performance requirements
$this->assertGreaterThan(15, $results['throughput'], 'E-commerce throughput below minimum');
$this->assertGreaterThan(95.0, $results['success_rate'], 'E-commerce success rate below 95%');
$this->assertLessThan(100.0, $results['avg_response_time'], 'E-commerce response time too high');
$this->assertLessThan(200.0, $results['p95_response_time'], 'E-commerce P95 response time too high');
}
public function testMediaProcessingWorkloadScenario(): void
{
echo "\nMedia Processing Workload Scenario:\n";
echo "Simulating video transcoding and image processing pipeline\n";
// Setup: Fewer, high-capacity workers for CPU/memory intensive tasks
$workers = [
// Heavy-duty workers for video processing
...$this->createWorkers(4, 100, 'video_processor'),
// Medium workers for image processing
...$this->createWorkers(8, 50, 'image_processor'),
// Light workers for metadata extraction
...$this->createWorkers(12, 25, 'metadata_worker')
];
$this->registerWorkers($workers);
// Simulate media processing: Lower frequency but CPU/memory intensive
$scenarioDuration = 600; // 10 minutes
$jobsPerSecond = 3; // Lower rate due to intensive processing
echo "Target load: {$jobsPerSecond} jobs/second (CPU/memory intensive)\n";
echo "Duration: {$scenarioDuration} seconds\n";
$results = $this->simulateRealisticLoad(
$scenarioDuration,
$jobsPerSecond,
$this->createMediaProcessingJobMix(),
$enableResourceMonitoring = true
);
echo "\nMedia Processing Results:\n";
echo "Actual throughput: {$results['throughput']} jobs/second\n";
echo "Success rate: {$results['success_rate']}%\n";
echo "Average processing time: {$results['avg_response_time']}ms\n";
echo "Memory efficiency: {$results['memory_efficiency']}%\n";
echo "Resource utilization: {$results['resource_utilization']}%\n";
// Validate media processing performance requirements
$this->assertGreaterThan(2.5, $results['throughput'], 'Media processing throughput below minimum');
$this->assertGreaterThan(98.0, $results['success_rate'], 'Media processing success rate below 98%');
$this->assertLessThan(500.0, $results['avg_response_time'], 'Media processing time too high');
$this->assertGreaterThan(80.0, $results['resource_utilization'], 'Resource utilization too low');
}
public function testFinancialTransactionProcessingScenario(): void
{
echo "\nFinancial Transaction Processing Scenario:\n";
echo "Simulating real-time payment processing with low latency requirements\n";
// Setup: Many workers optimized for low-latency processing
$workers = [
// High-speed transaction processors
...$this->createWorkers(20, 20, 'payment_processor'),
// Fraud detection workers
...$this->createWorkers(10, 15, 'fraud_detector'),
// Settlement workers
...$this->createWorkers(5, 30, 'settlement_worker')
];
$this->registerWorkers($workers);
// Simulate financial processing: High frequency, low latency requirements
$scenarioDuration = 120; // 2 minutes
$jobsPerSecond = 50; // High frequency transactions
echo "Target load: {$jobsPerSecond} jobs/second (low latency requirement)\n";
echo "Duration: {$scenarioDuration} seconds\n";
$results = $this->simulateRealisticLoad(
$scenarioDuration,
$jobsPerSecond,
$this->createFinancialJobMix(),
$enableResourceMonitoring = false,
$lowLatencyMode = true
);
echo "\nFinancial Processing Results:\n";
echo "Actual throughput: {$results['throughput']} jobs/second\n";
echo "Success rate: {$results['success_rate']}%\n";
echo "Average latency: {$results['avg_response_time']}ms\n";
echo "P95 latency: {$results['p95_response_time']}ms\n";
echo "P99 latency: {$results['p99_response_time']}ms\n";
// Validate financial processing requirements (strict latency)
$this->assertGreaterThan(40, $results['throughput'], 'Financial throughput below minimum');
$this->assertGreaterThan(99.9, $results['success_rate'], 'Financial success rate below 99.9%');
$this->assertLessThan(20.0, $results['avg_response_time'], 'Financial latency too high');
$this->assertLessThan(50.0, $results['p95_response_time'], 'Financial P95 latency too high');
$this->assertLessThan(100.0, $results['p99_response_time'], 'Financial P99 latency too high');
}
public function testBatchProcessingScenario(): void
{
echo "\nBatch Processing Scenario:\n";
echo "Simulating ETL pipeline with high throughput requirements\n";
// Setup: High-capacity workers optimized for batch processing
$workers = [
// ETL workers for data transformation
...$this->createWorkers(6, 100, 'etl_worker'),
// Data validation workers
...$this->createWorkers(8, 75, 'validator'),
// Report generation workers
...$this->createWorkers(4, 150, 'report_generator')
];
$this->registerWorkers($workers);
// Simulate batch processing: Very high throughput
$scenarioDuration = 300; // 5 minutes
$jobsPerSecond = 100; // High throughput batch processing
echo "Target load: {$jobsPerSecond} jobs/second (high throughput batch)\n";
echo "Duration: {$scenarioDuration} seconds\n";
$results = $this->simulateRealisticLoad(
$scenarioDuration,
$jobsPerSecond,
$this->createBatchProcessingJobMix(),
$enableResourceMonitoring = true
);
echo "\nBatch Processing Results:\n";
echo "Actual throughput: {$results['throughput']} jobs/second\n";
echo "Success rate: {$results['success_rate']}%\n";
echo "Batch efficiency: {$results['batch_efficiency']}%\n";
echo "Resource utilization: {$results['resource_utilization']}%\n";
echo "Memory stability: {$results['memory_stability']}\n";
// Validate batch processing requirements
$this->assertGreaterThan(80, $results['throughput'], 'Batch throughput below minimum');
$this->assertGreaterThan(99.0, $results['success_rate'], 'Batch success rate below 99%');
$this->assertGreaterThan(85.0, $results['batch_efficiency'], 'Batch efficiency too low');
$this->assertGreaterThan(75.0, $results['resource_utilization'], 'Batch resource utilization too low');
}
public function testMixedWorkloadStressTest(): void
{
echo "\nMixed Workload Stress Test:\n";
echo "Simulating real-world environment with multiple concurrent workload types\n";
// Setup: Diverse worker pool handling multiple workload types
$workers = [
// Web request processors
...$this->createWorkers(15, 30, 'web_processor'),
// Background task workers
...$this->createWorkers(10, 20, 'background_worker'),
// Heavy computation workers
...$this->createWorkers(5, 80, 'compute_worker'),
// Notification workers
...$this->createWorkers(20, 10, 'notification_worker')
];
$this->registerWorkers($workers);
// Simulate mixed workload with varying intensity
$phases = [
['duration' => 60, 'rate' => 20, 'mix' => 'normal'],
['duration' => 120, 'rate' => 50, 'mix' => 'peak'],
['duration' => 60, 'rate' => 15, 'mix' => 'background'],
['duration' => 90, 'rate' => 35, 'mix' => 'mixed']
];
$overallResults = [];
foreach ($phases as $phaseIndex => $phase) {
echo "\nPhase " . ($phaseIndex + 1) . ": {$phase['mix']} workload\n";
echo "Duration: {$phase['duration']}s, Rate: {$phase['rate']} jobs/sec\n";
$jobMix = $this->createMixedWorkloadJobMix($phase['mix']);
$results = $this->simulateRealisticLoad(
$phase['duration'],
$phase['rate'],
$jobMix,
$enableResourceMonitoring = true
);
echo "Phase Results - Throughput: {$results['throughput']}, Success: {$results['success_rate']}%\n";
$overallResults[] = $results;
// Brief pause between phases
sleep(2);
}
// Analyze overall performance across all phases
$this->analyzeOverallPerformance($overallResults);
}
public function testFailoverUnderRealWorldLoad(): void
{
echo "\nFailover Under Real-World Load Test:\n";
echo "Simulating worker failures during active production load\n";
// Setup: Production-like worker configuration
$workers = [
...$this->createWorkers(12, 25, 'primary_worker'),
...$this->createWorkers(8, 30, 'secondary_worker'),
...$this->createWorkers(6, 20, 'backup_worker')
];
$this->registerWorkers($workers);
// Start sustained load
$testDuration = 180; // 3 minutes
$baseJobRate = 30; // jobs per second
echo "Base load: {$baseJobRate} jobs/second\n";
echo "Test duration: {$testDuration} seconds\n";
$startTime = microtime(true);
$endTime = $startTime + $testDuration;
$metrics = [
'jobs_processed' => 0,
'jobs_failed' => 0,
'response_times' => [],
'failover_events' => []
];
$failoverTriggered = false;
while (microtime(true) < $endTime) {
$cycleStart = microtime(true);
// Trigger failover at 1/3 of test duration
if (!$failoverTriggered && (microtime(true) - $startTime) > ($testDuration / 3)) {
echo "\nTriggering failover scenario...\n";
// Fail primary workers
for ($i = 1; $i <= 4; $i++) {
$this->updateWorkerStatus("primary_worker_{$i}", WorkerStatus::FAILED);
}
$failoverTime = PerformanceTestHelper::measureTime(function() {
$this->failoverService->performFullSystemRecovery();
});
$metrics['failover_events'][] = [
'time' => microtime(true) - $startTime,
'recovery_time' => $failoverTime
];
echo "Failover completed in {$failoverTime}ms\n";
$failoverTriggered = true;
}
// Process jobs
for ($i = 0; $i < $baseJobRate; $i++) {
$job = PerformanceTestHelper::createTestJob("realworld_job_{$metrics['jobs_processed']}");
$result = PerformanceTestHelper::measureTimeWithResult(function() use ($job) {
try {
return $this->distributionService->distributeJob($job);
} catch (\Exception $e) {
return null;
}
});
$metrics['response_times'][] = $result['time_ms'];
if ($result['result'] !== null) {
$metrics['jobs_processed']++;
} else {
$metrics['jobs_failed']++;
}
}
// Maintain rate
$cycleTime = microtime(true) - $cycleStart;
$sleepTime = 1.0 - $cycleTime;
if ($sleepTime > 0) {
usleep($sleepTime * 1000000);
}
}
$actualDuration = microtime(true) - $startTime;
$actualThroughput = $metrics['jobs_processed'] / $actualDuration;
$successRate = $metrics['jobs_processed'] / ($metrics['jobs_processed'] + $metrics['jobs_failed']) * 100;
$responseStats = PerformanceTestHelper::calculateStatistics($metrics['response_times']);
echo "\nFailover Test Results:\n";
echo "Actual throughput: {$actualThroughput} jobs/second\n";
echo "Success rate: {$successRate}%\n";
echo "Response times: " . PerformanceTestHelper::formatStatistics($responseStats) . "\n";
if (!empty($metrics['failover_events'])) {
echo "Failover recovery time: {$metrics['failover_events'][0]['recovery_time']}ms\n";
}
// System should maintain reasonable performance during failover
$this->assertGreaterThan(20, $actualThroughput, 'Throughput too low during failover');
$this->assertGreaterThan(90.0, $successRate, 'Success rate too low during failover');
$this->assertLessThan(100.0, $responseStats['avg'], 'Response time too high during failover');
}
private function simulateRealisticLoad(
int $duration,
float $jobsPerSecond,
array $jobMix,
bool $enableResourceMonitoring = false,
bool $lowLatencyMode = false
): array {
$startTime = microtime(true);
$endTime = $startTime + $duration;
$metrics = [
'jobs_processed' => 0,
'jobs_failed' => 0,
'response_times' => [],
'memory_snapshots' => [],
'start_memory' => null,
'end_memory' => null
];
if ($enableResourceMonitoring) {
$metrics['start_memory'] = PerformanceTestHelper::getMemoryUsage();
}
$jobCounter = 0;
$snapshotInterval = $enableResourceMonitoring ? 30 : 0; // Take snapshots every 30 seconds
$nextSnapshotTime = $startTime + $snapshotInterval;
while (microtime(true) < $endTime) {
$cycleStart = microtime(true);
// Determine job type based on mix
$jobType = $this->selectJobType($jobMix);
$job = $this->createJobForType($jobType, $jobCounter);
$result = PerformanceTestHelper::measureTimeWithResult(function() use ($job) {
try {
return $this->distributionService->distributeJob($job);
} catch (\Exception $e) {
return null;
}
});
$metrics['response_times'][] = $result['time_ms'];
if ($result['result'] !== null) {
$metrics['jobs_processed']++;
} else {
$metrics['jobs_failed']++;
}
$jobCounter++;
// Take memory snapshots
if ($enableResourceMonitoring && microtime(true) >= $nextSnapshotTime) {
$metrics['memory_snapshots'][] = [
'time' => microtime(true) - $startTime,
'memory' => PerformanceTestHelper::getMemoryUsage()
];
$nextSnapshotTime += $snapshotInterval;
}
// Rate limiting
if ($lowLatencyMode) {
// Minimal delay for low latency requirements
usleep(10); // 0.01ms
} else {
// Calculate delay to maintain target rate
$targetCycleTime = 1.0 / $jobsPerSecond;
$actualCycleTime = microtime(true) - $cycleStart;
$sleepTime = $targetCycleTime - $actualCycleTime;
if ($sleepTime > 0) {
usleep($sleepTime * 1000000);
}
}
}
if ($enableResourceMonitoring) {
$metrics['end_memory'] = PerformanceTestHelper::getMemoryUsage();
}
return $this->calculateScenarioResults($metrics, microtime(true) - $startTime, $enableResourceMonitoring);
}
private function calculateScenarioResults(array $metrics, float $actualDuration, bool $includeResourceMetrics): array
{
$throughput = $metrics['jobs_processed'] / $actualDuration;
$successRate = $metrics['jobs_processed'] / max(1, $metrics['jobs_processed'] + $metrics['jobs_failed']) * 100;
$responseStats = PerformanceTestHelper::calculateStatistics($metrics['response_times']);
$results = [
'throughput' => round($throughput, 1),
'success_rate' => round($successRate, 2),
'avg_response_time' => $responseStats['avg'],
'p95_response_time' => $responseStats['p95'],
'p99_response_time' => $responseStats['p99']
];
if ($includeResourceMetrics && isset($metrics['start_memory'], $metrics['end_memory'])) {
$startMem = $metrics['start_memory']['current_mb'];
$endMem = $metrics['end_memory']['current_mb'];
$peakMem = $metrics['end_memory']['peak_mb'];
$results['memory_usage'] = $endMem;
$results['memory_efficiency'] = round((1 - ($endMem - $startMem) / max(1, $startMem)) * 100, 1);
$results['resource_utilization'] = round(($endMem / $peakMem) * 100, 1);
$results['memory_stability'] = abs($endMem - $startMem) < 10 ? 'stable' : 'unstable';
$results['batch_efficiency'] = round($throughput / max(1, $endMem) * 100, 1);
}
return $results;
}
private function createEcommerceJobMix(): array
{
return [
'order_processing' => 40,
'inventory_update' => 25,
'payment_processing' => 20,
'email_notification' => 10,
'user_analytics' => 5
];
}
private function createMediaProcessingJobMix(): array
{
return [
'video_transcode' => 30,
'image_resize' => 40,
'thumbnail_generation' => 20,
'metadata_extraction' => 10
];
}
private function createFinancialJobMix(): array
{
return [
'payment_processing' => 50,
'fraud_detection' => 25,
'account_verification' => 15,
'transaction_logging' => 10
];
}
private function createBatchProcessingJobMix(): array
{
return [
'data_transformation' => 40,
'data_validation' => 30,
'report_generation' => 20,
'data_archival' => 10
];
}
private function createMixedWorkloadJobMix(string $mixType): array
{
return match($mixType) {
'normal' => [
'web_request' => 50,
'background_task' => 30,
'notification' => 20
],
'peak' => [
'web_request' => 60,
'background_task' => 20,
'notification' => 15,
'compute_task' => 5
],
'background' => [
'background_task' => 60,
'compute_task' => 30,
'notification' => 10
],
'mixed' => [
'web_request' => 35,
'background_task' => 25,
'compute_task' => 25,
'notification' => 15
],
default => ['web_request' => 100]
};
}
private function selectJobType(array $jobMix): string
{
$rand = rand(1, 100);
$cumulative = 0;
foreach ($jobMix as $type => $percentage) {
$cumulative += $percentage;
if ($rand <= $cumulative) {
return $type;
}
}
return array_key_first($jobMix);
}
private function createJobForType(string $jobType, int $counter): \App\Framework\Queue\Jobs\Job
{
$priority = match($jobType) {
'payment_processing', 'fraud_detection' => JobPriority::CRITICAL,
'order_processing', 'web_request' => JobPriority::HIGH,
'inventory_update', 'background_task' => JobPriority::NORMAL,
default => JobPriority::LOW
};
$payloadSize = match($jobType) {
'video_transcode', 'compute_task' => 1000, // Large payload
'image_resize', 'data_transformation' => 500, // Medium payload
default => 100 // Small payload
};
return PerformanceTestHelper::createTestJob(
"{$jobType}_job_{$counter}",
$priority,
['type' => $jobType, 'data' => str_repeat('x', $payloadSize)]
);
}
private function analyzeOverallPerformance(array $phaseResults): void
{
echo "\nOverall Mixed Workload Analysis:\n";
$totalThroughput = array_sum(array_column($phaseResults, 'throughput')) / count($phaseResults);
$averageSuccessRate = array_sum(array_column($phaseResults, 'success_rate')) / count($phaseResults);
$averageResponseTime = array_sum(array_column($phaseResults, 'avg_response_time')) / count($phaseResults);
echo "Average throughput across phases: {$totalThroughput} jobs/second\n";
echo "Average success rate: {$averageSuccessRate}%\n";
echo "Average response time: {$averageResponseTime}ms\n";
// Validate mixed workload performance
$this->assertGreaterThan(25, $totalThroughput, 'Mixed workload throughput below minimum');
$this->assertGreaterThan(95.0, $averageSuccessRate, 'Mixed workload success rate below 95%');
$this->assertLessThan(80.0, $averageResponseTime, 'Mixed workload response time too high');
// Check performance consistency across phases
$throughputStdDev = $this->calculateStandardDeviation(array_column($phaseResults, 'throughput'));
$this->assertLessThan(10.0, $throughputStdDev, 'Throughput too inconsistent across phases');
}
private function calculateStandardDeviation(array $values): float
{
$mean = array_sum($values) / count($values);
$sumSquaredDiffs = array_sum(array_map(fn($v) => pow($v - $mean, 2), $values));
return sqrt($sumSquaredDiffs / count($values));
}
private function createWorkers(int $count, int $capacity, string $prefix): array
{
$workers = [];
for ($i = 1; $i <= $count; $i++) {
$workers[] = PerformanceTestHelper::createTestWorker(
"{$prefix}_{$i}",
$capacity,
WorkerStatus::AVAILABLE
);
}
return $workers;
}
private function registerWorkers(array $workers): void
{
foreach ($workers as $worker) {
$this->workerRegistry->registerWorker($worker);
}
}
private function updateWorkerStatus(string $workerId, WorkerStatus $status): void
{
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('UPDATE workers SET status = ? WHERE id = ?');
$stmt->execute([$status->value, $workerId]);
}
private function createTestDatabase(): DatabaseManager
{
$pdo = new \PDO('sqlite::memory:');
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$pdo->exec('
CREATE TABLE workers (
id TEXT PRIMARY KEY,
queue_names TEXT NOT NULL,
capacity INTEGER NOT NULL,
status TEXT NOT NULL,
last_heartbeat TEXT NOT NULL,
metadata TEXT
)
');
$pdo->exec('
CREATE TABLE jobs (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
payload TEXT NOT NULL,
queue_name TEXT NOT NULL,
priority INTEGER NOT NULL,
status TEXT NOT NULL,
worker_id TEXT,
created_at TEXT NOT NULL,
started_at TEXT,
completed_at TEXT,
attempts INTEGER DEFAULT 0,
error_message TEXT
)
');
// Performance indexes
$pdo->exec('CREATE INDEX idx_workers_status ON workers(status)');
$pdo->exec('CREATE INDEX idx_jobs_status ON jobs(status)');
$pdo->exec('CREATE INDEX idx_jobs_priority ON jobs(priority)');
$pdo->exec('CREATE INDEX idx_jobs_worker ON jobs(worker_id)');
return new DatabaseManager($pdo);
}
private function cleanupTestData(): void
{
$pdo = $this->database->getConnection();
$pdo->exec('DELETE FROM workers');
$pdo->exec('DELETE FROM jobs');
}
}

View File

@@ -0,0 +1,617 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Queue\Performance;
use App\Framework\Database\DatabaseManager;
use App\Framework\Queue\Distribution\JobDistributionService;
use App\Framework\Queue\Jobs\JobPriority;
use App\Framework\Queue\Workers\WorkerRegistry;
use PHPUnit\Framework\TestCase;
final class SystemResourcesTest extends TestCase
{
private DatabaseManager $database;
private WorkerRegistry $workerRegistry;
private JobDistributionService $distributionService;
protected function setUp(): void
{
$this->database = $this->createTestDatabase();
$this->workerRegistry = new WorkerRegistry($this->database);
$this->distributionService = new JobDistributionService(
$this->database,
$this->workerRegistry
);
$this->cleanupTestData();
PerformanceTestHelper::warmupDatabase($this->database->getConnection());
}
protected function tearDown(): void
{
$this->cleanupTestData();
}
public function testMemoryUsageUnderLoad(): void
{
$workers = $this->createWorkers(10, 25);
$this->registerWorkers($workers);
$initialMemory = PerformanceTestHelper::getMemoryUsage();
$memorySnapshots = [$initialMemory];
echo "\nMemory Usage Under Load Test:\n";
echo "Initial memory: {$initialMemory['current_mb']}MB (Peak: {$initialMemory['peak_mb']}MB)\n";
// Phase 1: Moderate load
$this->simulateJobLoad(500, 'moderate', $memorySnapshots);
// Phase 2: High load
$this->simulateJobLoad(1000, 'high', $memorySnapshots);
// Phase 3: Sustained load
$this->simulateSustainedLoad(30, 50, $memorySnapshots); // 30 seconds, 50 jobs/sec
$finalMemory = PerformanceTestHelper::getMemoryUsage();
$memorySnapshots[] = $finalMemory;
echo "Final memory: {$finalMemory['current_mb']}MB (Peak: {$finalMemory['peak_mb']}MB)\n";
// Analyze memory usage patterns
$this->analyzeMemoryUsage($memorySnapshots);
// Memory usage should stay within reasonable bounds
$this->assertLessThan(
100.0,
$finalMemory['current_mb'],
'Current memory usage exceeds 100MB'
);
$this->assertLessThan(
200.0,
$finalMemory['peak_mb'],
'Peak memory usage exceeds 200MB'
);
// Check for potential memory leaks
$memoryIncrease = $finalMemory['current_mb'] - $initialMemory['current_mb'];
$this->assertLessThan(
50.0,
$memoryIncrease,
'Memory increase suggests potential memory leak'
);
}
public function testMemoryEfficiencyWithBulkOperations(): void
{
$workers = $this->createWorkers(5, 30);
$this->registerWorkers($workers);
echo "\nMemory Efficiency with Bulk Operations:\n";
$testCases = [
['batch_size' => 10, 'batches' => 10],
['batch_size' => 50, 'batches' => 10],
['batch_size' => 100, 'batches' => 10],
['batch_size' => 500, 'batches' => 5],
['batch_size' => 1000, 'batches' => 3]
];
foreach ($testCases as $case) {
$batchSize = $case['batch_size'];
$batchCount = $case['batches'];
$beforeMemory = PerformanceTestHelper::getMemoryUsage();
// Process batches
$totalProcessingTime = 0;
for ($batch = 0; $batch < $batchCount; $batch++) {
$jobs = PerformanceTestHelper::createBulkJobs($batchSize);
$batchTime = PerformanceTestHelper::measureTime(function() use ($jobs) {
foreach ($jobs as $job) {
$this->distributionService->distributeJob($job);
}
});
$totalProcessingTime += $batchTime;
// Clean up completed jobs to simulate real processing
if ($batch % 2 === 0) {
$this->cleanupCompletedJobs();
}
}
$afterMemory = PerformanceTestHelper::getMemoryUsage();
$memoryIncrease = $afterMemory['current_mb'] - $beforeMemory['current_mb'];
$totalJobs = $batchSize * $batchCount;
$avgTimePerJob = $totalProcessingTime / $totalJobs;
echo sprintf(
"Batch size: %4d, Total jobs: %4d, Memory increase: %6.2fMB, Avg time: %6.3fms/job\n",
$batchSize,
$totalJobs,
$memoryIncrease,
$avgTimePerJob
);
// Memory increase should not grow linearly with batch size
$memoryPerJob = $memoryIncrease / $totalJobs;
$this->assertLessThan(
0.1,
$memoryPerJob,
"Memory usage per job too high for batch size {$batchSize}"
);
$this->cleanupTestData();
}
}
public function testGarbageCollectionImpact(): void
{
$workers = $this->createWorkers(8, 20);
$this->registerWorkers($workers);
echo "\nGarbage Collection Impact Test:\n";
$gcStats = [];
$operationTimes = [];
// Force garbage collection and measure baseline
gc_collect_cycles();
$initialGcStats = gc_status();
// Perform operations that generate objects
$iterations = 1000;
for ($i = 0; $i < $iterations; $i++) {
$job = PerformanceTestHelper::createTestJob("gc_test_job_{$i}");
$operationTime = PerformanceTestHelper::measureTime(function() use ($job) {
return $this->distributionService->distributeJob($job);
});
$operationTimes[] = $operationTime;
// Collect GC stats every 100 operations
if ($i % 100 === 0) {
$gcStats[] = [
'operation' => $i,
'memory' => PerformanceTestHelper::getMemoryUsage(),
'gc_stats' => gc_status()
];
}
}
// Force final garbage collection
$gcCycles = gc_collect_cycles();
$finalGcStats = gc_status();
echo "GC cycles collected: {$gcCycles}\n";
echo "Initial GC runs: {$initialGcStats['runs']}\n";
echo "Final GC runs: {$finalGcStats['runs']}\n";
echo "GC runs during test: " . ($finalGcStats['runs'] - $initialGcStats['runs']) . "\n";
// Analyze operation times for GC impact
$stats = PerformanceTestHelper::calculateStatistics($operationTimes);
echo "Operation performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// GC should not cause significant performance degradation
$this->assertLessThan(
20.0,
$stats['avg'],
'Average operation time too high - possible GC impact'
);
// P99 should not be extremely high (indicating GC pauses)
$this->assertLessThan(
100.0,
$stats['p99'],
'P99 operation time too high - possible GC pause impact'
);
// Analyze memory usage patterns during GC
foreach ($gcStats as $snapshot) {
$memory = $snapshot['memory'];
echo sprintf(
"Operation %4d: Memory %6.2fMB, GC runs: %d\n",
$snapshot['operation'],
$memory['current_mb'],
$snapshot['gc_stats']['runs']
);
}
}
public function testConcurrentOperationResourceUsage(): void
{
$workers = $this->createWorkers(15, 20);
$this->registerWorkers($workers);
echo "\nConcurrent Operation Resource Usage Test:\n";
$concurrencyLevels = [1, 5, 10, 20, 50];
$operationsPerLevel = 200;
foreach ($concurrencyLevels as $concurrency) {
$beforeMemory = PerformanceTestHelper::getMemoryUsage();
// Simulate concurrent operations
$results = $this->simulateConcurrentOperations($concurrency, $operationsPerLevel);
$afterMemory = PerformanceTestHelper::getMemoryUsage();
$memoryIncrease = $afterMemory['current_mb'] - $beforeMemory['current_mb'];
$avgTime = array_sum($results['times']) / count($results['times']);
$throughput = $results['total_operations'] / $results['duration'];
echo sprintf(
"Concurrency: %2d, Throughput: %6.1f ops/sec, Avg time: %6.3fms, Memory: +%6.2fMB\n",
$concurrency,
$throughput,
$avgTime,
$memoryIncrease
);
// Memory usage should not grow excessively with concurrency
$this->assertLessThan(
10.0,
$memoryIncrease,
"Memory increase too high for concurrency level {$concurrency}"
);
// Throughput should generally increase with concurrency (up to a point)
if ($concurrency <= 10) {
$this->assertGreaterThan(
$concurrency * 5, // At least 5 ops/sec per concurrent operation
$throughput,
"Throughput too low for concurrency level {$concurrency}"
);
}
$this->cleanupTestData();
}
}
public function testLongRunningProcessMemoryStability(): void
{
$workers = $this->createWorkers(6, 25);
$this->registerWorkers($workers);
echo "\nLong Running Process Memory Stability Test:\n";
$duration = 120; // 2 minutes
$operationsPerSecond = 20;
$memorySnapshots = [];
$startTime = microtime(true);
$endTime = $startTime + $duration;
$operationCount = 0;
while (microtime(true) < $endTime) {
$cycleStart = microtime(true);
// Perform operations for one second
for ($i = 0; $i < $operationsPerSecond; $i++) {
$job = PerformanceTestHelper::createTestJob("stability_job_{$operationCount}");
$this->distributionService->distributeJob($job);
$operationCount++;
}
// Take memory snapshot every 10 seconds
if ($operationCount % ($operationsPerSecond * 10) === 0) {
$memory = PerformanceTestHelper::getMemoryUsage();
$elapsed = microtime(true) - $startTime;
$memorySnapshots[] = [
'time' => $elapsed,
'operations' => $operationCount,
'memory' => $memory
];
echo sprintf(
"Time: %3ds, Operations: %5d, Memory: %6.2fMB (Peak: %6.2fMB)\n",
$elapsed,
$operationCount,
$memory['current_mb'],
$memory['peak_mb']
);
}
// Clean up periodically to simulate real-world processing
if ($operationCount % ($operationsPerSecond * 5) === 0) {
$this->cleanupCompletedJobs();
}
// Maintain target operations per second
$cycleTime = microtime(true) - $cycleStart;
$sleepTime = 1.0 - $cycleTime;
if ($sleepTime > 0) {
usleep($sleepTime * 1000000);
}
}
$actualDuration = microtime(true) - $startTime;
$actualThroughput = $operationCount / $actualDuration;
echo "Total operations: {$operationCount}\n";
echo "Actual duration: {$actualDuration} seconds\n";
echo "Actual throughput: {$actualThroughput} ops/sec\n";
// Analyze memory stability
$this->analyzeMemoryStability($memorySnapshots);
// Memory should remain stable over time
$firstSnapshot = $memorySnapshots[0];
$lastSnapshot = end($memorySnapshots);
$memoryDrift = $lastSnapshot['memory']['current_mb'] - $firstSnapshot['memory']['current_mb'];
echo "Memory drift: {$memoryDrift}MB\n";
$this->assertLessThan(
20.0,
abs($memoryDrift),
'Memory drift too high - indicates memory leak or accumulation'
);
// Throughput should remain stable
$this->assertGreaterThan(
$operationsPerSecond * 0.8,
$actualThroughput,
'Throughput degraded too much during long run'
);
}
public function testResourceCleanupEfficiency(): void
{
$workers = $this->createWorkers(5, 20);
$this->registerWorkers($workers);
echo "\nResource Cleanup Efficiency Test:\n";
// Create many jobs
$jobCount = 2000;
$jobs = PerformanceTestHelper::createBulkJobs($jobCount);
$beforeMemory = PerformanceTestHelper::getMemoryUsage();
echo "Memory before job creation: {$beforeMemory['current_mb']}MB\n";
// Distribute all jobs
foreach ($jobs as $job) {
$this->distributionService->distributeJob($job);
}
$afterDistribution = PerformanceTestHelper::getMemoryUsage();
echo "Memory after distribution: {$afterDistribution['current_mb']}MB\n";
// Measure cleanup time
$cleanupTime = PerformanceTestHelper::measureTime(function() {
$this->cleanupCompletedJobs();
});
$afterCleanup = PerformanceTestHelper::getMemoryUsage();
echo "Memory after cleanup: {$afterCleanup['current_mb']}MB\n";
echo "Cleanup time: {$cleanupTime}ms\n";
$memoryRecovered = $afterDistribution['current_mb'] - $afterCleanup['current_mb'];
echo "Memory recovered: {$memoryRecovered}MB\n";
// Cleanup should be efficient
$this->assertLessThan(
200.0,
$cleanupTime,
'Cleanup time too slow for 2000 jobs'
);
// Should recover most of the memory
$distributionMemoryUsage = $afterDistribution['current_mb'] - $beforeMemory['current_mb'];
$recoveryRatio = $memoryRecovered / max(1, $distributionMemoryUsage);
echo "Memory recovery ratio: " . round($recoveryRatio * 100, 1) . "%\n";
$this->assertGreaterThan(
0.5,
$recoveryRatio,
'Should recover at least 50% of memory used during distribution'
);
}
private function simulateJobLoad(int $jobCount, string $phase, array &$memorySnapshots): void
{
echo "Phase: {$phase} ({$jobCount} jobs)\n";
$beforeMemory = PerformanceTestHelper::getMemoryUsage();
$memorySnapshots[] = $beforeMemory;
$jobs = PerformanceTestHelper::createBulkJobs($jobCount);
$distributionTimes = [];
foreach ($jobs as $job) {
$time = PerformanceTestHelper::measureTime(function() use ($job) {
return $this->distributionService->distributeJob($job);
});
$distributionTimes[] = $time;
}
$afterMemory = PerformanceTestHelper::getMemoryUsage();
$memorySnapshots[] = $afterMemory;
$stats = PerformanceTestHelper::calculateStatistics($distributionTimes);
$memoryIncrease = $afterMemory['current_mb'] - $beforeMemory['current_mb'];
echo " Memory increase: {$memoryIncrease}MB\n";
echo " Distribution performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
}
private function simulateSustainedLoad(int $duration, int $jobsPerSecond, array &$memorySnapshots): void
{
echo "Sustained load: {$jobsPerSecond} jobs/sec for {$duration} seconds\n";
$startTime = microtime(true);
$endTime = $startTime + $duration;
$jobCount = 0;
$snapshotInterval = 5; // Take snapshot every 5 seconds
$nextSnapshotTime = $startTime + $snapshotInterval;
while (microtime(true) < $endTime) {
$job = PerformanceTestHelper::createTestJob("sustained_job_{$jobCount}");
$this->distributionService->distributeJob($job);
$jobCount++;
// Take memory snapshot
if (microtime(true) >= $nextSnapshotTime) {
$memory = PerformanceTestHelper::getMemoryUsage();
$memorySnapshots[] = $memory;
$elapsed = microtime(true) - $startTime;
echo " {$elapsed}s: {$memory['current_mb']}MB\n";
$nextSnapshotTime += $snapshotInterval;
}
// Maintain target rate
usleep(1000000 / $jobsPerSecond); // Convert to microseconds
}
echo " Total jobs: {$jobCount}\n";
}
private function simulateConcurrentOperations(int $concurrency, int $totalOperations): array
{
$times = [];
$startTime = microtime(true);
$operationsPerWorker = intval($totalOperations / $concurrency);
$actualOperations = 0;
// Simulate concurrent operations (simplified for single-threaded PHP)
for ($worker = 0; $worker < $concurrency; $worker++) {
for ($op = 0; $op < $operationsPerWorker; $op++) {
$job = PerformanceTestHelper::createTestJob("concurrent_job_{$worker}_{$op}");
$time = PerformanceTestHelper::measureTime(function() use ($job) {
return $this->distributionService->distributeJob($job);
});
$times[] = $time;
$actualOperations++;
}
}
$endTime = microtime(true);
return [
'times' => $times,
'total_operations' => $actualOperations,
'duration' => $endTime - $startTime
];
}
private function analyzeMemoryUsage(array $memorySnapshots): void
{
echo "\nMemory Usage Analysis:\n";
$memoryValues = array_column($memorySnapshots, 'current_mb');
$peakValues = array_column($memorySnapshots, 'peak_mb');
$memoryStats = PerformanceTestHelper::calculateStatistics($memoryValues);
$peakStats = PerformanceTestHelper::calculateStatistics($peakValues);
echo "Current Memory: " . PerformanceTestHelper::formatStatistics($memoryStats, 'MB') . "\n";
echo "Peak Memory: " . PerformanceTestHelper::formatStatistics($peakStats, 'MB') . "\n";
// Check for memory growth pattern
$memoryTrend = end($memoryValues) - $memoryValues[0];
echo "Memory trend: " . ($memoryTrend >= 0 ? '+' : '') . "{$memoryTrend}MB\n";
}
private function analyzeMemoryStability(array $memorySnapshots): void
{
echo "\nMemory Stability Analysis:\n";
$memoryValues = array_column(array_column($memorySnapshots, 'memory'), 'current_mb');
$timeValues = array_column($memorySnapshots, 'time');
// Calculate memory growth rate
if (count($memoryValues) >= 2) {
$firstMemory = $memoryValues[0];
$lastMemory = end($memoryValues);
$timeSpan = end($timeValues) - $timeValues[0];
$growthRate = ($lastMemory - $firstMemory) / $timeSpan; // MB per second
echo "Memory growth rate: " . round($growthRate * 60, 3) . " MB/minute\n";
$this->assertLessThan(
0.1, // Less than 0.1 MB/sec = 6 MB/minute
abs($growthRate),
'Memory growth rate too high - indicates potential leak'
);
}
}
private function createWorkers(int $count, int $capacity): array
{
$workers = [];
for ($i = 1; $i <= $count; $i++) {
$workers[] = PerformanceTestHelper::createTestWorker(
"resource_worker_{$i}",
$capacity
);
}
return $workers;
}
private function registerWorkers(array $workers): void
{
foreach ($workers as $worker) {
$this->workerRegistry->registerWorker($worker);
}
}
private function cleanupCompletedJobs(): void
{
$pdo = $this->database->getConnection();
$pdo->exec('DELETE FROM jobs WHERE status IN ("COMPLETED", "FAILED")');
}
private function createTestDatabase(): DatabaseManager
{
$pdo = new \PDO('sqlite::memory:');
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$pdo->exec('
CREATE TABLE workers (
id TEXT PRIMARY KEY,
queue_names TEXT NOT NULL,
capacity INTEGER NOT NULL,
status TEXT NOT NULL,
last_heartbeat TEXT NOT NULL,
metadata TEXT
)
');
$pdo->exec('
CREATE TABLE jobs (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
payload TEXT NOT NULL,
queue_name TEXT NOT NULL,
priority INTEGER NOT NULL,
status TEXT NOT NULL,
worker_id TEXT,
created_at TEXT NOT NULL,
started_at TEXT,
completed_at TEXT,
attempts INTEGER DEFAULT 0,
error_message TEXT
)
');
return new DatabaseManager($pdo);
}
private function cleanupTestData(): void
{
$pdo = $this->database->getConnection();
$pdo->exec('DELETE FROM workers');
$pdo->exec('DELETE FROM jobs');
}
}