Skip to content

Commit 305eb7f

Browse files
committed
feat(template): V6 compiler optimization with execution plan
Introduce a new compilation pipeline transforming AST into an instruction-based execution plan. Core changes: - add Instruction and ExecutionPlan (linear instruction model) - introduce Optimizer (AST normalization, text merging) - implement Compiler pipeline: optimize -> build execution plan - upgrade Template to support execution plan rendering - update Renderer to execute compiled plans (with filters support) - preserve extends/block inheritance via AST fallback path - fix string literal handling and filter propagation Architecture: - move from AST interpreter to execution plan (bytecode-like model) - improve rendering performance and CPU efficiency - establish foundation for future optimizations (constant folding, JIT) Environment: - now uses Compiler instead of direct Template construction Benchmarks: - add render benchmark (Google Benchmark optional) - add cache benchmark - measure performance across expressions, loops, includes Tests: - update compiler, renderer, integration tests for V6 - fix cache test to support ExecutionPlan - improve coverage for filters, expressions, inheritance Build: - make benchmarks optional in CMake - fix benchmark linking (benchmark::benchmark) This release introduces a major internal redesign of the rendering engine, shifting to a compiled execution model for better performance and scalability.
1 parent e1d5bd4 commit 305eb7f

21 files changed

Lines changed: 2097 additions & 411 deletions

CMakeLists.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ add_library(template
5252
src/StringLoader.cpp
5353
src/Template.cpp
5454
src/Value.cpp
55+
src/Optimizer.cpp
5556
)
5657

5758
add_library(vix::template ALIAS template)
@@ -115,7 +116,13 @@ endif()
115116
# ============================================================================
116117

117118
if(TEMPLATE_BUILD_BENCHMARKS)
118-
add_subdirectory(benchmarks)
119+
find_package(benchmark QUIET)
120+
121+
if(benchmark_FOUND)
122+
add_subdirectory(benchmarks)
123+
else()
124+
message(STATUS "Google Benchmark not found, benchmarks will not be built")
125+
endif()
119126
endif()
120127

121128
# ============================================================================

benchmarks/CMakeLists.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,22 @@ if(NOT TARGET vix::template)
44
message(FATAL_ERROR "Target vix::template not found")
55
endif()
66

7+
if(NOT TARGET benchmark::benchmark)
8+
message(FATAL_ERROR "Target benchmark::benchmark not found")
9+
endif()
10+
711
add_executable(template_render_bench
812
render_bench.cpp
913
)
1014

1115
target_link_libraries(template_render_bench
1216
PRIVATE
1317
vix::template
18+
benchmark::benchmark
1419
)
1520

21+
target_compile_features(template_render_bench PRIVATE cxx_std_20)
22+
1623
add_executable(template_parse_bench
1724
parse_bench.cpp
1825
)
@@ -22,6 +29,8 @@ target_link_libraries(template_parse_bench
2229
vix::template
2330
)
2431

32+
target_compile_features(template_parse_bench PRIVATE cxx_std_20)
33+
2534
add_executable(template_cache_bench
2635
cache_bench.cpp
2736
)
@@ -31,6 +40,8 @@ target_link_libraries(template_cache_bench
3140
vix::template
3241
)
3342

