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")'); } }