Add README and simplify ViewEngine API

- ViewEngine.init() no longer requires allocator
- render() and renderTpl() accept allocator parameter
- Remove deinit() - no resources to clean up
- Remove unused parse/renderDoc methods
- Add memory management guidance to runtime.zig
- Clean up unused imports and options
This commit is contained in:
2026-01-17 23:59:22 +05:30
parent 1fff91d7d9
commit 458de03c02
19 changed files with 1036 additions and 366 deletions

View File

@@ -0,0 +1,388 @@
//! Pugz Rendering Benchmark
//!
//! Measures template rendering performance with various template complexities.
//! Run with: zig build bench
//!
//! Metrics reported:
//! - Total time for N iterations
//! - Average time per render
//! - Renders per second
//! - Memory usage per render
const std = @import("std");
const pugz = @import("pugz");
const Allocator = std.mem.Allocator;
/// Benchmark configuration
const Config = struct {
warmup_iterations: usize = 200,
benchmark_iterations: usize = 20_000,
show_output: bool = false,
};
/// Benchmark result
const Result = struct {
name: []const u8,
iterations: usize,
total_ns: u64,
min_ns: u64,
max_ns: u64,
avg_ns: u64,
ops_per_sec: f64,
bytes_per_render: usize,
arena_peak_bytes: usize,
pub fn print(self: Result) void {
std.debug.print("\n{s}\n", .{self.name});
std.debug.print(" Iterations: {d:>10}\n", .{self.iterations});
std.debug.print(" Total time: {d:>10.2} ms\n", .{@as(f64, @floatFromInt(self.total_ns)) / 1_000_000.0});
std.debug.print(" Avg per render: {d:>10.2} us\n", .{@as(f64, @floatFromInt(self.avg_ns)) / 1_000.0});
std.debug.print(" Min: {d:>10.2} us\n", .{@as(f64, @floatFromInt(self.min_ns)) / 1_000.0});
std.debug.print(" Max: {d:>10.2} us\n", .{@as(f64, @floatFromInt(self.max_ns)) / 1_000.0});
std.debug.print(" Renders/sec: {d:>10.0}\n", .{self.ops_per_sec});
std.debug.print(" Output size: {d:>10} bytes\n", .{self.bytes_per_render});
std.debug.print(" Memory/render: {d:>10} bytes\n", .{self.arena_peak_bytes});
}
};
/// Run a benchmark for a template
fn runBenchmark(
allocator: Allocator,
comptime name: []const u8,
template: []const u8,
data: anytype,
config: Config,
) !Result {
// Warmup phase
for (0..config.warmup_iterations) |_| {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
_ = try pugz.renderTemplate(arena.allocator(), template, data);
}
// Benchmark phase
var total_ns: u64 = 0;
var min_ns: u64 = std.math.maxInt(u64);
var max_ns: u64 = 0;
var output_size: usize = 0;
var peak_memory: usize = 0;
var timer = try std.time.Timer.start();
for (0..config.benchmark_iterations) |i| {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
timer.reset();
const result = try pugz.renderTemplate(arena.allocator(), template, data);
const elapsed = timer.read();
total_ns += elapsed;
min_ns = @min(min_ns, elapsed);
max_ns = @max(max_ns, elapsed);
if (i == 0) {
output_size = result.len;
// Measure memory used by arena (query state before deinit)
const state = arena.queryCapacity();
peak_memory = state;
if (config.show_output) {
std.debug.print("\n--- {s} output ---\n{s}\n", .{ name, result });
}
}
}
const avg_ns = total_ns / config.benchmark_iterations;
const ops_per_sec = @as(f64, @floatFromInt(config.benchmark_iterations)) / (@as(f64, @floatFromInt(total_ns)) / 1_000_000_000.0);
return .{
.name = name,
.iterations = config.benchmark_iterations,
.total_ns = total_ns,
.min_ns = min_ns,
.max_ns = max_ns,
.avg_ns = avg_ns,
.ops_per_sec = ops_per_sec,
.bytes_per_render = output_size,
.arena_peak_bytes = peak_memory,
};
}
/// Simple template - just a few elements
const simple_template =
\\doctype html
\\html
\\ head
\\ title= title
\\ body
\\ h1 Hello, #{name}!
\\ p Welcome to our site.
;
/// Medium template - with conditionals and loops
const medium_template =
\\doctype html
\\html
\\ head
\\ title= title
\\ meta(charset="utf-8")
\\ meta(name="viewport" content="width=device-width, initial-scale=1")
\\ body
\\ header
\\ nav.navbar
\\ a.brand(href="/") Brand
\\ ul.nav-links
\\ each link in navLinks
\\ li
\\ a(href=link.href)= link.text
\\ main.container
\\ h1= title
\\ if showIntro
\\ p.intro Welcome, #{userName}!
\\ section.content
\\ each item in items
\\ .card
\\ h3= item.title
\\ p= item.description
\\ footer
\\ p Copyright 2024
;
/// Complex template - with mixins, nested loops, conditionals
const complex_template =
\\mixin card(title, description)
\\ .card
\\ .card-header
\\ h3= title
\\ .card-body
\\ p= description
\\ block
\\
\\mixin button(text, type="primary")
\\ button(class="btn btn-" + type)= text
\\
\\mixin navItem(href, text)
\\ li
\\ a(href=href)= text
\\
\\doctype html
\\html
\\ head
\\ title= title
\\ meta(charset="utf-8")
\\ meta(name="viewport" content="width=device-width, initial-scale=1")
\\ link(rel="stylesheet" href="/css/style.css")
\\ body
\\ header.site-header
\\ .container
\\ a.logo(href="/")
\\ img(src="/img/logo.png" alt="Logo")
\\ nav.main-nav
\\ ul
\\ each link in navLinks
\\ +navItem(link.href, link.text)
\\ .user-menu
\\ if user
\\ span.greeting Hello, #{user.name}!
\\ +button("Logout", "secondary")
\\ else
\\ +button("Login")
\\ +button("Sign Up", "success")
\\ main.site-content
\\ .container
\\ .page-header
\\ h1= pageTitle
\\ if subtitle
\\ p.subtitle= subtitle
\\ .content-grid
\\ each category in categories
\\ section.category
\\ h2= category.name
\\ .cards
\\ each item in category.items
\\ +card(item.title, item.description)
\\ .card-footer
\\ +button("View Details")
\\ aside.sidebar
\\ .widget
\\ h4 Recent Posts
\\ ul.post-list
\\ each post in recentPosts
\\ li
\\ a(href=post.url)= post.title
\\ .widget
\\ h4 Tags
\\ .tag-cloud
\\ each tag in allTags
\\ span.tag= tag
\\ footer.site-footer
\\ .container
\\ .footer-grid
\\ .footer-col
\\ h4 About
\\ p Some description text here.
\\ .footer-col
\\ h4 Links
\\ ul
\\ each link in footerLinks
\\ li
\\ a(href=link.href)= link.text
\\ .footer-col
\\ h4 Contact
\\ p Email: contact@example.com
\\ .copyright
\\ p Copyright #{year} Example Inc.
;
pub fn main() !void {
// Use GPA with leak detection enabled
var gpa = std.heap.GeneralPurposeAllocator(.{
.stack_trace_frames = 10,
.safety = true,
}){};
defer {
const leaked = gpa.deinit();
if (leaked == .leak) {
std.debug.print("\n⚠️ MEMORY LEAK DETECTED!\n", .{});
} else {
std.debug.print("\n✓ No memory leaks detected.\n", .{});
}
}
const allocator = gpa.allocator();
const config = Config{
.warmup_iterations = 200,
.benchmark_iterations = 20_000,
.show_output = false,
};
std.debug.print("\n", .{});
std.debug.print("╔══════════════════════════════════════════════════════════════╗\n", .{});
std.debug.print("║ Pugz Template Rendering Benchmark ║\n", .{});
std.debug.print("╠══════════════════════════════════════════════════════════════╣\n", .{});
std.debug.print("║ Warmup iterations: {d:>6} ║\n", .{config.warmup_iterations});
std.debug.print("║ Benchmark iterations: {d:>6} ║\n", .{config.benchmark_iterations});
std.debug.print("╚══════════════════════════════════════════════════════════════╝\n", .{});
// Simple template benchmark
const simple_result = try runBenchmark(
allocator,
"Simple Template (basic elements, interpolation)",
simple_template,
.{
.title = "Welcome",
.name = "World",
},
config,
);
simple_result.print();
// Medium template benchmark
const NavLink = struct { href: []const u8, text: []const u8 };
const Item = struct { title: []const u8, description: []const u8 };
const medium_result = try runBenchmark(
allocator,
"Medium Template (loops, conditionals, nested elements)",
medium_template,
.{
.title = "Dashboard",
.userName = "Alice",
.showIntro = true,
.navLinks = &[_]NavLink{
.{ .href = "/", .text = "Home" },
.{ .href = "/about", .text = "About" },
.{ .href = "/contact", .text = "Contact" },
},
.items = &[_]Item{
.{ .title = "Item 1", .description = "Description for item 1" },
.{ .title = "Item 2", .description = "Description for item 2" },
.{ .title = "Item 3", .description = "Description for item 3" },
.{ .title = "Item 4", .description = "Description for item 4" },
},
},
config,
);
medium_result.print();
// Complex template benchmark
const User = struct { name: []const u8 };
const SimpleItem = struct { title: []const u8, description: []const u8 };
const Category = struct { name: []const u8, items: []const SimpleItem };
const Post = struct { url: []const u8, title: []const u8 };
const FooterLink = struct { href: []const u8, text: []const u8 };
const complex_result = try runBenchmark(
allocator,
"Complex Template (mixins, nested loops, conditionals)",
complex_template,
.{
.title = "Example Site",
.pageTitle = "Welcome to Our Site",
.subtitle = "The best place on the web",
.year = "2024",
.user = User{ .name = "Alice" },
.navLinks = &[_]NavLink{
.{ .href = "/", .text = "Home" },
.{ .href = "/products", .text = "Products" },
.{ .href = "/about", .text = "About" },
.{ .href = "/contact", .text = "Contact" },
},
.categories = &[_]Category{
.{
.name = "Featured",
.items = &[_]SimpleItem{
.{ .title = "Product A", .description = "Amazing product A" },
.{ .title = "Product B", .description = "Wonderful product B" },
},
},
.{
.name = "Popular",
.items = &[_]SimpleItem{
.{ .title = "Product C", .description = "Popular product C" },
.{ .title = "Product D", .description = "Trending product D" },
},
},
},
.recentPosts = &[_]Post{
.{ .url = "/blog/post-1", .title = "First Blog Post" },
.{ .url = "/blog/post-2", .title = "Second Blog Post" },
.{ .url = "/blog/post-3", .title = "Third Blog Post" },
},
.allTags = &[_][]const u8{ "tech", "news", "tutorial", "review", "guide" },
.footerLinks = &[_]FooterLink{
.{ .href = "/privacy", .text = "Privacy Policy" },
.{ .href = "/terms", .text = "Terms of Service" },
.{ .href = "/sitemap", .text = "Sitemap" },
},
},
config,
);
complex_result.print();
// Summary
std.debug.print("\n", .{});
std.debug.print("╔══════════════════════════════════════════════════════════════╗\n", .{});
std.debug.print("║ Summary ║\n", .{});
std.debug.print("╠══════════════════════════════════════════════════════════════╣\n", .{});
std.debug.print("║ Template │ Avg (us) │ Renders/sec │ Output (bytes) ║\n", .{});
std.debug.print("╠──────────────────┼──────────┼─────────────┼─────────────────╣\n", .{});
std.debug.print("║ Simple │ {d:>8.2} │ {d:>11.0} │ {d:>15} ║\n", .{
@as(f64, @floatFromInt(simple_result.avg_ns)) / 1_000.0,
simple_result.ops_per_sec,
simple_result.bytes_per_render,
});
std.debug.print("║ Medium │ {d:>8.2} │ {d:>11.0} │ {d:>15} ║\n", .{
@as(f64, @floatFromInt(medium_result.avg_ns)) / 1_000.0,
medium_result.ops_per_sec,
medium_result.bytes_per_render,
});
std.debug.print("║ Complex │ {d:>8.2} │ {d:>11.0} │ {d:>15} ║\n", .{
@as(f64, @floatFromInt(complex_result.avg_ns)) / 1_000.0,
complex_result.ops_per_sec,
complex_result.bytes_per_render,
});
std.debug.print("╚══════════════════════════════════════════════════════════════╝\n", .{});
std.debug.print("\n", .{});
}

