- Added GitHub spec-kit for development workflow - Standardized file signatures to Descomplicar® format - Updated development configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
509 lines
18 KiB
PHP
509 lines
18 KiB
PHP
/**
|
|
* Descomplicar® Crescimento Digital
|
|
* https://descomplicar.pt
|
|
*/
|
|
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace DeskMoloni\Tests\Performance;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use DeskMoloni\Tests\TestHelpers;
|
|
|
|
/**
|
|
* Performance Test: Queue Processing and API Rate Limiting
|
|
*
|
|
* This test MUST FAIL initially as part of TDD methodology.
|
|
* Tests performance requirements and benchmarks.
|
|
*
|
|
* @group performance
|
|
* @group queue
|
|
*/
|
|
class QueuePerformanceTest extends TestCase
|
|
{
|
|
private array $testConfig;
|
|
private \PDO $pdo;
|
|
private \DeskMoloni\QueueProcessor $queueProcessor;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
global $testConfig;
|
|
$this->testConfig = $testConfig;
|
|
|
|
$this->pdo = new \PDO(
|
|
"mysql:host={$testConfig['database']['hostname']};dbname={$testConfig['database']['database']}",
|
|
$testConfig['database']['username'],
|
|
$testConfig['database']['password'],
|
|
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]
|
|
);
|
|
|
|
// This will fail initially until QueueProcessor is implemented
|
|
$this->queueProcessor = new \DeskMoloni\QueueProcessor($testConfig);
|
|
|
|
// Clean test data
|
|
TestHelpers::clearTestData();
|
|
}
|
|
|
|
/**
|
|
* Test queue processing performance requirements
|
|
* Requirement: Process 50 tasks in under 30 seconds
|
|
*/
|
|
public function testQueueProcessingPerformance(): void
|
|
{
|
|
$taskCount = 50;
|
|
$maxExecutionTime = 30; // seconds
|
|
|
|
// Create test tasks
|
|
$tasks = $this->createTestTasks($taskCount);
|
|
$this->insertTasksIntoQueue($tasks);
|
|
|
|
// Measure processing time
|
|
$startTime = microtime(true);
|
|
|
|
$result = $this->queueProcessor->processBatch($taskCount);
|
|
|
|
$endTime = microtime(true);
|
|
$executionTime = $endTime - $startTime;
|
|
|
|
// Performance assertions
|
|
$this->assertLessThan($maxExecutionTime, $executionTime, "Queue should process {$taskCount} tasks in under {$maxExecutionTime} seconds");
|
|
|
|
$this->assertIsArray($result);
|
|
$this->assertArrayHasKey('processed_count', $result);
|
|
$this->assertArrayHasKey('successful_count', $result);
|
|
$this->assertArrayHasKey('failed_count', $result);
|
|
$this->assertArrayHasKey('average_task_time', $result);
|
|
|
|
$this->assertEquals($taskCount, $result['processed_count']);
|
|
$this->assertGreaterThan(0, $result['successful_count']);
|
|
$this->assertLessThan(1000, $result['average_task_time'], 'Average task time should be under 1 second');
|
|
|
|
// Verify tasks were processed
|
|
$stmt = $this->pdo->query("SELECT COUNT(*) as count FROM tbl_desk_moloni_sync_queue WHERE status = 'completed'");
|
|
$completedCount = $stmt->fetch();
|
|
|
|
$this->assertGreaterThan(0, $completedCount['count'], 'Some tasks should be completed');
|
|
}
|
|
|
|
/**
|
|
* Test concurrent queue processing
|
|
*/
|
|
public function testConcurrentQueueProcessing(): void
|
|
{
|
|
$taskCount = 100;
|
|
$workerCount = 4;
|
|
|
|
// Create test tasks
|
|
$tasks = $this->createTestTasks($taskCount);
|
|
$this->insertTasksIntoQueue($tasks);
|
|
|
|
$startTime = microtime(true);
|
|
|
|
// Simulate concurrent workers
|
|
$workers = [];
|
|
for ($i = 0; $i < $workerCount; $i++) {
|
|
$workers[$i] = $this->queueProcessor->createWorker("worker_{$i}");
|
|
}
|
|
|
|
// Process tasks concurrently (simulated)
|
|
$results = [];
|
|
foreach ($workers as $workerId => $worker) {
|
|
$results[$workerId] = $worker->processBatch($taskCount / $workerCount);
|
|
}
|
|
|
|
$endTime = microtime(true);
|
|
$executionTime = $endTime - $startTime;
|
|
|
|
// Should be faster than sequential processing
|
|
$this->assertLessThan(20, $executionTime, 'Concurrent processing should be faster');
|
|
|
|
// Verify no task conflicts
|
|
$stmt = $this->pdo->query("SELECT COUNT(*) as count FROM tbl_desk_moloni_sync_queue WHERE status = 'processing'");
|
|
$processingCount = $stmt->fetch();
|
|
|
|
$this->assertEquals(0, $processingCount['count'], 'No tasks should be stuck in processing state');
|
|
|
|
// Verify all tasks processed exactly once
|
|
$stmt = $this->pdo->query("SELECT COUNT(*) as count FROM tbl_desk_moloni_sync_queue WHERE status IN ('completed', 'failed')");
|
|
$processedCount = $stmt->fetch();
|
|
|
|
$this->assertEquals($taskCount, $processedCount['count'], 'All tasks should be processed exactly once');
|
|
}
|
|
|
|
/**
|
|
* Test API rate limiting performance
|
|
* Requirement: Respect Moloni rate limits without excessive delays
|
|
*/
|
|
public function testApiRateLimitingPerformance(): void
|
|
{
|
|
$rateLimiter = new \DeskMoloni\ApiRateLimiter($this->testConfig);
|
|
|
|
$requestCount = 50;
|
|
$maxTotalTime = 60; // seconds - should not take too long due to rate limiting
|
|
|
|
$startTime = microtime(true);
|
|
$successfulRequests = 0;
|
|
$rateLimitedRequests = 0;
|
|
|
|
for ($i = 0; $i < $requestCount; $i++) {
|
|
$requestStart = microtime(true);
|
|
|
|
$allowed = $rateLimiter->allowRequest('test_endpoint');
|
|
|
|
if ($allowed) {
|
|
$successfulRequests++;
|
|
|
|
// Simulate API call
|
|
usleep(100000); // 100ms
|
|
} else {
|
|
$rateLimitedRequests++;
|
|
|
|
// Test wait time calculation
|
|
$waitTime = $rateLimiter->getWaitTime('test_endpoint');
|
|
$this->assertIsFloat($waitTime);
|
|
$this->assertGreaterThanOrEqual(0, $waitTime);
|
|
$this->assertLessThan(60, $waitTime, 'Wait time should not exceed 60 seconds');
|
|
}
|
|
|
|
$requestTime = microtime(true) - $requestStart;
|
|
$this->assertLessThan(5, $requestTime, 'Individual request processing should be under 5 seconds');
|
|
}
|
|
|
|
$endTime = microtime(true);
|
|
$totalTime = $endTime - $startTime;
|
|
|
|
$this->assertLessThan($maxTotalTime, $totalTime, "Rate limited requests should complete in under {$maxTotalTime} seconds");
|
|
$this->assertGreaterThan(0, $successfulRequests, 'Some requests should be allowed');
|
|
|
|
// Verify rate limiting data
|
|
$stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_rate_limits WHERE api_endpoint = ?");
|
|
$stmt->execute(['test_endpoint']);
|
|
$rateLimitData = $stmt->fetch();
|
|
|
|
$this->assertNotFalse($rateLimitData, 'Rate limit data should be recorded');
|
|
$this->assertGreaterThan(0, $rateLimitData['calls_made']);
|
|
$this->assertLessThanOrEqual($rateLimitData['limit_per_window'], $rateLimitData['calls_made']);
|
|
}
|
|
|
|
/**
|
|
* Test memory usage during bulk operations
|
|
*/
|
|
public function testMemoryUsageDuringBulkOperations(): void
|
|
{
|
|
$initialMemory = memory_get_usage(true);
|
|
$maxAllowedMemory = 128 * 1024 * 1024; // 128MB
|
|
|
|
// Create large batch of tasks
|
|
$largeBatchSize = 1000;
|
|
$tasks = $this->createTestTasks($largeBatchSize);
|
|
|
|
$memoryAfterCreation = memory_get_usage(true);
|
|
$creationMemoryIncrease = $memoryAfterCreation - $initialMemory;
|
|
|
|
$this->assertLessThan($maxAllowedMemory / 4, $creationMemoryIncrease, 'Task creation should not use excessive memory');
|
|
|
|
// Insert tasks
|
|
$this->insertTasksIntoQueue($tasks);
|
|
|
|
$memoryAfterInsert = memory_get_usage(true);
|
|
$insertMemoryIncrease = $memoryAfterInsert - $memoryAfterCreation;
|
|
|
|
$this->assertLessThan($maxAllowedMemory / 4, $insertMemoryIncrease, 'Task insertion should not use excessive memory');
|
|
|
|
// Process tasks in chunks to test memory management
|
|
$chunkSize = 100;
|
|
$chunksProcessed = 0;
|
|
|
|
while ($chunksProcessed * $chunkSize < $largeBatchSize) {
|
|
$chunkStartMemory = memory_get_usage(true);
|
|
|
|
$result = $this->queueProcessor->processBatch($chunkSize);
|
|
|
|
$chunkEndMemory = memory_get_usage(true);
|
|
$chunkMemoryIncrease = $chunkEndMemory - $chunkStartMemory;
|
|
|
|
$this->assertLessThan($maxAllowedMemory / 8, $chunkMemoryIncrease, "Chunk processing should not leak memory (chunk {$chunksProcessed})");
|
|
|
|
$chunksProcessed++;
|
|
|
|
// Force garbage collection
|
|
gc_collect_cycles();
|
|
}
|
|
|
|
$finalMemory = memory_get_usage(true);
|
|
$totalMemoryIncrease = $finalMemory - $initialMemory;
|
|
|
|
$this->assertLessThan($maxAllowedMemory, $totalMemoryIncrease, 'Total memory usage should stay within limits');
|
|
}
|
|
|
|
/**
|
|
* Test database query performance
|
|
*/
|
|
public function testDatabaseQueryPerformance(): void
|
|
{
|
|
// Create test data for performance testing
|
|
$testDataCount = 10000;
|
|
$this->createLargeTestDataset($testDataCount);
|
|
|
|
// Test queue selection performance
|
|
$startTime = microtime(true);
|
|
|
|
$stmt = $this->pdo->query("
|
|
SELECT * FROM tbl_desk_moloni_sync_queue
|
|
WHERE status = 'pending'
|
|
ORDER BY priority ASC, scheduled_at ASC
|
|
LIMIT 100
|
|
");
|
|
$tasks = $stmt->fetchAll();
|
|
|
|
$queryTime = microtime(true) - $startTime;
|
|
|
|
$this->assertLessThan(0.5, $queryTime, 'Queue selection query should complete in under 500ms');
|
|
$this->assertLessThanOrEqual(100, count($tasks));
|
|
|
|
// Test mapping lookup performance
|
|
$startTime = microtime(true);
|
|
|
|
$stmt = $this->pdo->query("
|
|
SELECT m.*, l.execution_time_ms
|
|
FROM tbl_desk_moloni_mapping m
|
|
LEFT JOIN tbl_desk_moloni_sync_log l ON l.perfex_id = m.perfex_id AND l.entity_type = m.entity_type
|
|
WHERE m.entity_type = 'client'
|
|
ORDER BY m.last_sync_at DESC
|
|
LIMIT 100
|
|
");
|
|
$mappings = $stmt->fetchAll();
|
|
|
|
$queryTime = microtime(true) - $startTime;
|
|
|
|
$this->assertLessThan(0.3, $queryTime, 'Mapping lookup query should complete in under 300ms');
|
|
|
|
// Test log aggregation performance
|
|
$startTime = microtime(true);
|
|
|
|
$stmt = $this->pdo->query("
|
|
SELECT
|
|
entity_type,
|
|
status,
|
|
COUNT(*) as count,
|
|
AVG(execution_time_ms) as avg_time,
|
|
MAX(created_at) as latest
|
|
FROM tbl_desk_moloni_sync_log
|
|
WHERE created_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
|
GROUP BY entity_type, status
|
|
");
|
|
$stats = $stmt->fetchAll();
|
|
|
|
$queryTime = microtime(true) - $startTime;
|
|
|
|
$this->assertLessThan(1.0, $queryTime, 'Log aggregation query should complete in under 1 second');
|
|
$this->assertIsArray($stats);
|
|
}
|
|
|
|
/**
|
|
* Test Redis cache performance
|
|
*/
|
|
public function testRedisCachePerformance(): void
|
|
{
|
|
if (!isset($GLOBALS['test_redis'])) {
|
|
$this->markTestSkipped('Redis not available for testing');
|
|
}
|
|
|
|
$redis = $GLOBALS['test_redis'];
|
|
$cacheManager = new \DeskMoloni\CacheManager($redis);
|
|
|
|
$testDataSize = 1000;
|
|
$testData = [];
|
|
|
|
// Generate test data
|
|
for ($i = 0; $i < $testDataSize; $i++) {
|
|
$testData["test_key_{$i}"] = [
|
|
'id' => $i,
|
|
'name' => "Test Item {$i}",
|
|
'data' => str_repeat('x', 100) // 100 byte payload
|
|
];
|
|
}
|
|
|
|
// Test write performance
|
|
$startTime = microtime(true);
|
|
|
|
foreach ($testData as $key => $data) {
|
|
$cacheManager->set($key, $data, 300); // 5 minute TTL
|
|
}
|
|
|
|
$writeTime = microtime(true) - $startTime;
|
|
$writeOpsPerSecond = $testDataSize / $writeTime;
|
|
|
|
$this->assertGreaterThan(500, $writeOpsPerSecond, 'Cache should handle at least 500 writes per second');
|
|
|
|
// Test read performance
|
|
$startTime = microtime(true);
|
|
|
|
foreach (array_keys($testData) as $key) {
|
|
$cached = $cacheManager->get($key);
|
|
$this->assertNotNull($cached, "Cached data should exist for key {$key}");
|
|
}
|
|
|
|
$readTime = microtime(true) - $startTime;
|
|
$readOpsPerSecond = $testDataSize / $readTime;
|
|
|
|
$this->assertGreaterThan(1000, $readOpsPerSecond, 'Cache should handle at least 1000 reads per second');
|
|
|
|
// Test batch operations
|
|
$batchKeys = array_slice(array_keys($testData), 0, 100);
|
|
|
|
$startTime = microtime(true);
|
|
$batchResult = $cacheManager->multiGet($batchKeys);
|
|
$batchTime = microtime(true) - $startTime;
|
|
|
|
$this->assertLessThan(0.1, $batchTime, 'Batch get should complete in under 100ms');
|
|
$this->assertCount(100, $batchResult);
|
|
}
|
|
|
|
/**
|
|
* Test sync operation performance benchmarks
|
|
*/
|
|
public function testSyncOperationPerformanceBenchmarks(): void
|
|
{
|
|
$syncService = new \DeskMoloni\ClientSyncService();
|
|
|
|
// Benchmark single client sync
|
|
$testClient = TestHelpers::createTestClient([
|
|
'userid' => 99999,
|
|
'company' => 'Performance Test Company',
|
|
'vat' => '999999999'
|
|
]);
|
|
|
|
$syncTimes = [];
|
|
$iterations = 10;
|
|
|
|
for ($i = 0; $i < $iterations; $i++) {
|
|
$startTime = microtime(true);
|
|
|
|
$result = $syncService->syncPerfexToMoloni($testClient);
|
|
|
|
$syncTime = microtime(true) - $startTime;
|
|
$syncTimes[] = $syncTime;
|
|
|
|
// Clean up for next iteration
|
|
if ($result['success'] ?? false) {
|
|
$this->pdo->exec("DELETE FROM tbl_desk_moloni_mapping WHERE perfex_id = 99999");
|
|
$this->pdo->exec("DELETE FROM tbl_desk_moloni_sync_log WHERE perfex_id = 99999");
|
|
}
|
|
}
|
|
|
|
$avgSyncTime = array_sum($syncTimes) / count($syncTimes);
|
|
$maxSyncTime = max($syncTimes);
|
|
$minSyncTime = min($syncTimes);
|
|
|
|
$this->assertLessThan(5.0, $avgSyncTime, 'Average sync time should be under 5 seconds');
|
|
$this->assertLessThan(10.0, $maxSyncTime, 'Maximum sync time should be under 10 seconds');
|
|
$this->assertGreaterThan(0.1, $minSyncTime, 'Minimum sync time should be realistic (over 100ms)');
|
|
|
|
// Calculate performance metrics
|
|
$standardDeviation = sqrt(array_sum(array_map(function($x) use ($avgSyncTime) {
|
|
return pow($x - $avgSyncTime, 2);
|
|
}, $syncTimes)) / count($syncTimes));
|
|
|
|
$this->assertLessThan($avgSyncTime * 0.5, $standardDeviation, 'Sync times should be consistent (low standard deviation)');
|
|
}
|
|
|
|
private function createTestTasks(int $count): array
|
|
{
|
|
$tasks = [];
|
|
|
|
for ($i = 1; $i <= $count; $i++) {
|
|
$tasks[] = [
|
|
'task_type' => 'sync_client',
|
|
'entity_type' => 'client',
|
|
'entity_id' => 1000 + $i,
|
|
'priority' => rand(1, 9),
|
|
'payload' => json_encode([
|
|
'client_data' => [
|
|
'id' => 1000 + $i,
|
|
'name' => "Test Client {$i}",
|
|
'email' => "test{$i}@example.com"
|
|
]
|
|
]),
|
|
'scheduled_at' => date('Y-m-d H:i:s')
|
|
];
|
|
}
|
|
|
|
return $tasks;
|
|
}
|
|
|
|
private function insertTasksIntoQueue(array $tasks): void
|
|
{
|
|
$stmt = $this->pdo->prepare("
|
|
INSERT INTO tbl_desk_moloni_sync_queue
|
|
(task_type, entity_type, entity_id, priority, payload, scheduled_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
");
|
|
|
|
foreach ($tasks as $task) {
|
|
$stmt->execute([
|
|
$task['task_type'],
|
|
$task['entity_type'],
|
|
$task['entity_id'],
|
|
$task['priority'],
|
|
$task['payload'],
|
|
$task['scheduled_at']
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function createLargeTestDataset(int $count): void
|
|
{
|
|
// Create test mappings
|
|
$stmt = $this->pdo->prepare("
|
|
INSERT INTO tbl_desk_moloni_mapping
|
|
(entity_type, perfex_id, moloni_id, sync_direction, last_sync_at)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
");
|
|
|
|
for ($i = 1; $i <= $count / 4; $i++) {
|
|
$stmt->execute([
|
|
'client',
|
|
10000 + $i,
|
|
20000 + $i,
|
|
'bidirectional',
|
|
date('Y-m-d H:i:s', strtotime("-{$i} minutes"))
|
|
]);
|
|
}
|
|
|
|
// Create test logs
|
|
$stmt = $this->pdo->prepare("
|
|
INSERT INTO tbl_desk_moloni_sync_log
|
|
(operation_type, entity_type, perfex_id, moloni_id, direction, status, execution_time_ms, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
");
|
|
|
|
for ($i = 1; $i <= $count; $i++) {
|
|
$stmt->execute([
|
|
'create',
|
|
'client',
|
|
10000 + ($i % ($count / 4)),
|
|
20000 + ($i % ($count / 4)),
|
|
'perfex_to_moloni',
|
|
rand(0, 10) < 9 ? 'success' : 'error', // 90% success rate
|
|
rand(100, 5000), // Random execution time
|
|
date('Y-m-d H:i:s', strtotime("-{$i} seconds"))
|
|
]);
|
|
}
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
// Clean up performance test data
|
|
$this->pdo->exec("DELETE FROM tbl_desk_moloni_sync_queue WHERE entity_id >= 1000");
|
|
$this->pdo->exec("DELETE FROM tbl_desk_moloni_mapping WHERE perfex_id >= 10000");
|
|
$this->pdo->exec("DELETE FROM tbl_desk_moloni_sync_log WHERE perfex_id >= 10000");
|
|
$this->pdo->exec("DELETE FROM tbl_desk_moloni_rate_limits WHERE api_endpoint = 'test_endpoint'");
|
|
|
|
if (isset($GLOBALS['test_redis'])) {
|
|
$GLOBALS['test_redis']->flushdb();
|
|
}
|
|
}
|
|
} |