Skip to content

Commit e0279c1

Browse files
committed
Add benchmarks
1 parent e8566a1 commit e0279c1

File tree

4 files changed

+359
-2
lines changed

4 files changed

+359
-2
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,8 @@ $compiler->setOptions([
205205
'silenceDeprecations' => true,
206206
'verbose' => true,
207207
]);
208-
```
208+
```
209+
210+
## Benchmarks
211+
212+
See the [benchmark.md](benchmark.md) file for performance benchmarks.

README.ru.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,8 @@ $compiler->setOptions([
205205
'silenceDeprecations' => true,
206206
'verbose' => true,
207207
]);
208-
```
208+
```
209+
210+
## Бенчмарки
211+
212+
Смотрите файл [benchmark.md](benchmark.md) для просмотра результатов производительности.

benchmark.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
## Results
2+
3+
| Compiler | Time (sec) | CSS Size (KB) | Memory (MB) |
4+
|------------|-------------|---------------|-------------|
5+
| bugo/sass-embedded-php | 1.4567 | 3,559.42 | 28.00 |
6+
| bugo/sass-embedded-php (optimized) | 1.4455 | 3,559.42 | 2.00 |
7+
8+
## Process Caching Performance (5 iterations)
9+
| Compiler | Avg Time (sec) | Avg Memory (MB) |
10+
|------------|----------------|----------------|
11+
| bugo/sass-embedded-php | 1.4911 | 2.40 |
12+
| bugo/sass-embedded-php (optimized) | 1.4922 | 0.00 |
13+
14+
## Optimizations Implemented
15+
16+
- **Process Caching**: Node.js processes are cached and reused to avoid spawning overhead
17+
- **Streaming for Large Data**: Input data is processed in chunks to prevent memory exhaustion
18+
- **Reduced JSON Overhead**: Only necessary data is transmitted between PHP and Node.js
19+
- **Generator Support**: Large compilation results can be processed chunk-by-chunk using generators
20+
- **Memory Limits**: Automatic streaming activation for files larger than 1MB
21+
22+
*Note: These results are approximate. To get actual results, run `php benchmark.php` in the project root.*

