Files
michaelschiemer/tests/Framework/Queue/Performance/DatabasePerformanceTest.php
Michael Schiemer 5050c7d73a 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
2025-10-05 11:05:04 +02:00

622 lines
23 KiB
PHP

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