Skip to content

Commit 9d543c5

Browse files
committed
Benchmark compilation and rendering speed
1 parent 8187895 commit 9d543c5

9 files changed

Lines changed: 576 additions & 0 deletions

File tree

tests/benchmark.php

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
<?php
2+
3+
/**
4+
* Compiler and runtime benchmark script. Default iterations: 1000.
5+
*
6+
* Usage: php -d opcache.enable_cli=1 -d opcache.jit=tracing tests/benchmark.php
7+
*/
8+
9+
use DevTheorem\Handlebars\Handlebars;
10+
use DevTheorem\Handlebars\HelperOptions;
11+
use DevTheorem\Handlebars\Options;
12+
13+
require __DIR__ . '/../vendor/autoload.php';
14+
15+
$iterations = (int) ($argv[1] ?? 1000);
16+
17+
// A large, complex template exercising as many syntax features as possible.
18+
$template = loadTemplate('large-page');
19+
$partialNames = ['alert', 'breadcrumbs', 'footer-col', 'nav-item', 'page-header', 'pagination', 'side-panel'];
20+
$partialTemplates = [];
21+
22+
foreach ($partialNames as $name) {
23+
$partialTemplates[$name] = loadTemplate($name);
24+
}
25+
26+
$translations = [
27+
'nav.profile' => 'Profile',
28+
'nav.settings' => 'Settings',
29+
'nav.admin' => 'Admin',
30+
'nav.logout' => 'Log Out',
31+
'nav.login' => 'Log In',
32+
'table.actions' => 'Actions',
33+
'table.empty' => 'No records found.',
34+
'pagination.label' => 'Page navigation',
35+
'pagination.prev' => 'Previous',
36+
'pagination.next' => 'Next',
37+
'pagination.showing' => 'Showing {start}–{end} of {total}',
38+
'edit' => 'Edit',
39+
'delete' => 'Delete',
40+
'confirm_delete' => 'Are you sure you want to delete this?',
41+
];
42+
43+
$helpers = [
44+
't' => function (string $key, HelperOptions $options) use ($translations) {
45+
$str = $translations[$key] ?? $key;
46+
foreach ($options->hash as $k => $v) {
47+
// for pagination.showing
48+
$str = str_replace('{' . $k . '}', (string) $v, $str);
49+
}
50+
return $str;
51+
},
52+
'formatDate' => function (mixed $value, string $format) {
53+
return date($format, strtotime($value));
54+
},
55+
'formatCurrency' => function (mixed $value, ?string $format) {
56+
return ($format ? "$format " : '') . number_format($value, 2);
57+
},
58+
'replace' => function (string $subject, string $search, ?string $replace) {
59+
return str_replace($search, $replace ?? '', $subject);
60+
},
61+
'eq' => function (mixed $a, mixed $b) {
62+
if ($a === null || $b === null) {
63+
// in JS, null is not equal to blank string or false or zero
64+
return $a === $b;
65+
}
66+
67+
return $a == $b;
68+
},
69+
'and' => fn(mixed $a, mixed $b) => $a && $b,
70+
'not' => fn(mixed $a) => !$a,
71+
'gt' => fn(mixed $a, mixed $b) => $a > $b,
72+
];
73+
74+
$knownHelpersOnly = ($argv[2] ?? 1) != 0;
75+
76+
$options = new Options(
77+
knownHelpers: $knownHelpersOnly ? array_fill_keys(array_keys($helpers), true) : [],
78+
knownHelpersOnly: $knownHelpersOnly,
79+
);
80+
81+
// Warm up: give the JIT a chance to compile hot paths before we measure.
82+
for ($i = 0; $i < 50; $i++) {
83+
Handlebars::precompile($template, $options);
84+
foreach ($partialTemplates as $src) {
85+
Handlebars::precompile($src, $options);
86+
}
87+
}
88+
89+
memory_reset_peak_usage();
90+
$start = hrtime(true);
91+
92+
for ($i = 0; $i < $iterations; $i++) {
93+
Handlebars::precompile($template, $options);
94+
foreach ($partialTemplates as $src) {
95+
Handlebars::precompile($src, $options);
96+
}
97+
}
98+
99+
$elapsed = (hrtime(true) - $start) / 1e9;
100+
$compilePeakMB = memory_get_peak_usage() / 1024 / 1024;
101+
$perParse = $elapsed / $iterations * 1000;
102+
$php = Handlebars::precompile($template, $options);
103+
$codeBytes = strlen($php);
104+
$partials = [];
105+
106+
foreach ($partialTemplates as $name => $src) {
107+
$code = Handlebars::precompile($src, $options);
108+
$codeBytes += strlen($code);
109+
$partials[$name] = Handlebars::template($code);
110+
}
111+
112+
printf(
113+
"Compiled %d times | %.2f ms/compile | %6.1f KB code | %.1f MB peak\n",
114+
$iterations,
115+
$perParse,
116+
$codeBytes / 1024,
117+
$compilePeakMB,
118+
);
119+
120+
$data = [
121+
'lang' => 'en',
122+
'pageTitle' => 'Dashboard',
123+
'siteName' => 'MyApp',
124+
'stylesheets' => [
125+
['url' => '/css/app.css'],
126+
['url' => '/css/print.css', 'media' => 'print'],
127+
],
128+
'bodyClass' => 'page-dashboard',
129+
'sticky' => true,
130+
'rootUrl' => '/',
131+
'logoHtml' => '<img src="/logo.svg" alt="">',
132+
'user' => [
133+
'id' => 1,
134+
'name' => 'Alice',
135+
'avatar' => '/avatars/alice.jpg',
136+
'isAdmin' => true,
137+
'verified' => true,
138+
],
139+
'navItems' => [
140+
['label' => 'Home', 'url' => '/', 'active' => true],
141+
['label' => 'Reports', 'url' => '/reports', 'badge' => '3'],
142+
['label' => 'More', 'url' => '#', 'icon' => 'chevron', 'children' => [
143+
['label' => 'Sub A', 'url' => '/a'],
144+
['label' => 'Sub B', 'url' => '/b'],
145+
]],
146+
],
147+
'alerts' => [
148+
['type' => 'success', 'message' => 'Saved!', 'dismissible' => true, 'icon' => 'check'],
149+
],
150+
'breadcrumbs' => [
151+
['label' => 'Home', 'url' => '/'],
152+
['label' => 'Orders', 'url' => '/orders'],
153+
['label' => 'List', 'url' => '/orders/list'],
154+
],
155+
'heading' => 'Orders',
156+
'headingBadge' => ['type' => 'primary', 'text' => 'Live'],
157+
'subheading' => 'All orders',
158+
'actions' => [
159+
['label' => 'New', 'url' => '/orders/new', 'primary' => true, 'icon' => 'plus'],
160+
],
161+
'hoverable' => true,
162+
'bordered' => false,
163+
'sortBaseUrl' => '/orders',
164+
'currentSort' => ['key' => 'date', 'dir' => 'asc'],
165+
'showActions' => true,
166+
'selectedIndex' => 2,
167+
'columnCount' => 5,
168+
'currency' => 'USD',
169+
'columns' => [
170+
['key' => 'id', 'label' => '#', 'sortable' => true, 'type' => 'text'],
171+
['key' => 'name', 'label' => 'Customer', 'type' => 'link', 'linkTemplate' => '/c/{id}'],
172+
['key' => 'created', 'label' => 'Date', 'sortable' => true, 'type' => 'date', 'format' => 'M j, Y'],
173+
['key' => 'total', 'label' => 'Total', 'type' => 'currency', 'showTotal' => true],
174+
['key' => 'active', 'label' => 'Active', 'type' => 'boolean'],
175+
],
176+
'items' => array_map(fn($i) => [
177+
'id' => (string) $i,
178+
'name' => "Customer $i",
179+
'created' => date('Y-m-d', mktime(0, 0, 0, (int) ceil($i / 28), (($i - 1) % 28) + 1, 2024) ?: null),
180+
'total' => 100.0 * $i,
181+
'active' => (bool) ($i % 2),
182+
'deleted' => false,
183+
'currency' => 'USD',
184+
], range(1, 100)),
185+
'rowActions' => [
186+
['icon' => 'edit', 'style' => 'secondary', 'labelKey' => 'edit', 'urlTemplate' => '/orders/{id}/edit', 'requiresAdmin' => false],
187+
['icon' => 'trash', 'style' => 'danger', 'labelKey' => 'delete', 'urlTemplate' => '/orders/{id}', 'confirm' => true, 'confirmKey' => 'confirm_delete', 'requiresAdmin' => true],
188+
],
189+
'showTotals' => true,
190+
'totals' => ['total' => 5500.00],
191+
'pagination' => [
192+
'hasPrev' => false,
193+
'hasNext' => true,
194+
'prevUrl' => '#',
195+
'nextUrl' => '/orders?page=2',
196+
'start' => 1,
197+
'end' => 10,
198+
'total' => 42,
199+
'pages' => [
200+
['active' => true, 'number' => 1, 'url' => '/orders'],
201+
['active' => false, 'number' => 2, 'url' => '/orders?page=2'],
202+
['ellipsis' => true, 'number' => null, 'url' => ''],
203+
['active' => false, 'number' => 5, 'url' => '/orders?page=5'],
204+
],
205+
],
206+
'sidePanels' => [
207+
[
208+
'id' => 'summary',
209+
'title' => 'Summary',
210+
'type' => 'stats',
211+
'collapsible' => true,
212+
'collapsed' => false,
213+
'stats' => [
214+
['label' => 'Total Orders', 'value' => 42, 'trend' => 'up', 'delta' => 5],
215+
['label' => 'Revenue', 'value' => '$5,500', 'unit' => 'USD', 'delta' => 0],
216+
],
217+
],
218+
],
219+
'footerColumns' => [
220+
['heading' => 'Product', 'links' => [
221+
['label' => 'Features', 'url' => '/features'],
222+
['label' => 'Pricing', 'url' => '/pricing'],
223+
]],
224+
['heading' => 'Legal', 'links' => [
225+
['label' => 'Privacy', 'url' => '/privacy'],
226+
['label' => 'Terms', 'url' => '/terms'],
227+
]],
228+
],
229+
'copyright' => '©',
230+
'showYear' => true,
231+
'currentYear' => 2024,
232+
'social' => [
233+
['name' => 'GitHub', 'url' => 'https://github.com/myapp', 'icon' => 'github'],
234+
],
235+
'scripts' => [
236+
['url' => '/js/vendor.js'],
237+
['url' => '/js/app.js', 'defer' => true],
238+
],
239+
];
240+
241+
$renderer = Handlebars::template($php);
242+
243+
// Warm up
244+
for ($i = 0; $i < 50; $i++) {
245+
$renderer($data, ['helpers' => $helpers, 'partials' => $partials]);
246+
}
247+
248+
memory_reset_peak_usage();
249+
$start = hrtime(true);
250+
251+
for ($i = 0; $i < $iterations; $i++) {
252+
$renderer($data, ['helpers' => $helpers, 'partials' => $partials]);
253+
}
254+
255+
$elapsed = (hrtime(true) - $start) / 1e9;
256+
$renderPeakMB = memory_get_peak_usage() / 1024 / 1024;
257+
$perRun = $elapsed / $iterations * 1000;
258+
$outputBytes = strlen($renderer($data, ['helpers' => $helpers, 'partials' => $partials]));
259+
260+
printf(
261+
"Executed %d times | %.2f ms/render | %6.1f KB output | %.1f MB peak\n",
262+
$iterations,
263+
$perRun,
264+
$outputBytes / 1024,
265+
$renderPeakMB,
266+
);
267+
268+
if (isset($argv[2])) {
269+
echo "<?php\n", $php, "\n";
270+
}
271+
272+
function loadTemplate(string $name): string
273+
{
274+
$filename = __DIR__ . "/templates/$name.hbs";
275+
$template = file_get_contents($filename);
276+
277+
if ($template === false) {
278+
exit("Failed to open $filename");
279+
}
280+
281+
return $template;
282+
}