View File

@@ -0,0 +1,479 @@
//! Pugz Benchmark - Comparison with template-engine-bench
//!
//! These benchmarks use the exact same templates from:
//! https://github.com/itsarnaud/template-engine-bench
//!
//! Run individual benchmarks:
//! zig build test-bench -- simple-0
//! zig build test-bench -- friends
//!
//! Run all benchmarks:
//! zig build test-bench
//!
//! Pug.js reference (2000 iterations on MacBook Air M2):
//! - simple-0: pug => 2ms
//! - simple-1: pug => 9ms
//! - simple-2: pug => 9ms
//! - if-expression: pug => 12ms
//! - projects-escaped: pug => 86ms
//! - search-results: pug => 41ms
//! - friends: pug => 110ms
const std = @import("std");
const pugz = @import("pugz");
const iterations: usize = 2000;
// ═══════════════════════════════════════════════════════════════════════════
// simple-0
// ═══════════════════════════════════════════════════════════════════════════
const simple_0_tpl = "h1 Hello, #{name}";
test "bench: simple-0" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa.deinit() == .leak) @panic("leak!");
const engine = pugz.ViewEngine.init(.{});
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
var total_ns: u64 = 0;
var timer = try std.time.Timer.start();
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
timer.reset();
_ = try engine.renderTpl(arena.allocator(), simple_0_tpl, .{
.name = "John",
});
total_ns += timer.read();
}
printResult("simple-0", total_ns, 2);
}
// ═══════════════════════════════════════════════════════════════════════════
// simple-1
// ═══════════════════════════════════════════════════════════════════════════
const simple_1_tpl =
\\.simple-1(style="background-color: blue; border: 1px solid black")
\\ .colors
\\ span.hello Hello #{name}!
\\ strong You have #{messageCount} messages!
\\ if colors
\\ ul
\\ each color in colors
\\ li.color= color
\\ else
\\ div No colors!
\\ if primary
\\ button(type="button" class="primary") Click me!
\\ else
\\ button(type="button" class="secondary") Click me!
;
test "bench: simple-1" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const engine = pugz.ViewEngine.init(.{});
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
var total_ns: u64 = 0;
var timer = try std.time.Timer.start();
const data = .{
.name = "George Washington",
.messageCount = 999,
.colors = &[_][]const u8{ "red", "green", "blue", "yellow", "orange", "pink", "black", "white", "beige", "brown", "cyan", "magenta" },
.primary = true,
};
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
timer.reset();
_ = try engine.renderTpl(arena.allocator(), simple_1_tpl, data);
total_ns += timer.read();
}
printResult("simple-1", total_ns, 9);
}
// ═══════════════════════════════════════════════════════════════════════════
// simple-2
// ═══════════════════════════════════════════════════════════════════════════
const simple_2_tpl =
\\div
\\ h1.header #{header}
\\ h2.header2 #{header2}
\\ h3.header3 #{header3}
\\ h4.header4 #{header4}
\\ h5.header5 #{header5}
\\ h6.header6 #{header6}
\\ ul.list
\\ each item in list
\\ li.item #{item}
;
test "bench: simple-2" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const engine = pugz.ViewEngine.init(.{});
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
var total_ns: u64 = 0;
var timer = try std.time.Timer.start();
const data = .{
.header = "Header",
.header2 = "Header2",
.header3 = "Header3",
.header4 = "Header4",
.header5 = "Header5",
.header6 = "Header6",
.list = &[_][]const u8{ "1000000000", "2", "3", "4", "5", "6", "7", "8", "9", "10" },
};
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
timer.reset();
_ = try engine.renderTpl(arena.allocator(), simple_2_tpl, data);
total_ns += timer.read();
}
printResult("simple-2", total_ns, 9);
}
// ═══════════════════════════════════════════════════════════════════════════
// if-expression
// ═══════════════════════════════════════════════════════════════════════════
const if_expression_tpl =
\\each account in accounts
\\ div
\\ if account.status == "closed"
\\ div Your account has been closed!
\\ if account.status == "suspended"
\\ div Your account has been temporarily suspended
\\ if account.status == "open"
\\ div
\\ | Bank balance:
\\ if account.negative
\\ span.negative= account.balanceFormatted
\\ else
\\ span.positive= account.balanceFormatted
;
const Account = struct {
balance: i32,
balanceFormatted: []const u8,
status: []const u8,
negative: bool,
};
test "bench: if-expression" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const engine = pugz.ViewEngine.init(.{});
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
var total_ns: u64 = 0;
var timer = try std.time.Timer.start();
const data = .{
.accounts = &[_]Account{
.{ .balance = 0, .balanceFormatted = "$0.00", .status = "open", .negative = false },
.{ .balance = 10, .balanceFormatted = "$10.00", .status = "closed", .negative = false },
.{ .balance = -100, .balanceFormatted = "$-100.00", .status = "suspended", .negative = true },
.{ .balance = 999, .balanceFormatted = "$999.00", .status = "open", .negative = false },
},
};
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
timer.reset();
_ = try engine.renderTpl(arena.allocator(), if_expression_tpl, data);
total_ns += timer.read();
}
printResult("if-expression", total_ns, 12);
}
// ═══════════════════════════════════════════════════════════════════════════
// projects-escaped
// ═══════════════════════════════════════════════════════════════════════════
const projects_escaped_tpl =
\\doctype html
\\html
\\ head
\\ title #{title}
\\ body
\\ p #{text}
\\ each project in projects
\\ a(href=project.url) #{project.name}
\\ p #{project.description}
\\ else
\\ p No projects
;
const Project = struct {
name: []const u8,
url: []const u8,
description: []const u8,
};
test "bench: projects-escaped" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const engine = pugz.ViewEngine.init(.{});
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
var total_ns: u64 = 0;
var timer = try std.time.Timer.start();
const data = .{
.title = "Projects",
.text = "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>",
.projects = &[_]Project{
.{ .name = "<strong>Facebook</strong>", .url = "http://facebook.com", .description = "Social network" },
.{ .name = "<strong>Google</strong>", .url = "http://google.com", .description = "Search engine" },
.{ .name = "<strong>Twitter</strong>", .url = "http://twitter.com", .description = "Microblogging service" },
.{ .name = "<strong>Amazon</strong>", .url = "http://amazon.com", .description = "Online retailer" },
.{ .name = "<strong>eBay</strong>", .url = "http://ebay.com", .description = "Online auction" },
.{ .name = "<strong>Wikipedia</strong>", .url = "http://wikipedia.org", .description = "A free encyclopedia" },
.{ .name = "<strong>LiveJournal</strong>", .url = "http://livejournal.com", .description = "Blogging platform" },
},
};
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
timer.reset();
_ = try engine.renderTpl(arena.allocator(), projects_escaped_tpl, data);
total_ns += timer.read();
}
printResult("projects-escaped", total_ns, 86);
}
// ═══════════════════════════════════════════════════════════════════════════
// search-results
// ═══════════════════════════════════════════════════════════════════════════
// Simplified to match original JS benchmark template exactly
const search_results_tpl =
\\.search-results.view-gallery
\\ each searchRecord in searchRecords
\\ .search-item
\\ .search-item-container.drop-shadow
\\ .img-container
\\ img(src=searchRecord.imgUrl)
\\ h4.title
\\ a(href=searchRecord.viewItemUrl)= searchRecord.title
\\ | #{searchRecord.description}
\\ if searchRecord.featured
\\ div Featured!
\\ if searchRecord.sizes
\\ div
\\ | Sizes available:
\\ ul
\\ each size in searchRecord.sizes
\\ li= size
;
const SearchRecord = struct {
imgUrl: []const u8,
viewItemUrl: []const u8,
title: []const u8,
description: []const u8,
featured: bool,
sizes: ?[]const []const u8,
};
test "bench: search-results" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const engine = pugz.ViewEngine.init(.{});
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
const sizes = &[_][]const u8{ "S", "M", "L", "XL", "XXL" };
// Long descriptions matching original benchmark (Lorem ipsum paragraphs)
const desc1 = "Duis laborum nostrud consectetur exercitation minim ad laborum velit adipisicing. Dolore adipisicing pariatur in fugiat nulla voluptate aliquip esse laboris quis exercitation aliqua labore.";
const desc2 = "Incididunt ea mollit commodo velit officia. Enim officia occaecat nulla aute. Esse sunt laborum excepteur sint elit sit esse ad.";
const desc3 = "Aliquip Lorem consequat sunt ipsum dolor amet amet cupidatat deserunt eiusmod qui anim cillum sint. Dolor exercitation tempor aliquip sunt nisi ipsum ullamco adipisicing.";
const desc4 = "Est ad amet irure veniam dolore velit amet irure fugiat ut elit. Tempor fugiat dolor tempor aute enim. Ad sint mollit laboris id sint ullamco eu do irure nostrud magna sunt voluptate.";
const desc5 = "Sunt ex magna culpa cillum esse irure consequat Lorem aliquip enim sit reprehenderit sunt. Exercitation esse irure magna proident ex ut elit magna mollit aliqua amet.";
var total_ns: u64 = 0;
var timer = try std.time.Timer.start();
const data = .{
.searchRecords = &[_]SearchRecord{
.{ .imgUrl = "img1.jpg", .viewItemUrl = "http://foo/1", .title = "Namebox", .description = desc1, .featured = true, .sizes = sizes },
.{ .imgUrl = "img2.jpg", .viewItemUrl = "http://foo/2", .title = "Arctiq", .description = desc2, .featured = false, .sizes = sizes },
.{ .imgUrl = "img3.jpg", .viewItemUrl = "http://foo/3", .title = "Niquent", .description = desc3, .featured = true, .sizes = sizes },
.{ .imgUrl = "img4.jpg", .viewItemUrl = "http://foo/4", .title = "Remotion", .description = desc4, .featured = true, .sizes = sizes },
.{ .imgUrl = "img5.jpg", .viewItemUrl = "http://foo/5", .title = "Octocore", .description = desc5, .featured = true, .sizes = sizes },
.{ .imgUrl = "img6.jpg", .viewItemUrl = "http://foo/6", .title = "Spherix", .description = desc1, .featured = true, .sizes = sizes },
.{ .imgUrl = "img7.jpg", .viewItemUrl = "http://foo/7", .title = "Quarex", .description = desc2, .featured = true, .sizes = sizes },
.{ .imgUrl = "img8.jpg", .viewItemUrl = "http://foo/8", .title = "Supremia", .description = desc3, .featured = false, .sizes = sizes },
.{ .imgUrl = "img9.jpg", .viewItemUrl = "http://foo/9", .title = "Amtap", .description = desc4, .featured = false, .sizes = sizes },
.{ .imgUrl = "img10.jpg", .viewItemUrl = "http://foo/10", .title = "Qiao", .description = desc5, .featured = false, .sizes = sizes },
.{ .imgUrl = "img11.jpg", .viewItemUrl = "http://foo/11", .title = "Pushcart", .description = desc1, .featured = true, .sizes = sizes },
.{ .imgUrl = "img12.jpg", .viewItemUrl = "http://foo/12", .title = "Eweville", .description = desc2, .featured = false, .sizes = sizes },
.{ .imgUrl = "img13.jpg", .viewItemUrl = "http://foo/13", .title = "Senmei", .description = desc3, .featured = true, .sizes = sizes },
.{ .imgUrl = "img14.jpg", .viewItemUrl = "http://foo/14", .title = "Maximind", .description = desc4, .featured = true, .sizes = sizes },
.{ .imgUrl = "img15.jpg", .viewItemUrl = "http://foo/15", .title = "Blurrybus", .description = desc5, .featured = true, .sizes = sizes },
.{ .imgUrl = "img16.jpg", .viewItemUrl = "http://foo/16", .title = "Virva", .description = desc1, .featured = true, .sizes = sizes },
.{ .imgUrl = "img17.jpg", .viewItemUrl = "http://foo/17", .title = "Centregy", .description = desc2, .featured = true, .sizes = sizes },
.{ .imgUrl = "img18.jpg", .viewItemUrl = "http://foo/18", .title = "Dancerity", .description = desc3, .featured = true, .sizes = sizes },
.{ .imgUrl = "img19.jpg", .viewItemUrl = "http://foo/19", .title = "Oceanica", .description = desc4, .featured = true, .sizes = sizes },
.{ .imgUrl = "img20.jpg", .viewItemUrl = "http://foo/20", .title = "Synkgen", .description = desc5, .featured = false, .sizes = null },
},
};
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
timer.reset();
_ = try engine.renderTpl(arena.allocator(), search_results_tpl, data);
total_ns += timer.read();
}
printResult("search-results", total_ns, 41);
}
// ═══════════════════════════════════════════════════════════════════════════
// friends
// ═══════════════════════════════════════════════════════════════════════════
const friends_tpl =
\\doctype html
\\html(lang="en")
\\ head
\\ meta(charset="UTF-8")
\\ title Friends
\\ body
\\ div.friends
\\ each friend in friends
\\ div.friend
\\ ul
\\ li Name: #{friend.name}
\\ li Balance: #{friend.balance}
\\ li Age: #{friend.age}
\\ li Address: #{friend.address}
\\ li Image:
\\ img(src=friend.picture)
\\ li Company: #{friend.company}
\\ li Email:
\\ a(href=friend.emailHref) #{friend.email}
\\ li About: #{friend.about}
\\ if friend.tags
\\ li Tags:
\\ ul
\\ each tag in friend.tags
\\ li #{tag}
\\ if friend.friends
\\ li Friends:
\\ ul
\\ each subFriend in friend.friends
\\ li #{subFriend.name} (#{subFriend.id})
;
const SubFriend = struct {
id: i32,
name: []const u8,
};
const Friend = struct {
name: []const u8,
balance: []const u8,
age: i32,
address: []const u8,
picture: []const u8,
company: []const u8,
email: []const u8,
emailHref: []const u8,
about: []const u8,
tags: ?[]const []const u8,
friends: ?[]const SubFriend,
};
test "bench: friends" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa.deinit() == .leak) @panic("leadk");
const engine = pugz.ViewEngine.init(.{});
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
const friend_tags = &[_][]const u8{ "id", "amet", "non", "ut", "dolore", "commodo", "consequat" };
const sub_friends = &[_]SubFriend{
.{ .id = 0, .name = "Gates Lewis" },
.{ .id = 1, .name = "Britt Stokes" },
.{ .id = 2, .name = "Reed Wade" },
};
var friends_data: [100]Friend = undefined;
for (&friends_data, 0..) |*f, i| {
f.* = .{
.name = "Gardner Alvarez",
.balance = "$1,509.00",
.age = 30 + @as(i32, @intCast(i % 20)),
.address = "282 Lancaster Avenue, Bowden, Kansas, 666",
.picture = "http://placehold.it/32x32",
.company = "Dentrex",
.email = "gardneralvarez@dentrex.com",
.emailHref = "mailto:gardneralvarez@dentrex.com",
.about = "Minim elit tempor enim voluptate labore do non nisi sint nulla deserunt officia proident excepteur.",
.tags = friend_tags,
.friends = sub_friends,
};
}
var total_ns: u64 = 0;
var timer = try std.time.Timer.start();
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
timer.reset();
_ = try engine.renderTpl(arena.allocator(), friends_tpl, .{
.friends = &friends_data,
});
total_ns += timer.read();
}
printResult("friends", total_ns, 110);
}
// ═══════════════════════════════════════════════════════════════════════════
// Helper
// ═══════════════════════════════════════════════════════════════════════════
fn printResult(name: []const u8, total_ns: u64, pug_ref_ms: f64) void {
const total_ms = @as(f64, @floatFromInt(total_ns)) / 1_000_000.0;
const avg_us = @as(f64, @floatFromInt(total_ns)) / @as(f64, @floatFromInt(iterations)) / 1_000.0;
const speedup = pug_ref_ms / total_ms;
std.debug.print("\n{s:<20} => {d:>6.1}ms ({d:.2}us/render) | Pug.js: {d:.0}ms | {d:.1}x\n", .{
name,
total_ms,
avg_us,
pug_ref_ms,
speedup,
});
}

