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:
26
src/pug.zig
26
src/pug.zig
@@ -99,12 +99,17 @@ pub fn compile(
|
|||||||
source: []const u8,
|
source: []const u8,
|
||||||
options: CompileOptions,
|
options: CompileOptions,
|
||||||
) CompileError!CompileResult {
|
) 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{
|
var result = CompileResult{
|
||||||
.html = &[_]u8{},
|
.html = &[_]u8{},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stage 1: Lex the source
|
// Stage 1: Lex the source
|
||||||
var lex_inst = Lexer.init(allocator, source, .{
|
var lex_inst = Lexer.init(temp_allocator, source, .{
|
||||||
.filename = options.filename,
|
.filename = options.filename,
|
||||||
}) catch {
|
}) catch {
|
||||||
return error.LexerError;
|
return error.LexerError;
|
||||||
@@ -139,7 +144,7 @@ pub fn compile(
|
|||||||
|
|
||||||
// Stage 2: Strip comments
|
// Stage 2: Strip comments
|
||||||
var stripped = strip_comments.stripComments(
|
var stripped = strip_comments.stripComments(
|
||||||
allocator,
|
temp_allocator,
|
||||||
tokens,
|
tokens,
|
||||||
.{
|
.{
|
||||||
.strip_unbuffered = options.strip_unbuffered_comments,
|
.strip_unbuffered = options.strip_unbuffered_comments,
|
||||||
@@ -149,10 +154,10 @@ pub fn compile(
|
|||||||
) catch {
|
) catch {
|
||||||
return error.LexerError;
|
return error.LexerError;
|
||||||
};
|
};
|
||||||
defer stripped.deinit(allocator);
|
defer stripped.deinit(temp_allocator);
|
||||||
|
|
||||||
// Stage 3: Parse tokens to AST
|
// 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();
|
defer parse.deinit();
|
||||||
|
|
||||||
const ast = parse.parse() catch {
|
const ast = parse.parse() catch {
|
||||||
@@ -181,18 +186,18 @@ pub fn compile(
|
|||||||
return error.ParserError;
|
return error.ParserError;
|
||||||
};
|
};
|
||||||
defer {
|
defer {
|
||||||
ast.deinit(allocator);
|
ast.deinit(temp_allocator);
|
||||||
allocator.destroy(ast);
|
temp_allocator.destroy(ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage 4: Link (resolve extends/blocks)
|
// 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;
|
return error.LinkerError;
|
||||||
};
|
};
|
||||||
defer link_result.deinit(allocator);
|
defer link_result.deinit(temp_allocator);
|
||||||
|
|
||||||
// Stage 5: Generate HTML
|
// Stage 5: Generate HTML
|
||||||
var compiler = Compiler.init(allocator, .{
|
var compiler = Compiler.init(temp_allocator, .{
|
||||||
.pretty = options.pretty,
|
.pretty = options.pretty,
|
||||||
.doctype = options.doctype,
|
.doctype = options.doctype,
|
||||||
.debug = options.debug,
|
.debug = options.debug,
|
||||||
@@ -203,7 +208,8 @@ pub fn compile(
|
|||||||
return error.CodegenError;
|
return error.CodegenError;
|
||||||
};
|
};
|
||||||
|
|
||||||
result.html = html;
|
// Dupe final HTML to base allocator before arena cleanup
|
||||||
|
result.html = try allocator.dupe(u8, html);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,29 +59,38 @@ pub const RenderContext = struct {
|
|||||||
|
|
||||||
/// Render a template with data
|
/// Render a template with data
|
||||||
pub fn renderWithData(allocator: Allocator, source: []const u8, data: anytype) ![]const u8 {
|
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
|
// 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();
|
defer lex.deinit();
|
||||||
|
|
||||||
const tokens = lex.getTokens() catch return error.LexerError;
|
const tokens = lex.getTokens() catch return error.LexerError;
|
||||||
|
|
||||||
// Strip comments
|
// Strip comments
|
||||||
var stripped = pug.strip_comments.stripComments(allocator, tokens, .{}) catch return error.OutOfMemory;
|
var stripped = pug.strip_comments.stripComments(temp_allocator, tokens, .{}) catch return error.OutOfMemory;
|
||||||
defer stripped.deinit(allocator);
|
defer stripped.deinit(temp_allocator);
|
||||||
|
|
||||||
// Parse
|
// 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();
|
defer pug_parser.deinit();
|
||||||
|
|
||||||
const ast = pug_parser.parse() catch {
|
const ast = pug_parser.parse() catch {
|
||||||
return error.ParserError;
|
return error.ParserError;
|
||||||
};
|
};
|
||||||
defer {
|
defer {
|
||||||
ast.deinit(allocator);
|
ast.deinit(temp_allocator);
|
||||||
allocator.destroy(ast);
|
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
|
/// 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.
|
/// AST string values are slices into normalized_source, so it must stay alive.
|
||||||
/// Caller must call result.deinit(allocator) when done.
|
/// Caller must call result.deinit(allocator) when done.
|
||||||
pub fn parseWithSource(allocator: Allocator, source: []const u8) !ParseResult {
|
pub fn parseWithSource(allocator: Allocator, source: []const u8) !ParseResult {
|
||||||
|
// Note: Cannot use ArenaAllocator here since returned AST must outlive function scope
|
||||||
// Lex
|
// Lex
|
||||||
var lex = pug.lexer.Lexer.init(allocator, source, .{}) catch return error.OutOfMemory;
|
var lex = pug.lexer.Lexer.init(allocator, source, .{}) catch return error.OutOfMemory;
|
||||||
errdefer lex.deinit();
|
errdefer lex.deinit();
|
||||||
|
|||||||
Reference in New Issue
Block a user