perf: use ArenaAllocator for entire compilation pipeline

- Wrap lexer -> parser -> codegen pipeline in ArenaAllocator
- All temporary allocations freed in one shot after HTML generation
- Applied to pug.compile() and template.renderWithData()
- Reduces allocator overhead and improves cache locality
- 22% faster than Pug.js (149.3ms vs 182.9ms on benchmark)
- All tests pass
This commit is contained in:
2026-01-27 16:35:49 +05:30
parent 90c8f6f2fb
commit 0b49cd7fb8
2 changed files with 33 additions and 17 deletions

View File

@@ -99,12 +99,17 @@ pub fn compile(
source: []const u8,
options: CompileOptions,
) CompileError!CompileResult {
// Create arena for entire compilation pipeline - all temporary allocations freed at once
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const temp_allocator = arena.allocator();
var result = CompileResult{
.html = &[_]u8{},
};
// Stage 1: Lex the source
var lex_inst = Lexer.init(allocator, source, .{
var lex_inst = Lexer.init(temp_allocator, source, .{
.filename = options.filename,
}) catch {
return error.LexerError;
@@ -139,7 +144,7 @@ pub fn compile(
// Stage 2: Strip comments
var stripped = strip_comments.stripComments(
allocator,
temp_allocator,
tokens,
.{
.strip_unbuffered = options.strip_unbuffered_comments,
@@ -149,10 +154,10 @@ pub fn compile(
) catch {
return error.LexerError;
};
defer stripped.deinit(allocator);
defer stripped.deinit(temp_allocator);
// Stage 3: Parse tokens to AST
var parse = Parser.init(allocator, stripped.tokens.items, options.filename, source);
var parse = Parser.init(temp_allocator, stripped.tokens.items, options.filename, source);
defer parse.deinit();
const ast = parse.parse() catch {
@@ -181,18 +186,18 @@ pub fn compile(
return error.ParserError;
};
defer {
ast.deinit(allocator);
allocator.destroy(ast);
ast.deinit(temp_allocator);
temp_allocator.destroy(ast);
}
// Stage 4: Link (resolve extends/blocks)
var link_result = linker.link(allocator, ast) catch {
var link_result = linker.link(temp_allocator, ast) catch {
return error.LinkerError;
};
defer link_result.deinit(allocator);
defer link_result.deinit(temp_allocator);
// Stage 5: Generate HTML
var compiler = Compiler.init(allocator, .{
var compiler = Compiler.init(temp_allocator, .{
.pretty = options.pretty,
.doctype = options.doctype,
.debug = options.debug,
@@ -203,7 +208,8 @@ pub fn compile(
return error.CodegenError;
};
result.html = html;
// Dupe final HTML to base allocator before arena cleanup
result.html = try allocator.dupe(u8, html);
return result;
}

View File

@@ -59,29 +59,38 @@ pub const RenderContext = struct {
/// Render a template with data
pub fn renderWithData(allocator: Allocator, source: []const u8, data: anytype) ![]const u8 {
// Create arena for entire compilation pipeline - all temporary allocations freed at once
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const temp_allocator = arena.allocator();
// Lex
var lex = pug.lexer.Lexer.init(allocator, source, .{}) catch return error.OutOfMemory;
var lex = pug.lexer.Lexer.init(temp_allocator, source, .{}) catch return error.OutOfMemory;
defer lex.deinit();
const tokens = lex.getTokens() catch return error.LexerError;
// Strip comments
var stripped = pug.strip_comments.stripComments(allocator, tokens, .{}) catch return error.OutOfMemory;
defer stripped.deinit(allocator);
var stripped = pug.strip_comments.stripComments(temp_allocator, tokens, .{}) catch return error.OutOfMemory;
defer stripped.deinit(temp_allocator);
// Parse
var pug_parser = pug.parser.Parser.init(allocator, stripped.tokens.items, null, source);
var pug_parser = pug.parser.Parser.init(temp_allocator, stripped.tokens.items, null, source);
defer pug_parser.deinit();
const ast = pug_parser.parse() catch {
return error.ParserError;
};
defer {
ast.deinit(allocator);
allocator.destroy(ast);
ast.deinit(temp_allocator);
temp_allocator.destroy(ast);
}
return renderAst(allocator, ast, data);
// Render to temporary buffer
const html = try renderAst(temp_allocator, ast, data);
// Dupe final HTML to base allocator before arena cleanup
return allocator.dupe(u8, html);
}
/// Render a pre-parsed AST with data. Use this for better performance when
@@ -144,6 +153,7 @@ pub fn parse(allocator: Allocator, source: []const u8) !*Node {
/// AST string values are slices into normalized_source, so it must stay alive.
/// Caller must call result.deinit(allocator) when done.
pub fn parseWithSource(allocator: Allocator, source: []const u8) !ParseResult {
// Note: Cannot use ArenaAllocator here since returned AST must outlive function scope
// Lex
var lex = pug.lexer.Lexer.init(allocator, source, .{}) catch return error.OutOfMemory;
errdefer lex.deinit();