Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -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
217 changes: 217 additions & 0 deletions benchmarks/RendererMatchingBench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
<?php

declare(strict_types=1);

/**
* Benchmark for renderer extension matching performance.
*
* This benchmark tests the performance of:
* 1. Sorting renderer extensions by length (uksort operation)
* 2. Matching file extensions using str_ends_with()
* 3. Impact of sorting on overall matching performance
*
* Run with: php benchmarks/RendererMatchingBench.php
*/

namespace Yiisoft\View\Benchmarks;

require_once __DIR__ . '/../vendor/autoload.php';

use Yiisoft\View\PhpTemplateRenderer;
use Yiisoft\View\TemplateRendererInterface;

/**
* Simple benchmark runner for renderer matching operations.
*/
class RendererMatchingBench
{
/**
* Number of iterations for each benchmark test.
* 100,000 iterations provides a good balance between:
* - Accurate timing measurements (reduces noise)
* - Reasonable execution time (completes in seconds, not minutes)
*/
private const ITERATIONS = 100000;

/**
* Run all benchmarks and display results.
*/
public function run(): void
{
echo "Renderer Matching Performance Benchmark\n";
echo str_repeat('=', 60) . "\n\n";

$this->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();
Loading