43+
target_compile_features(template_cache_bench PRIVATE cxx_std_20)
44+
3445
set_target_properties(
3546
template_render_bench
3647
template_parse_bench

benchmarks/cache_bench.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <string>
2121

2222
#include <vix/template/Cache.hpp>
23+
#include <vix/template/ExecutionPlan.hpp>
2324
#include <vix/template/Template.hpp>
2425

2526
using namespace vix::template_;
@@ -63,7 +64,10 @@ class MemoryCache : public Cache
6364

6465
static TemplatePtr make_template(const std::string &name)
6566
{
66-
return std::make_shared<Template>(name, RootNode{});
67+
return std::make_shared<Template>(
68+
name,
69+
RootNode{},
70+
ExecutionPlan{});
6771
}
6872

6973
int main()

benchmarks/render_bench.cpp

Lines changed: 169 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,71 +13,198 @@
1313
* Vix.cpp
1414
*
1515
*/
16-
#include <chrono>
17-
#include <iostream>
16+
#include <benchmark/benchmark.h>
17+
18+
#include <memory>
1819
#include <string>
20+
#include <utility>
1921

22+
#include <vix/template/Compiler.hpp>
23+
#include <vix/template/Context.hpp>
2024
#include <vix/template/Lexer.hpp>
2125
#include <vix/template/Parser.hpp>
22-
#include <vix/template/Renderer.hpp>
23-
#include <vix/template/Context.hpp>
26+
#include <vix/template/StringLoader.hpp>
27+
#include <vix/template/Template.hpp>
2428

2529
using namespace vix::template_;
2630

27-
static RootNode build_ast(const std::string &tpl)
31+
namespace
2832
{
29-
Lexer lexer(tpl);
30-
auto tokens = lexer.tokenize();
33+
[[nodiscard]] Template compile_template(
34+
const std::string &name,
35+
const std::string &source,
36+
std::shared_ptr<Loader> loader = nullptr)
37+
{
38+
Lexer lexer(source);
39+
auto tokens = lexer.tokenize();
40+
41+
Parser parser(std::move(tokens));
42+
RootNode root = parser.parse();
43+
44+
Compiler compiler;
45+
return compiler.compile(name, std::move(root), std::move(loader));
46+
}
47+
48+
[[nodiscard]] Context make_basic_context()
49+
{
50+
Context ctx;
51+
ctx.set("name", "Alice");
52+
ctx.set("bio", "offline-first systems");
53+
ctx.set("price", 6);
54+
ctx.set("quantity", 7);
55+
ctx.set("enabled", true);
56+
57+
Object user;
58+
user["name"] = Value("Alice");
59+
user["role"] = Value("admin");
60+
ctx.set("user", user);
61+
62+
Array items;
63+
items.emplace_back("Book");
64+
items.emplace_back("Phone");
65+
items.emplace_back("Pen");
66+
items.emplace_back("Bag");
67+
items.emplace_back("Mouse");
68+
ctx.set("items", items);
69+
70+
return ctx;
71+
}
72+
73+
} // namespace
3174

32-
Parser parser(std::move(tokens));
33-
return parser.parse();
75+
static void BM_render_plain_text(benchmark::State &state)
76+
{
77+
const Template tpl = compile_template(
78+
"plain_text",
79+
"Hello world. This is a simple static template used for benchmarking.");
80+
const Context ctx;
81+
82+
for (auto _ : state)
83+
{
84+
const RenderResult result = tpl.render(ctx);
85+
benchmark::DoNotOptimize(result.output);
86+
}
87+
88+
state.SetItemsProcessed(
89+
static_cast<std::int64_t>(state.iterations()));
3490
}
91+
BENCHMARK(BM_render_plain_text);
3592

36-
int main()
93+
static void BM_render_variable(benchmark::State &state)
3794
{
38-
const std::string tpl =
39-
"<h1>{{ title }}</h1>\n"
40-
"{% if user %}Hello {{ user }}{% endif %}\n"
41-
"<ul>\n"
42-
"{% for item in items %}<li>{{ item }}</li>{% endfor %}\n"
43-
"</ul>";
95+
const Template tpl = compile_template(
96+
"variable",
97+
"Hello {{ name }}");
98+
const Context ctx = make_basic_context();
4499

45-
// Build AST once (simulate compiled template)
46-
RootNode root = build_ast(tpl);
100+
for (auto _ : state)
101+
{
102+
const RenderResult result = tpl.render(ctx);
103+
benchmark::DoNotOptimize(result.output);
104+
}
47105

48-
// Prepare context
49-
Context ctx;
50-
ctx.set("title", "Benchmark");
51-
ctx.set("user", "Gaspard");
106+
state.SetItemsProcessed(
107+
static_cast<std::int64_t>(state.iterations()));
108+
}
109+
BENCHMARK(BM_render_variable);
52110

53-
Array items;
54-
items.emplace_back("Laptop");
55-
items.emplace_back("Phone");
56-
items.emplace_back("Book");
57-
ctx.set("items", items);
111+
static void BM_render_expression(benchmark::State &state)
112+
{
113+
const Template tpl = compile_template(
114+
"expression",
115+
"{{ price * quantity }}");
116+
const Context ctx = make_basic_context();
58117

59-
Renderer renderer(true);
118+
for (auto _ : state)
119+
{
120+
const RenderResult result = tpl.render(ctx);
121+
benchmark::DoNotOptimize(result.output);
122+
}
123+
124+
state.SetItemsProcessed(
125+
static_cast<std::int64_t>(state.iterations()));
126+
}
127+
BENCHMARK(BM_render_expression);
128+
129+
static void BM_render_if(benchmark::State &state)
130+
{
131+
const Template tpl = compile_template(
132+
"if_template",
133+
"{% if enabled %}Hello {{ name }}{% endif %}");
134+
const Context ctx = make_basic_context();
60135

61-
const int iterations = 100000;
136+
for (auto _ : state)
137+
{
138+
const RenderResult result = tpl.render(ctx);
139+
benchmark::DoNotOptimize(result.output);
140+
}
62141

63-
auto start = std::chrono::high_resolution_clock::now();
142+
state.SetItemsProcessed(
143+
static_cast<std::int64_t>(state.iterations()));
144+
}
145+
BENCHMARK(BM_render_if);
146+
147+
static void BM_render_for_loop(benchmark::State &state)
148+
{
149+
const Template tpl = compile_template(
150+
"for_loop",
151+
"{% for item in items %}[{{ item }}]{% endfor %}");
152+
const Context ctx = make_basic_context();
64153

65-
for (int i = 0; i < iterations; ++i)
154+
for (auto _ : state)
66155
{
67-
auto result = renderer.render(root, ctx);
68-
(void)result;
156+
const RenderResult result = tpl.render(ctx);
157+
benchmark::DoNotOptimize(result.output);
69158
}
70159

71-
auto end = std::chrono::high_resolution_clock::now();
160+
state.SetItemsProcessed(
161+
static_cast<std::int64_t>(state.iterations()));
162+
}
163+
BENCHMARK(BM_render_for_loop);
164+
165+
static void BM_render_mixed_template(benchmark::State &state)
166+
{
167+
const Template tpl = compile_template(
168+
"mixed",
169+
"Hello {{ user.name }}\n"
170+
"{% if enabled %}"
171+
"Role: {{ user.role }}\n"
172+
"Total: {{ price * quantity }}\n"
173+
"{% endif %}"
174+
"Items: {% for item in items %}[{{ item | upper }}]{% endfor %}");
175+
const Context ctx = make_basic_context();
176+
177+
for (auto _ : state)
178+
{
179+
const RenderResult result = tpl.render(ctx);
180+
benchmark::DoNotOptimize(result.output);
181+
}
72182

73-
const auto duration =
74-
std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
183+
state.SetItemsProcessed(
184+
static_cast<std::int64_t>(state.iterations()));
185+
}
186+
BENCHMARK(BM_render_mixed_template);
75187

76-
std::cout << "Render benchmark\n";
77-
std::cout << "Iterations: " << iterations << '\n';
78-
std::cout << "Total time (ms): " << duration.count() << '\n';
79-
std::cout << "Avg per render (us): "
80-
<< (duration.count() * 1000.0 / iterations) << '\n';
188+
static void BM_render_include(benchmark::State &state)
189+
{
190+
auto loader = std::make_shared<StringLoader>();
191+
loader->set("header.html", "Header {{ name }}\n");
192+
loader->set("body.html", "{% include \"header.html\" %}Total: {{ price * quantity }}");
193+
const Template tpl = compile_template(
194+
"include_template",
195+
"{% include \"body.html\" %}",
196+
loader);
197+
const Context ctx = make_basic_context();
198+
199+
for (auto _ : state)
200+
{
201+
const RenderResult result = tpl.render(ctx);
202+
benchmark::DoNotOptimize(result.output);
203+
}
81204

82-
return 0;
205+
state.SetItemsProcessed(
206+
static_cast<std::int64_t>(state.iterations()));
83207
}
208+
BENCHMARK(BM_render_include);
209+
210+
BENCHMARK_MAIN();

0 commit comments

Comments
 (0)