diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 000000000..cc6ac9de6 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,88 @@ +# View Performance Benchmarks + +This directory contains performance benchmarks for critical code paths in the yiisoft/view library. + +## Running Benchmarks + +To run the benchmarks, ensure you have installed the project dependencies: + +```bash +composer install +``` + +Then run individual benchmark files: + +```bash +php benchmarks/RendererMatchingBench.php +``` + +## Available Benchmarks + +### RendererMatchingBench.php + +Benchmarks the performance of renderer extension matching, specifically: + +1. **Extension Sorting Performance** - Measures the overhead of sorting renderer extensions by length using `uksort()`. This sorting is done in `ViewTrait::withRenderers()` to ensure more specific extensions (like "blade.php") are matched before generic ones (like "php"). + +2. **Extension Matching Performance** - Measures the performance of `str_ends_with()` function used to match file extensions against registered renderers. + +3. **Overall Matching Performance** - Compares the end-to-end performance of renderer matching with and without sorting, showing the trade-off between correctness (sorted) and raw performance (unsorted). + +## Interpreting Results + +### Extension Sorting + +The sorting operation adds overhead on every `withRenderers()` call. With typical renderer counts (5-10 renderers), the overhead is in the microsecond range (10-20 μs per operation). Since `withRenderers()` is typically called once during application initialization, this overhead is acceptable. + +Example output: +``` +1. Extension Sorting Performance (uksort) +------------------------------------------------------------ + Total time: 1.7517 seconds + Per operation: 17.5174 μs + Operations/sec: 57086 +``` + +This shows that sorting 8 renderer extensions takes about 17.5 microseconds per operation. + +### Extension Matching + +The `str_ends_with()` function is very fast, with operations completing in the nanosecond range (200-300 ns). This is the hot path that runs on every view render, so the low overhead is important. + +Example output: +``` +2. Extension Matching Performance (str_ends_with) +------------------------------------------------------------ + 'simple.php' -> 'php': 203.46 ns/op + 'template.blade.php' -> 'blade.php': 201.17 ns/op +``` + +### Overall Impact + +The overall matching benchmark shows the trade-off between correctness and performance. Sorted matching ensures that files like "test.blade.php" are matched against "blade.php" renderer before "php" renderer, which is the correct behavior. The performance difference shown in the benchmark reflects the fact that sorting happens on setup (withRenderers call), not on every render. + +Example output: +``` +3. Overall Matching Performance (sorted vs unsorted) +------------------------------------------------------------ + Unsorted: 0.2873 seconds (0.5746 μs/op) + Sorted: 0.7353 seconds (1.4706 μs/op) + Difference: +155.95% +``` + +**Note**: The "sorted vs unsorted" comparison in this benchmark includes the sorting operation in each iteration to show the total cost. In real-world usage, sorting happens once during initialization, so the actual runtime impact is much lower. + +## Why We Benchmark + +These benchmarks help us: + +1. **Ensure acceptable performance** - Verify that the renderer matching logic doesn't introduce significant overhead +2. **Track performance regressions** - Compare results across versions to catch performance degradations +3. **Make informed trade-offs** - Understand the cost of correctness features like extension sorting +4. **Optimize hot paths** - Identify which operations need the most optimization + +## Related Issues + +- [#289](https://github.com/yiisoft/view/issues/289) - Fix template file searching for double extensions +- [#291](https://github.com/yiisoft/view/pull/291) - Initial fix for double extensions +- [#292](https://github.com/yiisoft/view/pull/292) - Sort renderer extensions by length diff --git a/benchmarks/RendererMatchingBench.php b/benchmarks/RendererMatchingBench.php new file mode 100644 index 000000000..0265bd324 --- /dev/null +++ b/benchmarks/RendererMatchingBench.php @@ -0,0 +1,217 @@ +benchmarkExtensionSorting(); + echo "\n"; + + $this->benchmarkExtensionMatching(); + echo "\n"; + + $this->benchmarkOverallImpact(); + echo "\n"; + } + + /** + * Benchmark the uksort operation for sorting extensions by length. + */ + private function benchmarkExtensionSorting(): void + { + echo "1. Extension Sorting Performance (uksort)\n"; + echo str_repeat('-', 60) . "\n"; + + $renderers = $this->createRendererArray(); + + // Benchmark sorting + $start = hrtime(true); + for ($i = 0; $i < self::ITERATIONS; $i++) { + $copy = $renderers; + uksort($copy, static fn (string $a, string $b): int => strlen($b) <=> strlen($a)); + } + $end = hrtime(true); + + $duration = ($end - $start) / 1e9; // Convert to seconds + $perOperation = ($duration / self::ITERATIONS) * 1e6; // Convert to microseconds + + echo sprintf(" Total time: %.4f seconds\n", $duration); + echo sprintf(" Per operation: %.4f μs\n", $perOperation); + echo sprintf(" Operations/sec: %.0f\n", self::ITERATIONS / $duration); + } + + /** + * Benchmark str_ends_with() performance for extension matching. + */ + private function benchmarkExtensionMatching(): void + { + echo "2. Extension Matching Performance (str_ends_with)\n"; + echo str_repeat('-', 60) . "\n"; + + $testCases = [ + 'simple.php' => 'php', + 'template.blade.php' => 'blade.php', + 'view.twig' => 'twig', + 'component.blade.php' => 'blade.php', + ]; + + foreach ($testCases as $filename => $expectedExt) { + $start = hrtime(true); + for ($i = 0; $i < self::ITERATIONS; $i++) { + str_ends_with($filename, '.' . $expectedExt); + } + $end = hrtime(true); + + $duration = ($end - $start) / 1e9; + $perOperation = ($duration / self::ITERATIONS) * 1e9; // Convert to nanoseconds + + echo sprintf(" '%s' -> '%s': %.2f ns/op\n", $filename, $expectedExt, $perOperation); + } + } + + /** + * Benchmark the overall impact of sorting on renderer matching. + */ + private function benchmarkOverallImpact(): void + { + echo "3. Overall Matching Performance (sorted vs unsorted)\n"; + echo str_repeat('-', 60) . "\n"; + + $renderers = $this->createRendererArray(); + $sortedRenderers = $renderers; + uksort($sortedRenderers, static fn (string $a, string $b): int => strlen($b) <=> strlen($a)); + + $testFiles = [ + 'simple.php', + 'template.blade.php', + 'view.twig', + 'component.blade.php', + 'page.php', + ]; + + // Benchmark unsorted matching + $start = hrtime(true); + for ($i = 0; $i < self::ITERATIONS; $i++) { + foreach ($testFiles as $filename) { + $this->findRenderer($filename, $renderers); + } + } + $end = hrtime(true); + $unsortedDuration = ($end - $start) / 1e9; + + // Benchmark sorted matching + $start = hrtime(true); + for ($i = 0; $i < self::ITERATIONS; $i++) { + foreach ($testFiles as $filename) { + $this->findRenderer($filename, $sortedRenderers); + } + } + $end = hrtime(true); + $sortedDuration = ($end - $start) / 1e9; + + echo sprintf( + " Unsorted: %.4f seconds (%.4f μs/op)\n", + $unsortedDuration, + ($unsortedDuration / (self::ITERATIONS * count($testFiles))) * 1e6 + ); + echo sprintf( + " Sorted: %.4f seconds (%.4f μs/op)\n", + $sortedDuration, + ($sortedDuration / (self::ITERATIONS * count($testFiles))) * 1e6 + ); + + $diff = (($sortedDuration - $unsortedDuration) / $unsortedDuration) * 100; + echo sprintf(" Difference: %+.2f%%\n", $diff); + + if ($diff < 0) { + echo sprintf(" Sorted matching is %.2f%% faster\n", abs($diff)); + } else { + echo sprintf(" Sorted matching is %.2f%% slower\n", $diff); + } + } + + /** + * Simulate the renderer matching logic from ViewTrait. + */ + private function findRenderer(string $filename, array $renderers): ?TemplateRendererInterface + { + foreach ($renderers as $extension => $candidateRenderer) { + if ($extension === '') { + continue; + } + + if (!str_ends_with($filename, '.' . $extension)) { + continue; + } + + return $candidateRenderer; + } + + return null; + } + + /** + * Create a sample renderer array for testing. + * + * Note: All extensions use the same PhpTemplateRenderer instance because + * this benchmark focuses on extension matching performance, not renderer + * execution. In real-world usage, different extensions would typically + * have different renderer implementations (e.g., Blade, Twig, etc.). + */ + private function createRendererArray(): array + { + $phpRenderer = new PhpTemplateRenderer(); + + return [ + 'php' => $phpRenderer, + 'blade.php' => $phpRenderer, + 'twig' => $phpRenderer, + 'html.php' => $phpRenderer, + 'phtml' => $phpRenderer, + 'tpl.php' => $phpRenderer, + 'view.php' => $phpRenderer, + 'inc.php' => $phpRenderer, + ]; + } +} + +// Run the benchmark +$bench = new RendererMatchingBench(); +$bench->run();