tests/templates/alert.hbs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div class="alert alert-{{type}} {{#if dismissible}}alert-dismissible{{/if}}" role="alert">
2+
{{#if icon}}<i class="icon-{{icon}}"></i>{{/if}}
3+
{{{message}}}
4+
{{#if dismissible}}<button type="button" class="close" data-dismiss="alert">&times;</button>{{/if}}
5+
</div>

tests/templates/breadcrumbs.hbs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{{#if breadcrumbs}}
2+
<nav aria-label="breadcrumb">
3+
<ol class="breadcrumb">
4+
{{#each breadcrumbs as |crumb idx|}}
5+
<li class="breadcrumb-item{{#if @last}} active{{/if}}">
6+
{{#if @last}}
7+
{{crumb.label}}
8+
{{else}}
9+
<a href="{{crumb.url}}">{{crumb.label}}</a>
10+
{{/if}}
11+
</li>
12+
{{/each}}
13+
</ol>
14+
</nav>
15+
{{/if}}

tests/templates/footer-col.hbs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<div class="footer-col">
2+
{{#if heading}}<h5>{{heading}}</h5>{{/if}}
3+
<ul>
4+
{{#each links}}
5+
<li><a href="{{url}}"{{#if external}} target="_blank" rel="noopener noreferrer"{{/if}}>{{label}}</a></li>
6+
{{/each}}
7+
</ul>
8+
</div>

0 commit comments

Comments
 (0)