benchmark.php

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
<?php declare(strict_types=1);
2+
3+
require_once 'vendor/autoload.php';
4+
5+
use Bugo\Sass\Compiler as EmbeddedCompiler;
6+
7+
// Function for generating large random SCSS code
8+
function generateLargeScss(int $numClasses = 100, int $nestedLevels = 3): string
9+
{
10+
$scss = '';
11+
12+
// Generate variables with math functions
13+
$scss .= '$primary-color: #007bff;' . PHP_EOL;
14+
$scss .= '$secondary-color: #6c757d;' . PHP_EOL;
15+
$scss .= '$font-size: 14px;' . PHP_EOL;
16+
$scss .= '$border-radius: 5px;' . PHP_EOL;
17+
$scss .= '$max-width: max(800px, 50vw);' . PHP_EOL;
18+
$scss .= '$min-padding: min(10px, 2vw);' . PHP_EOL;
19+
$scss .= '$clamped-size: clamp(12px, 2.5vw, 20px);' . PHP_EOL;
20+
21+
for ($i = 0; $i < 20; $i++) {
22+
$randomVal = rand(-50, 50);
23+
$scss .= '$var' . $i . ': ' . 'abs(' . $randomVal . 'px);' . PHP_EOL;
24+
$scss .= '$rounded-var' . $i . ': ' . 'round(' . (rand(0, 100) / 3.14) . ');' . PHP_EOL;
25+
$scss .= '$ceiled-var' . $i . ': ' . 'ceil(' . (rand(0, 100) / 2.7) . 'px);' . PHP_EOL;
26+
$scss .= '$floored-var' . $i . ': ' . 'floor(' . (rand(0, 100) / 1.8) . 'px);' . PHP_EOL;
27+
}
28+
29+
// Generate functions
30+
$scss .= '@function calculate-size($base, $multiplier: 1) {' . PHP_EOL;
31+
$scss .= ' @return $base * $multiplier;' . PHP_EOL;
32+
$scss .= '}' . PHP_EOL;
33+
34+
// Generate mixins with color functions
35+
$scss .= '@mixin flex-center {' . PHP_EOL;
36+
$scss .= ' display: flex;' . PHP_EOL;
37+
$scss .= ' justify-content: center;' . PHP_EOL;
38+
$scss .= ' align-items: center;' . PHP_EOL;
39+
$scss .= '}' . PHP_EOL;
40+
$scss .= '@mixin button-style($color) {' . PHP_EOL;
41+
$scss .= ' background-color: lighten($color, 5%);' . PHP_EOL;
42+
$scss .= ' border: 1px solid saturate($color, 20%);' . PHP_EOL;
43+
$scss .= ' border-radius: calc($border-radius + 2px);' . PHP_EOL;
44+
$scss .= ' padding: max(8px, $min-padding) max(15px, calc($min-padding * 2));' . PHP_EOL;
45+
$scss .= ' &:hover {' . PHP_EOL;
46+
$scss .= ' background-color: desaturate($color, 10%);' . PHP_EOL;
47+
$scss .= ' transform: scale(calc(1.05));' . PHP_EOL;
48+
$scss .= ' }' . PHP_EOL;
49+
$scss .= '}' . PHP_EOL;
50+
$scss .= '@mixin color-variations($base-color) {' . PHP_EOL;
51+
$scss .= ' .light { color: lighten($base-color, 20%); }' . PHP_EOL;
52+
$scss .= ' .dark { color: darken($base-color, 15%); }' . PHP_EOL;
53+
$scss .= ' .saturated { color: saturate($base-color, 30%); }' . PHP_EOL;
54+
$scss .= ' .desaturated { color: desaturate($base-color, 25%); }' . PHP_EOL;
55+
$scss .= ' .hue-rotated { filter: hue-rotate(45deg); }' . PHP_EOL;
56+
$scss .= '}' . PHP_EOL;
57+
58+
// Generate classes with nesting and conditional directives using color and math functions
59+
for ($i = 0; $i < $numClasses; $i++) {
60+
$scss .= '.class-' . $i . ' {' . PHP_EOL;
61+
$scss .= ' background-color: mix($primary-color, $secondary-color, ' . rand(20, 80) . '%);' . PHP_EOL;
62+
$scss .= ' font-size: clamp($clamped-size, calculate-size($font-size, ' . (rand(1, 3)) . '), 24px);' . PHP_EOL;
63+
$scss .= ' padding: max($var' . rand(0, 19) . ', $min-padding);' . PHP_EOL;
64+
$scss .= ' margin: calc($var' . rand(0, 19) . ' + 5px);' . PHP_EOL;
65+
$scss .= ' border-radius: $border-radius;' . PHP_EOL;
66+
$scss .= ' max-width: $max-width;' . PHP_EOL;
67+
$scss .= ' @include color-variations($primary-color);' . PHP_EOL;
68+
69+
// Conditional directive
70+
$randomVal = rand(0, 1);
71+
$scss .= ' @if ' . $randomVal . ' == 1 {' . PHP_EOL;
72+
$scss .= ' color: lighten($primary-color, 40%);' . PHP_EOL;
73+
$scss .= ' text-shadow: 1px 1px 2px rgba(0,0,0,0.3);' . PHP_EOL;
74+
$scss .= ' } @else {' . PHP_EOL;
75+
$scss .= ' color: darken($primary-color, 20%);' . PHP_EOL;
76+
$scss .= ' border: 1px solid saturate($primary-color, 15%);' . PHP_EOL;
77+
$scss .= ' }' . PHP_EOL;
78+
79+
// Add nesting with color functions
80+
for ($level = 1; $level <= $nestedLevels; $level++) {
81+
$scss .= str_repeat(' ', $level) . '&.nested-' . $level . ' {' . PHP_EOL;
82+
$scss .= str_repeat(' ', $level + 1) . 'filter: hue-rotate(' . (rand(0, 360)) . 'deg) saturate(' . (100 + rand(-20, 20)) . '%);' . PHP_EOL;
83+
$scss .= str_repeat(' ', $level + 1) . 'background-color: lighten($secondary-color, ' . rand(10, 30) . '%);' . PHP_EOL;
84+
$scss .= str_repeat(' ', $level + 1) . '@include flex-center;' . PHP_EOL;
85+
$scss .= str_repeat(' ', $level + 1) . 'transform: scale(calc(1 + ' . (rand(1, 10) / 100) . '));' . PHP_EOL;
86+
$scss .= str_repeat(' ', $level) . '}' . PHP_EOL;
87+
}
88+
89+
$scss .= ' &:hover {' . PHP_EOL;
90+
$scss .= ' @include button-style(lighten($primary-color, 10%));' . PHP_EOL;
91+
$scss .= ' }' . PHP_EOL;
92+
93+
$scss .= '}' . PHP_EOL;
94+
}
95+
96+
// Add @for loops with math and color functions
97+
$scss .= '@for $i from 1 through 20 {' . PHP_EOL;
98+
$scss .= ' .for-class-#{$i} {' . PHP_EOL;
99+
$scss .= ' width: calc(10px * $i);' . PHP_EOL;
100+
$scss .= ' height: min(50px, calc(20px + $i * 2px));' . PHP_EOL;
101+
$scss .= ' @include button-style(saturate($secondary-color, calc($i * 2%)));' . PHP_EOL;
102+
$scss .= ' border-radius: clamp(3px, calc($i * 2px), 15px);' . PHP_EOL;
103+
$scss .= ' filter: hue-rotate(calc($i * 18deg));' . PHP_EOL;
104+
$scss .= ' }' . PHP_EOL;
105+
$scss .= '}' . PHP_EOL;
106+
107+
// Add @each loop with color functions
108+
$scss .= '$color-names: red, green, blue, yellow, magenta, cyan;' . PHP_EOL;
109+
$scss .= '$color-values: #ff0000, #00ff00, #0000ff, #ffff00, #ff00ff, #00ffff;' . PHP_EOL;
110+
$scss .= '@for $i from 1 through length($color-names) {' . PHP_EOL;
111+
$scss .= ' $name: nth($color-names, $i);' . PHP_EOL;
112+
$scss .= ' $color: nth($color-values, $i);' . PHP_EOL;
113+
$scss .= ' .color-#{$name} {' . PHP_EOL;
114+
$scss .= ' background-color: lighten($color, 10%);' . PHP_EOL;
115+
$scss .= ' border: 2px solid saturate($color, 20%);' . PHP_EOL;
116+
$scss .= ' &:hover {' . PHP_EOL;
117+
$scss .= ' background-color: desaturate($color, 15%);' . PHP_EOL;
118+
$scss .= ' transform: rotate(calc(var(--rotation, 0deg) + 5deg));' . PHP_EOL;
119+
$scss .= ' }' . PHP_EOL;
120+
$scss .= ' }' . PHP_EOL;
121+
$scss .= '}' . PHP_EOL;
122+
123+
// Add @while loop with math functions
124+
$scss .= '$counter: 1;' . PHP_EOL;
125+
$scss .= '@while $counter <= 15 {' . PHP_EOL;
126+
$scss .= ' .while-class-#{$counter} {' . PHP_EOL;
127+
$scss .= ' opacity: calc(0.1 * $counter);' . PHP_EOL;
128+
$scss .= ' z-index: $counter;' . PHP_EOL;
129+
$scss .= ' font-size: max(10px, calc(8px + $counter * 0.5px));' . PHP_EOL;
130+
$scss .= ' }' . PHP_EOL;
131+
$scss .= ' $counter: $counter + 1;' . PHP_EOL;
132+
$scss .= '}' . PHP_EOL;
133+
134+
return $scss;
135+
}
136+
137+
// Function to format result data
138+
function formatResultData(array $data): array
139+
{
140+
return [
141+
'time' => is_numeric($data['time']) ? number_format($data['time'], 4) : $data['time'],
142+
'size' => is_numeric($data['size']) ? number_format($data['size'], 2) : $data['size'],
143+
'memory' => is_numeric($data['memory']) ? number_format($data['memory'], 2) : $data['memory'],
144+
];
145+
}
146+
147+
// Generate large SCSS code
148+
$scss = generateLargeScss(2000, 4);
149+
150+
// Write SCSS to file in UTF-8
151+
file_put_contents('generated.scss', $scss, LOCK_EX);
152+
153+
// Display message about SCSS generation
154+
echo "Generated SCSS saved to generated.scss\n";
155+
echo "SCSS size: " . strlen($scss) . " bytes\n";
156+
157+
// Array of compilers
158+
$compilers = [
159+
'bugo/sass-embedded-php' => function() {
160+
$compiler = new EmbeddedCompiler();
161+
$compiler->setOptions([
162+
'sourceMap' => true,
163+
'sourceFile' => 'generated.scss',
164+
'sourceMapPath' => 'result.css.map',
165+
'includeSources' => true
166+
]);
167+
168+
return $compiler;
169+
},
170+
'bugo/sass-embedded-php (optimized)' => function() {
171+
$compiler = new EmbeddedCompiler();
172+
$compiler->setOptions([
173+
'sourceMap' => true,
174+
'sourceFile' => 'generated.scss',
175+
'sourceMapPath' => 'result-optimized.css.map',
176+
'includeSources' => true,
177+
'streamResult' => true // Enable streaming for large results
178+
]);
179+
180+
return $compiler;
181+
},
182+
];
183+
184+
// Results
185+
$results = [];
186+
187+
foreach ($compilers as $name => $compilerFactory) {
188+
$start = microtime(true);
189+
$memStart = memory_get_peak_usage(true);
190+
191+
try {
192+
$compiler = $compilerFactory();
193+
194+
// For optimized version, use generator for large files if available
195+
if (str_contains($name, 'optimized') && method_exists($compiler, 'compileStringAsGenerator')) {
196+
$cssParts = [];
197+
foreach ($compiler->compileStringAsGenerator($scss) as $chunk) {
198+
$cssParts[] = $chunk;
199+
}
200+
$css = implode('', $cssParts);
201+
} else {
202+
$css = $compiler->compileString($scss);
203+
}
204+
205+
// Handle source maps
206+
$mapFile = str_contains($name, 'optimized') ? 'result-optimized.css.map' : 'result-bugo-sass-embedded-php.css.map';
207+
if (file_exists($mapFile)) {
208+
$map = file_get_contents($mapFile);
209+
}
210+
211+
$time = microtime(true) - $start;
212+
$memEnd = memory_get_peak_usage(true);
213+
$memUsed = ($memEnd - $memStart) / 1024 / 1024;
214+
215+
$package = str_replace('/', '-', $name);
216+
$package = str_replace(['(', ')'], ['', ''], $package); // Remove parentheses for filename
217+
$package = str_replace(' ', '-', $package); // Replace spaces with dashes
218+
file_put_contents("result-$package.css", $css, LOCK_EX);
219+
$cssSize = filesize("result-$package.css") / 1024;
220+
221+
$results[$name] = ['time' => $time, 'size' => $cssSize, 'memory' => $memUsed];
222+
223+
echo "Compiler: $name, Time: " . number_format($time, 4) . " sec, Size: " . number_format($cssSize, 2) . " KB, Memory: " . number_format($memUsed, 2) . " MB" . PHP_EOL;
224+
} catch (Exception $e) {
225+
echo "General error in $name: " . $e->getMessage() . PHP_EOL;
226+
227+
$results[$name] = ['time' => 'Error', 'size' => 'N/A', 'memory' => 'N/A'];
228+
}
229+
}
230+
231+
// Output results in Markdown table format
232+
echo PHP_EOL . '## Performance Comparison with Process Caching' . PHP_EOL;
233+
234+
// Test process caching performance
235+
echo "Testing process caching with multiple compilations..." . PHP_EOL;
236+
237+
$numIterations = 5;
238+
$cacheTestResults = [];
239+
240+
foreach ($compilers as $name => $compilerFactory) {
241+
$times = [];
242+
$memories = [];
243+
244+
for ($i = 0; $i < $numIterations; $i++) {
245+
$start = microtime(true);
246+
$memStart = memory_get_peak_usage(true);
247+
248+
try {
249+
$compiler = $compilerFactory();
250+
$css = $compiler->compileString($scss);
251+
252+
$time = microtime(true) - $start;
253+
$memEnd = memory_get_peak_usage(true);
254+
$memUsed = ($memEnd - $memStart) / 1024 / 1024;
255+
256+
$times[] = $time;
257+
$memories[] = $memUsed;
258+
} catch (Exception $e) {
259+
$times[] = 'Error';
260+
$memories[] = 'N/A';
261+
}
262+
}
263+
264+
$avgTime = is_numeric($times[0]) ? array_sum($times) / count($times) : 'Error';
265+
$avgMemory = is_numeric($memories[0]) ? array_sum($memories) / count($memories) : 'N/A';
266+
267+
$cacheTestResults[$name] = [
268+
'avg_time' => $avgTime,
269+
'avg_memory' => $avgMemory,
270+
'iterations' => $numIterations
271+
];
272+
273+
echo "Cached Compiler: $name, Avg Time: " . (is_numeric($avgTime) ? number_format($avgTime, 4) : $avgTime) . " sec, Avg Memory: " . (is_numeric($avgMemory) ? number_format($avgMemory, 2) : $avgMemory) . " MB" . PHP_EOL;
274+
}
275+
276+
echo PHP_EOL . '## Results' . PHP_EOL;
277+
echo '| Compiler | Time (sec) | CSS Size (KB) | Memory (MB) |' . PHP_EOL;
278+
echo '|------------|-------------|---------------|-------------|' . PHP_EOL;
279+
280+
foreach ($results as $name => $data) {
281+
$formatted = formatResultData($data);
282+
echo "| $name | {$formatted['time']} | {$formatted['size']} | {$formatted['memory']} |" . PHP_EOL;
283+
}
284+
285+
// Now update the table
286+
$mdContent = file_get_contents('benchmark.md');
287+
288+
// Add caching test results to markdown
289+
$cacheTable = PHP_EOL . '## Process Caching Performance (' . $numIterations . ' iterations)' . PHP_EOL;
290+
$cacheTable .= '| Compiler | Avg Time (sec) | Avg Memory (MB) |' . PHP_EOL;
291+
$cacheTable .= '|------------|----------------|----------------|' . PHP_EOL;
292+
293+
foreach ($cacheTestResults as $name => $data) {
294+
$timeStr = is_numeric($data['avg_time']) ? number_format($data['avg_time'], 4) : $data['avg_time'];
295+
$memStr = is_numeric($data['avg_memory']) ? number_format($data['avg_memory'], 2) : $data['avg_memory'];
296+
$cacheTable .= "| $name | $timeStr | $memStr |" . PHP_EOL;
297+
}
298+
299+
$tableStart = strpos($mdContent, '| Compiler');
300+
$tableOld = substr($mdContent, $tableStart);
301+
302+
$newTable = "| Compiler | Time (sec) | CSS Size (KB) | Memory (MB) |\n|------------|-------------|---------------|-------------|\n";
303+
foreach ($results as $name => $data) {
304+
$formatted = formatResultData($data);
305+
$newTable .= "| $name | {$formatted['time']} | {$formatted['size']} | {$formatted['memory']} |\n";
306+
}
307+
308+
$mdContent = str_replace($tableOld, $newTable, $mdContent);
309+
310+
// Insert caching table before the note
311+
$notePos = strpos($mdContent, '*Note:');
312+
if ($notePos !== false) {
313+
$mdContent = substr_replace($mdContent, $cacheTable . PHP_EOL, $notePos, 0);
314+
} else {
315+
$mdContent .= $cacheTable;
316+
}
317+
318+
$scssContent = file_get_contents('generated.scss');
319+
$mdContent .= "\n## Optimizations Implemented\n\n";
320+
$mdContent .= "- **Process Caching**: Node.js processes are cached and reused to avoid spawning overhead\n";
321+
$mdContent .= "- **Streaming for Large Data**: Input data is processed in chunks to prevent memory exhaustion\n";
322+
$mdContent .= "- **Reduced JSON Overhead**: Only necessary data is transmitted between PHP and Node.js\n";
323+
$mdContent .= "- **Generator Support**: Large compilation results can be processed chunk-by-chunk using generators\n";
324+
$mdContent .= "- **Memory Limits**: Automatic streaming activation for files larger than 1MB\n\n";
325+
$mdContent .= "*Note: These results are approximate. To get actual results, run `php benchmark.php` in the project root.*\n";
326+
327+
file_put_contents('benchmark.md', $mdContent);

0 commit comments

Comments
 (0)