Skip to content

Commit 1867ca7

Browse files
authored
[llvm] Add benchmarks for Mustache (#160164)
The mustache library still has many issues with performance. While not critical, template rendering is fairly simple, and should be fast. This is hard to reason about without a benchmark. Since there is no standard benchmark suite for Mustache, we can construct some benchmarks that will stress our implementation. This commit adds a new benchmark suite (MustacheBench) that tests several key performance areas: - Parsing (Complex): Measures the one-time parse cost of a large template. - Parsing (Small): Measures the parse cost of a small template with deep keys. - Data Traversal: Stresses hot, cached data traversal using a 5k-iteration loop inside the template. - Context Stack: Tests the render cost of deeply nested sections (50 levels). - Array Iteration: Benchmarks section iteration over a 100,000-item array. - Partial Rendering: Tests partial tag performance inside a large loop. - String Escaping: Measures HTML-escaping a large string. - String Unescaped (Triple): Baseline for {{{...}}} syntax. - String Unescaped (Ampersand): Baseline for {{& ...}} syntax. - Output Buffer: Tests rendering a single 1MB string. Initial results show that Partial rendering is by far the slowest operation. The cost per-item is ~3x higher than simple iteration, because partial templates are being re-parsed on every call instead of being cached. It also shows that the logic for HTML escaping is very expensive likely due to the character by character parsing done to escapes.
1 parent 55906e0 commit 1867ca7

File tree

2 files changed

+257
-0
lines changed

2 files changed

+257
-0
lines changed

llvm/benchmarks/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ add_benchmark(GetIntrinsicForClangBuiltin GetIntrinsicForClangBuiltin.cpp PARTIA
1010
add_benchmark(FormatVariadicBM FormatVariadicBM.cpp PARTIAL_SOURCES_INTENDED)
1111
add_benchmark(GetIntrinsicInfoTableEntriesBM GetIntrinsicInfoTableEntriesBM.cpp PARTIAL_SOURCES_INTENDED)
1212
add_benchmark(SandboxIRBench SandboxIRBench.cpp PARTIAL_SOURCES_INTENDED)
13+
add_benchmark(MustacheBench Mustache.cpp PARTIAL_SOURCES_INTENDED)
1314

1415
add_benchmark(RuntimeLibcallsBench RuntimeLibcalls.cpp PARTIAL_SOURCES_INTENDED)
1516

llvm/benchmarks/Mustache.cpp

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
#include "llvm/Support/Mustache.h"
2+
#include "benchmark/benchmark.h"
3+
#include "llvm/Support/JSON.h"
4+
#include "llvm/Support/raw_ostream.h"
5+
#include <string>
6+
7+
// A large, raw string with many characters that require HTML escaping.
8+
static const std::string LongHtmlString = [] {
9+
std::string S;
10+
S.reserve(500000);
11+
for (int i = 0; i < 50000; ++i) {
12+
S += "<script>alert('xss');</script>";
13+
}
14+
return S;
15+
}();
16+
17+
// A deep AND wide JSON object for testing traversal.
18+
static const llvm::json::Value DeepJsonData = [] {
19+
llvm::json::Value Root = llvm::json::Object();
20+
llvm::json::Object *Current = Root.getAsObject();
21+
for (int i = 0; i < 50; ++i) { // 50 levels deep
22+
for (int j = 0; j < 100; ++j) {
23+
(*Current)["sibling_" + std::to_string(j)] = llvm::json::Value("noise");
24+
}
25+
std::string Key = "level_" + std::to_string(i);
26+
(*Current)[Key] = llvm::json::Object();
27+
Current = (*Current)[Key].getAsObject();
28+
}
29+
(*Current)["final_value"] = llvm::json::Value("Success!");
30+
31+
llvm::json::Array Arr;
32+
for (int i = 0; i < 5000; ++i) { // 5,000 iterations
33+
Arr.push_back(llvm::json::Value(i));
34+
}
35+
36+
llvm::json::Object NewRoot;
37+
NewRoot["deep_data"] = std::move(Root);
38+
NewRoot["loop_array"] = std::move(Arr);
39+
return llvm::json::Value(std::move(NewRoot));
40+
}();
41+
42+
// A huge array for testing iteration performance.
43+
static const llvm::json::Value HugeArrayData = [] {
44+
llvm::json::Array Arr;
45+
for (int i = 0; i < 100000; ++i) { // 100,000 array items
46+
Arr.push_back(llvm::json::Object(
47+
{{"id", llvm::json::Value(static_cast<long long>(i))},
48+
{"is_even", llvm::json::Value(i % 2 == 0)},
49+
{"data", llvm::json::Value("Item data for " + std::to_string(i))}}));
50+
}
51+
return llvm::json::Object({{"items", std::move(Arr)}});
52+
}();
53+
54+
// The main template that includes a partial within a loop.
55+
static const std::string ComplexPartialTemplate =
56+
"Header\n"
57+
"{{#items}}{{> item_partial}}{{/items}}\n"
58+
"Footer";
59+
60+
// The partial template is now more complex, rendering multiple fields and a
61+
// conditional section.
62+
static const std::string ItemPartialTemplate =
63+
"<div class=\"item\" id=\"{{id}}\">\n"
64+
" <p>{{data}}</p>\n"
65+
" {{#is_even}}<span>(Even)</span>{{/is_even}}\n"
66+
"</div>\n";
67+
68+
// A single large string to stress the output buffer.
69+
static const llvm::json::Value LargeOutputData = llvm::json::Object({
70+
{"long_string",
71+
llvm::json::Value(std::string(1024 * 1024, 'A'))} // 1MB string
72+
});
73+
74+
// --- Static Data (Templates) ---
75+
76+
static const std::string BulkEscapingTemplate = "{{content}}";
77+
static const std::string BulkUnescapedTemplate = "{{{content}}}";
78+
static const std::string BulkUnescapedAmpersandTemplate = "{{& content}}";
79+
80+
static const std::string DeepTraversalTemplate = [] {
81+
std::string LongKey =
82+
"deep_data.level_0.level_1.level_2.level_3.level_4.level_5."
83+
"level_6.level_7.level_8.level_9."
84+
"level_10.level_11.level_12.level_13.level_14.level_"
85+
"15.level_16.level_17.level_18.level_19."
86+
"level_20.level_21.level_22.level_23.level_24.level_"
87+
"25.level_26.level_27.level_28.level_29."
88+
"level_30.level_31.level_32.level_33.level_34.level_"
89+
"35.level_36.level_37.level_38.level_39."
90+
"level_40.level_41.level_42.level_43.level_44.level_"
91+
"45.level_46.level_47.level_48.level_49.final_value";
92+
return "{{#loop_array}}{{" + LongKey + "}}{{/loop_array}}";
93+
}();
94+
95+
static const std::string DeeplyNestedRenderingTemplate = [] {
96+
std::string NestedTemplate = "{{#deep_data}}";
97+
for (int i = 0; i < 50; ++i) {
98+
NestedTemplate += "{{#level_" + std::to_string(i) + "}}";
99+
}
100+
NestedTemplate += "{{final_value}}";
101+
for (int i = 49; i >= 0; --i) {
102+
NestedTemplate += "{{/level_" + std::to_string(i) + "}}";
103+
}
104+
NestedTemplate += "{{/deep_data}}";
105+
return NestedTemplate;
106+
}();
107+
108+
static const std::string HugeArrayIterationTemplate =
109+
"{{#items}}ID: {{id}}.{{/items}}";
110+
111+
static const std::string ComplexTemplateParsingTemplate = [] {
112+
std::string LargeTemplate;
113+
LargeTemplate.reserve(100000);
114+
for (int i = 0; i < 1000; ++i) {
115+
LargeTemplate += "{{var_" + std::to_string(i) +
116+
"}}"
117+
"{{#section_" +
118+
std::to_string(i) + "}}Content{{/section_" +
119+
std::to_string(i) +
120+
"}}"
121+
"{{!comment_" +
122+
std::to_string(i) +
123+
"}}"
124+
"{{=<% %>=}}"
125+
"<%var_tag_changed_to_percent_sign_" +
126+
std::to_string(i) +
127+
"%>"
128+
"<%={{ }}=%>"
129+
"{{^inverted_" +
130+
std::to_string(i) + "}}Not Present{{/inverted_" +
131+
std::to_string(i) + "}}";
132+
}
133+
return LargeTemplate;
134+
}();
135+
136+
static const std::string SmallTemplateParsingTemplate =
137+
"{{level_0.sibling_99}}\n"
138+
"{{level_0.level_1.level_2.level_3.level_4.level_5.sibling_50}}\n"
139+
"{{level_0.level_1.level_2.level_3.level_4.level_5."
140+
"level_6.level_7.level_8.level_9."
141+
"level_10.level_11.level_12.level_13.level_14.level_"
142+
"15.level_16.level_17.level_18.level_19."
143+
"level_20.level_21.level_22.level_23.level_24.level_"
144+
"25.level_26.level_27.level_28.level_29."
145+
"level_30.level_31.level_32.level_33.level_34.level_"
146+
"35.level_36.level_37.level_38.level_39."
147+
"level_40.level_41.level_42.level_43.level_44.level_"
148+
"45.level_46.level_47.level_48.level_49.final_value}}\n";
149+
150+
static const std::string LargeOutputStringTemplate = "{{long_string}}";
151+
152+
// Tests the performance of rendering a large string with various escaping
153+
// syntaxes.
154+
static void BM_Mustache_StringRendering(benchmark::State &state,
155+
const std::string &TplStr) {
156+
llvm::mustache::Template Tpl(TplStr);
157+
llvm::json::Value Data =
158+
llvm::json::Object({{"content", llvm::json::Value(LongHtmlString)}});
159+
for (auto _ : state) {
160+
std::string Result;
161+
llvm::raw_string_ostream OS(Result);
162+
Tpl.render(Data, OS);
163+
benchmark::DoNotOptimize(Result);
164+
}
165+
}
166+
BENCHMARK_CAPTURE(BM_Mustache_StringRendering, Escaped, BulkEscapingTemplate);
167+
BENCHMARK_CAPTURE(BM_Mustache_StringRendering, Unescaped_Triple,
168+
BulkUnescapedTemplate);
169+
BENCHMARK_CAPTURE(BM_Mustache_StringRendering, Unescaped_Ampersand,
170+
BulkUnescapedAmpersandTemplate);
171+
172+
// Tests the "hot render" cost of repeatedly traversing a deep and wide
173+
// JSON object.
174+
static void BM_Mustache_DeepTraversal(benchmark::State &state) {
175+
llvm::mustache::Template Tpl(DeepTraversalTemplate);
176+
for (auto _ : state) {
177+
std::string Result;
178+
llvm::raw_string_ostream OS(Result);
179+
Tpl.render(DeepJsonData, OS);
180+
benchmark::DoNotOptimize(Result);
181+
}
182+
}
183+
BENCHMARK(BM_Mustache_DeepTraversal);
184+
185+
// Tests the "hot render" cost of pushing and popping a deep context stack.
186+
static void BM_Mustache_DeeplyNestedRendering(benchmark::State &state) {
187+
llvm::mustache::Template Tpl(DeeplyNestedRenderingTemplate);
188+
for (auto _ : state) {
189+
std::string Result;
190+
llvm::raw_string_ostream OS(Result);
191+
Tpl.render(DeepJsonData, OS);
192+
benchmark::DoNotOptimize(Result);
193+
}
194+
}
195+
BENCHMARK(BM_Mustache_DeeplyNestedRendering);
196+
197+
// Tests the performance of the loop logic when iterating over a huge number of
198+
// items.
199+
static void BM_Mustache_HugeArrayIteration(benchmark::State &state) {
200+
llvm::mustache::Template Tpl(HugeArrayIterationTemplate);
201+
for (auto _ : state) {
202+
std::string Result;
203+
llvm::raw_string_ostream OS(Result);
204+
Tpl.render(HugeArrayData, OS);
205+
benchmark::DoNotOptimize(Result);
206+
}
207+
}
208+
BENCHMARK(BM_Mustache_HugeArrayIteration);
209+
210+
// Tests the performance of the parser on a large, "wide" template.
211+
static void BM_Mustache_ComplexTemplateParsing(benchmark::State &state) {
212+
for (auto _ : state) {
213+
llvm::mustache::Template Tpl(ComplexTemplateParsingTemplate);
214+
benchmark::DoNotOptimize(Tpl);
215+
}
216+
}
217+
BENCHMARK(BM_Mustache_ComplexTemplateParsing);
218+
219+
// Tests the performance of the parser on a small, "deep" template.
220+
static void BM_Mustache_SmallTemplateParsing(benchmark::State &state) {
221+
for (auto _ : state) {
222+
llvm::mustache::Template Tpl(SmallTemplateParsingTemplate);
223+
benchmark::DoNotOptimize(Tpl);
224+
}
225+
}
226+
BENCHMARK(BM_Mustache_SmallTemplateParsing);
227+
228+
// Tests the performance of rendering a template that includes a partial.
229+
static void BM_Mustache_PartialsRendering(benchmark::State &state) {
230+
llvm::mustache::Template Tpl(ComplexPartialTemplate);
231+
Tpl.registerPartial("item_partial", ItemPartialTemplate);
232+
llvm::json::Value Data = HugeArrayData;
233+
234+
for (auto _ : state) {
235+
std::string Result;
236+
llvm::raw_string_ostream OS(Result);
237+
Tpl.render(Data, OS);
238+
benchmark::DoNotOptimize(Result);
239+
}
240+
}
241+
BENCHMARK(BM_Mustache_PartialsRendering);
242+
243+
// Tests the performance of the underlying buffer management when generating a
244+
// very large output.
245+
static void BM_Mustache_LargeOutputString(benchmark::State &state) {
246+
llvm::mustache::Template Tpl(LargeOutputStringTemplate);
247+
for (auto _ : state) {
248+
std::string Result;
249+
llvm::raw_string_ostream OS(Result);
250+
Tpl.render(LargeOutputData, OS);
251+
benchmark::DoNotOptimize(Result);
252+
}
253+
}
254+
BENCHMARK(BM_Mustache_LargeOutputString);
255+
256+
BENCHMARK_MAIN();

0 commit comments

Comments
 (0)