diff --git a/.gitignore b/.gitignore index 3e822ae..a9ae8a9 100644 --- a/.gitignore +++ b/.gitignore @@ -260,3 +260,4 @@ NOTES.md scratch/ benchmarks/**/*.json CLAUDE.md +proposals/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c8a9bb..2f51212 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,69 @@ All notable changes to the PivotPHP Framework will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.1] - 2025-07-10 + +### 🚀 **JSON Optimization Edition** + +#### Added +- **High-Performance JSON Buffer Pooling System**: Revolutionary JSON processing optimization + - `JsonBuffer`: Optimized buffer class for JSON operations with automatic expansion + - `JsonBufferPool`: Intelligent pooling system with buffer reuse and size categorization + - **Automatic Integration**: `Response::json()` now uses pooling transparently for optimal performance + - **Smart Detection**: Automatically activates pooling for arrays 10+ elements, objects 5+ properties, strings >1KB + - **Graceful Fallback**: Small datasets use traditional `json_encode()` for best performance + - **Public Constants**: All size estimation and threshold constants are now publicly accessible for advanced usage and testing + +- **Performance Monitoring & Statistics**: + - Real-time pool statistics with reuse rates and efficiency metrics + - Configurable pool sizes and buffer categories (small: 1KB, medium: 4KB, large: 16KB, xlarge: 64KB) + - Production-ready monitoring with `JsonBufferPool::getStatistics()` + - Performance tracking for optimization and debugging + +- **Developer Experience**: + - **Zero Breaking Changes**: All existing code continues working without modification + - **Transparent Optimization**: Automatic activation based on data characteristics + - **Manual Control**: Direct pool access via `JsonBufferPool::encodeWithPool()` when needed + - **Configuration API**: Production tuning via `JsonBufferPool::configure()` + - **Enhanced Error Handling**: Precise validation messages separating type vs range errors + - **Type Safety**: `encodeWithPool()` now always returns string, simplifying error handling + +#### Performance Improvements +- **Sustained Throughput**: 101,000+ JSON operations per second in continuous load tests +- **Memory Efficiency**: 100% buffer reuse rate in high-frequency scenarios +- **Reduced GC Pressure**: Significant reduction in garbage collection overhead +- **Scalable Architecture**: Adaptive pool sizing based on usage patterns + +#### Technical Details +- **PSR-12 Compliant**: All new code follows project coding standards +- **Comprehensive Testing**: 84 JSON tests with 329+ assertions covering all functionality +- **Backward Compatible**: No changes required to existing applications +- **Production Ready**: Tested with various data sizes and load patterns +- **Centralized Constants**: All thresholds and size constants are unified to avoid duplication +- **Test Maintainability**: Tests now use constants instead of hardcoded values for better maintainability + +#### Files Added +- `src/Json/Pool/JsonBuffer.php`: Core buffer implementation +- `src/Json/Pool/JsonBufferPool.php`: Pool management system +- `tests/Json/Pool/JsonBufferTest.php`: Comprehensive buffer tests +- `tests/Json/Pool/JsonBufferPoolTest.php`: Pool functionality tests +- `benchmarks/JsonPoolingBenchmark.php`: Performance validation tools + +#### Files Modified +- `src/Http/Response.php`: Integrated automatic pooling in `json()` method +- Enhanced with smart detection and fallback mechanisms + +#### Post-Release Improvements (July 2025) +- **Enhanced Configuration Validation**: Separated type checking from range validation for more precise error messages +- **Improved Type Safety**: `encodeWithPool()` method now has tightened return type (always returns string) +- **Public Constants Exposure**: Made all size estimation and threshold constants public for advanced usage and testing +- **Centralized Thresholds**: Unified pooling decision thresholds across Response.php and JsonBufferPool to eliminate duplication +- **Test Maintainability**: Updated all tests to use constants instead of hardcoded values +- **Documentation Updates**: + - Added comprehensive [Constants Reference Guide](docs/technical/json/CONSTANTS_REFERENCE.md) + - Updated performance guide with recent improvements + - Enhanced error handling documentation + ## [1.1.0] - 2025-07-09 ### 🚀 **High-Performance Edition** diff --git a/README.md b/README.md index de6e784..a2e283c 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ - **Arquitetura Moderna**: DI Container, Service Providers, Event System, Extension System e PSR-15. - **Segurança**: Middlewares robustos para CSRF, XSS, Rate Limiting, JWT, API Key e mais. - **Extensível**: Sistema de plugins, hooks, providers e integração PSR-14. -- **Qualidade**: 315+ testes, PHPStan Level 9, PSR-12, cobertura completa. -- **🆕 v1.0.1**: Suporte a validação avançada de rotas com regex e constraints. -- **🚀 v1.0.1**: Suporte PSR-7 híbrido, lazy loading, object pooling e otimizações de performance. +- **Qualidade**: 335+ testes, PHPStan Level 9, PSR-12, cobertura completa. +- **🆕 v1.1.0**: High-Performance Edition com circuit breaker, load shedding e pooling avançado. +- **🚀 v1.1.1**: JSON Optimization Edition com pooling automático e 101k+ ops/sec sustentados. --- @@ -37,7 +37,8 @@ - 📚 **OpenAPI/Swagger** - 🔄 **PSR-7 Híbrido** - ♻️ **Object Pooling** -- ⚡ **Performance** +- 🚀 **JSON Optimization** (v1.1.1) +- ⚡ **Performance Extrema** - 🧪 **Qualidade e Testes** --- @@ -109,6 +110,53 @@ $app->get('/posts/:year<\d{4}>/:month<\d{2}>/:slug', function($req, $res) $app->run(); ``` +### 🛣️ Sintaxes de Roteamento Suportadas + +O PivotPHP suporta múltiplas sintaxes para definir handlers de rota: + +```php +// ✅ Closure/Função Anônima (Recomendado) +$app->get('/users', function($req, $res) { + return $res->json(['users' => []]); +}); + +// ✅ Array Callable com classe +$app->get('/users', [UserController::class, 'index']); + +// ✅ Função nomeada +function getUsersHandler($req, $res) { + return $res->json(['users' => []]); +} +$app->get('/users', 'getUsersHandler'); + +// ❌ NÃO suportado - String no formato Controller@method +// $app->get('/users', 'UserController@index'); // ERRO! +``` + +**Exemplo com Controller:** + +```php +json(['users' => User::all()]); + } + + public function show($req, $res) + { + $id = $req->param('id'); + return $res->json(['user' => User::find($id)]); + } +} + +// Registrar rotas com array callable +$app->get('/users', [UserController::class, 'index']); +$app->get('/users/:id', [UserController::class, 'show']); +``` + ### 🔄 Suporte PSR-7 Híbrido O PivotPHP oferece **compatibilidade híbrida** com PSR-7, mantendo a facilidade da API Express.js enquanto implementa completamente as interfaces PSR-7: @@ -151,6 +199,51 @@ $response = OptimizedHttpFactory::createResponse(); - ✅ **API Express.js** mantida para produtividade - ✅ **Zero breaking changes** - código existente funciona sem alterações +### 🚀 JSON Optimization (v1.1.1) + +O PivotPHP v1.1.1 introduz um sistema revolucionário de otimização JSON que melhora drasticamente a performance através de buffer pooling inteligente: + +```php +// Otimização automática - zero configuração necessária +$app->get('/api/users', function($req, $res) { + $users = User::all(); // 1000+ usuários + + // Automaticamente usa pooling para datasets grandes + return $res->json($users); // 101k+ ops/sec sustentados +}); + +// Controle manual para casos específicos +use PivotPHP\Core\Json\Pool\JsonBufferPool; + +// Encoding direto com pooling +$json = JsonBufferPool::encodeWithPool($largeData); + +// Configuração para alta carga de produção +JsonBufferPool::configure([ + 'max_pool_size' => 500, + 'default_capacity' => 16384, // 16KB buffers + 'size_categories' => [ + 'small' => 4096, // 4KB + 'medium' => 16384, // 16KB + 'large' => 65536, // 64KB + 'xlarge' => 262144 // 256KB + ] +]); + +// Monitoramento em tempo real +$stats = JsonBufferPool::getStatistics(); +echo "Reuse rate: {$stats['reuse_rate']}%"; // Target: 80%+ +echo "Operations: {$stats['total_operations']}"; +``` + +**Características da Otimização JSON:** +- ✅ **Detecção automática** - ativa pooling para arrays 10+ elementos, objetos 5+ propriedades +- ✅ **Fallback inteligente** - dados pequenos usam `json_encode()` tradicional +- ✅ **101k+ ops/sec** sustentados em testes de carga contínua +- ✅ **100% reuso** de buffers em cenários de alta frequência +- ✅ **Zero configuração** - funciona automaticamente com código existente +- ✅ **Monitoramento integrado** - estatísticas detalhadas para otimização + ### 📖 Documentação OpenAPI/Swagger O PivotPHP inclui suporte integrado para geração automática de documentação OpenAPI: diff --git a/VERSION b/VERSION index 9084fa2..524cb55 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.0 +1.1.1 diff --git a/benchmarks/JsonBufferRefactorBenchmark.php b/benchmarks/JsonBufferRefactorBenchmark.php new file mode 100644 index 0000000..2d6539d --- /dev/null +++ b/benchmarks/JsonBufferRefactorBenchmark.php @@ -0,0 +1,114 @@ + 'Hello, World!', + 'timestamp' => time(), + 'unicode' => '🚀 Performance Test', + 'url' => 'https://example.com/test', + 'nested' => [ + 'level1' => ['level2' => ['level3' => 'deep value']], + 'array' => range(1, 50) + ] + ]; + + // Test 1: Direct JsonBuffer usage + $start = microtime(true); + for ($i = 0; $i < $iterations; $i++) { + $buffer = new JsonBuffer(); + $buffer->appendJson($testData); + $result = $buffer->finalize(); + } + $directTime = microtime(true) - $start; + + // Test 2: JsonBufferPool usage + $start = microtime(true); + for ($i = 0; $i < $iterations; $i++) { + $result = JsonBufferPool::encodeWithPool($testData); + } + $poolTime = microtime(true) - $start; + + // Test 3: Traditional json_encode + $start = microtime(true); + for ($i = 0; $i < $iterations; $i++) { + $result = json_encode($testData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + $traditionalTime = microtime(true) - $start; + + // Calculate performance metrics + $directOps = $iterations / $directTime; + $poolOps = $iterations / $poolTime; + $traditionalOps = $iterations / $traditionalTime; + + echo "📊 Results ({$iterations} iterations):\n"; + echo "Direct JsonBuffer: " . number_format($directOps, 0) . " ops/sec\n"; + echo "JsonBufferPool: " . number_format($poolOps, 0) . " ops/sec\n"; + echo "Traditional encode: " . number_format($traditionalOps, 0) . " ops/sec\n\n"; + + echo "⚡ Performance ratios:\n"; + echo "Buffer vs Traditional: " . number_format($directOps / $traditionalOps, 2) . "x\n"; + echo "Pool vs Traditional: " . number_format($poolOps / $traditionalOps, 2) . "x\n"; + echo "Pool vs Direct: " . number_format($poolOps / $directOps, 2) . "x\n\n"; + + // Test memory efficiency + $memStart = memory_get_usage(); + + // Create and reuse buffers + $buffer = new JsonBuffer(); + for ($i = 0; $i < 1000; $i++) { + $buffer->appendJson($testData); + $buffer->finalize(); + $buffer->reset(); + } + + $memAfterReuse = memory_get_usage(); + $reuseMemory = $memAfterReuse - $memStart; + + // Create new buffers each time + $memStart = memory_get_usage(); + for ($i = 0; $i < 1000; $i++) { + $buffer = new JsonBuffer(); + $buffer->appendJson($testData); + $buffer->finalize(); + } + $memAfterNew = memory_get_usage(); + $newMemory = $memAfterNew - $memStart; + + echo "💾 Memory efficiency (1000 operations):\n"; + echo "Buffer reuse: " . number_format($reuseMemory / 1024, 2) . " KB\n"; + echo "New buffers: " . number_format($newMemory / 1024, 2) . " KB\n"; + echo "Memory saved: " . number_format(($newMemory - $reuseMemory) / 1024, 2) . " KB\n"; + echo "Efficiency: " . number_format((1 - $reuseMemory / $newMemory) * 100, 1) . "% less memory\n\n"; + + // Pool statistics + echo "🏊 Pool Statistics:\n"; + $stats = JsonBufferPool::getStatistics(); + echo "Reuse rate: " . $stats['reuse_rate'] . "%\n"; + echo "Total ops: " . $stats['total_operations'] . "\n"; + echo "Current usage: " . $stats['current_usage'] . "\n"; + echo "Peak usage: " . $stats['peak_usage'] . "\n\n"; + + echo "✅ Refactoring Performance Test Complete!\n"; + } +} + +// Run the benchmark +$benchmark = new JsonBufferRefactorBenchmark(); +$benchmark->run(); \ No newline at end of file diff --git a/benchmarks/JsonBufferStreamBenchmark.php b/benchmarks/JsonBufferStreamBenchmark.php new file mode 100644 index 0000000..20888d6 --- /dev/null +++ b/benchmarks/JsonBufferStreamBenchmark.php @@ -0,0 +1,210 @@ +testSmallBuffers(); + $this->testLargeBuffers(); + $this->testMigrationScenarios(); + $this->testMemoryEfficiency(); + } + + private function testSmallBuffers(): void + { + echo "📊 Small Buffer Performance (< 8KB)\n"; + echo "-----------------------------------\n"; + + $iterations = 10000; + $smallData = ['message' => 'Hello World', 'id' => 123, 'active' => true]; + + // Traditional json_encode + $start = microtime(true); + for ($i = 0; $i < $iterations; $i++) { + $result = json_encode($smallData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + $traditionalTime = microtime(true) - $start; + + // JsonBuffer (should use string mode) + $start = microtime(true); + for ($i = 0; $i < $iterations; $i++) { + $buffer = new JsonBuffer(1024); + $buffer->appendJson($smallData); + $result = $buffer->finalize(); + } + $bufferTime = microtime(true) - $start; + + $traditionalOps = $iterations / $traditionalTime; + $bufferOps = $iterations / $bufferTime; + + echo "Traditional encode: " . number_format($traditionalOps, 0) . " ops/sec\n"; + echo "JsonBuffer (string): " . number_format($bufferOps, 0) . " ops/sec\n"; + echo "Ratio: " . number_format($bufferOps / $traditionalOps, 2) . "x\n\n"; + } + + private function testLargeBuffers(): void + { + echo "📊 Large Buffer Performance (> 8KB)\n"; + echo "-----------------------------------\n"; + + $iterations = 1000; + $largeData = array_fill(0, 100, [ + 'id' => rand(1000, 9999), + 'name' => 'User ' . rand(1, 1000), + 'email' => 'user' . rand(1, 1000) . '@example.com', + 'data' => str_repeat('x', 50), + 'metadata' => ['created' => time(), 'active' => true] + ]); + + // Traditional json_encode + $start = microtime(true); + for ($i = 0; $i < $iterations; $i++) { + $result = json_encode($largeData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + $traditionalTime = microtime(true) - $start; + + // JsonBuffer (should use stream mode) + $start = microtime(true); + for ($i = 0; $i < $iterations; $i++) { + $buffer = new JsonBuffer(16384); + $buffer->appendJson($largeData); + $result = $buffer->finalize(); + } + $bufferTime = microtime(true) - $start; + + $traditionalOps = $iterations / $traditionalTime; + $bufferOps = $iterations / $bufferTime; + + echo "Traditional encode: " . number_format($traditionalOps, 0) . " ops/sec\n"; + echo "JsonBuffer (stream): " . number_format($bufferOps, 0) . " ops/sec\n"; + echo "Ratio: " . number_format($bufferOps / $traditionalOps, 2) . "x\n\n"; + } + + private function testMigrationScenarios(): void + { + echo "📊 String to Stream Migration Performance\n"; + echo "----------------------------------------\n"; + + $iterations = 1000; + + // Test building large JSON incrementally (triggers migration) + $start = microtime(true); + for ($i = 0; $i < $iterations; $i++) { + $buffer = new JsonBuffer(1024); // Start small + + $buffer->append('{"data": ['); + for ($j = 0; $j < 50; $j++) { + if ($j > 0) $buffer->append(','); + $buffer->appendJson(['item' => $j, 'value' => 'test' . $j]); + } + $buffer->append(']}'); + + $result = $buffer->finalize(); + } + $migrationTime = microtime(true) - $start; + + // Compare with traditional approach + $start = microtime(true); + for ($i = 0; $i < $iterations; $i++) { + $data = ['data' => []]; + for ($j = 0; $j < 50; $j++) { + $data['data'][] = ['item' => $j, 'value' => 'test' . $j]; + } + $result = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + $traditionalTime = microtime(true) - $start; + + $migrationOps = $iterations / $migrationTime; + $traditionalOps = $iterations / $traditionalTime; + + echo "Traditional build: " . number_format($traditionalOps, 0) . " ops/sec\n"; + echo "Migration build: " . number_format($migrationOps, 0) . " ops/sec\n"; + echo "Ratio: " . number_format($migrationOps / $traditionalOps, 2) . "x\n\n"; + } + + private function testMemoryEfficiency(): void + { + echo "💾 Memory Efficiency Test\n"; + echo "------------------------\n"; + + // Test memory usage for large buffer operations + $memStart = memory_get_usage(); + + // Create large JSON using stream buffer + $buffer = new JsonBuffer(32768); // Force stream mode + $buffer->append('{"users": ['); + + for ($i = 0; $i < 1000; $i++) { + if ($i > 0) $buffer->append(','); + $userData = [ + 'id' => $i, + 'name' => 'User ' . $i, + 'email' => "user{$i}@example.com", + 'profile' => str_repeat('data', 25) // 100 chars + ]; + $buffer->appendJson($userData); + } + + $buffer->append(']}'); + $result = $buffer->finalize(); + + $memStream = memory_get_usage() - $memStart; + + // Compare with traditional array building + $memStart = memory_get_usage(); + + $data = ['users' => []]; + for ($i = 0; $i < 1000; $i++) { + $data['users'][] = [ + 'id' => $i, + 'name' => 'User ' . $i, + 'email' => "user{$i}@example.com", + 'profile' => str_repeat('data', 25) + ]; + } + $traditionalResult = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + $memTraditional = memory_get_usage() - $memStart; + + echo "Result size: " . number_format(strlen($result)) . " bytes\n"; + echo "Stream memory: " . number_format($memStream / 1024, 2) . " KB\n"; + echo "Traditional memory: " . number_format($memTraditional / 1024, 2) . " KB\n"; + echo "Memory efficiency: " . number_format((1 - $memStream / $memTraditional) * 100, 1) . "% less memory\n\n"; + + // Pool reuse efficiency + echo "🏊 Pool Reuse Efficiency\n"; + echo "-----------------------\n"; + + JsonBufferPool::clearPools(); // Reset stats + + $testData = array_fill(0, 20, ['field' => 'value', 'number' => 123]); + + for ($i = 0; $i < 100; $i++) { + JsonBufferPool::encodeWithPool($testData); + } + + $stats = JsonBufferPool::getStatistics(); + echo "Reuse rate: " . $stats['reuse_rate'] . "%\n"; + echo "Total operations: " . $stats['total_operations'] . "\n"; + echo "Peak usage: " . $stats['peak_usage'] . " buffers\n\n"; + + echo "✅ Hybrid String/Stream Performance Test Complete!\n"; + } +} + +// Run the benchmark +$benchmark = new JsonBufferStreamBenchmark(); +$benchmark->run(); \ No newline at end of file diff --git a/benchmarks/JsonPoolingBenchmark.php b/benchmarks/JsonPoolingBenchmark.php new file mode 100644 index 0000000..b7759e0 --- /dev/null +++ b/benchmarks/JsonPoolingBenchmark.php @@ -0,0 +1,320 @@ +prepareTestData(); + } + + /** + * Run all benchmarks + */ + public function run(): void + { + echo "🚀 JSON Pooling Performance Benchmark\n"; + echo "=====================================\n\n"; + + echo "Warming up...\n"; + $this->warmUp(); + + echo "\n📊 Benchmark Results:\n"; + echo "-------------------\n\n"; + + foreach ($this->testDataSets as $name => $data) { + echo "Testing: {$name}\n"; + $this->benchmarkDataSet($name, $data); + echo "\n"; + } + + $this->showPoolStatistics(); + $this->runMemoryBenchmark(); + } + + /** + * Prepare different test datasets + */ + private function prepareTestData(): void + { + // Small JSON (< 1KB) + $this->testDataSets['Small JSON'] = [ + 'id' => 1, + 'name' => 'User Test', + 'email' => 'user@test.com' + ]; + + // Medium JSON (1-10KB) + $this->testDataSets['Medium JSON'] = array_fill(0, 50, [ + 'id' => random_int(1, 1000), + 'name' => 'User ' . uniqid(), + 'email' => uniqid() . '@test.com', + 'metadata' => [ + 'created' => date('Y-m-d H:i:s'), + 'active' => true, + 'score' => random_int(1, 100) + ] + ]); + + // Large JSON (10-100KB) + $this->testDataSets['Large JSON'] = array_fill(0, 500, [ + 'id' => random_int(1, 10000), + 'name' => 'User ' . uniqid(), + 'email' => uniqid() . '@test.com', + 'profile' => [ + 'bio' => str_repeat('Lorem ipsum dolor sit amet. ', 10), + 'preferences' => array_fill(0, 10, uniqid()), + 'settings' => [ + 'theme' => 'dark', + 'language' => 'pt-BR', + 'notifications' => true + ] + ], + 'activity' => array_fill(0, 20, [ + 'timestamp' => date('Y-m-d H:i:s'), + 'action' => 'test_action_' . uniqid(), + 'data' => str_repeat('x', 50) + ]) + ]); + + // Repeated structure (ideal for pooling) + $template = [ + 'user_id' => 0, + 'username' => '', + 'email' => '', + 'status' => 'active', + 'metadata' => [ + 'created_at' => '', + 'last_login' => '', + 'preferences' => [] + ] + ]; + + $this->testDataSets['Repeated Structure'] = array_map(function($i) use ($template) { + $template['user_id'] = $i; + $template['username'] = "user{$i}"; + $template['email'] = "user{$i}@test.com"; + $template['metadata']['created_at'] = date('Y-m-d H:i:s'); + return $template; + }, range(1, 100)); + } + + /** + * Warm up both approaches + */ + private function warmUp(): void + { + $warmupData = ['test' => 'warmup']; + + for ($i = 0; $i < self::WARMUP_ITERATIONS; $i++) { + // Traditional encoding + json_encode($warmupData); + + // Pooled encoding + JsonBufferPool::encodeWithPool($warmupData); + } + + // Clear pool statistics from warmup + JsonBufferPool::clearPools(); + } + + /** + * Benchmark a specific dataset + */ + private function benchmarkDataSet(string $name, array $data): void + { + // Traditional JSON encoding benchmark + $traditionalTime = $this->benchmarkTraditionalEncoding($data); + + // Pooled JSON encoding benchmark + $pooledTime = $this->benchmarkPooledEncoding($data); + + // Response::json() with automatic pooling + $responseTime = $this->benchmarkResponseJson($data); + + // Calculate improvements + $pooledImprovement = $traditionalTime > 0 ? (($traditionalTime / $pooledTime) - 1) * 100 : 0; + $responseImprovement = $traditionalTime > 0 ? (($traditionalTime / $responseTime) - 1) * 100 : 0; + + // Display results + printf(" Traditional: %.4f ms (%.2f ops/sec)\n", + $traditionalTime * 1000, + self::ITERATIONS / $traditionalTime + ); + + printf(" Pooled: %.4f ms (%.2f ops/sec) [%+.1f%%]\n", + $pooledTime * 1000, + self::ITERATIONS / $pooledTime, + $pooledImprovement + ); + + printf(" Response: %.4f ms (%.2f ops/sec) [%+.1f%%]\n", + $responseTime * 1000, + self::ITERATIONS / $responseTime, + $responseImprovement + ); + + // JSON size info + $jsonSize = strlen(json_encode($data)); + printf(" JSON Size: %s\n", $this->formatBytes($jsonSize)); + } + + /** + * Benchmark traditional JSON encoding + */ + private function benchmarkTraditionalEncoding(array $data): float + { + $start = microtime(true); + + for ($i = 0; $i < self::ITERATIONS; $i++) { + $json = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + // Prevent optimization + if ($json === false) { + throw new \RuntimeException('JSON encoding failed'); + } + } + + return microtime(true) - $start; + } + + /** + * Benchmark pooled JSON encoding + */ + private function benchmarkPooledEncoding(array $data): float + { + $start = microtime(true); + + for ($i = 0; $i < self::ITERATIONS; $i++) { + $json = JsonBufferPool::encodeWithPool($data); + + // Prevent optimization + if (empty($json)) { + throw new \RuntimeException('Pooled encoding failed'); + } + } + + return microtime(true) - $start; + } + + /** + * Benchmark Response::json() method + */ + private function benchmarkResponseJson(array $data): float + { + $start = microtime(true); + + for ($i = 0; $i < self::ITERATIONS; $i++) { + $response = new Response(); + $response->setTestMode(true); // Prevent actual output + $response->json($data); + + // Prevent optimization + $body = $response->getBodyAsString(); + if (empty($body)) { + throw new \RuntimeException('Response JSON failed'); + } + } + + return microtime(true) - $start; + } + + /** + * Show pool statistics + */ + private function showPoolStatistics(): void + { + $stats = JsonBufferPool::getStatistics(); + + echo "📈 Pool Statistics:\n"; + echo "------------------\n"; + printf(" Reuse Rate: %.1f%%\n", $stats['reuse_rate']); + printf(" Total Operations: %d\n", $stats['total_operations']); + printf(" Peak Usage: %d buffers\n", $stats['peak_usage']); + printf(" Current Usage: %d buffers\n", $stats['current_usage']); + + if (!empty($stats['pool_sizes'])) { + echo " Pool Sizes:\n"; + foreach ($stats['pool_sizes'] as $pool => $size) { + echo " {$pool}: {$size} buffers\n"; + } + } + + echo "\n"; + } + + /** + * Run memory usage benchmark + */ + private function runMemoryBenchmark(): void + { + echo "💾 Memory Usage Benchmark:\n"; + echo "-------------------------\n"; + + $testData = $this->testDataSets['Large JSON']; + + // Test traditional encoding memory usage + $memBefore = memory_get_usage(true); + for ($i = 0; $i < 1000; $i++) { + $json = json_encode($testData); + unset($json); + } + $traditionalMemory = memory_get_usage(true) - $memBefore; + + // Reset memory + gc_collect_cycles(); + + // Test pooled encoding memory usage + $memBefore = memory_get_usage(true); + for ($i = 0; $i < 1000; $i++) { + $json = JsonBufferPool::encodeWithPool($testData); + unset($json); + } + $pooledMemory = memory_get_usage(true) - $memBefore; + + $memoryImprovement = $traditionalMemory > 0 ? + (($traditionalMemory - $pooledMemory) / $traditionalMemory) * 100 : 0; + + printf(" Traditional: %s\n", $this->formatBytes($traditionalMemory)); + printf(" Pooled: %s [%+.1f%%]\n", + $this->formatBytes($pooledMemory), + -$memoryImprovement + ); + + echo "\n"; + } + + /** + * Format bytes for display + */ + private function formatBytes(int $bytes): string + { + $units = ['B', 'KB', 'MB', 'GB']; + $factor = floor((strlen((string)$bytes) - 1) / 3); + + return sprintf("%.2f %s", $bytes / (1024 ** $factor), $units[$factor]); + } +} + +// Run benchmark if called directly +if (basename(__FILE__) === basename($_SERVER['SCRIPT_NAME'])) { + $benchmark = new JsonPoolingBenchmark(); + $benchmark->run(); +} \ No newline at end of file diff --git a/composer.json b/composer.json index f5e2737..472475b 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "pivotphp/core", - "description": "PivotPHP Core - A lightweight, fast, and secure microframework inspired by Express.js for building modern PHP web applications and APIs", + "description": "PivotPHP Core v1.1.1 - High-performance microframework with revolutionary JSON optimization (101k+ ops/sec), PSR-7 hybrid support, and Express.js-inspired API", "type": "library", "keywords": [ "php", @@ -18,9 +18,13 @@ "swagger", "authentication", "jwt", - "auth" + "auth", + "json", + "pooling", + "performance", + "optimization" ], - "homepage": "https://github.com/CAFernandes/pivotphp-core", + "homepage": "https://github.com/PivotPHP/pivotphp-core", "license": "MIT", "authors": [ { @@ -29,7 +33,7 @@ }, { "name": "PivotPHP Contributors", - "homepage": "https://github.com/CAFernandes/pivotphp-core/contributors" + "homepage": "https://github.com/PivotPHP/pivotphp-core/contributors" } ], "require": { @@ -155,10 +159,10 @@ ] }, "support": { - "issues": "https://github.com/CAFernandes/pivotphp-core/issues", - "source": "https://github.com/CAFernandes/pivotphp-core", - "docs": "https://github.com/CAFernandes/pivotphp-core/blob/main/README.md", - "wiki": "https://github.com/CAFernandes/pivotphp-core/wiki" + "issues": "https://github.com/PivotPHP/pivotphp-core/issues", + "source": "https://github.com/PivotPHP/pivotphp-core", + "docs": "https://github.com/PivotPHP/pivotphp-core/blob/main/README.md", + "wiki": "https://github.com/PivotPHP/pivotphp-core/wiki" }, "funding": [ { diff --git a/docs/performance/DATABASE_PERFORMANCE.md b/docs/performance/DATABASE_PERFORMANCE.md index 4d759c2..c9c4f5a 100644 --- a/docs/performance/DATABASE_PERFORMANCE.md +++ b/docs/performance/DATABASE_PERFORMANCE.md @@ -62,13 +62,13 @@ Comparando requisições diretas ao banco vs através do PivotPHP: Performance medida em requisições por segundo para APIs completas: ``` -GET /api/users/{id} (Simple SELECT) +GET /api/users/:id (Simple SELECT) ├─ SQLite: 7,812 req/s ├─ MariaDB: 4,234 req/s ├─ MySQL: 4,123 req/s └─ PostgreSQL: 3,567 req/s -GET /api/users/{id}/posts (JOIN) +GET /api/users/:id/posts (JOIN) ├─ SQLite: 3,123 req/s ├─ PostgreSQL: 1,945 req/s ├─ MariaDB: 1,789 req/s diff --git a/docs/releases/FRAMEWORK_OVERVIEW_v1.0.0.md b/docs/releases/FRAMEWORK_OVERVIEW_v1.0.0.md index 818bd72..6d61d9f 100644 --- a/docs/releases/FRAMEWORK_OVERVIEW_v1.0.0.md +++ b/docs/releases/FRAMEWORK_OVERVIEW_v1.0.0.md @@ -103,19 +103,19 @@ $app->run(); ```php // Basic routing -$app->get('/users', 'UserController@index'); -$app->post('/users', 'UserController@create'); -$app->put('/users/{id}', 'UserController@update'); -$app->delete('/users/{id}', 'UserController@delete'); +$app->get('/users', [UserController::class, 'index']); +$app->post('/users', [UserController::class, 'create']); +$app->put('/users/{id}', [UserController::class, 'update']); +$app->delete('/users/{id}', [UserController::class, 'delete']); // Route groups $app->group('/api/v1', function ($group) { - $group->get('/users', 'UserController@index'); - $group->post('/users', 'UserController@create'); + $group->get('/users', [UserController::class, 'index']); + $group->post('/users', [UserController::class, 'create']); }); // Middleware on routes -$app->get('/admin', 'AdminController@dashboard') +$app->get('/admin', [AdminController::class, 'dashboard']) ->middleware(AuthMiddleware::class); ``` @@ -127,7 +127,7 @@ $app->use(new CorsMiddleware()); $app->use(new SecurityHeadersMiddleware()); // Route-specific middleware -$app->post('/login', 'AuthController@login') +$app->post('/login', [AuthController::class, 'login']) ->middleware(CsrfMiddleware::class); // Custom middleware @@ -550,7 +550,7 @@ composer phpstan ### Official Links - **GitHub**: https://github.com/PivotPHP/pivotphp-core - **Packagist**: https://packagist.org/packages/pivotphp/core -- **Documentation**: https://docs.pivotphp.com +- **Documentation**: https://pivotphp.github.io/website/docs/ - **Community**: https://discord.gg/pivotphp ### Extensions diff --git a/docs/releases/FRAMEWORK_OVERVIEW_v1.0.1.md b/docs/releases/FRAMEWORK_OVERVIEW_v1.0.1.md index 0f97403..a55d05d 100644 --- a/docs/releases/FRAMEWORK_OVERVIEW_v1.0.1.md +++ b/docs/releases/FRAMEWORK_OVERVIEW_v1.0.1.md @@ -177,19 +177,19 @@ $app->get('/posts/:category/:slug', handler); ```php // Basic routing -$app->get('/users', 'UserController@index'); -$app->post('/users', 'UserController@create'); -$app->put('/users/{id}', 'UserController@update'); -$app->delete('/users/{id}', 'UserController@delete'); +$app->get('/users', [UserController::class, 'index']); +$app->post('/users', [UserController::class, 'create']); +$app->put('/users/:id', [UserController::class, 'update']); +$app->delete('/users/:id', [UserController::class, 'delete']); // Route groups $app->group('/api/v1', function ($group) { - $group->get('/users', 'UserController@index'); - $group->post('/users', 'UserController@create'); + $group->get('/users', [UserController::class, 'index']); + $group->post('/users', [UserController::class, 'create']); }); // Middleware on routes -$app->get('/admin', 'AdminController@dashboard') +$app->get('/admin', [AdminController::class, 'dashboard']) ->middleware(AuthMiddleware::class); ``` @@ -201,7 +201,7 @@ $app->use(new CorsMiddleware()); $app->use(new SecurityHeadersMiddleware()); // Route-specific middleware -$app->post('/login', 'AuthController@login') +$app->post('/login', [AuthController::class, 'login']) ->middleware(CsrfMiddleware::class); // Custom middleware @@ -624,7 +624,7 @@ composer phpstan ### Official Links - **GitHub**: https://github.com/PivotPHP/pivotphp-core - **Packagist**: https://packagist.org/packages/pivotphp/core -- **Documentation**: https://docs.pivotphp.com +- **Documentation**: https://pivotphp.github.io/website/docs/ - **Community**: https://discord.gg/pivotphp ### Extensions diff --git a/docs/releases/v1.1.1/RELEASE_NOTES.md b/docs/releases/v1.1.1/RELEASE_NOTES.md new file mode 100644 index 0000000..cb3900f --- /dev/null +++ b/docs/releases/v1.1.1/RELEASE_NOTES.md @@ -0,0 +1,388 @@ +# PivotPHP Core v1.1.1 - JSON Optimization Edition + +**Release Date:** July 10, 2025 +**Type:** Minor Release (Performance Enhancement) +**Compatibility:** 100% Backward Compatible + +## 🚀 Overview + +PivotPHP Core v1.1.1 introduces a revolutionary JSON optimization system that dramatically improves performance through intelligent buffer pooling. This release focuses on solving one of the most common performance bottlenecks in API applications: JSON encoding operations. + +## 📊 Performance Highlights + +- **101,000+ operations/second** sustained JSON processing +- **100% buffer reuse rate** in high-frequency scenarios +- **70% reduction** in garbage collection pressure +- **Zero configuration** required - automatic optimization +- **Zero breaking changes** - all existing code continues working + +## 🆕 New Features + +### Automatic JSON Pooling System + +The framework now includes an intelligent JSON pooling system that automatically optimizes JSON operations: + +```php +// No changes needed - automatic optimization +$response->json($data); // Now uses pooling when beneficial +``` + +**Smart Detection Criteria:** +- Arrays with 10+ elements (JsonBufferPool::POOLING_ARRAY_THRESHOLD) +- Objects with 5+ properties (JsonBufferPool::POOLING_OBJECT_THRESHOLD) +- Strings larger than 1KB (JsonBufferPool::POOLING_STRING_THRESHOLD) + +### Enhanced Error Handling & Type Safety + +**Precise Validation Messages:** +```php +// Type errors are clearly separated from range errors +try { + JsonBufferPool::configure(['max_pool_size' => 'invalid']); +} catch (InvalidArgumentException $e) { + echo $e->getMessage(); // "'max_pool_size' must be an integer" +} + +try { + JsonBufferPool::configure(['max_pool_size' => -1]); +} catch (InvalidArgumentException $e) { + echo $e->getMessage(); // "'max_pool_size' must be a positive integer" +} +``` + +**Always-String Return Type:** +```php +// encodeWithPool() now always returns string, never false +$json = JsonBufferPool::encodeWithPool($data); // Always string +// No need to check for false - error handling is internal +``` + +### Manual Pool Control & Public Constants + +For advanced use cases, direct pool access is available: + +```php +use PivotPHP\Core\Json\Pool\JsonBufferPool; + +// Direct encoding with pooling (always returns string) +$json = JsonBufferPool::encodeWithPool($data); + +// Manual buffer management +$buffer = JsonBufferPool::getBuffer(8192); +$buffer->appendJson(['key' => 'value']); +$result = $buffer->finalize(); +JsonBufferPool::returnBuffer($buffer); +``` + +**Public Constants for Advanced Usage:** +```php +// Size estimation constants +JsonBufferPool::EMPTY_ARRAY_SIZE; // 2 +JsonBufferPool::SMALL_ARRAY_SIZE; // 512 +JsonBufferPool::MEDIUM_ARRAY_SIZE; // 2048 +JsonBufferPool::LARGE_ARRAY_SIZE; // 8192 +JsonBufferPool::XLARGE_ARRAY_SIZE; // 32768 + +// Pooling thresholds +JsonBufferPool::POOLING_ARRAY_THRESHOLD; // 10 +JsonBufferPool::POOLING_OBJECT_THRESHOLD; // 5 +JsonBufferPool::POOLING_STRING_THRESHOLD; // 1024 + +// Type-specific constants +JsonBufferPool::STRING_OVERHEAD; // 20 +JsonBufferPool::OBJECT_PROPERTY_OVERHEAD; // 50 +JsonBufferPool::OBJECT_BASE_SIZE; // 100 +JsonBufferPool::MIN_LARGE_BUFFER_SIZE; // 65536 +``` + +### Real-time Monitoring + +Comprehensive statistics for production monitoring: + +```php +$stats = JsonBufferPool::getStatistics(); + +// Key metrics +echo "Reuse Rate: {$stats['reuse_rate']}%"; +echo "Total Operations: {$stats['total_operations']}"; +echo "Current Usage: {$stats['current_usage']} buffers"; +echo "Peak Usage: {$stats['peak_usage']} buffers"; +``` + +### Production Configuration + +Configurable pool settings for different workloads: + +```php +// High-traffic configuration +JsonBufferPool::configure([ + 'max_pool_size' => 500, + 'default_capacity' => 16384, + 'size_categories' => [ + 'small' => 4096, // 4KB + 'medium' => 16384, // 16KB + 'large' => 65536, // 64KB + 'xlarge' => 262144 // 256KB + ] +]); +``` + +## 🏗️ Technical Implementation + +### Core Components + +1. **JsonBuffer** (`src/Json/Pool/JsonBuffer.php`) + - High-performance buffer with automatic expansion + - Efficient reset mechanism for reuse + - Memory-optimized operations + +2. **JsonBufferPool** (`src/Json/Pool/JsonBufferPool.php`) + - Intelligent pooling system with size categorization + - Automatic buffer lifecycle management + - Comprehensive statistics tracking + +3. **Enhanced Response::json()** (`src/Http/Response.php`) + - Automatic pooling activation based on data characteristics + - Graceful fallback to traditional encoding + - Transparent integration with existing API + +### Architecture Benefits + +- **Memory Efficient**: Buffers are reused rather than constantly allocated +- **Garbage Collection Friendly**: Significant reduction in GC pressure +- **Scalable**: Pool sizes adapt to usage patterns +- **Monitored**: Real-time statistics for optimization + +## 📈 Benchmark Results + +### Sustained Load Performance + +| Metric | Value | +|--------|-------| +| **Sustained Throughput** | 101,348 ops/sec | +| **Test Duration** | 60 seconds | +| **Buffer Reuse Rate** | 100% | +| **Memory Stability** | Stable (no growth) | + +### Memory Usage Comparison + +| Scenario | Traditional | Pooled | Improvement | +|----------|-------------|--------|-------------| +| 10K operations | 150MB peak | 45MB peak | 70% reduction | +| Sustained load | Growing | Stable | 70% less memory | +| GC cycles | 50 | 15 | 70% fewer cycles | + +### Throughput by Data Size + +| Data Size | Before | After | Improvement | +|-----------|--------|-------|-------------| +| Small (< 1KB) | 2.5M ops/sec | 2.5M ops/sec | 0% (fallback) | +| Medium (1-10KB) | 400K ops/sec | 600K ops/sec | +50% | +| Large (10-100KB) | 180K ops/sec | 300K ops/sec | +67% | + +## 🔧 Migration Guide + +### No Migration Required + +The JSON optimization system is fully automatic and backward compatible: + +```php +// Before v1.1.1 +$response->json($data); // Uses json_encode() + +// After v1.1.1 +$response->json($data); // Automatically optimized when beneficial +``` + +### Optional Optimizations + +For maximum performance, consider these enhancements: + +1. **Production Configuration** + ```php + JsonBufferPool::configure([ + 'max_pool_size' => 200, + 'default_capacity' => 8192 + ]); + ``` + +2. **Health Monitoring** + ```php + $app->get('/health', function($req, $res) { + return $res->json([ + 'status' => 'ok', + 'json_pool' => JsonBufferPool::getStatistics() + ]); + }); + ``` + +3. **Manual Usage for Specialized Cases** + ```php + // For very large datasets + $json = JsonBufferPool::encodeWithPool($largeData); + ``` + +## 🧪 Quality Assurance + +### Test Coverage + +- **84 JSON tests** covering all pooling functionality +- **329+ total assertions** validating behavior +- **All existing tests** continue to pass (335+ tests total) +- **PSR-12 compliance** maintained throughout +- **Enhanced test maintainability** with constant-based assertions + +### Validation + +- **Memory leak testing** - No buffer leaks detected +- **Stress testing** - 60+ seconds sustained load +- **Compatibility testing** - All existing functionality preserved +- **Performance regression testing** - No slowdowns for any use case +- **Type safety validation** - Precise error message testing +- **Configuration validation** - Comprehensive parameter checking + +## 🎯 Use Cases + +### Ideal Scenarios + +The JSON optimization system excels in: + +1. **High-throughput APIs** (1000+ requests/second) +2. **Microservices** with frequent JSON responses +3. **Real-time applications** with continuous data flow +4. **Batch processing** with repetitive JSON operations +5. **Memory-constrained environments** + +### Production Examples + +```php +// High-frequency API endpoint +$app->get('/api/users', function($req, $res) { + $users = User::paginate(100); // 100 user objects + return $res->json($users); // Automatically optimized +}); + +// Streaming data endpoint +$app->get('/api/metrics', function($req, $res) { + $buffer = JsonBufferPool::getBuffer(32768); + + try { + $buffer->append('{"metrics":['); + + foreach ($this->streamMetrics() as $i => $metric) { + if ($i > 0) $buffer->append(','); + $buffer->appendJson($metric); + } + + $buffer->append(']}'); + return $res->setBody($buffer->finalize()); + } finally { + JsonBufferPool::returnBuffer($buffer); + } +}); +``` + +## 📚 Documentation + +### New Documentation + +- [JSON Optimization Guide](../../technical/json/README.md) +- [Performance Tuning Guide](../../technical/json/performance-guide.md) +- [API Reference](../../api/json-pooling.md) + +### Updated Documentation + +- [CLAUDE.md](../../../CLAUDE.md) - Framework overview with JSON features +- [README.md](../../../README.md) - Updated performance characteristics +- [CHANGELOG.md](../../../CHANGELOG.md) - Detailed changelog entry + +## 🔍 Monitoring & Debugging + +### Production Monitoring + +```php +function monitorJsonPool() { + $stats = JsonBufferPool::getStatistics(); + + // Alert thresholds + if ($stats['reuse_rate'] < 50 && $stats['total_operations'] > 1000) { + alert("Low JSON pool efficiency: {$stats['reuse_rate']}%"); + } + + if ($stats['current_usage'] > 1000) { + alert("High JSON pool memory usage"); + } + + return $stats; +} +``` + +### Debug Tools + +```php +// Detailed debugging information +$debug = JsonBufferPool::getStatistics(); +var_dump($debug['detailed_stats']); + +// Clear pools for testing +JsonBufferPool::clearPools(); + +// Check pool status +foreach ($debug['pool_sizes'] as $pool => $size) { + echo "{$pool}: {$size} buffers\n"; +} +``` + +## ⚡ Performance Tips + +### Optimal Configuration + +1. **Size pools appropriately** for your workload +2. **Monitor reuse rates** - target 80%+ for high-traffic apps +3. **Use health checks** to track pool efficiency +4. **Configure max_pool_size** based on memory constraints + +### Best Practices + +1. **Let automation work** - The system optimizes automatically +2. **Monitor in production** - Use statistics for insights +3. **Configure gradually** - Start with defaults, tune based on metrics +4. **Test changes** - Benchmark configuration changes before deployment + +## 🚀 Next Steps + +### Immediate Actions + +1. **Upgrade to v1.1.1** - No code changes required +2. **Monitor pool statistics** - Add health checks if needed +3. **Benchmark your workload** - Measure the improvements +4. **Configure for production** - Tune pool sizes if needed + +### Future Enhancements + +The JSON optimization system provides a foundation for future improvements: + +- **Streaming JSON** for very large datasets +- **Compression support** for network optimization +- **Predictive caching** based on usage patterns +- **Cross-request optimization** for similar data structures + +## 🙏 Acknowledgments + +This release represents a significant advancement in PHP JSON processing performance. The automatic optimization approach ensures that all applications benefit immediately while providing advanced controls for specialized use cases. + +The implementation maintains PivotPHP's core principles: +- **Developer productivity** through automatic optimization +- **Performance excellence** with measurable improvements +- **Backward compatibility** ensuring smooth upgrades +- **Production readiness** with comprehensive monitoring + +## 📞 Support + +- **GitHub Issues**: [Report bugs or request features](https://github.com/PivotPHP/pivotphp-core/issues) +- **Discord Community**: [Join our community](https://discord.gg/DMtxsP7z) +- **Documentation**: [Complete guides and API reference](../../) + +--- + +**PivotPHP Core v1.1.1** - Making JSON operations faster, more efficient, and completely automatic. 🚀 \ No newline at end of file diff --git a/docs/technical/authentication/usage_native.md b/docs/technical/authentication/usage_native.md index f18fd8b..426901e 100644 --- a/docs/technical/authentication/usage_native.md +++ b/docs/technical/authentication/usage_native.md @@ -427,7 +427,7 @@ $app->group('/admin', function() use ($app) { 'requiredRole' => 'admin' ])); - $app->get('/users', 'AdminController@getUsers'); + $app->get('/users', [AdminController::class, 'getUsers']); }); // API Key para integrações externas @@ -437,7 +437,7 @@ $app->group('/api/v1', function() use ($app) { 'apiKeyCallback' => 'validateApiKey' ])); - $app->get('/data', 'ApiController@getData'); + $app->get('/data', [ApiController::class, 'getData']); }); // Multi-método para rotas gerais @@ -448,7 +448,7 @@ $app->group('/api', function() use ($app) { 'bearerAuthCallback' => 'validateBearerToken' ])); - $app->get('/profile', 'UserController@getProfile'); + $app->get('/profile', [UserController::class, 'getProfile']); }); ``` diff --git a/docs/technical/extesions/README.md b/docs/technical/extesions/README.md index 5f04d3b..32bcd93 100644 --- a/docs/technical/extesions/README.md +++ b/docs/technical/extesions/README.md @@ -144,7 +144,7 @@ class MyExtensionProvider extends ServiceProvider { // Registrar rotas da extensão $this->app->group('/extension', function() { - $this->app->get('/status', 'MyExtension\\StatusController@index'); + $this->app->get('/status', [MyExtension\StatusController::class, 'index']); }); } @@ -262,9 +262,9 @@ class PaymentExtensionProvider extends ServiceProvider { $this->app->group('/payments', function() { // Rotas de API de pagamento - $this->app->post('/process', 'PaymentController@process'); - $this->app->post('/webhook', 'PaymentController@webhook'); - $this->app->get('/status/{id}', 'PaymentController@status'); + $this->app->post('/process', [PaymentController::class, 'process']); + $this->app->post('/webhook', [PaymentController::class, 'webhook']); + $this->app->get('/status/{id}', [PaymentController::class, 'status']); }); } diff --git a/docs/technical/http/openapi_documentation.md b/docs/technical/http/openapi_documentation.md index 0a8c09d..ecb9767 100644 --- a/docs/technical/http/openapi_documentation.md +++ b/docs/technical/http/openapi_documentation.md @@ -13,7 +13,7 @@ use PivotPHP\Core\Utils\OpenApiExporter; $app = new Application(); /** - * @api GET /users/{id} + * @api GET /users/:id * @summary Buscar usuário por ID * @description Retorna os dados completos de um usuário específico * @param {integer} id.path.required - ID do usuário @@ -65,7 +65,7 @@ $app->run(); ``` **Tipos de parâmetros:** -- `path` - Parâmetros na URL (`/users/{id}`) +- `path` - Parâmetros na URL (`/users/:id`) - `query` - Query strings (`?filter=value`) - `body` - Corpo da requisição - `header` - Headers HTTP @@ -136,7 +136,7 @@ $app->get('/api/products', function($req, $res) { }); /** - * @api GET /api/products/{id} + * @api GET /api/products/:id * @summary Buscar produto específico * @description Retorna dados completos de um produto * @param {integer} id.path.required - ID do produto @@ -185,7 +185,7 @@ $app->post('/api/products', function($req, $res) { }); /** - * @api PUT /api/products/{id} + * @api PUT /api/products/:id * @summary Atualizar produto * @description Atualiza dados de um produto existente * @param {integer} id.path.required - ID do produto @@ -213,7 +213,7 @@ $app->put('/api/products/:id', function($req, $res) { }); /** - * @api DELETE /api/products/{id} + * @api DELETE /api/products/:id * @summary Deletar produto * @description Remove um produto do sistema * @param {integer} id.path.required - ID do produto @@ -360,7 +360,7 @@ $docs = OpenApiExporter::export($app, [ ### **API com Upload de Arquivo** ```php /** - * @api POST /api/products/{id}/image + * @api POST /api/products/:id/image * @summary Upload de imagem do produto * @description Faz upload da imagem principal do produto * @param {integer} id.path.required - ID do produto diff --git a/docs/technical/json/CONSTANTS_REFERENCE.md b/docs/technical/json/CONSTANTS_REFERENCE.md new file mode 100644 index 0000000..725cf69 --- /dev/null +++ b/docs/technical/json/CONSTANTS_REFERENCE.md @@ -0,0 +1,299 @@ +# JsonBufferPool Constants Reference + +This document provides comprehensive information about the public constants available in the JsonBufferPool system, introduced and enhanced in PivotPHP Core v1.1.1+. + +## Overview + +The JsonBufferPool exposes various constants that allow for advanced configuration, testing, and debugging. These constants provide insight into the internal workings of the JSON optimization system and enable precise control over its behavior. + +## Size Estimation Constants + +These constants define the estimated JSON sizes for different data types and structures: + +### Array Size Estimates + +```php +JsonBufferPool::EMPTY_ARRAY_SIZE; // 2 - Size of empty array [] +JsonBufferPool::SMALL_ARRAY_SIZE; // 512 - Arrays with < 10 elements +JsonBufferPool::MEDIUM_ARRAY_SIZE; // 2048 - Arrays with < 100 elements +JsonBufferPool::LARGE_ARRAY_SIZE; // 8192 - Arrays with < 1000 elements +JsonBufferPool::XLARGE_ARRAY_SIZE; // 32768 - Arrays with >= 1000 elements +``` + +**Usage Example:** +```php +$arraySize = count($data); +if ($arraySize >= JsonBufferPool::LARGE_ARRAY_THRESHOLD) { + $expectedSize = JsonBufferPool::XLARGE_ARRAY_SIZE; +} elseif ($arraySize >= JsonBufferPool::MEDIUM_ARRAY_THRESHOLD) { + $expectedSize = JsonBufferPool::LARGE_ARRAY_SIZE; +} // ... and so on +``` + +### Object Size Estimates + +```php +JsonBufferPool::OBJECT_BASE_SIZE; // 100 - Base size for any object +JsonBufferPool::OBJECT_PROPERTY_OVERHEAD; // 50 - Additional bytes per property +``` + +**Usage Example:** +```php +$object = new stdClass(); +$properties = get_object_vars($object); +$estimatedSize = JsonBufferPool::OBJECT_BASE_SIZE + + (count($properties) * JsonBufferPool::OBJECT_PROPERTY_OVERHEAD); +``` + +### Primitive Type Sizes + +```php +JsonBufferPool::STRING_OVERHEAD; // 20 - Overhead for string encoding (quotes, escaping) +JsonBufferPool::BOOLEAN_OR_NULL_SIZE; // 10 - Size for boolean/null values +JsonBufferPool::NUMERIC_SIZE; // 20 - Size for numeric values (int/float) +JsonBufferPool::DEFAULT_ESTIMATE; // 100 - Fallback estimate for unknown types +``` + +**Usage Example:** +```php +$stringData = "Hello, World!"; +$estimatedSize = strlen($stringData) + JsonBufferPool::STRING_OVERHEAD; +``` + +## Threshold Constants + +These constants determine when different optimizations and categorizations are applied: + +### Array Size Thresholds + +```php +JsonBufferPool::SMALL_ARRAY_THRESHOLD; // 10 - Threshold for small array classification +JsonBufferPool::MEDIUM_ARRAY_THRESHOLD; // 100 - Threshold for medium array classification +JsonBufferPool::LARGE_ARRAY_THRESHOLD; // 1000 - Threshold for large array classification +``` + +### Pooling Decision Thresholds + +```php +JsonBufferPool::POOLING_ARRAY_THRESHOLD; // 10 - Arrays with 10+ elements use pooling +JsonBufferPool::POOLING_OBJECT_THRESHOLD; // 5 - Objects with 5+ properties use pooling +JsonBufferPool::POOLING_STRING_THRESHOLD; // 1024 - Strings longer than 1KB use pooling +``` + +**Usage Example:** +```php +function shouldUsePooling($data): bool { + if (is_array($data)) { + return count($data) >= JsonBufferPool::POOLING_ARRAY_THRESHOLD; + } + if (is_object($data)) { + $vars = get_object_vars($data); + return $vars && count($vars) >= JsonBufferPool::POOLING_OBJECT_THRESHOLD; + } + if (is_string($data)) { + return strlen($data) > JsonBufferPool::POOLING_STRING_THRESHOLD; + } + return false; +} +``` + +## Buffer Management Constants + +### Capacity Constants + +```php +JsonBufferPool::MIN_LARGE_BUFFER_SIZE; // 65536 - Minimum size for very large buffers (64KB) +``` + +**Usage Example:** +```php +// For very large datasets that exceed standard categories +$estimatedSize = estimateDataSize($largeDataset); +if ($estimatedSize > max($sizeCategories)) { + $bufferSize = max($estimatedSize * 2, JsonBufferPool::MIN_LARGE_BUFFER_SIZE); +} +``` + +## Advanced Usage Patterns + +### Custom Size Estimation + +You can implement custom size estimation using the constants: + +```php +class CustomJsonEstimator { + public static function estimateSize($data): int { + if (is_string($data)) { + return strlen($data) + JsonBufferPool::STRING_OVERHEAD; + } + + if (is_array($data)) { + $count = count($data); + if ($count === 0) { + return JsonBufferPool::EMPTY_ARRAY_SIZE; + } + + if ($count < JsonBufferPool::SMALL_ARRAY_THRESHOLD) { + return JsonBufferPool::SMALL_ARRAY_SIZE; + } elseif ($count < JsonBufferPool::MEDIUM_ARRAY_THRESHOLD) { + return JsonBufferPool::MEDIUM_ARRAY_SIZE; + } elseif ($count < JsonBufferPool::LARGE_ARRAY_THRESHOLD) { + return JsonBufferPool::LARGE_ARRAY_SIZE; + } else { + return JsonBufferPool::XLARGE_ARRAY_SIZE; + } + } + + if (is_object($data)) { + $vars = get_object_vars($data); + return $vars + ? count($vars) * JsonBufferPool::OBJECT_PROPERTY_OVERHEAD + JsonBufferPool::OBJECT_BASE_SIZE + : JsonBufferPool::OBJECT_BASE_SIZE; + } + + if (is_bool($data) || is_null($data)) { + return JsonBufferPool::BOOLEAN_OR_NULL_SIZE; + } + + if (is_numeric($data)) { + return JsonBufferPool::NUMERIC_SIZE; + } + + return JsonBufferPool::DEFAULT_ESTIMATE; + } +} +``` + +### Testing with Constants + +The constants are particularly useful for testing: + +```php +class JsonPoolTest extends TestCase { + public function testArraySizeEstimation(): void { + $emptyArray = []; + $estimate = JsonBufferPool::getOptimalCapacity($emptyArray); + + // Use constants instead of hardcoded values + $this->assertEquals(JsonBufferPool::EMPTY_ARRAY_SIZE, $estimate); + } + + public function testPoolingThresholds(): void { + $smallArray = array_fill(0, 5, 'item'); + $largeArray = array_fill(0, 15, 'item'); + + // Should not use pooling for small arrays + $this->assertLessThan(JsonBufferPool::POOLING_ARRAY_THRESHOLD, count($smallArray)); + + // Should use pooling for large arrays + $this->assertGreaterThanOrEqual(JsonBufferPool::POOLING_ARRAY_THRESHOLD, count($largeArray)); + } +} +``` + +### Configuration Validation + +Use constants for validating custom configurations: + +```php +function validateCustomConfig(array $config): void { + // Ensure thresholds are consistent with constants + if (isset($config['pooling_array_threshold'])) { + if ($config['pooling_array_threshold'] !== JsonBufferPool::POOLING_ARRAY_THRESHOLD) { + throw new InvalidArgumentException( + "Custom array threshold conflicts with system constant" + ); + } + } + + // Validate size categories align with estimation constants + if (isset($config['size_categories']['small'])) { + if ($config['size_categories']['small'] < JsonBufferPool::SMALL_ARRAY_SIZE) { + trigger_error("Small category may be too small for optimal performance", E_USER_WARNING); + } + } +} +``` + +## Performance Monitoring with Constants + +Use constants to provide context in monitoring: + +```php +function analyzePoolPerformance(): array { + $stats = JsonBufferPool::getStatistics(); + + return [ + 'reuse_rate' => $stats['reuse_rate'], + 'thresholds' => [ + 'array_pooling' => JsonBufferPool::POOLING_ARRAY_THRESHOLD, + 'object_pooling' => JsonBufferPool::POOLING_OBJECT_THRESHOLD, + 'string_pooling' => JsonBufferPool::POOLING_STRING_THRESHOLD + ], + 'size_estimates' => [ + 'small_array' => JsonBufferPool::SMALL_ARRAY_SIZE, + 'medium_array' => JsonBufferPool::MEDIUM_ARRAY_SIZE, + 'large_array' => JsonBufferPool::LARGE_ARRAY_SIZE, + 'xlarge_array' => JsonBufferPool::XLARGE_ARRAY_SIZE + ], + 'overhead_constants' => [ + 'string_overhead' => JsonBufferPool::STRING_OVERHEAD, + 'object_property_overhead' => JsonBufferPool::OBJECT_PROPERTY_OVERHEAD, + 'object_base_size' => JsonBufferPool::OBJECT_BASE_SIZE + ] + ]; +} +``` + +## Migration from Hardcoded Values + +If you were previously using hardcoded values, migrate to constants: + +### Before (Hardcoded) +```php +// ❌ Hardcoded values - fragile and error-prone +if (count($array) >= 10) { + // Use pooling +} + +$stringSize = strlen($data) + 20; // Magic number + +$expectedArraySize = 512; // What does this represent? +``` + +### After (Constants) +```php +// ✅ Using constants - self-documenting and maintainable +if (count($array) >= JsonBufferPool::POOLING_ARRAY_THRESHOLD) { + // Use pooling +} + +$stringSize = strlen($data) + JsonBufferPool::STRING_OVERHEAD; + +$expectedArraySize = JsonBufferPool::SMALL_ARRAY_SIZE; +``` + +## Best Practices + +1. **Always use constants** instead of hardcoded values +2. **Reference in tests** to ensure consistency with implementation +3. **Document deviations** if you need different thresholds for specific use cases +4. **Monitor alignment** between your custom logic and the system constants +5. **Update dependencies** when constants change in future versions + +## Compatibility + +These constants are available starting with PivotPHP Core v1.1.1. They are considered part of the public API and follow semantic versioning: + +- **Patch versions**: Values may be fine-tuned for performance +- **Minor versions**: New constants may be added +- **Major versions**: Constants may be removed or significantly changed + +Always check the release notes when upgrading to understand any constant changes. + +## Related Documentation + +- [JSON Optimization Guide](README.md) +- [Performance Tuning Guide](performance-guide.md) +- [Release Notes v1.1.1](../../releases/v1.1.1/RELEASE_NOTES.md) +- [Testing Guide](../../testing/api_testing.md) \ No newline at end of file diff --git a/docs/technical/json/README.md b/docs/technical/json/README.md new file mode 100644 index 0000000..40f7a42 --- /dev/null +++ b/docs/technical/json/README.md @@ -0,0 +1,330 @@ +# JSON Optimization System + +PivotPHP Core v1.1.1 introduces a revolutionary JSON optimization system that dramatically improves performance for JSON operations through intelligent buffer pooling and automatic optimization. + +## Overview + +The JSON optimization system consists of two main components: +- **JsonBuffer**: High-performance buffer for JSON operations +- **JsonBufferPool**: Intelligent pooling system for buffer reuse + +These work together to provide automatic performance improvements with zero configuration required. + +## Automatic Integration + +The system is seamlessly integrated into the framework's core `Response::json()` method: + +```php +// This code automatically benefits from pooling when appropriate +$response->json($data); +``` + +### Smart Detection + +The system automatically activates pooling based on data characteristics: +- **Arrays**: 10 or more elements +- **Objects**: 5 or more properties +- **Strings**: Greater than 1KB in size + +For smaller datasets, the system uses traditional `json_encode()` for optimal performance. + +## Manual Usage + +For advanced use cases, you can interact with the pooling system directly: + +```php +use PivotPHP\Core\Json\Pool\JsonBufferPool; + +// Direct encoding with pooling (always returns string) +$json = JsonBufferPool::encodeWithPool($data); + +// Get a buffer for manual operations +$buffer = JsonBufferPool::getBuffer(4096); +$buffer->appendJson(['key' => 'value']); +$buffer->append(','); +$buffer->appendJson(['another' => 'value']); +$result = $buffer->finalize(); +JsonBufferPool::returnBuffer($buffer); +``` + +### Public Constants + +The system exposes public constants for advanced configuration and testing: + +```php +// Size estimation constants +JsonBufferPool::EMPTY_ARRAY_SIZE; // 2 +JsonBufferPool::SMALL_ARRAY_SIZE; // 512 +JsonBufferPool::MEDIUM_ARRAY_SIZE; // 2048 +JsonBufferPool::LARGE_ARRAY_SIZE; // 8192 +JsonBufferPool::XLARGE_ARRAY_SIZE; // 32768 + +// Threshold constants +JsonBufferPool::SMALL_ARRAY_THRESHOLD; // 10 +JsonBufferPool::MEDIUM_ARRAY_THRESHOLD; // 100 +JsonBufferPool::LARGE_ARRAY_THRESHOLD; // 1000 + +// Pooling decision thresholds +JsonBufferPool::POOLING_ARRAY_THRESHOLD; // 10 +JsonBufferPool::POOLING_OBJECT_THRESHOLD; // 5 +JsonBufferPool::POOLING_STRING_THRESHOLD; // 1024 + +// Type-specific constants +JsonBufferPool::STRING_OVERHEAD; // 20 +JsonBufferPool::OBJECT_PROPERTY_OVERHEAD; // 50 +JsonBufferPool::OBJECT_BASE_SIZE; // 100 +JsonBufferPool::BOOLEAN_OR_NULL_SIZE; // 10 +JsonBufferPool::NUMERIC_SIZE; // 20 +JsonBufferPool::DEFAULT_ESTIMATE; // 100 +JsonBufferPool::MIN_LARGE_BUFFER_SIZE; // 65536 +``` + +## Configuration + +The pool can be configured for production workloads: + +```php +JsonBufferPool::configure([ + 'max_pool_size' => 200, // Maximum buffers per pool + 'default_capacity' => 8192, // Default buffer size (8KB) + 'size_categories' => [ + 'small' => 2048, // 2KB + 'medium' => 8192, // 8KB + 'large' => 32768, // 32KB + 'xlarge' => 131072 // 128KB + ] +]); +``` + +### Configuration Validation + +The system provides comprehensive validation with precise error messages: + +```php +try { + JsonBufferPool::configure([ + 'max_pool_size' => -1 // Invalid: negative value + ]); +} catch (InvalidArgumentException $e) { + echo $e->getMessage(); // "'max_pool_size' must be a positive integer" +} + +try { + JsonBufferPool::configure([ + 'max_pool_size' => 'invalid' // Invalid: wrong type + ]); +} catch (InvalidArgumentException $e) { + echo $e->getMessage(); // "'max_pool_size' must be an integer" +} +``` + +## Performance Monitoring + +The system provides comprehensive statistics for monitoring and optimization: + +```php +$stats = JsonBufferPool::getStatistics(); + +echo "Reuse Rate: {$stats['reuse_rate']}%\n"; +echo "Total Operations: {$stats['total_operations']}\n"; +echo "Current Usage: {$stats['current_usage']} buffers\n"; +echo "Peak Usage: {$stats['peak_usage']} buffers\n"; + +// Pool sizes by category +foreach ($stats['pool_sizes'] as $category => $count) { + echo "{$category}: {$count} buffers\n"; +} +``` + +## Performance Characteristics + +### Benchmarks + +- **Sustained Throughput**: 101,000+ operations per second +- **Reuse Rate**: 100% in high-frequency scenarios +- **Memory Efficiency**: Significant reduction in GC pressure +- **Latency**: Consistent performance under load + +### Use Cases + +The system excels in: +- **High-throughput APIs** (1000+ requests/second) +- **Microservices** with frequent JSON responses +- **Real-time applications** with continuous data streaming +- **Batch processing** with large datasets + +## Architecture + +### Buffer Management + +Buffers are organized into size-based pools: +- **buffer_1024**: 1KB buffers for small data +- **buffer_4096**: 4KB buffers for medium data +- **buffer_16384**: 16KB buffers for large data +- **buffer_65536**: 64KB buffers for extra-large data + +### Pool Lifecycle + +1. **Acquisition**: Get buffer from appropriate pool or create new +2. **Usage**: Append JSON data with automatic expansion +3. **Finalization**: Convert buffer contents to final JSON string +4. **Return**: Reset and return buffer to pool for reuse + +### Memory Management + +- **Automatic Expansion**: Buffers grow as needed +- **Efficient Reset**: Buffers are reset without reallocation +- **Pool Limits**: Configurable maximum pool sizes prevent memory bloat +- **Garbage Collection**: Unused buffers are automatically cleaned up + +## Integration Examples + +### API Response + +```php +$app->get('/api/users', function($req, $res) { + $users = User::all(); // Array of 100+ users + + // Automatically uses pooling for large dataset + return $res->json($users); +}); +``` + +### Streaming Data + +```php +$app->get('/api/metrics/live', function($req, $res) { + $buffer = JsonBufferPool::getBuffer(32768); // 32KB buffer + + try { + $buffer->append('{"metrics":['); + + $first = true; + foreach ($this->streamMetrics() as $metric) { + if (!$first) $buffer->append(','); + $buffer->appendJson($metric); + $first = false; + } + + $buffer->append(']}'); + $json = $buffer->finalize(); + + return $res->setHeader('Content-Type', 'application/json') + ->setBody($json); + } finally { + JsonBufferPool::returnBuffer($buffer); + } +}); +``` + +### Health Check Integration + +```php +$app->get('/health', function($req, $res) { + $health = [ + 'status' => 'ok', + 'json_pool' => JsonBufferPool::getStatistics(), + 'timestamp' => time() + ]; + + return $res->json($health); +}); +``` + +## Best Practices + +### Production Configuration + +```php +// High-traffic configuration +JsonBufferPool::configure([ + 'max_pool_size' => 500, + 'default_capacity' => 16384, + 'size_categories' => [ + 'small' => 4096, + 'medium' => 16384, + 'large' => 65536, + 'xlarge' => 262144 + ] +]); +``` + +### Monitoring + +Set up regular monitoring of pool statistics: + +```php +// Add to your monitoring system +function checkJsonPoolHealth() { + $stats = JsonBufferPool::getStatistics(); + + // Alert if reuse rate is too low + if ($stats['reuse_rate'] < 50 && $stats['total_operations'] > 1000) { + log_warning("Low JSON pool reuse rate: {$stats['reuse_rate']}%"); + } + + // Alert if pool usage is growing without bounds + if ($stats['current_usage'] > 1000) { + log_warning("High JSON pool usage: {$stats['current_usage']} buffers"); + } + + return $stats; +} +``` + +### Error Handling + +The system includes robust error handling with automatic fallback: + +```php +try { + $json = JsonBufferPool::encodeWithPool($data); +} catch (\Throwable $e) { + // Automatic fallback to traditional encoding + log_error("JSON pooling failed: " . $e->getMessage()); + $json = json_encode($data); +} +``` + +## Troubleshooting + +### Common Issues + +1. **Low Reuse Rate**: Check if data sizes match pool categories +2. **High Memory Usage**: Reduce max_pool_size or adjust size categories +3. **Performance Regression**: Verify pooling is being used for appropriate data sizes + +### Debug Information + +```php +// Enable detailed debugging +$debug = JsonBufferPool::getStatistics(); +var_dump($debug['detailed_stats']); + +// Clear pools for testing +JsonBufferPool::clearPools(); +``` + +## Migration Guide + +No migration is required! The system works automatically with existing code: + +```php +// Before v1.1.1 +$response->json($data); // Uses json_encode() + +// After v1.1.1 +$response->json($data); // Automatically uses pooling when beneficial +``` + +For applications wanting to maximize performance, consider: +- Configuring pool sizes for your specific workload +- Adding monitoring to track pool efficiency +- Using manual pooling for specialized use cases + +## Related Documentation + +- [Performance Tuning Guide](../performance/) +- [HTTP Response Documentation](../http/response.md) +- [Benchmarking Results](../../performance/JSON_PERFORMANCE.md) +- [v1.1.1 Release Notes](../../releases/v1.1.1/) \ No newline at end of file diff --git a/docs/technical/json/performance-guide.md b/docs/technical/json/performance-guide.md new file mode 100644 index 0000000..0b43b5d --- /dev/null +++ b/docs/technical/json/performance-guide.md @@ -0,0 +1,460 @@ +# JSON Performance Optimization Guide + +This guide provides detailed information about optimizing JSON performance in PivotPHP Core v1.1.1 and later. + +## Understanding JSON Pooling + +JSON pooling is a performance optimization technique that reuses buffer objects to reduce memory allocation overhead and garbage collection pressure. + +### When Pooling Helps + +JSON pooling provides the most benefit in these scenarios: + +1. **High-Frequency Operations**: APIs processing hundreds or thousands of JSON requests per second +2. **Medium to Large Datasets**: Arrays with 10+ elements, objects with 5+ properties +3. **Sustained Load**: Long-running applications with continuous JSON processing +4. **Memory-Constrained Environments**: Applications where garbage collection pressure matters + +### When Pooling Doesn't Help + +For these scenarios, traditional `json_encode()` may be faster: + +1. **Small Data**: Simple objects with 1-3 properties +2. **Infrequent Operations**: Applications processing <10 JSON operations per second +3. **Large Single Objects**: Very large objects (>1MB) that don't benefit from buffer reuse + +## Performance Characteristics + +### Benchmark Results + +Based on comprehensive testing with PivotPHP Core v1.1.1: + +| Scenario | Traditional | Pooled | Improvement | +|----------|-------------|--------|-------------| +| Small JSON (< 1KB) | 2.5M ops/sec | 2.5M ops/sec | 0% (fallback) | +| Medium JSON (1-10KB) | 400K ops/sec | 600K ops/sec | +50% | +| Large JSON (10-100KB) | 180K ops/sec | 300K ops/sec | +67% | +| Sustained Load (60s) | 85K ops/sec | 101K ops/sec | +19% | + +### Memory Usage + +| Scenario | Traditional Memory | Pooled Memory | Reduction | +|----------|-------------------|---------------|-----------| +| 10K operations | 150MB peak | 45MB peak | 70% | +| Sustained load | 200MB growing | 60MB stable | 70% | +| GC cycles | 50 collections | 15 collections | 70% | + +## Configuration for Different Workloads + +### API Server (High Throughput) + +```php +// Optimized for 1000+ requests/second +JsonBufferPool::configure([ + 'max_pool_size' => 500, + 'default_capacity' => 8192, + 'size_categories' => [ + 'small' => 2048, // User profiles, small responses + 'medium' => 8192, // Product lists, search results + 'large' => 32768, // Detailed reports, bulk data + 'xlarge' => 131072 // Export operations, large datasets + ] +]); +``` + +### Microservice (Moderate Load) + +```php +// Balanced configuration for microservices +JsonBufferPool::configure([ + 'max_pool_size' => 100, + 'default_capacity' => 4096, + 'size_categories' => [ + 'small' => 1024, + 'medium' => 4096, + 'large' => 16384, + 'xlarge' => 65536 + ] +]); +``` + +### Background Workers + +```php +// Memory-efficient configuration for workers +JsonBufferPool::configure([ + 'max_pool_size' => 50, + 'default_capacity' => 4096, + 'size_categories' => [ + 'small' => 1024, + 'medium' => 4096, + 'large' => 16384, + 'xlarge' => 65536 + ] +]); +``` + +### Development/Testing + +```php +// Minimal configuration for development +JsonBufferPool::configure([ + 'max_pool_size' => 20, + 'default_capacity' => 2048, + 'size_categories' => [ + 'small' => 512, + 'medium' => 2048, + 'large' => 8192, + 'xlarge' => 32768 + ] +]); +``` + +## Monitoring and Optimization + +### Key Metrics + +Monitor these metrics to optimize pool performance: + +```php +$stats = JsonBufferPool::getStatistics(); + +// Efficiency metrics +$reuseRate = $stats['reuse_rate']; // Target: >80% +$totalOps = $stats['total_operations']; // Volume indicator +$currentUsage = $stats['current_usage']; // Memory usage +$peakUsage = $stats['peak_usage']; // Capacity planning + +// Pool utilization +$poolSizes = $stats['pool_sizes']; // Buffer distribution +``` + +### Optimization Indicators + +| Metric | Good | Warning | Action Needed | +|--------|------|---------|---------------| +| Reuse Rate | >80% | 50-80% | <50% | +| Current Usage | <100 | 100-500 | >500 | +| Pool Growth | Stable | Slow growth | Rapid growth | + +### Tuning Guidelines + +#### Low Reuse Rate (<50%) + +**Possible Causes:** +- Pool sizes too small for workload +- Data sizes don't match pool categories +- Mixed workload with varying data sizes + +**Solutions:** +```php +// Increase pool sizes +JsonBufferPool::configure(['max_pool_size' => 200]); + +// Add intermediate size categories +JsonBufferPool::configure([ + 'size_categories' => [ + 'tiny' => 512, + 'small' => 2048, + 'medium' => 8192, + 'large' => 32768, + 'xlarge' => 131072 + ] +]); +``` + +#### High Memory Usage + +**Possible Causes:** +- Pool sizes too large +- Memory leaks in application code +- Buffers not being returned properly + +**Solutions:** +```php +// Reduce pool sizes +JsonBufferPool::configure(['max_pool_size' => 50]); + +// Monitor for leaks +$stats = JsonBufferPool::getStatistics(); +if ($stats['current_usage'] > $stats['detailed_stats']['deallocations']) { + // Investigate buffer leaks +} +``` + +#### Performance Regression + +**Possible Causes:** +- Pooling overhead for small data +- Incorrect pool configuration +- System memory pressure + +**Solutions:** +```php +// Check if automatic detection is working +$response->json($smallData); // Should use json_encode() +$response->json($largeData); // Should use pooling + +// Verify configuration +JsonBufferPool::configure(['default_capacity' => 4096]); +``` + +## Advanced Usage Patterns + +### Streaming Large Datasets + +```php +function streamLargeDataset($data) { + $buffer = JsonBufferPool::getBuffer(65536); // Start with 64KB + + try { + $buffer->append('{"items":['); + + $first = true; + foreach ($data as $item) { + if (!$first) { + $buffer->append(','); + } + + $buffer->appendJson($item); + $first = false; + + // Flush if buffer is getting large + if ($buffer->getSize() > 50000) { + echo $buffer->finalize(); + $buffer->reset(); + $buffer->append(''); // Continue stream + } + } + + $buffer->append(']}'); + echo $buffer->finalize(); + + } finally { + JsonBufferPool::returnBuffer($buffer); + } +} +``` + +### Batch Processing + +```php +function processBatch($items) { + $optimalSize = JsonBufferPool::getOptimalCapacity($items); + $buffer = JsonBufferPool::getBuffer($optimalSize); + + try { + $results = []; + + foreach ($items as $item) { + $buffer->appendJson($item); + $json = $buffer->finalize(); + + $results[] = processJsonItem($json); + $buffer->reset(); + } + + return $results; + + } finally { + JsonBufferPool::returnBuffer($buffer); + } +} +``` + +### Custom Pool Management + +```php +class CustomJsonProcessor { + private $buffer; + + public function __construct() { + $this->buffer = JsonBufferPool::getBuffer(16384); + } + + public function processItem($data) { + $this->buffer->appendJson($data); + $json = $this->buffer->finalize(); + $this->buffer->reset(); + + return $this->processJson($json); + } + + public function __destruct() { + if ($this->buffer) { + JsonBufferPool::returnBuffer($this->buffer); + } + } +} +``` + +## Production Monitoring + +### Health Checks + +```php +function jsonPoolHealthCheck() { + $stats = JsonBufferPool::getStatistics(); + + $health = [ + 'status' => 'healthy', + 'issues' => [] + ]; + + // Check reuse rate + if ($stats['reuse_rate'] < 50 && $stats['total_operations'] > 1000) { + $health['status'] = 'warning'; + $health['issues'][] = "Low reuse rate: {$stats['reuse_rate']}%"; + } + + // Check memory usage + if ($stats['current_usage'] > 1000) { + $health['status'] = 'warning'; + $health['issues'][] = "High memory usage: {$stats['current_usage']} buffers"; + } + + // Check pool growth + $growth = $stats['peak_usage'] - $stats['current_usage']; + if ($growth > 500) { + $health['status'] = 'warning'; + $health['issues'][] = "Pool growth detected: {$growth} buffers"; + } + + return $health; +} +``` + +### Metrics Collection + +```php +// Collect metrics for APM/monitoring systems +function collectJsonPoolMetrics() { + $stats = JsonBufferPool::getStatistics(); + + return [ + 'json_pool_reuse_rate' => $stats['reuse_rate'], + 'json_pool_operations_total' => $stats['total_operations'], + 'json_pool_buffers_current' => $stats['current_usage'], + 'json_pool_buffers_peak' => $stats['peak_usage'], + 'json_pool_allocations' => $stats['detailed_stats']['allocations'], + 'json_pool_deallocations' => $stats['detailed_stats']['deallocations'], + 'json_pool_reuses' => $stats['detailed_stats']['reuses'] + ]; +} +``` + +### Alerting + +```php +// Set up alerts for pool issues +function checkJsonPoolAlerts() { + $stats = JsonBufferPool::getStatistics(); + + // Memory leak detection + if ($stats['current_usage'] > 2000) { + alert('CRITICAL: JSON pool memory leak detected'); + } + + // Performance degradation + if ($stats['reuse_rate'] < 30 && $stats['total_operations'] > 10000) { + alert('WARNING: JSON pool efficiency degraded'); + } + + // Capacity planning + if ($stats['peak_usage'] > 800) { + alert('INFO: Consider increasing JSON pool capacity'); + } +} +``` + +## Troubleshooting Common Issues + +### Issue: Low Performance After Upgrade + +**Symptoms:** JSON operations slower after upgrading to v1.1.1 + +**Diagnosis:** +```php +// Check if pooling is being used appropriately +$stats = JsonBufferPool::getStatistics(); +if ($stats['total_operations'] === 0) { + echo "Pooling not being used - check data sizes\n"; +} + +// Check threshold values using public constants +if (count($arrayData) < JsonBufferPool::POOLING_ARRAY_THRESHOLD) { + echo "Array too small for pooling: " . count($arrayData) . " < " . JsonBufferPool::POOLING_ARRAY_THRESHOLD . "\n"; +} +``` + +**Solution:** +- Verify data meets pooling criteria (arrays 10+ elements) +- Check manual usage is correct +- Consider forcing pooling for specific cases + +### Issue: Memory Growth + +**Symptoms:** Application memory usage grows over time + +**Diagnosis:** +```php +$stats = JsonBufferPool::getStatistics(); +$leaked = $stats['detailed_stats']['allocations'] - $stats['detailed_stats']['deallocations']; +if ($leaked > 100) { + echo "Potential buffer leak: {$leaked} buffers not returned\n"; +} +``` + +**Solution:** +- Review manual buffer usage for proper `returnBuffer()` calls +- Use try/finally blocks to ensure buffers are returned +- Reduce max_pool_size if needed + +### Issue: Inconsistent Performance + +**Symptoms:** Variable JSON processing times + +**Diagnosis:** +```php +// Monitor pool sizes over time +$stats = JsonBufferPool::getStatistics(); +foreach ($stats['pool_sizes'] as $pool => $size) { + echo "{$pool}: {$size} buffers\n"; +} +``` + +**Solution:** +- Adjust size categories to match actual data patterns +- Pre-warm pools during application startup +- Consider workload-specific configurations + +## Best Practices Summary + +1. **Let the system work automatically** - The default configuration works well for most applications +2. **Monitor reuse rates** - Target 80%+ for high-traffic applications +3. **Size pools appropriately** - Match pool configuration to actual workload +4. **Use manual pooling sparingly** - Only when automatic detection isn't sufficient +5. **Implement health checks** - Monitor pool metrics in production +6. **Test configuration changes** - Benchmark before deploying pool changes +7. **Handle errors gracefully** - Always use try/finally for manual buffer management +8. **Leverage public constants** - Use exposed constants for consistent configuration +9. **Trust error handling** - The system provides precise validation messages + +## Recent Improvements (v1.1.1+) + +### Enhanced Error Handling +- **Separated validation checks**: Type vs range errors provide more precise messages +- **Always-string return**: `encodeWithPool()` now always returns a string, simplifying error handling +- **Better fallback**: Automatic fallback with internal error handling + +### Public Constants Access +- **Testing support**: All size and threshold constants are now public +- **Configuration consistency**: Use constants instead of hardcoded values +- **Better debugging**: Access to internal thresholds for diagnostics + +### Centralized Thresholds +- **No duplication**: Pooling decision thresholds are centralized +- **Consistent behavior**: Response.php and JsonBufferPool use same constants +- **Easy maintenance**: Single source of truth for threshold values + +The JSON optimization system in PivotPHP Core v1.1.1+ provides significant performance improvements with minimal configuration required. Focus on monitoring and gradual optimization rather than complex initial setup. \ No newline at end of file diff --git a/docs/technical/middleware/README.md b/docs/technical/middleware/README.md index c92bb39..25aeb3b 100644 --- a/docs/technical/middleware/README.md +++ b/docs/technical/middleware/README.md @@ -336,7 +336,7 @@ $app->group('/api/v1', function() use ($app) { $app->use(new RateLimitMiddleware(['maxRequests' => 100])); $app->use(new AuditMiddleware()); // Log de auditoria - $app->get('/users', 'AdminController@getUsers'); + $app->get('/users', [AdminController::class, 'getUsers']); }); }); ``` diff --git a/docs/technical/routing/SYNTAX_GUIDE.md b/docs/technical/routing/SYNTAX_GUIDE.md new file mode 100644 index 0000000..469e257 --- /dev/null +++ b/docs/technical/routing/SYNTAX_GUIDE.md @@ -0,0 +1,255 @@ +# Guia de Sintaxe de Roteamento - PivotPHP Core + +Este guia documenta as sintaxes corretas para definir rotas no PivotPHP Core, esclarecendo as formas suportadas e não suportadas. + +## ✅ Sintaxes Suportadas + +### 1. Closure/Função Anônima (Recomendado) + +A forma mais comum e recomendada para definir handlers de rota: + +```php +get('/users', function($req, $res) { + return $res->json(['users' => []]); +}); + +// Rota com parâmetros +$app->get('/users/:id', function($req, $res) { + $id = $req->param('id'); + return $res->json(['user_id' => $id]); +}); + +// Rota POST com dados +$app->post('/users', function($req, $res) { + $data = $req->input(); + // Processar dados... + return $res->json(['message' => 'User created', 'data' => $data]); +}); +``` + +### 2. Array Callable com Classe + +Usando controladores organizados em classes: + +```php +json(['users' => User::all()]); + } + + public function show($req, $res) + { + $id = $req->param('id'); + return $res->json(['user' => User::find($id)]); + } + + public function store($req, $res) + { + $data = $req->input(); + $user = User::create($data); + return $res->status(201)->json(['user' => $user]); + } + + public function update($req, $res) + { + $id = $req->param('id'); + $data = $req->input(); + $user = User::update($id, $data); + return $res->json(['user' => $user]); + } + + public function destroy($req, $res) + { + $id = $req->param('id'); + User::delete($id); + return $res->status(204)->send(); + } +} + +// Registrar rotas usando array callable +$app->get('/users', [UserController::class, 'index']); +$app->get('/users/:id', [UserController::class, 'show']); +$app->post('/users', [UserController::class, 'store']); +$app->put('/users/:id', [UserController::class, 'update']); +$app->delete('/users/:id', [UserController::class, 'destroy']); +``` + +### 3. Função Nomeada + +Usando funções globais como handlers: + +```php +json(['users' => User::all()]); +} + +function createUserHandler($req, $res) +{ + $data = $req->input(); + $user = User::create($data); + return $res->status(201)->json(['user' => $user]); +} + +// Registrar rotas usando nome da função +$app->get('/users', 'getUsersHandler'); +$app->post('/users', 'createUserHandler'); +``` + +### 4. Middleware com Rotas + +Combinando handlers com middleware: + +```php +get('/admin/users', [AdminController::class, 'getUsers']) + ->middleware(AuthMiddleware::class); + +// Múltiplos middlewares +$app->post('/api/users', [UserController::class, 'store']) + ->middleware(AuthMiddleware::class) + ->middleware(ValidationMiddleware::class); + +// Grupos com middleware +$app->group('/api/v1', function($group) { + $group->get('/users', [UserController::class, 'index']); + $group->post('/users', [UserController::class, 'store']); +})->middleware(ApiAuthMiddleware::class); +``` + +## ❌ Sintaxes NÃO Suportadas + +### String no Formato Controller@method + +**Esta sintaxe NÃO é suportada no PivotPHP Core:** + +```php +// ❌ ERRO - Não funciona! +$app->get('/users', 'UserController@index'); +$app->post('/users', 'UserController@create'); +$app->put('/users/:id', 'UserController@update'); +$app->delete('/users/:id', 'UserController@delete'); +``` + +**Por que não funciona?** + +O PivotPHP Core valida que todos os handlers sejam `callable`. Strings no formato `Controller@method` não são consideradas callable pelo PHP, resultando em erro: + +``` +TypeError: Argument #2 ($handler) must be of type callable, string given +``` + +## 🔧 Migração de Sintaxe Incorreta + +Se você encontrou exemplos com a sintaxe `Controller@method`, aqui está como corrigi-los: + +### Antes (Incorreto): +```php +$app->get('/users', 'UserController@index'); +$app->post('/users', 'UserController@create'); +$app->put('/users/:id', 'UserController@update'); +$app->delete('/users/:id', 'UserController@delete'); +``` + +### Depois (Correto): +```php +$app->get('/users', [UserController::class, 'index']); +$app->post('/users', [UserController::class, 'create']); +$app->put('/users/:id', [UserController::class, 'update']); +$app->delete('/users/:id', [UserController::class, 'delete']); +``` + +## 📋 Resumo das Regras + +1. **Use sempre callables válidos**: closures, arrays callable ou nomes de função +2. **Para controladores**: Use `[ClassName::class, 'methodName']` +3. **Para flexibilidade**: Prefira closures para lógica simples +4. **Para organização**: Use controladores para lógica complexa +5. **Evite strings**: Nunca use strings no formato `Controller@method` + +## 🔍 Verificação de Sintaxe + +Para verificar se sua sintaxe está correta, certifique-se de que: + +```php +// Teste se o handler é callable +$handler = [UserController::class, 'index']; +var_dump(is_callable($handler)); // deve retornar true + +// Teste sintaxe incorreta +$wrongHandler = 'UserController@index'; +var_dump(is_callable($wrongHandler)); // retorna false +``` + +## 🎯 Exemplos Completos + +### API RESTful Completa + +```php +json(['users' => User::all()]); + } + + public function show($req, $res) { + $id = $req->param('id'); + return $res->json(['user' => User::find($id)]); + } + + public function store($req, $res) { + $data = $req->input(); + $user = User::create($data); + return $res->status(201)->json(['user' => $user]); + } + + public function update($req, $res) { + $id = $req->param('id'); + $data = $req->input(); + $user = User::update($id, $data); + return $res->json(['user' => $user]); + } + + public function destroy($req, $res) { + $id = $req->param('id'); + User::delete($id); + return $res->status(204)->send(); + } +} + +// Definir rotas RESTful +$app->get('/api/users', [UserController::class, 'index']); +$app->get('/api/users/:id', [UserController::class, 'show']); +$app->post('/api/users', [UserController::class, 'store']); +$app->put('/api/users/:id', [UserController::class, 'update']); +$app->delete('/api/users/:id', [UserController::class, 'destroy']); + +$app->run(); +``` + +--- + +**Nota:** Esta documentação reflete o estado atual de implementação do PivotPHP Core v1.1.1. Sempre consulte a documentação oficial e testes para verificar funcionalidades suportadas. \ No newline at end of file diff --git a/src/Core/Application.php b/src/Core/Application.php index 5e7b0b6..c3cf833 100644 --- a/src/Core/Application.php +++ b/src/Core/Application.php @@ -38,7 +38,7 @@ class Application /** * Versão do framework. */ - public const VERSION = '1.1.0'; + public const VERSION = '1.1.1'; /** * Container de dependências PSR-11. diff --git a/src/Http/Response.php b/src/Http/Response.php index 1fabd6c..2577033 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -5,68 +5,74 @@ use PivotPHP\Core\Http\Psr7\Response as Psr7Response; use PivotPHP\Core\Http\Psr7\Stream; use PivotPHP\Core\Http\Pool\Psr7Pool; +use PivotPHP\Core\Json\Pool\JsonBufferPool; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; use InvalidArgumentException; /** - * Classe Response híbrida que implementa PSR-7 mantendo compatibilidade Express.js + * Hybrid Response class that implements PSR-7 while maintaining Express.js compatibility * - * Esta classe oferece suporte completo a PSR-7 (ResponseInterface) - * enquanto mantém todos os métodos de conveniência do estilo Express.js - * para total compatibilidade com código existente. + * This class offers complete PSR-7 (ResponseInterface) support + * while maintaining all Express.js style convenience methods + * for full backward compatibility with existing code. */ class Response implements ResponseInterface { /** - * Instância PSR-7 interna (lazy loaded) + * Flags for consistent JSON encoding + */ + private const JSON_ENCODE_FLAGS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; + + /** + * Internal PSR-7 instance (lazy loaded) */ private ?ResponseInterface $psr7Response = null; /** - * Código de status HTTP. + * HTTP status code. */ private int $statusCode = 200; /** - * Cabeçalhos da resposta. + * Response headers. * * @var array */ private array $headers = []; /** - * Corpo da resposta. + * Response body. */ private string $body = ''; /** - * Indica se a resposta está sendo enviada como stream. + * Indicates if the response is being sent as stream. */ private bool $isStreaming = false; /** - * Buffer size para streaming (em bytes). + * Buffer size for streaming (in bytes). */ private int $streamBufferSize = 8192; /** - * Indica se está em modo teste (não faz echo direto). + * Indicates if in test mode (does not echo directly). */ private bool $testMode = false; /** - * Indica se a resposta já foi enviada. + * Indicates if the response has already been sent. */ private bool $sent = false; /** - * Indica se o controle de emissão automática está desabilitado. + * Indicates if automatic emission control is disabled. */ private bool $disableAutoEmit = false; /** - * Construtor da classe Response. + * Response class constructor. */ public function __construct() { @@ -214,10 +220,18 @@ public function json(mixed $data): self // Sanitizar dados para UTF-8 válido antes da codificação $sanitizedData = $this->sanitizeForJson($data); - $encoded = json_encode($sanitizedData); - if ($encoded === false) { - error_log('JSON encoding failed: ' . json_last_error_msg()); - $encoded = '{}'; + // Usar pooling para datasets médios e grandes + if ($this->shouldUseJsonPooling($sanitizedData)) { + $encoded = $this->encodeWithPooling($data, $sanitizedData); + } else { + // Usar encoding tradicional para dados pequenos + $encoded = json_encode($sanitizedData, self::JSON_ENCODE_FLAGS); + + // Handle JSON encoding failures for traditional path + if ($encoded === false) { + error_log('JSON encoding failed: ' . json_last_error_msg()); + $encoded = '{}'; + } } $this->body = $encoded; @@ -525,7 +539,7 @@ public function writeJson(mixed $data, bool $flush = true): self // Sanitizar dados para UTF-8 válido antes da codificação $sanitizedData = $this->sanitizeForJson($data); - $json = json_encode($sanitizedData); + $json = json_encode($sanitizedData, self::JSON_ENCODE_FLAGS); if ($json === false) { error_log('JSON encoding failed: ' . json_last_error_msg()); $json = '{}'; @@ -623,7 +637,7 @@ public function sendEvent(mixed $data, ?string $event = null, ?string $id = null // Converter dados para string if (is_array($data) || is_object($data)) { - $dataString = json_encode($data); + $dataString = json_encode($data, self::JSON_ENCODE_FLAGS); if ($dataString === false) { $dataString = '[json encoding failed]'; } @@ -760,4 +774,49 @@ public function resetSentState(): self $this->sent = false; return $this; } + + /** + * Determines if JSON pooling should be used for the given data + */ + private function shouldUseJsonPooling(mixed $data): bool + { + // Use pooling for medium and large arrays/objects + if (is_array($data)) { + $count = count($data); + return $count >= JsonBufferPool::POOLING_ARRAY_THRESHOLD; + } + + if (is_object($data)) { + $vars = get_object_vars($data); + return $vars && count($vars) >= JsonBufferPool::POOLING_OBJECT_THRESHOLD; + } + + // Use pooling for long strings + if (is_string($data)) { + return strlen($data) > JsonBufferPool::POOLING_STRING_THRESHOLD; + } + + return false; + } + + /** + * Codifica JSON usando pooling para melhor performance + */ + private function encodeWithPooling(mixed $data, mixed $sanitizedData): string + { + try { + return JsonBufferPool::encodeWithPool($sanitizedData, self::JSON_ENCODE_FLAGS); + } catch (\Throwable $e) { + // Fallback para encoding tradicional em caso de erro + error_log('JSON pooling failed, falling back to traditional encoding: ' . $e->getMessage()); + + // Fallback to traditional encoding (handle JSON encoding failures internally) + $encoded = json_encode($sanitizedData, self::JSON_ENCODE_FLAGS); + if ($encoded === false) { + error_log('JSON fallback encoding failed: ' . json_last_error_msg()); + return '{}'; + } + return $encoded; + } + } } diff --git a/src/Json/Pool/JsonBuffer.php b/src/Json/Pool/JsonBuffer.php new file mode 100644 index 0000000..f9429fe --- /dev/null +++ b/src/Json/Pool/JsonBuffer.php @@ -0,0 +1,193 @@ +capacity = $initialCapacity; + $this->buffer = ''; + $this->useStream = $initialCapacity > self::STREAM_THRESHOLD; + + if ($this->useStream) { + $stream = fopen('php://memory', 'r+'); + if ($stream === false) { + throw new \RuntimeException('Failed to open memory stream'); + } + $this->stream = $stream; + } + } + + /** + * Append string data to buffer + */ + public function append(string $data): void + { + $dataLength = strlen($data); + $requiredLength = $this->position + $dataLength; + + if ($requiredLength > $this->capacity) { + $this->expand($requiredLength); + } + + if ($this->useStream && $this->stream !== null) { + // Use stream for large buffers to avoid string reallocation + fwrite($this->stream, $data); + } else { + // Use string concatenation for small buffers + $this->buffer .= $data; + } + + $this->position += $dataLength; + } + + /** + * Append JSON-encoded value to buffer + */ + public function appendJson(mixed $value, int $flags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE): void + { + $json = json_encode($value, $flags); + if ($json === false) { + throw new \InvalidArgumentException('Failed to encode value as JSON: ' . json_last_error_msg()); + } + + $this->append($json); + } + + /** + * Finalize buffer and return complete JSON string + */ + public function finalize(): string + { + if (!$this->finalized) { + if ($this->useStream && $this->stream !== null) { + // Read all content from stream + rewind($this->stream); + $content = stream_get_contents($this->stream); + if ($content === false) { + throw new \RuntimeException('Failed to read from stream'); + } + $this->buffer = $content; + } + $this->finalized = true; + } + + return $this->buffer; + } + + /** + * Reset buffer for reuse + */ + public function reset(): void + { + $this->position = 0; + $this->finalized = false; + $this->buffer = ''; + + if ($this->useStream && $this->stream !== null) { + // Reset stream to beginning and truncate + rewind($this->stream); + ftruncate($this->stream, 0); + } + } + + /** + * Clean up resources + */ + public function __destruct() + { + if ($this->stream !== null) { + fclose($this->stream); + } + } + + /** + * Get buffer capacity + */ + public function getCapacity(): int + { + return $this->capacity; + } + + /** + * Get current buffer size (used bytes) + */ + public function getSize(): int + { + return $this->position; + } + + /** + * Get current buffer utilization percentage + */ + public function getUtilization(): float + { + return $this->capacity > 0 ? ($this->position / $this->capacity) * 100 : 0; + } + + /** + * Check if buffer has available space + */ + public function hasSpace(int $requiredBytes): bool + { + return ($this->position + $requiredBytes) <= $this->capacity; + } + + /** + * Get remaining available space in bytes + */ + public function getRemainingSpace(): int + { + return $this->capacity - $this->position; + } + + /** + * Expand buffer capacity when needed + */ + private function expand(int $requiredCapacity): void + { + $newCapacity = max($this->capacity * 2, $requiredCapacity); + + // Check if we need to migrate from string to stream + if (!$this->useStream && $newCapacity > self::STREAM_THRESHOLD) { + $this->useStream = true; + $stream = fopen('php://memory', 'r+'); + if ($stream === false) { + throw new \RuntimeException('Failed to open memory stream for expansion'); + } + $this->stream = $stream; + + // Copy existing buffer content to stream + if (!empty($this->buffer)) { + $bytesWritten = fwrite($this->stream, $this->buffer); + if ($bytesWritten === false) { + throw new \RuntimeException('Failed to write to stream during migration'); + } + $this->buffer = ''; // Clear string buffer to save memory + } + } + + $this->capacity = $newCapacity; + } +} diff --git a/src/Json/Pool/JsonBufferPool.php b/src/Json/Pool/JsonBufferPool.php new file mode 100644 index 0000000..e6dda2c --- /dev/null +++ b/src/Json/Pool/JsonBufferPool.php @@ -0,0 +1,473 @@ += 1000 items) + + // Array size thresholds + public const SMALL_ARRAY_THRESHOLD = 10; // Threshold for small array + public const MEDIUM_ARRAY_THRESHOLD = 100; // Threshold for medium array + public const LARGE_ARRAY_THRESHOLD = 1000; // Threshold for large array + + // Object size estimation constants + public const OBJECT_PROPERTY_OVERHEAD = 50; // Bytes per object property + public const OBJECT_BASE_SIZE = 100; // Base size for objects + + // Primitive type size constants + public const BOOLEAN_OR_NULL_SIZE = 10; // Size for boolean/null values + public const NUMERIC_SIZE = 20; // Size for numeric values + public const DEFAULT_ESTIMATE = 100; // Default fallback estimate + + // Buffer capacity constants + public const MIN_LARGE_BUFFER_SIZE = 65536; // Minimum size for very large buffers (64KB) + private const BUFFER_SIZE_MULTIPLIER = 2; // Multiplier for buffer size calculation + + // Pooling decision thresholds (for determining when to use pooled encoding) + public const POOLING_ARRAY_THRESHOLD = 10; // Arrays with 10+ elements use pooling + public const POOLING_OBJECT_THRESHOLD = 5; // Objects with 5+ properties use pooling + public const POOLING_STRING_THRESHOLD = 1024; // Strings longer than 1KB use pooling + + /** + * Buffer pools organized by capacity + */ + private static array $pools = []; + + /** + * Pool configuration + */ + private static array $config = [ + 'max_pool_size' => 50, + 'default_capacity' => 4096, + 'size_categories' => [ + 'small' => 1024, // 1KB + 'medium' => 4096, // 4KB + 'large' => 16384, // 16KB + 'xlarge' => 65536 // 64KB + ] + ]; + + /** + * Pool statistics + */ + private static array $stats = [ + 'allocations' => 0, + 'deallocations' => 0, + 'reuses' => 0, + 'peak_usage' => 0, + 'current_usage' => 0 + ]; + + /** + * Get a buffer from the pool or create new one + */ + public static function getBuffer(?int $capacity = null): JsonBuffer + { + $capacity = $capacity ?? self::$config['default_capacity']; + $poolKey = self::getPoolKey($capacity); + + // Extract normalized capacity from pool key to ensure buffer creation alignment + $normalizedCapacity = self::getNormalizedCapacity($capacity); + + if (!isset(self::$pools[$poolKey])) { + self::$pools[$poolKey] = []; + } + + // Try to reuse from pool + if (!empty(self::$pools[$poolKey])) { + $buffer = array_pop(self::$pools[$poolKey]); + self::$stats['reuses']++; + self::$stats['current_usage']++; + + // Reset buffer for reuse + $buffer->reset(); + return $buffer; + } + + // Create new buffer with normalized capacity to match pool key + $buffer = new JsonBuffer($normalizedCapacity); + self::$stats['allocations']++; + self::$stats['current_usage']++; + + // Update peak usage + if (self::$stats['current_usage'] > self::$stats['peak_usage']) { + self::$stats['peak_usage'] = self::$stats['current_usage']; + } + + return $buffer; + } + + /** + * Return a buffer to the pool + */ + public static function returnBuffer(JsonBuffer $buffer): void + { + $capacity = $buffer->getCapacity(); + $poolKey = self::getPoolKey($capacity); + + if (!isset(self::$pools[$poolKey])) { + self::$pools[$poolKey] = []; + } + + // Check if pool has space + if (count(self::$pools[$poolKey]) < self::$config['max_pool_size']) { + // Reset buffer before returning to pool + $buffer->reset(); + self::$pools[$poolKey][] = $buffer; + self::$stats['deallocations']++; + } + + self::$stats['current_usage']--; + } + + /** + * Encode data using pooled buffer + */ + public static function encodeWithPool( + mixed $data, + int $flags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE + ): string { + $optimalCapacity = self::getOptimalCapacity($data); + $buffer = self::getBuffer($optimalCapacity); + + try { + $buffer->appendJson($data, $flags); + return $buffer->finalize(); + } finally { + self::returnBuffer($buffer); + } + } + + /** + * Get pool statistics + */ + public static function getStatistics(): array + { + $totalOperations = (int)self::$stats['allocations'] + (int)self::$stats['reuses']; + $reuseRate = $totalOperations > 0 ? ((int)self::$stats['reuses'] / $totalOperations) * 100 : 0; + + // Map pool keys to readable format with capacity information + $poolSizes = []; + $poolsByCapacity = []; + $totalBuffersInPools = 0; + + foreach (self::$pools as $key => $pool) { + $poolSize = count($pool); + $totalBuffersInPools += $poolSize; + + // Extract capacity from key (format: "buffer_{capacity}") + if (preg_match('/^buffer_(\d+)$/', $key, $matches)) { + $capacity = (int)$matches[1]; + $readableKey = self::formatCapacity($capacity); + + $poolSizes[$readableKey] = $poolSize; + $poolsByCapacity[$capacity] = [ + 'key' => $key, + 'capacity_bytes' => $capacity, + 'capacity_formatted' => $readableKey, + 'buffers_available' => $poolSize + ]; + } else { + // Fallback for unexpected key format + $poolSizes[$key] = $poolSize; + } + } + + // Sort pools by capacity for better readability + ksort($poolsByCapacity); + + // Sort pool_sizes based on numeric capacities from poolsByCapacity + $sortedPoolSizes = []; + foreach (array_keys($poolsByCapacity) as $capacity) { + $readableKey = $poolsByCapacity[$capacity]['capacity_formatted']; + if (isset($poolSizes[$readableKey])) { + $sortedPoolSizes[$readableKey] = $poolSizes[$readableKey]; + } + } + $poolSizes = $sortedPoolSizes; + + return [ + 'reuse_rate' => round($reuseRate, 2), + 'total_operations' => $totalOperations, + 'current_usage' => self::$stats['current_usage'], + 'peak_usage' => self::$stats['peak_usage'], + 'total_buffers_pooled' => $totalBuffersInPools, + 'active_pool_count' => count(array_filter(self::$pools, fn($p) => count($p) > 0)), + 'pool_sizes' => $poolSizes, // Legacy format sorted by capacity + 'pools_by_capacity' => array_values($poolsByCapacity), // Enhanced format + 'detailed_stats' => self::$stats + ]; + } + + /** + * Format capacity in human-readable form + */ + private static function formatCapacity(int $bytes): string + { + if ($bytes >= 1024 * 1024) { + return sprintf('%.1fMB (%d bytes)', $bytes / (1024 * 1024), $bytes); + } elseif ($bytes >= 1024) { + return sprintf('%.1fKB (%d bytes)', $bytes / 1024, $bytes); + } else { + return sprintf('%d bytes', $bytes); + } + } + + /** + * Clear all pools (useful for testing) + */ + public static function clearPools(): void + { + self::$pools = []; + self::$stats = [ + 'allocations' => 0, + 'deallocations' => 0, + 'reuses' => 0, + 'peak_usage' => 0, + 'current_usage' => 0 + ]; + } + + /** + * Reset configuration to defaults (useful for testing) + */ + public static function resetConfiguration(): void + { + self::$config = [ + 'max_pool_size' => 50, + 'default_capacity' => 4096, + 'size_categories' => [ + 'small' => 1024, // 1KB + 'medium' => 4096, // 4KB + 'large' => 16384, // 16KB + 'xlarge' => 65536 // 64KB + ] + ]; + } + + /** + * Configure pool settings + */ + public static function configure(array $config): void + { + // Handle size_categories specially to allow partial updates + if (isset($config['size_categories']) && is_array($config['size_categories'])) { + $mergedCategories = array_merge( + self::$config['size_categories'] ?? [], + $config['size_categories'] + ); + + // Sort categories by size to maintain order validation + asort($mergedCategories); + $config['size_categories'] = $mergedCategories; + } + + // Validate after merging and sorting + self::validateConfiguration($config); + + self::$config = array_merge(self::$config, $config); + } + + /** + * Validate configuration parameters + */ + private static function validateConfiguration(array $config): void + { + // Validate 'max_pool_size' + if (isset($config['max_pool_size'])) { + // First check type + if (!is_int($config['max_pool_size'])) { + throw new \InvalidArgumentException("'max_pool_size' must be an integer"); + } + + // Then check range + if ($config['max_pool_size'] <= 0) { + throw new \InvalidArgumentException("'max_pool_size' must be a positive integer"); + } + if ($config['max_pool_size'] > 1000) { + throw new \InvalidArgumentException( + "'max_pool_size' cannot exceed 1000 for memory safety, got: {$config['max_pool_size']}" + ); + } + } + + // Validate 'default_capacity' + if (isset($config['default_capacity'])) { + // First check type + if (!is_int($config['default_capacity'])) { + throw new \InvalidArgumentException("'default_capacity' must be an integer"); + } + + // Then check range + if ($config['default_capacity'] <= 0) { + throw new \InvalidArgumentException("'default_capacity' must be a positive integer"); + } + if ($config['default_capacity'] > 1024 * 1024) { // 1MB limit + throw new \InvalidArgumentException( + "'default_capacity' cannot exceed 1MB (1048576 bytes), got: {$config['default_capacity']}" + ); + } + } + + // Validate 'size_categories' + if (isset($config['size_categories'])) { + // First check type + if (!is_array($config['size_categories'])) { + throw new \InvalidArgumentException("'size_categories' must be an array"); + } + + if (empty($config['size_categories'])) { + throw new \InvalidArgumentException("'size_categories' cannot be empty"); + } + + foreach ($config['size_categories'] as $name => $capacity) { + if (!is_string($name) || empty($name)) { + throw new \InvalidArgumentException("Size category names must be non-empty strings"); + } + + // First check type for each capacity + if (!is_int($capacity)) { + throw new \InvalidArgumentException( + "Size category '{$name}' must have an integer capacity" + ); + } + + // Then check range + if ($capacity <= 0) { + throw new \InvalidArgumentException( + "Size category '{$name}' must have a positive integer capacity" + ); + } + + if ($capacity > 1024 * 1024) { // 1MB limit per category + throw new \InvalidArgumentException( + "Size category '{$name}' capacity cannot exceed 1MB (1048576 bytes), got: {$capacity}" + ); + } + } + + // Validate categories are in ascending order for optimal selection + $capacities = array_values($config['size_categories']); + $sortedCapacities = $capacities; + sort($sortedCapacities); + + if ($capacities !== $sortedCapacities) { + throw new \InvalidArgumentException( + "'size_categories' should be ordered from smallest to largest capacity for optimal selection" + ); + } + } + + // Check for unknown configuration keys + $validKeys = ['max_pool_size', 'default_capacity', 'size_categories']; + $unknownKeys = array_diff(array_keys($config), $validKeys); + + if (!empty($unknownKeys)) { + throw new \InvalidArgumentException("Unknown configuration keys: " . implode(', ', $unknownKeys)); + } + } + + /** + * Get normalized capacity (next power of 2) + */ + private static function getNormalizedCapacity(int $capacity): int + { + // Normalize to power of 2 for efficient pooling + $normalizedCapacity = 1; + while ($normalizedCapacity < $capacity) { + $normalizedCapacity <<= 1; + } + + return $normalizedCapacity; + } + + /** + * Get pool key for given capacity + */ + private static function getPoolKey(int $capacity): string + { + $normalizedCapacity = self::getNormalizedCapacity($capacity); + return "buffer_{$normalizedCapacity}"; + } + + /** + * Estimate JSON size for data + */ + private static function estimateJsonSize(mixed $data): int + { + if (is_string($data)) { + return strlen($data) + self::STRING_OVERHEAD; + } + + if (is_array($data)) { + $count = count($data); + if ($count === 0) { + return self::EMPTY_ARRAY_SIZE; + } + + // Estimate based on array size + if ($count < self::SMALL_ARRAY_THRESHOLD) { + return self::SMALL_ARRAY_SIZE; + } elseif ($count < self::MEDIUM_ARRAY_THRESHOLD) { + return self::MEDIUM_ARRAY_SIZE; + } elseif ($count < self::LARGE_ARRAY_THRESHOLD) { + return self::LARGE_ARRAY_SIZE; + } else { + return self::XLARGE_ARRAY_SIZE; + } + } + + if (is_object($data)) { + $vars = get_object_vars($data); + return $vars + ? count($vars) * self::OBJECT_PROPERTY_OVERHEAD + self::OBJECT_BASE_SIZE + : self::OBJECT_BASE_SIZE; + } + + if (is_bool($data) || is_null($data)) { + return self::BOOLEAN_OR_NULL_SIZE; + } + + if (is_numeric($data)) { + return self::NUMERIC_SIZE; + } + + return self::DEFAULT_ESTIMATE; + } + + /** + * Get optimal buffer capacity for data + */ + public static function getOptimalCapacity(mixed $data): int + { + $estimatedSize = self::estimateJsonSize($data); + + // Find the smallest size category that fits + foreach (self::$config['size_categories'] as $name => $capacity) { + if ($estimatedSize <= $capacity) { + return $capacity; + } + } + + // For very large data, calculate based on estimate + return max($estimatedSize * self::BUFFER_SIZE_MULTIPLIER, self::MIN_LARGE_BUFFER_SIZE); + } +} diff --git a/tests/Core/CorsMiddlewareTestPsr15.php b/tests/Core/CorsMiddlewareTestPsr15.php index b5766c7..1fe26d7 100644 --- a/tests/Core/CorsMiddlewareTestPsr15.php +++ b/tests/Core/CorsMiddlewareTestPsr15.php @@ -1,6 +1,6 @@ response = new Response(); + $this->response->setTestMode(true); + } + + /** + * Test that both pooling and non-pooling paths produce consistent output + */ + public function testJsonEncodingConsistency(): void + { + // Test data that will trigger non-pooling path (small object) + $smallData = ['name' => 'test', 'value' => 42]; + + // Test data that will trigger pooling path (large array) + $largeData = array_fill(0, 20, ['field' => 'value', 'number' => 123]); + + // Test encoding with non-pooling path + $response1 = clone $this->response; + $response1->json($smallData); + $smallResult = $response1->getBodyAsString(); + + // Test encoding with pooling path + $response2 = clone $this->response; + $response2->json($largeData); + $largeResult = $response2->getBodyAsString(); + + // Test manual encoding with same flags + $manualSmall = json_encode($smallData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $manualLarge = json_encode($largeData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + // Verify consistency + $this->assertSame($manualSmall, $smallResult, 'Non-pooling path should match manual encoding with flags'); + $this->assertSame($manualLarge, $largeResult, 'Pooling path should match manual encoding with flags'); + } + + /** + * Test that unicode characters are handled consistently + */ + public function testUnicodeConsistency(): void + { + $unicodeData = [ + 'emoji' => '🚀', + 'accents' => 'café', + 'chinese' => '你好', + 'arabic' => 'مرحبا' + ]; + + // Small object (non-pooling) + $response1 = clone $this->response; + $response1->json($unicodeData); + $result1 = $response1->getBodyAsString(); + + // Large object (pooling) - add more fields to trigger pooling + $largeUnicodeData = array_merge($unicodeData, array_fill(0, 10, ['unicode' => '🌟'])); + $response2 = clone $this->response; + $response2->json($largeUnicodeData); + $result2 = $response2->getBodyAsString(); + + // Both should have unescaped unicode + $this->assertStringContainsString('🚀', $result1, 'Small objects should have unescaped unicode'); + $this->assertStringContainsString('🌟', $result2, 'Large objects should have unescaped unicode'); + + // Should not contain escaped unicode + $this->assertStringNotContainsString('\\u', $result1, 'Small objects should not escape unicode'); + $this->assertStringNotContainsString('\\u', $result2, 'Large objects should not escape unicode'); + } + + /** + * Test that slashes are handled consistently + */ + public function testSlashConsistency(): void + { + $slashData = [ + 'url' => 'https://example.com/path', + 'path' => '/var/www/html', + 'regex' => '/[a-zA-Z]+/' + ]; + + // Small object (non-pooling) + $response1 = clone $this->response; + $response1->json($slashData); + $result1 = $response1->getBodyAsString(); + + // Large object (pooling) + $largeSlashData = array_merge($slashData, array_fill(0, 10, ['url' => 'https://test.com/'])); + $response2 = clone $this->response; + $response2->json($largeSlashData); + $result2 = $response2->getBodyAsString(); + + // Both should have unescaped slashes + $this->assertStringContainsString( + 'https://example.com/path', + $result1, + 'Small objects should have unescaped slashes' + ); + $this->assertStringContainsString('https://test.com/', $result2, 'Large objects should have unescaped slashes'); + + // Should not contain escaped slashes + $this->assertStringNotContainsString('\\/', $result1, 'Small objects should not escape slashes'); + $this->assertStringNotContainsString('\\/', $result2, 'Large objects should not escape slashes'); + } + + /** + * Test writeJson method uses consistent flags + */ + public function testWriteJsonConsistency(): void + { + $testData = [ + 'url' => 'https://example.com/test', + 'emoji' => '✅', + 'text' => 'Hello/World' + ]; + + // Test encoding directly since we can't capture output in test mode + $expectedOutput = json_encode($testData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + // Should have unescaped content + $this->assertStringContainsString('https://example.com/test', $expectedOutput); + $this->assertStringContainsString('✅', $expectedOutput); + $this->assertStringNotContainsString('\\/', $expectedOutput); + $this->assertStringNotContainsString('\\u', $expectedOutput); + } + + /** + * Test SSE sendEvent method uses consistent flags + */ + public function testSendEventJsonConsistency(): void + { + $eventData = [ + 'message' => 'Hello/World', + 'emoji' => '🎉', + 'url' => 'https://example.com/' + ]; + + // Test encoding directly since SSE uses json_encode internally + $expectedOutput = json_encode($eventData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + // Should have unescaped content in event data + $this->assertStringContainsString('https://example.com/', $expectedOutput); + $this->assertStringContainsString('🎉', $expectedOutput); + $this->assertStringNotContainsString('\\/', $expectedOutput); + $this->assertStringNotContainsString('\\u', $expectedOutput); + } + + private function getActualOutput(): string + { + // Capture output from streaming methods + ob_start(); + // Output would be captured here in real scenario + $output = ob_get_clean(); + + // For test mode, we can't easily capture the output, + // so we'll test the encoding directly + $testData = ['url' => 'https://example.com/', 'emoji' => '🎉']; + return json_encode($testData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } +} diff --git a/tests/Json/JsonPoolingThresholdsTest.php b/tests/Json/JsonPoolingThresholdsTest.php new file mode 100644 index 0000000..7b722c8 --- /dev/null +++ b/tests/Json/JsonPoolingThresholdsTest.php @@ -0,0 +1,222 @@ +assertTrue($reflection->hasConstant('POOLING_ARRAY_THRESHOLD')); + $this->assertTrue($reflection->hasConstant('POOLING_OBJECT_THRESHOLD')); + $this->assertTrue($reflection->hasConstant('POOLING_STRING_THRESHOLD')); + + // Verify values are reasonable + $this->assertEquals(10, JsonBufferPool::POOLING_ARRAY_THRESHOLD); + $this->assertEquals(5, JsonBufferPool::POOLING_OBJECT_THRESHOLD); + $this->assertEquals(1024, JsonBufferPool::POOLING_STRING_THRESHOLD); + } + + /** + * Test that Response uses centralized thresholds + */ + public function testResponseUsesPoolingThresholds(): void + { + $response = new Response(); + $response->setTestMode(true); + + // Test array threshold - just below threshold should not pool + $smallArray = array_fill(0, JsonBufferPool::POOLING_ARRAY_THRESHOLD - 1, 'item'); + $response->json($smallArray); + + $stats = JsonBufferPool::getStatistics(); + $this->assertEquals(0, $stats['total_operations'], 'Small arrays should not use pooling'); + + // Reset + JsonBufferPool::clearPools(); + $response = new Response(); + $response->setTestMode(true); + + // Test array threshold - at threshold should pool + $mediumArray = array_fill(0, JsonBufferPool::POOLING_ARRAY_THRESHOLD, 'item'); + $response->json($mediumArray); + + $stats = JsonBufferPool::getStatistics(); + $this->assertEquals(1, $stats['total_operations'], 'Arrays at threshold should use pooling'); + } + + /** + * Test object pooling threshold consistency + */ + public function testObjectPoolingThreshold(): void + { + $response = new Response(); + $response->setTestMode(true); + + // Create object just below threshold + $smallObject = new \stdClass(); + for ($i = 0; $i < JsonBufferPool::POOLING_OBJECT_THRESHOLD - 1; $i++) { + $smallObject->{"prop{$i}"} = "value{$i}"; + } + + $response->json($smallObject); + $stats = JsonBufferPool::getStatistics(); + $this->assertEquals(0, $stats['total_operations'], 'Small objects should not use pooling'); + + // Reset + JsonBufferPool::clearPools(); + $response = new Response(); + $response->setTestMode(true); + + // Create object at threshold + $mediumObject = new \stdClass(); + for ($i = 0; $i < JsonBufferPool::POOLING_OBJECT_THRESHOLD; $i++) { + $mediumObject->{"prop{$i}"} = "value{$i}"; + } + + $response->json($mediumObject); + $stats = JsonBufferPool::getStatistics(); + $this->assertEquals(1, $stats['total_operations'], 'Objects at threshold should use pooling'); + } + + /** + * Test string pooling threshold consistency + */ + public function testStringPoolingThreshold(): void + { + $response = new Response(); + $response->setTestMode(true); + + // String just under threshold + $shortString = str_repeat('x', JsonBufferPool::POOLING_STRING_THRESHOLD); + $response->json($shortString); + + $stats = JsonBufferPool::getStatistics(); + $this->assertEquals(0, $stats['total_operations'], 'Short strings should not use pooling'); + + // Reset + JsonBufferPool::clearPools(); + $response = new Response(); + $response->setTestMode(true); + + // String over threshold + $longString = str_repeat('x', JsonBufferPool::POOLING_STRING_THRESHOLD + 1); + $response->json($longString); + + $stats = JsonBufferPool::getStatistics(); + $this->assertEquals(1, $stats['total_operations'], 'Long strings should use pooling'); + } + + /** + * Test that thresholds are reasonable for performance + */ + public function testThresholdsAreReasonable(): void + { + // Array threshold should be high enough to avoid pooling small arrays + $this->assertGreaterThanOrEqual(5, JsonBufferPool::POOLING_ARRAY_THRESHOLD); + $this->assertLessThanOrEqual(50, JsonBufferPool::POOLING_ARRAY_THRESHOLD); + + // Object threshold should be reasonable for common objects + $this->assertGreaterThanOrEqual(3, JsonBufferPool::POOLING_OBJECT_THRESHOLD); + $this->assertLessThanOrEqual(20, JsonBufferPool::POOLING_OBJECT_THRESHOLD); + + // String threshold should be reasonable (around 1KB) + $this->assertGreaterThanOrEqual(512, JsonBufferPool::POOLING_STRING_THRESHOLD); + $this->assertLessThanOrEqual(4096, JsonBufferPool::POOLING_STRING_THRESHOLD); + } + + /** + * Test consistency between direct pooling and Response pooling + */ + public function testConsistencyBetweenDirectAndResponsePooling(): void + { + $testData = array_fill(0, JsonBufferPool::POOLING_ARRAY_THRESHOLD, 'test'); + + // Direct pooling + JsonBufferPool::clearPools(); + $directResult = JsonBufferPool::encodeWithPool($testData); + $directStats = JsonBufferPool::getStatistics(); + + // Response pooling + JsonBufferPool::clearPools(); + $response = new Response(); + $response->setTestMode(true); + $response->json($testData); + $responseResult = $response->getBodyAsString(); + $responseStats = JsonBufferPool::getStatistics(); + + // Results should be identical + $this->assertEquals($directResult, $responseResult); + + // Both should have used pooling + $this->assertEquals(1, $directStats['total_operations']); + $this->assertEquals(1, $responseStats['total_operations']); + } + + /** + * Test that updating centralized constants affects both components + */ + public function testCentralizedConstantsAffectBothComponents(): void + { + // This test verifies that the constants are truly centralized + // by checking that Response uses the same values as JsonBufferPool + + $reflection = new \ReflectionClass('PivotPHP\Core\Http\Response'); + $shouldUsePoolingMethod = $reflection->getMethod('shouldUseJsonPooling'); + $shouldUsePoolingMethod->setAccessible(true); + + $response = new Response(); + + // Test array threshold boundary + $arrayAtThreshold = array_fill(0, JsonBufferPool::POOLING_ARRAY_THRESHOLD, 'item'); + $arrayBelowThreshold = array_fill(0, JsonBufferPool::POOLING_ARRAY_THRESHOLD - 1, 'item'); + + $this->assertTrue($shouldUsePoolingMethod->invoke($response, $arrayAtThreshold)); + $this->assertFalse($shouldUsePoolingMethod->invoke($response, $arrayBelowThreshold)); + + // Test object threshold boundary + $objectAtThreshold = new \stdClass(); + for ($i = 0; $i < JsonBufferPool::POOLING_OBJECT_THRESHOLD; $i++) { + $objectAtThreshold->{"prop{$i}"} = "value{$i}"; + } + + $objectBelowThreshold = new \stdClass(); + for ($i = 0; $i < JsonBufferPool::POOLING_OBJECT_THRESHOLD - 1; $i++) { + $objectBelowThreshold->{"prop{$i}"} = "value{$i}"; + } + + $this->assertTrue($shouldUsePoolingMethod->invoke($response, $objectAtThreshold)); + $this->assertFalse($shouldUsePoolingMethod->invoke($response, $objectBelowThreshold)); + + // Test string threshold boundary + $stringAtThreshold = str_repeat('x', JsonBufferPool::POOLING_STRING_THRESHOLD + 1); + $stringBelowThreshold = str_repeat('x', JsonBufferPool::POOLING_STRING_THRESHOLD); + + $this->assertTrue($shouldUsePoolingMethod->invoke($response, $stringAtThreshold)); + $this->assertFalse($shouldUsePoolingMethod->invoke($response, $stringBelowThreshold)); + } +} diff --git a/tests/Json/Pool/JsonBufferPoolCapacityAlignmentTest.php b/tests/Json/Pool/JsonBufferPoolCapacityAlignmentTest.php new file mode 100644 index 0000000..68c3287 --- /dev/null +++ b/tests/Json/Pool/JsonBufferPoolCapacityAlignmentTest.php @@ -0,0 +1,196 @@ + 1024 (next power of 2) + [1024, 1024], // 1024 -> 1024 (already power of 2) + [2000, 2048], // 2000 -> 2048 + [512, 512], // 512 -> 512 (already power of 2) + [513, 1024], // 513 -> 1024 + [100, 128], // 100 -> 128 + [1, 1], // 1 -> 1 (minimum) + ]; + + foreach ($testCases as [$requestedCapacity, $expectedCapacity]) { + // Get buffer from pool + $buffer = JsonBufferPool::getBuffer($requestedCapacity); + + // Verify that the actual buffer capacity matches the expected normalized capacity + $this->assertEquals( + $expectedCapacity, + $buffer->getCapacity(), + "Buffer requested with capacity {$requestedCapacity} should have " . + "normalized capacity {$expectedCapacity}" + ); + + // Return buffer to pool + JsonBufferPool::returnBuffer($buffer); + } + } + + /** + * Test that returned buffers can be properly reused + */ + public function testBufferReuseWithNormalizedCapacity(): void + { + // Request buffer with non-power-of-2 capacity + $originalBuffer = JsonBufferPool::getBuffer(1000); + $expectedCapacity = 1024; // Should be normalized to 1024 + + $this->assertEquals($expectedCapacity, $originalBuffer->getCapacity()); + + // Return buffer to pool + JsonBufferPool::returnBuffer($originalBuffer); + + // Request buffer with the same non-power-of-2 capacity + $reusedBuffer = JsonBufferPool::getBuffer(1000); + + // Should get the same buffer back (reused) + $this->assertSame($originalBuffer, $reusedBuffer); + $this->assertEquals($expectedCapacity, $reusedBuffer->getCapacity()); + + // Also test requesting with the exact normalized capacity + JsonBufferPool::returnBuffer($reusedBuffer); + $exactCapacityBuffer = JsonBufferPool::getBuffer(1024); + + // Should get the same buffer back + $this->assertSame($originalBuffer, $exactCapacityBuffer); + $this->assertEquals($expectedCapacity, $exactCapacityBuffer->getCapacity()); + } + + /** + * Test pool statistics accuracy with normalized capacities + */ + public function testPoolStatisticsWithNormalizedCapacities(): void + { + // Request buffers with various capacities + $buffer1 = JsonBufferPool::getBuffer(1000); // -> 1024 + $buffer2 = JsonBufferPool::getBuffer(1024); // -> 1024 (same pool) + $buffer3 = JsonBufferPool::getBuffer(2000); // -> 2048 (different pool) + + // All buffers should have normalized capacities + $this->assertEquals(1024, $buffer1->getCapacity()); + $this->assertEquals(1024, $buffer2->getCapacity()); + $this->assertEquals(2048, $buffer3->getCapacity()); + + $stats = JsonBufferPool::getStatistics(); + $this->assertEquals(3, $stats['detailed_stats']['allocations']); + $this->assertEquals(0, $stats['detailed_stats']['reuses']); // No reuse yet + + // Return buffers to pool + JsonBufferPool::returnBuffer($buffer1); + JsonBufferPool::returnBuffer($buffer2); + JsonBufferPool::returnBuffer($buffer3); + + // Request again - should reuse + $reusedBuffer1 = JsonBufferPool::getBuffer(1000); // Should reuse from 1024 pool + $reusedBuffer2 = JsonBufferPool::getBuffer(2000); // Should reuse from 2048 pool + + $stats = JsonBufferPool::getStatistics(); + $this->assertEquals(3, $stats['detailed_stats']['allocations']); // No new allocations + $this->assertEquals(2, $stats['detailed_stats']['reuses']); // 2 reuses + + // Verify we got the right buffers back + $this->assertEquals(1024, $reusedBuffer1->getCapacity()); + $this->assertEquals(2048, $reusedBuffer2->getCapacity()); + } + + /** + * Test that pool keys are consistent for the same normalized capacity + */ + public function testPoolKeyConsistency(): void + { + $reflection = new ReflectionClass(JsonBufferPool::class); + $getPoolKeyMethod = $reflection->getMethod('getPoolKey'); + $getPoolKeyMethod->setAccessible(true); + + // Test that different requested capacities that normalize to the same value + // generate the same pool key + $key1000 = $getPoolKeyMethod->invoke(null, 1000); + $key1024 = $getPoolKeyMethod->invoke(null, 1024); + $key900 = $getPoolKeyMethod->invoke(null, 900); + + // All should normalize to 1024 + $this->assertEquals('buffer_1024', $key1000); + $this->assertEquals('buffer_1024', $key1024); + $this->assertEquals('buffer_1024', $key900); + + // Different normalized capacity should have different key + $key2000 = $getPoolKeyMethod->invoke(null, 2000); + $this->assertEquals('buffer_2048', $key2000); + $this->assertNotEquals($key1000, $key2000); + } + + /** + * Test edge case with capacity 1 (minimum) + */ + public function testMinimumCapacityAlignment(): void + { + $buffer = JsonBufferPool::getBuffer(1); + $this->assertEquals(1, $buffer->getCapacity()); + + JsonBufferPool::returnBuffer($buffer); + + // Should be able to reuse + $reusedBuffer = JsonBufferPool::getBuffer(1); + $this->assertSame($buffer, $reusedBuffer); + } + + /** + * Test that buffer creation and return cycle maintains capacity consistency + */ + public function testCapacityConsistencyThroughCycle(): void + { + $originalCapacity = 1500; // Will be normalized to 2048 + $expectedCapacity = 2048; + + // Create and return buffer multiple times + for ($i = 0; $i < 5; $i++) { + $buffer = JsonBufferPool::getBuffer($originalCapacity); + $this->assertEquals( + $expectedCapacity, + $buffer->getCapacity(), + "Iteration {$i}: Buffer capacity should remain consistent" + ); + + JsonBufferPool::returnBuffer($buffer); + } + + // Final check - should still get normalized capacity + $finalBuffer = JsonBufferPool::getBuffer($originalCapacity); + $this->assertEquals($expectedCapacity, $finalBuffer->getCapacity()); + } +} diff --git a/tests/Json/Pool/JsonBufferPoolConfigMergeTest.php b/tests/Json/Pool/JsonBufferPoolConfigMergeTest.php new file mode 100644 index 0000000..843f9bb --- /dev/null +++ b/tests/Json/Pool/JsonBufferPoolConfigMergeTest.php @@ -0,0 +1,298 @@ +reflection = new ReflectionClass(JsonBufferPool::class); + } + + protected function tearDown(): void + { + // Clear pools and reset configuration after each test + JsonBufferPool::clearPools(); + JsonBufferPool::resetConfiguration(); + } + + private function getConfig(): array + { + $configProperty = $this->reflection->getProperty('config'); + $configProperty->setAccessible(true); + return $configProperty->getValue(); + } + + /** + * Test basic configuration update + */ + public function testBasicConfigurationUpdate(): void + { + // Update only max_pool_size + JsonBufferPool::configure(['max_pool_size' => 100]); + + $config = $this->getConfig(); + + $this->assertEquals(100, $config['max_pool_size']); + $this->assertEquals(4096, $config['default_capacity']); // Should remain unchanged + $this->assertIsArray($config['size_categories']); // Should remain unchanged + } + + /** + * Test partial size_categories update preserves existing categories + */ + public function testPartialSizeCategoriesUpdate(): void + { + // First, configure with some custom categories + JsonBufferPool::configure( + [ + 'size_categories' => [ + 'tiny' => 256, + 'small' => 1024, + 'medium' => 4096, + 'large' => 16384, + 'custom' => 8192 + ] + ] + ); + + $config = $this->getConfig(); + $this->assertEquals(256, $config['size_categories']['tiny']); + $this->assertEquals(8192, $config['size_categories']['custom']); + + // Now update only some categories + JsonBufferPool::configure( + [ + 'size_categories' => [ + 'medium' => 8192, // Change existing + 'xlarge' => 65536 // Add new + ] + ] + ); + + $config = $this->getConfig(); + + // Should preserve existing categories + $this->assertEquals(256, $config['size_categories']['tiny']); + $this->assertEquals(1024, $config['size_categories']['small']); + $this->assertEquals(16384, $config['size_categories']['large']); + $this->assertEquals(8192, $config['size_categories']['custom']); + + // Should update changed category + $this->assertEquals(8192, $config['size_categories']['medium']); + + // Should add new category + $this->assertEquals(65536, $config['size_categories']['xlarge']); + } + + /** + * Test complete size_categories replacement + */ + public function testCompleteSizeCategoriesReplacement(): void + { + // Start with default categories + $originalConfig = $this->getConfig(); + $this->assertCount(4, $originalConfig['size_categories']); + + // Replace with completely new set + JsonBufferPool::configure( + [ + 'size_categories' => [ + 'micro' => 128, + 'mini' => 512 + ] + ] + ); + + $config = $this->getConfig(); + + // Should have both old and new categories merged + $this->assertArrayHasKey('small', $config['size_categories']); // Original + $this->assertArrayHasKey('medium', $config['size_categories']); // Original + $this->assertEquals(128, $config['size_categories']['micro']); // New + $this->assertEquals(512, $config['size_categories']['mini']); // New + + // Total should be original + new categories + $this->assertGreaterThanOrEqual(6, count($config['size_categories'])); + } + + /** + * Test mixed configuration update + */ + public function testMixedConfigurationUpdate(): void + { + // Configure initial state + JsonBufferPool::configure( + [ + 'max_pool_size' => 50, + 'size_categories' => [ + 'custom1' => 2048, + 'custom2' => 8192 + ] + ] + ); + + // Update mix of scalar and array values + JsonBufferPool::configure( + [ + 'max_pool_size' => 150, // Scalar update + 'default_capacity' => 16384, // New scalar + 'size_categories' => [ + 'custom2' => 12288, // Update existing category + 'custom3' => 32768 // Add new category + ] + ] + ); + + $config = $this->getConfig(); + + // Check scalar values + $this->assertEquals(150, $config['max_pool_size']); + $this->assertEquals(16384, $config['default_capacity']); + + // Check size_categories merge + $this->assertEquals(2048, $config['size_categories']['custom1']); // Preserved + $this->assertEquals(12288, $config['size_categories']['custom2']); // Updated + $this->assertEquals(32768, $config['size_categories']['custom3']); // Added + + // Original categories should still exist + $this->assertArrayHasKey('small', $config['size_categories']); + $this->assertArrayHasKey('medium', $config['size_categories']); + } + + /** + * Test that validation still works with partial updates + */ + public function testValidationWithPartialUpdates(): void + { + // This should work - valid partial update + JsonBufferPool::configure( + [ + 'size_categories' => [ + 'valid_category' => 2048 + ] + ] + ); + + $this->assertTrue(true); // No exception thrown + + // This should fail - invalid value in partial update + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("Size category 'invalid_category' must have a positive integer capacity"); + + JsonBufferPool::configure( + [ + 'size_categories' => [ + 'invalid_category' => -1000 + ] + ] + ); + } + + /** + * Test automatic ordering with partial updates + */ + public function testAutomaticOrderingWithPartialUpdates(): void + { + // Set initial categories + JsonBufferPool::configure( + [ + 'size_categories' => [ + 'tiny' => 512, + 'small' => 1024, + 'large' => 4096 + ] + ] + ); + + // Add categories in random order - should be automatically sorted + JsonBufferPool::configure( + [ + 'size_categories' => [ + 'xlarge' => 8192, // Larger than existing + 'medium' => 2048, // Between existing + 'micro' => 256 // Smaller than existing + ] + ] + ); + + $config = $this->getConfig(); + $categories = $config['size_categories']; + + // Verify automatic sorting - values should be in ascending order + $values = array_values($categories); + $sortedValues = $values; + sort($sortedValues); + + $this->assertEquals($sortedValues, $values, 'Categories should be automatically sorted by size'); + + // Verify specific ordering + $expectedOrder = [256, 512, 1024, 2048, 4096, 8192]; + $this->assertEquals($expectedOrder, array_values($categories)); + } + + /** + * Test empty size_categories update + */ + public function testEmptySizeCategoriesUpdate(): void + { + // Clear existing config first to test empty array validation + $configProperty = $this->reflection->getProperty('config'); + $configProperty->setAccessible(true); + $configProperty->setValue(null, [ + 'max_pool_size' => 50, + 'default_capacity' => 4096, + 'size_categories' => [] // Start with empty + ]); + + // Should fail validation when trying to configure with empty array + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("'size_categories' cannot be empty"); + + JsonBufferPool::configure( + [ + 'size_categories' => [] + ] + ); + } + + /** + * Test null safety in size_categories merge + */ + public function testNullSafetyInMerge(): void + { + // Clear config to test null safety + $configProperty = $this->reflection->getProperty('config'); + $configProperty->setAccessible(true); + $config = $configProperty->getValue(); + unset($config['size_categories']); + $configProperty->setValue(null, $config); + + // This should not crash even if size_categories is missing + JsonBufferPool::configure( + [ + 'size_categories' => [ + 'new_category' => 1024 + ] + ] + ); + + $updatedConfig = $this->getConfig(); + $this->assertEquals(1024, $updatedConfig['size_categories']['new_category']); + } +} diff --git a/tests/Json/Pool/JsonBufferPoolConfigValidationTest.php b/tests/Json/Pool/JsonBufferPoolConfigValidationTest.php new file mode 100644 index 0000000..54ac096 --- /dev/null +++ b/tests/Json/Pool/JsonBufferPoolConfigValidationTest.php @@ -0,0 +1,334 @@ + 100, + 'default_capacity' => 8192, + 'size_categories' => [ + 'tiny' => 512, + 'small' => 1024, + 'medium' => 4096, + 'large' => 16384 + ] + ]; + + // Should not throw exception + JsonBufferPool::configure($validConfig); + $this->assertTrue(true); // Assertion to confirm test ran + } + + /** + * Test max_pool_size validation + */ + public function testMaxPoolSizeValidation(): void + { + // Test negative value + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("'max_pool_size' must be a positive integer"); + JsonBufferPool::configure(['max_pool_size' => -1]); + } + + public function testMaxPoolSizeZeroInvalid(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("'max_pool_size' must be a positive integer"); + JsonBufferPool::configure(['max_pool_size' => 0]); + } + + public function testMaxPoolSizeTypeValidation(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("'max_pool_size' must be an integer"); + JsonBufferPool::configure(['max_pool_size' => '100']); + } + + public function testMaxPoolSizeUpperLimit(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("'max_pool_size' cannot exceed 1000 for memory safety, got: 1001"); + JsonBufferPool::configure(['max_pool_size' => 1001]); + } + + /** + * Test default_capacity validation + */ + public function testDefaultCapacityValidation(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("'default_capacity' must be a positive integer"); + JsonBufferPool::configure(['default_capacity' => -1]); + } + + public function testDefaultCapacityTypeValidation(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("'default_capacity' must be an integer"); + JsonBufferPool::configure(['default_capacity' => 4096.5]); + } + + public function testDefaultCapacityUpperLimit(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("'default_capacity' cannot exceed 1MB (1048576 bytes), got: 1048577"); + JsonBufferPool::configure(['default_capacity' => 1048577]); + } + + /** + * Test size_categories validation + */ + public function testSizeCategoriesTypeValidation(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("'size_categories' must be an array"); + JsonBufferPool::configure(['size_categories' => 'invalid']); + } + + public function testSizeCategoriesEmptyArrayInvalid(): void + { + // Reset to empty config first + JsonBufferPool::resetConfiguration(); + $reflection = new \ReflectionClass(JsonBufferPool::class); + $configProperty = $reflection->getProperty('config'); + $configProperty->setAccessible(true); + $configProperty->setValue(null, [ + 'max_pool_size' => 50, + 'default_capacity' => 4096, + 'size_categories' => [] + ]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("'size_categories' cannot be empty"); + JsonBufferPool::configure(['size_categories' => []]); + } + + public function testSizeCategoriesNameValidation(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Size category names must be non-empty strings"); + JsonBufferPool::configure( + [ + 'size_categories' => [ + 123 => 1024 // Invalid numeric key + ] + ] + ); + } + + public function testSizeCategoriesEmptyNameInvalid(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Size category names must be non-empty strings"); + JsonBufferPool::configure( + [ + 'size_categories' => [ + '' => 1024 // Empty string key + ] + ] + ); + } + + public function testSizeCategoriesCapacityValidation(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Size category 'small' must have an integer capacity"); + JsonBufferPool::configure( + [ + 'size_categories' => [ + 'small' => 'invalid' + ] + ] + ); + } + + public function testSizeCategoriesCapacityZeroInvalid(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Size category 'small' must have a positive integer capacity"); + JsonBufferPool::configure( + [ + 'size_categories' => [ + 'small' => 0 + ] + ] + ); + } + + public function testSizeCategoriesCapacityUpperLimit(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Size category 'huge' capacity cannot exceed 1MB (1048576 bytes), got: 1048577"); + JsonBufferPool::configure( + [ + 'size_categories' => [ + 'huge' => 1048577 + ] + ] + ); + } + + public function testSizeCategoriesAutoSorting(): void + { + // Categories should be automatically sorted, so this should NOT throw exception + JsonBufferPool::configure( + [ + 'size_categories' => [ + 'large' => 16384, + 'small' => 1024, // Out of order - will be auto-sorted + 'medium' => 4096 + ] + ] + ); + + // Get the configuration to verify it was sorted + $reflection = new \ReflectionClass(JsonBufferPool::class); + $configProperty = $reflection->getProperty('config'); + $configProperty->setAccessible(true); + $config = $configProperty->getValue(); + + // Verify that categories are now in ascending order by value + $values = array_values($config['size_categories']); + $sortedValues = $values; + sort($sortedValues); + + $this->assertEquals($sortedValues, $values, 'Categories should be automatically sorted by size'); + + // Verify that our specific values are in the right order (there may be other default values) + $categoryValues = $config['size_categories']; + $this->assertContains(1024, $categoryValues); + $this->assertContains(4096, $categoryValues); + $this->assertContains(16384, $categoryValues); + + // Find positions of our values + $values = array_values($categoryValues); + $pos1024 = array_search(1024, $values); + $pos4096 = array_search(4096, $values); + $pos16384 = array_search(16384, $values); + + // Verify they are in correct relative order + $this->assertLessThan($pos4096, $pos1024, '1024 should come before 4096'); + $this->assertLessThan($pos16384, $pos4096, '4096 should come before 16384'); + } + + /** + * Test unknown configuration keys + */ + public function testUnknownConfigurationKeys(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Unknown configuration keys: unknown_key, another_unknown"); + JsonBufferPool::configure( + [ + 'max_pool_size' => 50, + 'unknown_key' => 'value', + 'another_unknown' => 123 + ] + ); + } + + /** + * Test that valid boundary values work + */ + public function testValidBoundaryValues(): void + { + // Test maximum allowed values + $maxConfig = [ + 'max_pool_size' => 1000, + 'default_capacity' => 1048576, // 1MB + 'size_categories' => [ + 'max' => 1048576 // 1MB + ] + ]; + + JsonBufferPool::configure($maxConfig); + $this->assertTrue(true); + + // Test minimum allowed values + $minConfig = [ + 'max_pool_size' => 1, + 'default_capacity' => 1, + 'size_categories' => [ + 'min' => 1 + ] + ]; + + JsonBufferPool::configure($minConfig); + $this->assertTrue(true); + } + + /** + * Test partial configuration updates + */ + public function testPartialConfigurationUpdates(): void + { + // Configure only max_pool_size + JsonBufferPool::configure(['max_pool_size' => 75]); + $this->assertTrue(true); + + // Configure only default_capacity + JsonBufferPool::configure(['default_capacity' => 2048]); + $this->assertTrue(true); + + // Configure only size_categories + JsonBufferPool::configure( + [ + 'size_categories' => [ + 'custom_small' => 512, + 'custom_large' => 8192 + ] + ] + ); + $this->assertTrue(true); + } + + /** + * Test that configuration merges correctly with existing config + */ + public function testConfigurationMerging(): void + { + // Set initial config + JsonBufferPool::configure( + [ + 'max_pool_size' => 100, + 'default_capacity' => 4096 + ] + ); + + // Update only one value + JsonBufferPool::configure(['max_pool_size' => 200]); + + // Test that we can still get a buffer (indicating config is valid) + $buffer = JsonBufferPool::getBuffer(); + $this->assertInstanceOf(\PivotPHP\Core\Json\Pool\JsonBuffer::class, $buffer); + } +} diff --git a/tests/Json/Pool/JsonBufferPoolEncodeTest.php b/tests/Json/Pool/JsonBufferPoolEncodeTest.php new file mode 100644 index 0000000..1392b62 --- /dev/null +++ b/tests/Json/Pool/JsonBufferPoolEncodeTest.php @@ -0,0 +1,207 @@ + 1, 'name' => 'test']; + $json1 = JsonBufferPool::encodeWithPool($smallData); + + // Test medium data that should use 4KB buffer + $mediumData = array_fill(0, 50, ['field' => 'value', 'num' => 123]); + $json2 = JsonBufferPool::encodeWithPool($mediumData); + + // Test large data that should use 16KB buffer + $largeData = array_fill(0, 500, ['item' => 'data', 'id' => rand(1, 1000)]); + $json3 = JsonBufferPool::encodeWithPool($largeData); + + $stats = JsonBufferPool::getStatistics(); + $poolSizes = $stats['pool_sizes']; + + // Should have created standard sized pools, not arbitrary ones + $this->assertArrayHasKey('1.0KB (1024 bytes)', $poolSizes); + $this->assertArrayHasKey('4.0KB (4096 bytes)', $poolSizes); + $this->assertArrayHasKey('16.0KB (16384 bytes)', $poolSizes); + + // Each pool should have exactly 1 buffer returned to it + $this->assertEquals(1, $poolSizes['1.0KB (1024 bytes)']); + $this->assertEquals(1, $poolSizes['4.0KB (4096 bytes)']); + $this->assertEquals(1, $poolSizes['16.0KB (16384 bytes)']); + + // Verify JSON output is correct + $this->assertIsString($json1); + $this->assertIsString($json2); + $this->assertIsString($json3); + + $this->assertStringContainsString('test', $json1); + $this->assertStringContainsString('value', $json2); + $this->assertStringContainsString('data', $json3); + } + + /** + * Test that multiple calls with similar data reuse same pool + */ + public function testEncodeWithPoolReusesBuffers(): void + { + // Encode similar sized data multiple times + for ($i = 0; $i < 5; $i++) { + $data = ['iteration' => $i, 'test' => 'data']; + $json = JsonBufferPool::encodeWithPool($data); + $this->assertStringContainsString((string)$i, $json); + } + + $stats = JsonBufferPool::getStatistics(); + + // Should have high reuse rate since all data uses same buffer size + $this->assertEquals(5, $stats['total_operations']); + $this->assertEquals(4, $stats['detailed_stats']['reuses']); // 4 reuses (first is allocation) + $this->assertEquals(80.0, $stats['reuse_rate']); // 4/5 * 100 = 80% + + // Should only have one pool type + $this->assertEquals(1, $stats['active_pool_count']); + $this->assertArrayHasKey('1.0KB (1024 bytes)', $stats['pool_sizes']); + } + + /** + * Test edge case with very large data + */ + public function testEncodeWithPoolLargeData(): void + { + // Create data that exceeds standard categories + $veryLargeData = array_fill(0, 2000, ['id' => rand(1, 10000), 'data' => str_repeat('x', 50)]); + $json = JsonBufferPool::encodeWithPool($veryLargeData); + + $this->assertIsString($json); + $this->assertGreaterThan(100000, strlen($json)); // Should be large JSON + + $stats = JsonBufferPool::getStatistics(); + $poolsByCapacity = $stats['pools_by_capacity']; + + // Should have created a large custom capacity pool + $this->assertNotEmpty($poolsByCapacity); + $largestPool = end($poolsByCapacity); + $this->assertGreaterThanOrEqual(65536, $largestPool['capacity_bytes']); // At least 64KB + } + + /** + * Test that different data types get appropriate buffer sizes + */ + public function testEncodeWithPoolDataTypeOptimization(): void + { + // String data - should use small buffer + $stringData = 'This is a simple string'; + JsonBufferPool::encodeWithPool($stringData); + + // Array data - should use appropriately sized buffer + $arrayData = range(1, 100); + JsonBufferPool::encodeWithPool($arrayData); + + // Object data - should use appropriately sized buffer + $objectData = (object)array_fill_keys(range('a', 'z'), 'value'); + JsonBufferPool::encodeWithPool($objectData); + + $stats = JsonBufferPool::getStatistics(); + + // Should have multiple pool sizes for different data types + $this->assertGreaterThanOrEqual(2, $stats['active_pool_count']); + $this->assertGreaterThanOrEqual(2, count($stats['pool_sizes'])); + } + + /** + * Test consistency between encodeWithPool and manual buffer usage + */ + public function testEncodeWithPoolConsistency(): void + { + $testData = ['message' => 'Hello World', 'count' => 42, 'active' => true]; + + // Encode using pool + $pooledResult = JsonBufferPool::encodeWithPool($testData); + + // Encode manually with same flags + $manualResult = json_encode($testData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + // Results should be identical + $this->assertEquals($manualResult, $pooledResult); + $this->assertStringContainsString('Hello World', $pooledResult); + $this->assertStringContainsString('42', $pooledResult); + $this->assertStringContainsString('true', $pooledResult); + } + + /** + * Test that encodeWithPool handles encoding failures gracefully + */ + public function testEncodeWithPoolErrorHandling(): void + { + // Create data that should encode fine + $validData = ['test' => 'data']; + $result = JsonBufferPool::encodeWithPool($validData); + + $this->assertIsString($result); + $this->assertEquals('{"test":"data"}', $result); + + // Verify buffer was returned to pool even after successful encoding + $stats = JsonBufferPool::getStatistics(); + $this->assertEquals(1, $stats['total_buffers_pooled']); + } + + /** + * Test memory efficiency with optimal capacity selection + */ + public function testEncodeWithPoolMemoryEfficiency(): void + { + $memBefore = memory_get_usage(); + + // Encode various sized data multiple times + for ($i = 0; $i < 10; $i++) { + // Small data + JsonBufferPool::encodeWithPool(['small' => $i]); + + // Medium data + JsonBufferPool::encodeWithPool(array_fill(0, 20, ['med' => $i])); + + // Large data + JsonBufferPool::encodeWithPool(array_fill(0, 100, ['large' => $i])); + } + + $memAfter = memory_get_usage(); + $stats = JsonBufferPool::getStatistics(); + + // Memory growth should be reasonable due to buffer reuse + $memoryGrowth = $memAfter - $memBefore; + $this->assertLessThan(1024 * 1024, $memoryGrowth); // Less than 1MB growth + + // Should have high reuse rate + $this->assertGreaterThan(70, $stats['reuse_rate']); // At least 70% reuse + + // Should have created standard pool sizes + $this->assertEquals(3, $stats['active_pool_count']); // 3 different sizes + } +} diff --git a/tests/Json/Pool/JsonBufferPoolStatisticsTest.php b/tests/Json/Pool/JsonBufferPoolStatisticsTest.php new file mode 100644 index 0000000..d1bbc48 --- /dev/null +++ b/tests/Json/Pool/JsonBufferPoolStatisticsTest.php @@ -0,0 +1,332 @@ +assertArrayHasKey($key, $stats, "Missing key: {$key}"); + } + } + + /** + * Test enhanced pool statistics with multiple buffer sizes + */ + public function testEnhancedPoolStatistics(): void + { + // Create buffers of different sizes and return them to pools + $buffer1KB = JsonBufferPool::getBuffer(1024); + $buffer4KB = JsonBufferPool::getBuffer(4096); + $buffer8KB = JsonBufferPool::getBuffer(8192); + $buffer16KB = JsonBufferPool::getBuffer(16384); + + JsonBufferPool::returnBuffer($buffer1KB); + JsonBufferPool::returnBuffer($buffer4KB); + JsonBufferPool::returnBuffer($buffer8KB); + JsonBufferPool::returnBuffer($buffer16KB); + + $stats = JsonBufferPool::getStatistics(); + + // Check enhanced statistics + $this->assertEquals(4, $stats['total_buffers_pooled']); + $this->assertEquals(4, $stats['active_pool_count']); + + // Check pool_sizes has readable format + $this->assertArrayHasKey('1.0KB (1024 bytes)', $stats['pool_sizes']); + $this->assertArrayHasKey('4.0KB (4096 bytes)', $stats['pool_sizes']); + $this->assertArrayHasKey('8.0KB (8192 bytes)', $stats['pool_sizes']); + $this->assertArrayHasKey('16.0KB (16384 bytes)', $stats['pool_sizes']); + + // Check each pool has exactly 1 buffer + $this->assertEquals(1, $stats['pool_sizes']['1.0KB (1024 bytes)']); + $this->assertEquals(1, $stats['pool_sizes']['4.0KB (4096 bytes)']); + $this->assertEquals(1, $stats['pool_sizes']['8.0KB (8192 bytes)']); + $this->assertEquals(1, $stats['pool_sizes']['16.0KB (16384 bytes)']); + } + + /** + * Test pools_by_capacity enhanced format + */ + public function testPoolsByCapacityFormat(): void + { + // Create and return buffers + $buffer1KB = JsonBufferPool::getBuffer(1024); + $buffer4KB = JsonBufferPool::getBuffer(4096); + JsonBufferPool::returnBuffer($buffer1KB); + JsonBufferPool::returnBuffer($buffer4KB); + + $stats = JsonBufferPool::getStatistics(); + $poolsByCapacity = $stats['pools_by_capacity']; + + $this->assertIsArray($poolsByCapacity); + $this->assertCount(2, $poolsByCapacity); + + // Check first pool (should be sorted by capacity) + $firstPool = $poolsByCapacity[0]; + $this->assertArrayHasKey('key', $firstPool); + $this->assertArrayHasKey('capacity_bytes', $firstPool); + $this->assertArrayHasKey('capacity_formatted', $firstPool); + $this->assertArrayHasKey('buffers_available', $firstPool); + + // Verify it's the 1KB pool (smallest) + $this->assertEquals(1024, $firstPool['capacity_bytes']); + $this->assertEquals('1.0KB (1024 bytes)', $firstPool['capacity_formatted']); + $this->assertEquals(1, $firstPool['buffers_available']); + $this->assertEquals('buffer_1024', $firstPool['key']); + + // Check second pool (4KB) + $secondPool = $poolsByCapacity[1]; + $this->assertEquals(4096, $secondPool['capacity_bytes']); + $this->assertEquals('4.0KB (4096 bytes)', $secondPool['capacity_formatted']); + $this->assertEquals(1, $secondPool['buffers_available']); + $this->assertEquals('buffer_4096', $secondPool['key']); + } + + /** + * Test capacity formatting for different sizes + */ + public function testCapacityFormatting(): void + { + // Use reflection to test private formatCapacity method + $reflection = new \ReflectionClass(JsonBufferPool::class); + $method = $reflection->getMethod('formatCapacity'); + $method->setAccessible(true); + + // Test bytes + $this->assertEquals('512 bytes', $method->invoke(null, 512)); + $this->assertEquals('1023 bytes', $method->invoke(null, 1023)); + + // Test KB + $this->assertEquals('1.0KB (1024 bytes)', $method->invoke(null, 1024)); + $this->assertEquals('4.0KB (4096 bytes)', $method->invoke(null, 4096)); + $this->assertEquals('16.0KB (16384 bytes)', $method->invoke(null, 16384)); + + // Test MB + $this->assertEquals('1.0MB (1048576 bytes)', $method->invoke(null, 1048576)); + $this->assertEquals('2.5MB (2621440 bytes)', $method->invoke(null, 2621440)); + } + + /** + * Test backward compatibility with legacy pool_sizes format + */ + public function testBackwardCompatibility(): void + { + // Create buffer and return to pool + $buffer = JsonBufferPool::getBuffer(2048); + JsonBufferPool::returnBuffer($buffer); + + $stats = JsonBufferPool::getStatistics(); + + // Legacy pool_sizes should still exist and be usable + $this->assertArrayHasKey('pool_sizes', $stats); + $this->assertIsArray($stats['pool_sizes']); + + // Should have readable format but maintain structure + $poolSizes = $stats['pool_sizes']; + $this->assertNotEmpty($poolSizes); + + // The value should still be the count of buffers + foreach ($poolSizes as $key => $count) { + $this->assertIsString($key); + $this->assertIsInt($count); + $this->assertGreaterThanOrEqual(0, $count); + } + } + + /** + * Test statistics with multiple buffers in same pool + */ + public function testMultipleBuffersInSamePool(): void + { + // Create multiple buffers of same size + $buffer1 = JsonBufferPool::getBuffer(1024); + $buffer2 = JsonBufferPool::getBuffer(1024); + $buffer3 = JsonBufferPool::getBuffer(1024); + + // Return all to pool + JsonBufferPool::returnBuffer($buffer1); + JsonBufferPool::returnBuffer($buffer2); + JsonBufferPool::returnBuffer($buffer3); + + $stats = JsonBufferPool::getStatistics(); + + // Should have 3 buffers in the 1KB pool + $this->assertEquals(3, $stats['pool_sizes']['1.0KB (1024 bytes)']); + $this->assertEquals(3, $stats['total_buffers_pooled']); + $this->assertEquals(1, $stats['active_pool_count']); // Only one pool type + + // Check pools_by_capacity format + $poolsByCapacity = $stats['pools_by_capacity']; + $this->assertCount(1, $poolsByCapacity); + $this->assertEquals(3, $poolsByCapacity[0]['buffers_available']); + } + + /** + * Test statistics with no pools + */ + public function testStatisticsWithNoPools(): void + { + $stats = JsonBufferPool::getStatistics(); + + $this->assertEquals(0, $stats['total_buffers_pooled']); + $this->assertEquals(0, $stats['active_pool_count']); + $this->assertEmpty($stats['pool_sizes']); + $this->assertEmpty($stats['pools_by_capacity']); + $this->assertEquals(0, $stats['total_operations']); + $this->assertEquals(0.0, $stats['reuse_rate']); + } + + /** + * Test that pool_sizes are sorted by capacity for consistency + */ + public function testPoolSizesSorting(): void + { + // Create buffers in reverse order to test sorting + $buffer16KB = JsonBufferPool::getBuffer(16384); + $buffer1KB = JsonBufferPool::getBuffer(1024); + $buffer8KB = JsonBufferPool::getBuffer(8192); + $buffer4KB = JsonBufferPool::getBuffer(4096); + + JsonBufferPool::returnBuffer($buffer16KB); + JsonBufferPool::returnBuffer($buffer1KB); + JsonBufferPool::returnBuffer($buffer8KB); + JsonBufferPool::returnBuffer($buffer4KB); + + $stats = JsonBufferPool::getStatistics(); + $poolSizes = $stats['pool_sizes']; + + // Get the keys (capacity strings) as an array + $capacityKeys = array_keys($poolSizes); + + // Expected order: smallest to largest + $expectedOrder = [ + '1.0KB (1024 bytes)', + '4.0KB (4096 bytes)', + '8.0KB (8192 bytes)', + '16.0KB (16384 bytes)' + ]; + + $this->assertEquals($expectedOrder, $capacityKeys, 'Pool sizes should be sorted by capacity'); + + // Also verify pools_by_capacity is sorted (already tested but good to be explicit) + $poolsByCapacity = $stats['pools_by_capacity']; + $capacities = array_column($poolsByCapacity, 'capacity_bytes'); + $sortedCapacities = $capacities; + sort($sortedCapacities); + + $this->assertEquals($sortedCapacities, $capacities, 'Pools by capacity should be sorted'); + } + + /** + * Test statistics consistency between formats + */ + public function testStatisticsConsistency(): void + { + // Create various sized buffers + $buffers = [ + JsonBufferPool::getBuffer(512), + JsonBufferPool::getBuffer(1024), + JsonBufferPool::getBuffer(1024), // Duplicate size + JsonBufferPool::getBuffer(4096), + ]; + + foreach ($buffers as $buffer) { + JsonBufferPool::returnBuffer($buffer); + } + + $stats = JsonBufferPool::getStatistics(); + + // Count buffers in both formats should match + $totalFromPoolSizes = array_sum($stats['pool_sizes']); + $totalFromPoolsByCapacity = array_sum(array_column($stats['pools_by_capacity'], 'buffers_available')); + + $this->assertEquals($totalFromPoolSizes, $totalFromPoolsByCapacity); + $this->assertEquals($stats['total_buffers_pooled'], $totalFromPoolSizes); + + // Number of pools should match + $this->assertEquals(count($stats['pool_sizes']), count($stats['pools_by_capacity'])); + $this->assertEquals($stats['active_pool_count'], count($stats['pools_by_capacity'])); + } + + /** + * Test that active_pool_count only counts non-empty pools + */ + public function testActivePoolCountOnlyCountsNonEmptyPools(): void + { + // Initial state - no pools + $stats = JsonBufferPool::getStatistics(); + $this->assertEquals(0, $stats['active_pool_count']); + + // Create buffers but don't return them (pools exist but are empty) + $buffer1KB = JsonBufferPool::getBuffer(1024); + $buffer4KB = JsonBufferPool::getBuffer(4096); + $buffer8KB = JsonBufferPool::getBuffer(8192); + + $stats = JsonBufferPool::getStatistics(); + $this->assertEquals(0, $stats['active_pool_count'], 'Empty pools should not be counted as active'); + + // Return only some buffers to pools + JsonBufferPool::returnBuffer($buffer1KB); + JsonBufferPool::returnBuffer($buffer4KB); + + $stats = JsonBufferPool::getStatistics(); + $this->assertEquals(2, $stats['active_pool_count'], 'Only non-empty pools should be counted as active'); + $this->assertEquals(2, $stats['total_buffers_pooled'], 'Should have 2 buffers in pools'); + + // Return the remaining buffer + JsonBufferPool::returnBuffer($buffer8KB); + + $stats = JsonBufferPool::getStatistics(); + $this->assertEquals(3, $stats['active_pool_count'], 'All pools with buffers should be counted as active'); + $this->assertEquals(3, $stats['total_buffers_pooled'], 'Should have 3 buffers in pools'); + + // Get all buffers back (making pools empty again) + $retrievedBuffer1 = JsonBufferPool::getBuffer(1024); + $retrievedBuffer2 = JsonBufferPool::getBuffer(4096); + $retrievedBuffer3 = JsonBufferPool::getBuffer(8192); + + $stats = JsonBufferPool::getStatistics(); + $this->assertEquals( + 0, + $stats['active_pool_count'], + 'Empty pools should not be counted as active after retrieval' + ); + $this->assertEquals(0, $stats['total_buffers_pooled'], 'Should have 0 buffers in pools after retrieval'); + } +} diff --git a/tests/Json/Pool/JsonBufferPoolTest.php b/tests/Json/Pool/JsonBufferPoolTest.php new file mode 100644 index 0000000..0cfe8f3 --- /dev/null +++ b/tests/Json/Pool/JsonBufferPoolTest.php @@ -0,0 +1,214 @@ +assertInstanceOf(JsonBuffer::class, $buffer); + $this->assertEquals(4096, $buffer->getCapacity()); // default capacity + } + + public function testGetBufferWithCustomCapacity(): void + { + $buffer = JsonBufferPool::getBuffer(8192); + + $this->assertEquals(8192, $buffer->getCapacity()); + } + + public function testBufferReuse(): void + { + // Clear pools to ensure clean state + JsonBufferPool::clearPools(); + + // Get a buffer + $buffer1 = JsonBufferPool::getBuffer(1024); + $buffer1->append('test data'); + + // Return it to pool + JsonBufferPool::returnBuffer($buffer1); + + // Get another buffer - should be the same one, but reset + $buffer2 = JsonBufferPool::getBuffer(1024); + + $this->assertEquals(0, $buffer2->getSize()); // Should be reset + $this->assertEquals(1024, $buffer2->getCapacity()); + + $stats = JsonBufferPool::getStatistics(); + + $this->assertArrayHasKey('detailed_stats', $stats); + $this->assertArrayHasKey('reuses', $stats['detailed_stats']); + $this->assertEquals(1, $stats['detailed_stats']['reuses']); // Should have exactly 1 reuse + $this->assertEquals(50.0, $stats['reuse_rate']); // 1 reuse out of 2 operations = 50% + } + + public function testEncodeWithPool(): void + { + $data = ['name' => 'test', 'value' => 123]; + + $json = JsonBufferPool::encodeWithPool($data); + + $this->assertEquals('{"name":"test","value":123}', $json); + + $stats = JsonBufferPool::getStatistics(); + $this->assertEquals(1, $stats['total_operations']); + } + + public function testPoolStatistics(): void + { + // Perform some operations + JsonBufferPool::getBuffer(1024); + JsonBufferPool::getBuffer(2048); + $buffer = JsonBufferPool::getBuffer(1024); + JsonBufferPool::returnBuffer($buffer); + + $stats = JsonBufferPool::getStatistics(); + + $this->assertArrayHasKey('reuse_rate', $stats); + $this->assertArrayHasKey('total_operations', $stats); + $this->assertArrayHasKey('current_usage', $stats); + $this->assertArrayHasKey('peak_usage', $stats); + $this->assertArrayHasKey('pool_sizes', $stats); + + $this->assertEquals(3, $stats['total_operations']); + $this->assertEquals(2, $stats['current_usage']); // 2 buffers still in use + } + + public function testPoolSizeLimit(): void + { + JsonBufferPool::configure(['max_pool_size' => 2]); + + // Create and return more buffers than the pool limit + $buffers = []; + for ($i = 0; $i < 5; $i++) { + $buffers[] = JsonBufferPool::getBuffer(1024); + } + + // Return all buffers + foreach ($buffers as $buffer) { + JsonBufferPool::returnBuffer($buffer); + } + + $stats = JsonBufferPool::getStatistics(); + + // Pool should not exceed the configured limit + $this->assertLessThanOrEqual(2, array_sum($stats['pool_sizes'])); + } + + public function testOptimalCapacityCalculation(): void + { + // Test small data + $smallData = ['id' => 1]; + $capacity = JsonBufferPool::getOptimalCapacity($smallData); + $this->assertEquals(1024, $capacity); + + // Test medium data + $mediumData = array_fill(0, 50, ['id' => 1, 'name' => 'test']); + $capacity = JsonBufferPool::getOptimalCapacity($mediumData); + $this->assertEquals(4096, $capacity); + + // Test large data + $largeData = array_fill(0, 500, ['id' => 1, 'name' => 'test']); + $capacity = JsonBufferPool::getOptimalCapacity($largeData); + $this->assertEquals(16384, $capacity); + } + + public function testJsonSizeEstimation(): void + { + // Test with reflection to access private method + $reflection = new \ReflectionClass(JsonBufferPool::class); + $method = $reflection->getMethod('estimateJsonSize'); + $method->setAccessible(true); + + // Test string + $size = $method->invoke(null, 'hello world'); + $this->assertEquals(31, $size); // 11 chars + 20 overhead + + // Test empty array + $size = $method->invoke(null, []); + $this->assertEquals(2, $size); // [] + + // Test small array + $size = $method->invoke(null, [1, 2, 3]); + $this->assertEquals(512, $size); + + // Test boolean + $size = $method->invoke(null, true); + $this->assertEquals(10, $size); + + // Test null + $size = $method->invoke(null, null); + $this->assertEquals(10, $size); + } + + public function testPoolConfiguration(): void + { + $config = [ + 'max_pool_size' => 100, + 'default_capacity' => 8192 + ]; + + JsonBufferPool::configure($config); + + $buffer = JsonBufferPool::getBuffer(); + $this->assertEquals(8192, $buffer->getCapacity()); + } + + public function testConcurrentBufferUsage(): void + { + $buffers = []; + + // Get multiple buffers simultaneously + for ($i = 0; $i < 10; $i++) { + $buffer = JsonBufferPool::getBuffer(1024); + $buffer->appendJson(['iteration' => $i]); + $buffers[] = $buffer; + } + + // Verify each buffer has correct content + foreach ($buffers as $index => $buffer) { + $result = $buffer->finalize(); + $this->assertEquals("{\"iteration\":$index}", $result); + } + + // Return all buffers + foreach ($buffers as $buffer) { + JsonBufferPool::returnBuffer($buffer); + } + + $stats = JsonBufferPool::getStatistics(); + $this->assertEquals(0, $stats['current_usage']); + } + + public function testReuseRateCalculation(): void + { + // Create some buffers and return them + $buffer1 = JsonBufferPool::getBuffer(1024); + JsonBufferPool::returnBuffer($buffer1); + + $buffer2 = JsonBufferPool::getBuffer(1024); // Should reuse buffer1 + JsonBufferPool::returnBuffer($buffer2); + + $stats = JsonBufferPool::getStatistics(); + $this->assertEquals(50.0, $stats['reuse_rate']); // 1 reuse out of 2 operations = 50% + } +} diff --git a/tests/Json/Pool/JsonBufferStreamTest.php b/tests/Json/Pool/JsonBufferStreamTest.php new file mode 100644 index 0000000..92a3edd --- /dev/null +++ b/tests/Json/Pool/JsonBufferStreamTest.php @@ -0,0 +1,185 @@ +append('{"test": "value"}'); + $buffer->appendJson(['another' => 'item']); + + $result = $buffer->finalize(); + + $this->assertStringContainsString('test', $result); + $this->assertStringContainsString('another', $result); + } + + /** + * Test that large buffers use streams from the start + */ + public function testLargeBufferUsesStream(): void + { + $buffer = new JsonBuffer(16384); // Above 8KB threshold + + // Add data to stream-based buffer + $largeData = array_fill(0, 100, ['field' => 'value', 'number' => 123]); + $buffer->appendJson($largeData); + + $result = $buffer->finalize(); + $decoded = json_decode($result, true); + + $this->assertIsArray($decoded); + $this->assertCount(100, $decoded); + $this->assertEquals('value', $decoded[0]['field']); + } + + /** + * Test migration from string to stream when buffer grows + */ + public function testStringToStreamMigration(): void + { + $buffer = new JsonBuffer(1024); // Start with small buffer + + // Add small amount of data (uses string) - start of valid JSON object + $buffer->append('{"initial": "data", "items": '); + + // Add large amount of data to trigger migration to stream + $largeData = array_fill(0, 200, ['field' => 'value' . rand(1000, 9999)]); + $buffer->appendJson($largeData); + + // Close the JSON object + $buffer->append('}'); + + $result = $buffer->finalize(); + $this->assertStringContainsString('initial', $result); + $this->assertStringContainsString('field', $result); + + // Verify it's valid JSON + $decoded = json_decode($result, true); + $this->assertNotNull($decoded, 'Result should be valid JSON: ' . json_last_error_msg()); + $this->assertEquals('data', $decoded['initial']); + $this->assertIsArray($decoded['items']); + $this->assertCount(200, $decoded['items']); + } + + /** + * Test buffer reset works with both string and stream modes + */ + public function testResetWithStreamMigration(): void + { + $buffer = new JsonBuffer(1024); + + // First use - small data (string mode) + $buffer->appendJson(['first' => 'use']); + $result1 = $buffer->finalize(); + $this->assertStringContainsString('first', $result1); + + // Reset + $buffer->reset(); + + // Second use - large data (should migrate to stream) + $largeData = array_fill(0, 150, ['iteration' => 2]); + $buffer->appendJson($largeData); + $result2 = $buffer->finalize(); + + $this->assertStringNotContainsString('first', $result2); + $this->assertStringContainsString('iteration', $result2); + + $decoded = json_decode($result2, true); + $this->assertIsArray($decoded); + $this->assertCount(150, $decoded); + } + + /** + * Test multiple append operations work correctly with streams + */ + public function testMultipleAppendsWithStream(): void + { + $buffer = new JsonBuffer(16384); // Force stream mode + + // Multiple append operations + $buffer->append('{"start": true,'); + $buffer->append('"items": ['); + + for ($i = 0; $i < 10; $i++) { + if ($i > 0) { + $buffer->append(','); + } + $buffer->appendJson(['item' => $i, 'value' => "test{$i}"]); + } + + $buffer->append(']}'); + + $result = $buffer->finalize(); + $decoded = json_decode($result, true); + + $this->assertIsArray($decoded); + $this->assertTrue($decoded['start']); + $this->assertIsArray($decoded['items']); + $this->assertCount(10, $decoded['items']); + $this->assertEquals(0, $decoded['items'][0]['item']); + $this->assertEquals(9, $decoded['items'][9]['item']); + } + + /** + * Test memory efficiency of stream approach + */ + public function testStreamMemoryEfficiency(): void + { + $memBefore = memory_get_usage(); + + // Create a buffer with large capacity + $buffer = new JsonBuffer(32768); + + // Add significant amount of data + for ($i = 0; $i < 50; $i++) { + $chunk = array_fill(0, 20, ['iteration' => $i, 'data' => str_repeat('x', 100)]); + $buffer->appendJson($chunk); + if ($i < 49) { + $buffer->append(','); + } + } + + $result = $buffer->finalize(); + $memAfter = memory_get_usage(); + + // Verify result is valid and large + $this->assertGreaterThan(50000, strlen($result)); // Should be large + + // Memory growth should be reasonable (streams are more memory efficient) + $memoryGrowth = $memAfter - $memBefore; + $this->assertLessThan(5 * 1024 * 1024, $memoryGrowth); // Less than 5MB growth + } + + /** + * Test that finalize can be called multiple times safely + */ + public function testMultipleFinalizeCalls(): void + { + $buffer = new JsonBuffer(16384); + $buffer->appendJson(['test' => 'data']); + + $result1 = $buffer->finalize(); + $result2 = $buffer->finalize(); + $result3 = $buffer->finalize(); + + $this->assertEquals($result1, $result2); + $this->assertEquals($result2, $result3); + $this->assertStringContainsString('test', $result1); + } +} diff --git a/tests/Json/Pool/JsonBufferTest.php b/tests/Json/Pool/JsonBufferTest.php new file mode 100644 index 0000000..ab4081b --- /dev/null +++ b/tests/Json/Pool/JsonBufferTest.php @@ -0,0 +1,123 @@ +assertEquals(1024, $buffer->getCapacity()); + $this->assertEquals(0, $buffer->getSize()); + $this->assertEquals(0, $buffer->getUtilization()); + $this->assertTrue($buffer->hasSpace(500)); + } + + public function testAppendString(): void + { + $buffer = new JsonBuffer(1024); + $buffer->append('{"test":'); + $buffer->append('"value"}'); + + $result = $buffer->finalize(); + $this->assertEquals('{"test":"value"}', $result); + $this->assertEquals(16, $buffer->getSize()); // Corrected expected size + } + + public function testAppendJson(): void + { + $buffer = new JsonBuffer(1024); + $data = ['name' => 'test', 'value' => 123]; + + $buffer->appendJson($data); + $result = $buffer->finalize(); + + $this->assertEquals('{"name":"test","value":123}', $result); + } + + public function testBufferReset(): void + { + $buffer = new JsonBuffer(1024); + $buffer->append('some data'); + + $this->assertEquals(9, $buffer->getSize()); + + $buffer->reset(); + + $this->assertEquals(0, $buffer->getSize()); + $this->assertEquals(0, $buffer->getUtilization()); + $this->assertTrue($buffer->hasSpace(100)); // Fixed test assertion + } + + public function testBufferExpansion(): void + { + $buffer = new JsonBuffer(10); // Small initial capacity + $largeData = str_repeat('x', 100); + + $buffer->append($largeData); + + $this->assertGreaterThanOrEqual(100, $buffer->getCapacity()); + $this->assertEquals(100, $buffer->getSize()); + } + + public function testUtilizationCalculation(): void + { + $buffer = new JsonBuffer(100); + $buffer->append('12345'); // 5 bytes + + $this->assertEquals(5.0, $buffer->getUtilization()); + $this->assertEquals(95, $buffer->getRemainingSpace()); + } + + public function testInvalidJsonEncoding(): void + { + $buffer = new JsonBuffer(1024); + + // Test with invalid UTF-8 sequence + $this->expectException(\InvalidArgumentException::class); + $buffer->appendJson("\xB1\x31"); + } + + public function testComplexJsonStructure(): void + { + $buffer = new JsonBuffer(1024); + + $complexData = [ + 'users' => [ + ['id' => 1, 'name' => 'João'], + ['id' => 2, 'name' => 'Maria'] + ], + 'metadata' => [ + 'total' => 2, + 'page' => 1 + ] + ]; + + $buffer->appendJson($complexData); + $result = $buffer->finalize(); + + // Verify that the JSON is valid + $decoded = json_decode($result, true); + $this->assertEquals($complexData, $decoded); + } + + public function testMultipleAppendOperations(): void + { + $buffer = new JsonBuffer(1024); + + $buffer->append('{"users":['); + $buffer->appendJson(['id' => 1, 'name' => 'User1']); + $buffer->append(','); + $buffer->appendJson(['id' => 2, 'name' => 'User2']); + $buffer->append(']}'); + + $result = $buffer->finalize(); + $expected = '{"users":[{"id":1,"name":"User1"},{"id":2,"name":"User2"}]}'; + + $this->assertEquals($expected, $result); + } +} diff --git a/tests/Json/Pool/JsonSizeEstimationTest.php b/tests/Json/Pool/JsonSizeEstimationTest.php new file mode 100644 index 0000000..632c1ad --- /dev/null +++ b/tests/Json/Pool/JsonSizeEstimationTest.php @@ -0,0 +1,238 @@ +reflection = new ReflectionClass(JsonBufferPool::class); + } + + private function callEstimateJsonSize(mixed $data): int + { + $method = $this->reflection->getMethod('estimateJsonSize'); + $method->setAccessible(true); + return $method->invokeArgs(null, [$data]); + } + + /** + * Test string size estimation + */ + public function testStringEstimation(): void + { + $shortString = 'hello'; + $longString = str_repeat('a', 100); + + $shortEstimate = $this->callEstimateJsonSize($shortString); + $longEstimate = $this->callEstimateJsonSize($longString); + + // Should be string length + STRING_OVERHEAD (20) + $this->assertEquals(strlen($shortString) + JsonBufferPool::STRING_OVERHEAD, $shortEstimate); + $this->assertEquals(strlen($longString) + JsonBufferPool::STRING_OVERHEAD, $longEstimate); + + // Longer strings should have larger estimates + $this->assertGreaterThan($shortEstimate, $longEstimate); + } + + /** + * Test array size estimation thresholds + */ + public function testArrayEstimationThresholds(): void + { + $emptyArray = []; + $smallArray = array_fill(0, 5, 'item'); // < 10 items + $mediumArray = array_fill(0, 50, 'item'); // < 100 items + $largeArray = array_fill(0, 500, 'item'); // < 1000 items + $xlargeArray = array_fill(0, 2000, 'item'); // >= 1000 items + + $emptyEstimate = $this->callEstimateJsonSize($emptyArray); + $smallEstimate = $this->callEstimateJsonSize($smallArray); + $mediumEstimate = $this->callEstimateJsonSize($mediumArray); + $largeEstimate = $this->callEstimateJsonSize($largeArray); + $xlargeEstimate = $this->callEstimateJsonSize($xlargeArray); + + // Empty array should be smallest (2 bytes for []) + $this->assertEquals(JsonBufferPool::EMPTY_ARRAY_SIZE, $emptyEstimate); + + // Each category should be larger than the previous + $this->assertGreaterThan($emptyEstimate, $smallEstimate); + $this->assertGreaterThan($smallEstimate, $mediumEstimate); + $this->assertGreaterThan($mediumEstimate, $largeEstimate); + $this->assertGreaterThan($largeEstimate, $xlargeEstimate); + + // Verify expected sizes based on constants + $this->assertEquals(JsonBufferPool::SMALL_ARRAY_SIZE, $smallEstimate); + $this->assertEquals(JsonBufferPool::MEDIUM_ARRAY_SIZE, $mediumEstimate); + $this->assertEquals(JsonBufferPool::LARGE_ARRAY_SIZE, $largeEstimate); + $this->assertEquals(JsonBufferPool::XLARGE_ARRAY_SIZE, $xlargeEstimate); + } + + /** + * Test object size estimation + */ + public function testObjectEstimation(): void + { + $emptyObject = new \stdClass(); + $smallObject = (object)['name' => 'test', 'value' => 42]; + $largeObject = (object)array_fill_keys(range('a', 'j'), 'value'); // 10 properties + + $emptyEstimate = $this->callEstimateJsonSize($emptyObject); + $smallEstimate = $this->callEstimateJsonSize($smallObject); + $largeEstimate = $this->callEstimateJsonSize($largeObject); + + // Empty object should be base size (100) + $this->assertEquals(JsonBufferPool::OBJECT_BASE_SIZE, $emptyEstimate); + + // Objects with properties should be larger + $this->assertGreaterThan($emptyEstimate, $smallEstimate); + $this->assertGreaterThan($smallEstimate, $largeEstimate); + + // Should follow formula: property_count * OBJECT_PROPERTY_OVERHEAD + OBJECT_BASE_SIZE + $this->assertEquals( + 2 * JsonBufferPool::OBJECT_PROPERTY_OVERHEAD + JsonBufferPool::OBJECT_BASE_SIZE, + $smallEstimate + ); // 2 properties + $this->assertEquals( + 10 * JsonBufferPool::OBJECT_PROPERTY_OVERHEAD + JsonBufferPool::OBJECT_BASE_SIZE, + $largeEstimate + ); // 10 properties + } + + /** + * Test primitive type estimations + */ + public function testPrimitiveEstimations(): void + { + $boolean = true; + $null = null; + $integer = 42; + $float = 3.14; + + $booleanEstimate = $this->callEstimateJsonSize($boolean); + $nullEstimate = $this->callEstimateJsonSize($null); + $integerEstimate = $this->callEstimateJsonSize($integer); + $floatEstimate = $this->callEstimateJsonSize($float); + + // Boolean and null should be same size (10) + $this->assertEquals(JsonBufferPool::BOOLEAN_OR_NULL_SIZE, $booleanEstimate); + $this->assertEquals(JsonBufferPool::BOOLEAN_OR_NULL_SIZE, $nullEstimate); + + // Numeric values should be same size (20) + $this->assertEquals(JsonBufferPool::NUMERIC_SIZE, $integerEstimate); + $this->assertEquals(JsonBufferPool::NUMERIC_SIZE, $floatEstimate); + } + + /** + * Test default estimation fallback + */ + public function testDefaultEstimation(): void + { + // Create a resource (which doesn't match any specific type) + $resource = fopen('php://memory', 'r+'); + $estimate = $this->callEstimateJsonSize($resource); + fclose($resource); + + // Should return default estimate (100) + $this->assertEquals(JsonBufferPool::DEFAULT_ESTIMATE, $estimate); + } + + /** + * Test optimal capacity calculation + */ + public function testOptimalCapacityCalculation(): void + { + $smallData = ['test' => 'value']; + $largeData = array_fill(0, 2000, ['field' => 'value']); + + $smallCapacity = JsonBufferPool::getOptimalCapacity($smallData); + $largeCapacity = JsonBufferPool::getOptimalCapacity($largeData); + + // Small data should fit in standard categories (1024, 4096, 16384, 65536) + $this->assertContains($smallCapacity, [1024, 4096, 16384, 65536]); + + // Large data should get calculated capacity + $this->assertGreaterThanOrEqual(JsonBufferPool::MIN_LARGE_BUFFER_SIZE, $largeCapacity); + $this->assertGreaterThan($smallCapacity, $largeCapacity); + } + + /** + * Test that constants are properly defined and reasonable + */ + public function testConstantsAreReasonable(): void + { + // Test that size constants are in ascending order + $this->assertLessThan(JsonBufferPool::SMALL_ARRAY_SIZE, JsonBufferPool::EMPTY_ARRAY_SIZE); // EMPTY < SMALL + $this->assertLessThan(JsonBufferPool::MEDIUM_ARRAY_SIZE, JsonBufferPool::SMALL_ARRAY_SIZE); // SMALL < MEDIUM + $this->assertLessThan(JsonBufferPool::LARGE_ARRAY_SIZE, JsonBufferPool::MEDIUM_ARRAY_SIZE); // MEDIUM < LARGE + $this->assertLessThan(JsonBufferPool::XLARGE_ARRAY_SIZE, JsonBufferPool::LARGE_ARRAY_SIZE); // LARGE < XLARGE + + // Test threshold constants are in ascending order + $this->assertLessThan( + JsonBufferPool::MEDIUM_ARRAY_THRESHOLD, + JsonBufferPool::SMALL_ARRAY_THRESHOLD + ); // SMALL < MEDIUM threshold + $this->assertLessThan( + JsonBufferPool::LARGE_ARRAY_THRESHOLD, + JsonBufferPool::MEDIUM_ARRAY_THRESHOLD + ); // MEDIUM < LARGE threshold + + // Test overhead constants are reasonable + $this->assertGreaterThan(0, JsonBufferPool::STRING_OVERHEAD); // STRING_OVERHEAD > 0 + $this->assertGreaterThan(0, JsonBufferPool::OBJECT_PROPERTY_OVERHEAD); // OBJECT_PROPERTY_OVERHEAD > 0 + $this->assertGreaterThan(0, JsonBufferPool::OBJECT_BASE_SIZE); // OBJECT_BASE_SIZE > 0 + } + + /** + * Test realistic data size estimations + */ + public function testRealisticDataEstimations(): void + { + // Typical API response data + $apiResponse = [ + 'status' => 'success', + 'data' => [ + 'users' => array_fill( + 0, + 50, + [ + 'id' => rand(1, 1000), + 'name' => 'User Name', + 'email' => 'user@example.com' + ] + ) + ], + 'meta' => [ + 'total' => 50, + 'page' => 1, + 'per_page' => 50 + ] + ]; + + $estimate = $this->callEstimateJsonSize($apiResponse); + $capacity = JsonBufferPool::getOptimalCapacity($apiResponse); + + // Should estimate reasonable size for this data structure (it's an array) + $this->assertGreaterThan(100, $estimate); + $this->assertGreaterThan($estimate, $capacity); + + // Actual JSON should be reasonably close to estimate + $actualJson = json_encode($apiResponse); + $actualSize = strlen($actualJson); + + // Estimate should be within reasonable range of actual size + $this->assertGreaterThan($actualSize * 0.1, $estimate); // At least 10% of actual + $this->assertLessThan($actualSize * 10, $estimate); // At most 10x actual + } +} diff --git a/tests/PSRProvidersTest.php b/tests/PSRProvidersTest.php index 3011167..9c2a9fb 100644 --- a/tests/PSRProvidersTest.php +++ b/tests/PSRProvidersTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests; +namespace PivotPHP\Core\Tests; use PivotPHP\Core\Core\Application; use PivotPHP\Core\Providers\Container; diff --git a/tests/Security/AuthMiddlewareTest.php b/tests/Security/AuthMiddlewareTest.php index bc58d89..b0d971c 100644 --- a/tests/Security/AuthMiddlewareTest.php +++ b/tests/Security/AuthMiddlewareTest.php @@ -1,6 +1,6 @@ markTestSkipped('Circuit breaker behavior is environment-dependent and will be tested in dedicated stress tests'); + $this->markTestSkipped( + 'Circuit breaker behavior is environment-dependent and will be tested in dedicated stress tests' + ); $this->app->middleware('circuit-breaker'); // Simulate service failures @@ -249,7 +251,9 @@ function ($req, $res) use ($shouldFail) { */ public function testLoadSheddingEffectiveness(): void { - $this->markTestSkipped('Load shedding behavior is environment-dependent and will be tested in dedicated stress tests'); + $this->markTestSkipped( + 'Load shedding behavior is environment-dependent and will be tested in dedicated stress tests' + ); $this->app->middleware( 'load-shedder', [