View File

@@ -0,0 +1,170 @@
const std = @import("std");
const pugz = @import("pugz");
const friends_tpl =
\\doctype html
\\html(lang="en")
\\ head
\\ meta(charset="UTF-8")
\\ title Friends
\\ body
\\ div.friends
\\ each friend in friends
\\ div.friend
\\ ul
\\ li Name: #{friend.name}
\\ li Balance: #{friend.balance}
\\ li Age: #{friend.age}
\\ li Address: #{friend.address}
\\ li Image:
\\ img(src=friend.picture)
\\ li Company: #{friend.company}
\\ li Email:
\\ a(href=friend.emailHref) #{friend.email}
\\ li About: #{friend.about}
\\ if friend.tags
\\ li Tags:
\\ ul
\\ each tag in friend.tags
\\ li #{tag}
\\ if friend.friends
\\ li Friends:
\\ ul
\\ each subFriend in friend.friends
\\ li #{subFriend.name} (#{subFriend.id})
;
const SubFriend = struct { id: i32, name: []const u8 };
const Friend = struct {
name: []const u8,
balance: []const u8,
age: i32,
address: []const u8,
picture: []const u8,
company: []const u8,
email: []const u8,
emailHref: []const u8,
about: []const u8,
tags: ?[]const []const u8,
friends: ?[]const SubFriend,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
const engine = pugz.ViewEngine.init(.{});
const friend_tags = &[_][]const u8{ "id", "amet", "non", "ut", "dolore", "commodo", "consequat" };
const sub_friends = &[_]SubFriend{
.{ .id = 0, .name = "Gates Lewis" },
.{ .id = 1, .name = "Britt Stokes" },
.{ .id = 2, .name = "Reed Wade" },
};
var friends_data: [100]Friend = undefined;
for (&friends_data, 0..) |*f, i| {
f.* = .{
.name = "Gardner Alvarez",
.balance = "$1,509.00",
.age = 30 + @as(i32, @intCast(i % 20)),
.address = "282 Lancaster Avenue, Bowden, Kansas, 666",
.picture = "http://placehold.it/32x32",
.company = "Dentrex",
.email = "gardneralvarez@dentrex.com",
.emailHref = "mailto:gardneralvarez@dentrex.com",
.about = "Minim elit tempor enim voluptate labore do non nisi sint nulla deserunt officia proident excepteur.",
.tags = friend_tags,
.friends = sub_friends,
};
}
const data = .{ .friends = &friends_data };
// Warmup
for (0..10) |_| {
_ = arena.reset(.retain_capacity);
_ = try engine.renderTpl(arena.allocator(), friends_tpl, data);
}
// Get output size
_ = arena.reset(.retain_capacity);
const output = try engine.renderTpl(arena.allocator(), friends_tpl, data);
const output_size = output.len;
// Profile render
const iterations: usize = 500;
var total_render: u64 = 0;
var timer = try std.time.Timer.start();
for (0..iterations) |_| {
_ = arena.reset(.retain_capacity);
timer.reset();
_ = try engine.renderTpl(arena.allocator(), friends_tpl, data);
total_render += timer.read();
}
const avg_render_us = @as(f64, @floatFromInt(total_render)) / @as(f64, @floatFromInt(iterations)) / 1000.0;
const total_ms = @as(f64, @floatFromInt(total_render)) / 1_000_000.0;
// Header
std.debug.print("\n", .{});
std.debug.print("╔══════════════════════════════════════════════════════════════╗\n", .{});
std.debug.print("║ FRIENDS TEMPLATE CPU PROFILE ║\n", .{});
std.debug.print("╠══════════════════════════════════════════════════════════════╣\n", .{});
std.debug.print("║ Iterations: {d:<6} Output size: {d:<6} bytes ║\n", .{ iterations, output_size });
std.debug.print("╚══════════════════════════════════════════════════════════════╝\n\n", .{});
// Results
std.debug.print("┌────────────────────────────────────┬─────────────────────────┐\n", .{});
std.debug.print("│ Metric │ Value │\n", .{});
std.debug.print("├────────────────────────────────────┼─────────────────────────┤\n", .{});
std.debug.print("│ Total time │ {d:>10.1} ms │\n", .{total_ms});
std.debug.print("│ Avg per render │ {d:>10.1} µs │\n", .{avg_render_us});
std.debug.print("│ Renders/sec │ {d:>10.0} │\n", .{1_000_000.0 / avg_render_us});
std.debug.print("└────────────────────────────────────┴─────────────────────────┘\n", .{});
// Template complexity breakdown
std.debug.print("\n📋 Template Complexity:\n", .{});
std.debug.print(" • 100 friends (outer loop)\n", .{});
std.debug.print(" • 7 tags per friend (nested loop) = 700 tag iterations\n", .{});
std.debug.print(" • 3 sub-friends per friend (nested loop) = 300 sub-friend iterations\n", .{});
std.debug.print(" • Total loop iterations: 100 + 700 + 300 = 1,100\n", .{});
std.debug.print(" • ~10 interpolations per friend = 1,000+ variable lookups\n", .{});
std.debug.print(" • 2 conditionals per friend = 200 conditional evaluations\n", .{});
// Cost breakdown estimate
const loop_iterations: f64 = 1100;
const var_lookups: f64 = 1500; // approximate
std.debug.print("\n💡 Estimated Cost Breakdown (per render):\n", .{});
std.debug.print(" Total: {d:.1} µs\n", .{avg_render_us});
std.debug.print(" Per loop iteration: ~{d:.2} µs ({d:.0} iterations)\n", .{ avg_render_us / loop_iterations, loop_iterations });
std.debug.print(" Per variable lookup: ~{d:.3} µs ({d:.0} lookups)\n", .{ avg_render_us / var_lookups, var_lookups });
// Comparison
std.debug.print("\n📊 Comparison with Pug.js:\n", .{});
const pugjs_us: f64 = 55.0; // From benchmark: 110ms / 2000 = 55µs
std.debug.print(" Pug.js: {d:.1} µs/render\n", .{pugjs_us});
std.debug.print(" Pugz: {d:.1} µs/render\n", .{avg_render_us});
const ratio = avg_render_us / pugjs_us;
if (ratio > 1.0) {
std.debug.print(" Status: Pugz is {d:.1}x SLOWER\n", .{ratio});
} else {
std.debug.print(" Status: Pugz is {d:.1}x FASTER\n", .{1.0 / ratio});
}
std.debug.print("\nKey Bottlenecks (likely):\n", .{});
std.debug.print(" 1. Data conversion: Zig struct -> pugz.Value (comptime reflection)\n", .{});
std.debug.print(" 2. Variable lookup: HashMap get() for each interpolation\n", .{});
std.debug.print(" 3. AST traversal: Walking tree nodes vs Pug.js compiled JS functions\n", .{});
std.debug.print(" 4. Loop scope: Creating/clearing scope per loop iteration\n", .{});
std.debug.print("\nAlready optimized:\n", .{});
std.debug.print(" - Scope pooling (reuse hashmap capacity)\n", .{});
std.debug.print(" - Batched HTML escaping\n", .{});
std.debug.print(" - Arena allocator with retain_capacity\n", .{});
}

