Skip to content

Commit c88197b

Browse files
committed
Benchmark v0.9.9 compilation and rendering speed
1 parent 96301cc commit c88197b

9 files changed

Lines changed: 560 additions & 0 deletions

File tree

tests/benchmark.php

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

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)