View File

@@ -0,0 +1,30 @@
doctype html
html(lang="en")
head
meta(charset="UTF-8")
title Friends
body
div.friends
each friend in friends
div.friend
ul
li Name: #{friend.name}
li Balance: #{friend.balance}
li Age: #{friend.age}
li Address: #{friend.address}
li Image:
img(src=friend.picture)
li Company: #{friend.company}
li Email:
a(href=friend.emailHref) #{friend.email}
li About: #{friend.about}
if friend.tags
li Tags:
ul
each tag in friend.tags
li #{tag}
if friend.friends
li Friends:
ul
each subFriend in friend.friends
li #{subFriend.name} (#{subFriend.id})

View File

@@ -0,0 +1,13 @@
each account in accounts
div
if account.status == "closed"
div Your account has been closed!
if account.status == "suspended"
div Your account has been temporarily suspended
if account.status == "open"
div
| Bank balance:
if account.negative
span.negative= account.balanceFormatted
else
span.positive= account.balanceFormatted

View File

@@ -0,0 +1,11 @@
doctype html
html
head
title #{title}
body
p #{text}
each project in projects
a(href=project.url) #{project.name}
p #{project.description}
else
p No projects

View File

@@ -0,0 +1,33 @@
.search-results-container
.searching#searching
.wait-indicator-icon Searching...
#resultsContainer
.hd
span.count
span#count= totalCount
| results
.view-modifiers
.view-select
| View:
.view-icon.view-icon-selected#viewIconGallery
i.icon-th
.view-icon#viewIconList
i.icon-th-list
#resultsTarget
.search-results.view-gallery
each searchRecord in searchRecords
.search-item
.search-item-container.drop-shadow
.img-container
img(src=searchRecord.imgUrl)
h4.title
a(href=searchRecord.viewItemUrl)= searchRecord.title
| #{searchRecord.description}
if searchRecord.featured
div Featured!
if searchRecord.sizes
div
| Sizes available:
ul
each size in searchRecord.sizes
li= size

View File

@@ -0,0 +1 @@
h1 Hello, #{name}

View File

@@ -0,0 +1,14 @@
.simple-1(style="background-color: blue; border: 1px solid black")
.colors
span.hello Hello #{name}!
strong You have #{messageCount} messages!
if colors
ul
each color in colors
li.color= color
else
div No colors!
if primary
button(type="button" class="primary") Click me!
else
button(type="button" class="secondary") Click me!

View File

@@ -0,0 +1,10 @@
div
h1.header #{header}
h2.header2 #{header2}
h3.header3 #{header3}
h4.header4 #{header4}
h5.header5 #{header5}
h6.header6 #{header6}
ul.list
each item in list
li.item #{item}