- demo build fix.

- README changes for bench values.
This commit is contained in:
2026-01-28 19:38:59 +05:30
parent 8db2e0df37
commit e2025d7de8
4 changed files with 172 additions and 67 deletions

125
README.md
View File

@@ -37,7 +37,7 @@ exe.root_module.addImport("pugz", pugz_dep.module("pugz"));
## Usage ## Usage
### ViewEngine (Recommended) ### ViewEngine
The `ViewEngine` provides file-based template management for web servers. The `ViewEngine` provides file-based template management for web servers.
@@ -112,6 +112,87 @@ fn handler(_: *Handler, _: *httpz.Request, res: *httpz.Response) !void {
} }
``` ```
### Compiled Templates (Maximum Performance)
For production deployments, pre-compile `.pug` templates to Zig functions at build time. This eliminates parsing overhead and provides type-safe data binding.
**Step 1: Update your `build.zig`**
```zig
const std = @import("std");
const pugz = @import("pugz");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const pugz_dep = b.dependency("pugz", .{
.target = target,
.optimize = optimize,
});
// Add template compilation step
const compile_templates = pugz.compile_tpls.addCompileStep(b, .{
.name = "compile-templates",
.source_dirs = &.{"views/pages", "views/partials"},
.output_dir = "generated",
});
// Templates module from compiled output
const templates_mod = b.createModule(.{
.root_source_file = compile_templates.getOutput(),
});
const exe = b.addExecutable(.{
.name = "myapp",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "pugz", .module = pugz_dep.module("pugz") },
.{ .name = "templates", .module = templates_mod },
},
}),
});
// Ensure templates compile before building
exe.step.dependOn(&compile_templates.step);
b.installArtifact(exe);
}
```
**Step 2: Use compiled templates**
```zig
const templates = @import("templates");
fn handler(res: *httpz.Response) !void {
res.content_type = .HTML;
res.body = try templates.pages_home.render(res.arena, .{
.title = "Home",
.name = "Alice",
});
}
```
**Template naming:**
- `views/pages/home.pug``templates.pages_home`
- `views/pages/product-detail.pug``templates.pages_product_detail`
- Directory separators and dashes become underscores
**Benefits:**
- Zero parsing overhead at runtime
- Type-safe data binding with compile-time errors
- Template inheritance (`extends`/`block`) fully resolved at build time
**Current limitations:**
- `each`/`if` statements not yet supported in compiled mode
- All data fields must be `[]const u8`
See `examples/demo/` for a complete working example.
--- ---
## ViewEngine Options ## ViewEngine Options
@@ -154,28 +235,40 @@ const html = try engine.render(arena.allocator(), "index", data);
## Benchmarks ## Benchmarks
Same templates and data (`benchmarks/templates/`), MacBook Air M2, 2000 iterations, best of 5 runs. Same templates and data (`src/tests/benchmarks/templates/`), MacBook Air M2, 2000 iterations, best of 5 runs.
Both Pug.js and Pugz parse templates once, then measure render-only time. ### Benchmark Modes
| Template | Pug.js | Pugz | Speedup | | Mode | Description |
|----------|--------|------|---------| |------|-------------|
| simple-0 | 0.8ms | 0.2ms | 4x | | **Pug.js** | Node.js Pug - compile once, render many |
| simple-1 | 1.5ms | 0.9ms | 1.7x | | **Prerender** | Pugz - parse + render every iteration (no caching) |
| simple-2 | 1.7ms | 2.4ms | 0.7x | | **Cached** | Pugz - parse once, render many (like Pug.js) |
| if-expression | 0.6ms | 0.4ms | 1.5x | | **Compiled** | Pugz - pre-compiled to Zig functions (zero parse overhead) |
| projects-escaped | 4.6ms | 2.4ms | 1.9x |
| search-results | 15.3ms | 17.7ms | 0.9x | ### Results
| friends | 156.7ms | 132.2ms | 1.2x |
| **TOTAL** | **181.3ms** | **156.2ms** | **1.16x** | | Template | Pug.js | Prerender | Cached | Compiled |
|----------|--------|-----------|--------|----------|
| simple-0 | 0.8ms | 23.1ms | 132.3µs | 15.9µs |
| simple-1 | 1.5ms | 33.5ms | 609.3µs | 17.3µs |
| simple-2 | 1.7ms | 38.4ms | 936.8µs | 17.8µs |
| if-expression | 0.6ms | 28.8ms | 23.0µs | 15.5µs |
| projects-escaped | 4.6ms | 34.2ms | 1.2ms | 15.8µs |
| search-results | 15.3ms | 34.0ms | 43.5µs | 15.6µs |
| friends | 156.7ms | 34.7ms | 739.0µs | 16.8µs |
| **TOTAL** | **181.3ms** | **227.7ms** | **3.7ms** | **114.8µs** |
Compiled templates are ~32x faster than cached and ~2000x faster than prerender.
### Run Benchmarks
Run benchmarks:
```bash ```bash
# Pugz # Pugz (all modes)
zig build bench zig build bench
# Pug.js (for comparison) # Pug.js (for comparison)
cd benchmarks/pugjs && npm install && npm run bench cd src/tests/benchmarks/pugjs && npm install && npm run bench
``` ```
--- ---

View File

@@ -39,27 +39,10 @@ pub fn build(b: *std.Build) void {
// =========================================================================== // ===========================================================================
// Main Executable // Main Executable
// =========================================================================== // ===========================================================================
// Check if compiled templates exist // Templates module - uses output from compile step
const has_templates = blk: { const templates_mod = b.createModule(.{
var dir = std.fs.cwd().openDir("generated", .{}) catch break :blk false; .root_source_file = compile_templates.getOutput(),
dir.close(); });
break :blk true;
};
// Build imports list
var imports: std.ArrayListUnmanaged(std.Build.Module.Import) = .{};
defer imports.deinit(b.allocator);
imports.append(b.allocator, .{ .name = "pugz", .module = pugz_mod }) catch @panic("OOM");
imports.append(b.allocator, .{ .name = "httpz", .module = httpz_dep.module("httpz") }) catch @panic("OOM");
// Only add templates module if they exist
if (has_templates) {
const templates_mod = b.createModule(.{
.root_source_file = b.path("generated/root.zig"),
});
imports.append(b.allocator, .{ .name = "templates", .module = templates_mod }) catch @panic("OOM");
}
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = "demo", .name = "demo",
@@ -67,10 +50,17 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("src/main.zig"), .root_source_file = b.path("src/main.zig"),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
.imports = imports.items, .imports = &.{
.{ .name = "pugz", .module = pugz_mod },
.{ .name = "httpz", .module = httpz_dep.module("httpz") },
.{ .name = "templates", .module = templates_mod },
},
}), }),
}); });
// Ensure templates are compiled before building the executable
exe.step.dependOn(&compile_templates.step);
b.installArtifact(exe); b.installArtifact(exe);
// Run step // Run step

View File

@@ -242,8 +242,7 @@ fn home(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
break :blk try templates.pages_home.render(res.arena, .{ break :blk try templates.pages_home.render(res.arena, .{
.title = "Home", .title = "Home",
.cartCount = "2", .cartCount = "2",
.authenticated = true, .authenticated = "true",
.items = sample_products,
}); });
} else app.view.render(res.arena, "pages/home", .{ } else app.view.render(res.arena, "pages/home", .{
.title = "Home", .title = "Home",
@@ -315,7 +314,6 @@ fn cart(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
.subtotal = sample_cart.subtotal, .subtotal = sample_cart.subtotal,
.tax = sample_cart.tax, .tax = sample_cart.tax,
.total = sample_cart.total, .total = sample_cart.total,
.cartItems = &sample_cart_items,
}); });
} else app.view.render(res.arena, "pages/cart", .{ } else app.view.render(res.arena, "pages/cart", .{
.title = "Shopping Cart", .title = "Shopping Cart",

View File

@@ -132,7 +132,8 @@ pub fn main() !void {
total_cached += try benchCached("friends", allocator, friends_ast, friends_data); total_cached += try benchCached("friends", allocator, friends_ast, friends_data);
std.debug.print("\n", .{}); std.debug.print("\n", .{});
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ "TOTAL (cached)", total_cached }); const t_cached = formatTime(total_cached);
std.debug.print(" {s:<20} => {d:>7.1}{s}\n", .{ "TOTAL (cached)", t_cached.value, t_cached.unit });
std.debug.print("\n", .{}); std.debug.print("\n", .{});
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
@@ -156,7 +157,8 @@ pub fn main() !void {
total_nocache += try benchNoCache("friends", allocator, friends_tpl, friends_data); total_nocache += try benchNoCache("friends", allocator, friends_tpl, friends_data);
std.debug.print("\n", .{}); std.debug.print("\n", .{});
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ "TOTAL (no cache)", total_nocache }); const t_nocache = formatTime(total_nocache);
std.debug.print(" {s:<20} => {d:>7.1}{s}\n", .{ "TOTAL (no cache)", t_nocache.value, t_nocache.unit });
std.debug.print("\n", .{}); std.debug.print("\n", .{});
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
@@ -180,34 +182,50 @@ pub fn main() !void {
total_compiled += try benchCompiled("friends", allocator, compiled.friends); total_compiled += try benchCompiled("friends", allocator, compiled.friends);
std.debug.print("\n", .{}); std.debug.print("\n", .{});
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ "TOTAL (compiled)", total_compiled }); const t_compiled = formatTime(total_compiled);
std.debug.print(" {s:<20} => {d:>7.1}{s}\n", .{ "TOTAL (compiled)", t_compiled.value, t_compiled.unit });
std.debug.print("\n", .{}); std.debug.print("\n", .{});
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
// Summary // Summary (all values in ns internally, formatted for display)
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
const t_cached_sum = formatTime(total_cached);
const t_nocache_sum = formatTime(total_nocache);
const t_compiled_sum = formatTime(total_compiled);
const t_parse_overhead = formatTime(total_nocache - total_cached);
std.debug.print("╔═══════════════════════════════════════════════════════════════╗\n", .{}); std.debug.print("╔═══════════════════════════════════════════════════════════════╗\n", .{});
std.debug.print("║ SUMMARY ║\n", .{}); std.debug.print("║ SUMMARY ║\n", .{});
std.debug.print("╚═══════════════════════════════════════════════════════════════╝\n", .{}); std.debug.print("╚═══════════════════════════════════════════════════════════════╝\n", .{});
std.debug.print(" Cached AST (render only): {d:>7.1}ms\n", .{total_cached}); std.debug.print(" Cached AST (render only): {d:>7.1}{s}\n", .{ t_cached_sum.value, t_cached_sum.unit });
std.debug.print(" No Cache (parse+render): {d:>7.1}ms\n", .{total_nocache}); std.debug.print(" No Cache (parse+render): {d:>7.1}{s}\n", .{ t_nocache_sum.value, t_nocache_sum.unit });
if (total_compiled > 0) { if (total_compiled > 0) {
std.debug.print(" Compiled (zero parse): {d:>7.1}ms\n", .{total_compiled}); std.debug.print(" Compiled (zero parse): {d:>7.1}{s}\n", .{ t_compiled_sum.value, t_compiled_sum.unit });
} }
std.debug.print("\n", .{}); std.debug.print("\n", .{});
std.debug.print(" Parse overhead: {d:>7.1}ms ({d:.1}%)\n", .{ std.debug.print(" Parse overhead: {d:>7.1}{s} ({d:.1}%)\n", .{
total_nocache - total_cached, t_parse_overhead.value,
t_parse_overhead.unit,
((total_nocache - total_cached) / total_nocache) * 100.0, ((total_nocache - total_cached) / total_nocache) * 100.0,
}); });
if (total_compiled > 0) { if (total_compiled > 0) {
std.debug.print(" Cached vs Compiled: {d:>7.1}ms ({d:.1}x faster)\n", .{ std.debug.print(" Cached vs Compiled: {d:.0}x faster\n", .{
total_cached - total_compiled,
total_cached / total_compiled, total_cached / total_compiled,
}); });
} }
std.debug.print("\n", .{}); std.debug.print("\n", .{});
} }
// Format time with automatic unit selection (µs or ms)
fn formatTime(ns: f64) struct { value: f64, unit: []const u8 } {
const us = ns / 1_000.0;
if (us < 1000.0) {
return .{ .value = us, .unit = "µs" };
} else {
return .{ .value = us / 1000.0, .unit = "ms" };
}
}
fn loadJson(comptime T: type, alloc: std.mem.Allocator, comptime filename: []const u8) !T { fn loadJson(comptime T: type, alloc: std.mem.Allocator, comptime filename: []const u8) !T {
const path = templates_dir ++ "/" ++ filename; const path = templates_dir ++ "/" ++ filename;
const content = try std.fs.cwd().readFileAlloc(alloc, path, 10 * 1024 * 1024); const content = try std.fs.cwd().readFileAlloc(alloc, path, 10 * 1024 * 1024);
@@ -221,6 +239,7 @@ fn loadTemplate(alloc: std.mem.Allocator, comptime filename: []const u8) ![]cons
} }
// Benchmark with cached AST (render only) - Best of 5 runs // Benchmark with cached AST (render only) - Best of 5 runs
// Returns nanoseconds for consistent comparison
fn benchCached( fn benchCached(
name: []const u8, name: []const u8,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
@@ -230,7 +249,7 @@ fn benchCached(
var arena = std.heap.ArenaAllocator.init(allocator); var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit(); defer arena.deinit();
var best_ms: f64 = std.math.inf(f64); var best_ns: f64 = std.math.inf(f64);
for (0..runs) |_| { for (0..runs) |_| {
_ = arena.reset(.retain_capacity); _ = arena.reset(.retain_capacity);
@@ -242,22 +261,24 @@ fn benchCached(
return 0; return 0;
}; };
} }
const ms = @as(f64, @floatFromInt(timer.read())) / 1_000_000.0; const ns = @as(f64, @floatFromInt(timer.read()));
if (ms < best_ms) best_ms = ms; if (ns < best_ns) best_ns = ns;
} }
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ name, best_ms }); const t = formatTime(best_ns);
return best_ms; std.debug.print(" {s:<20} => {d:>7.1}{s}\n", .{ name, t.value, t.unit });
return best_ns;
} }
// Benchmark without cache (parse + render every iteration) - Best of 5 runs // Benchmark without cache (parse + render every iteration) - Best of 5 runs
// Returns nanoseconds for consistent comparison
fn benchNoCache( fn benchNoCache(
name: []const u8, name: []const u8,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
source: []const u8, source: []const u8,
data: anytype, data: anytype,
) !f64 { ) !f64 {
var best_ms: f64 = std.math.inf(f64); var best_ns: f64 = std.math.inf(f64);
for (0..runs) |_| { for (0..runs) |_| {
var timer = try std.time.Timer.start(); var timer = try std.time.Timer.start();
@@ -270,15 +291,17 @@ fn benchNoCache(
return 0; return 0;
}; };
} }
const ms = @as(f64, @floatFromInt(timer.read())) / 1_000_000.0; const ns = @as(f64, @floatFromInt(timer.read()));
if (ms < best_ms) best_ms = ms; if (ns < best_ns) best_ns = ns;
} }
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ name, best_ms }); const t = formatTime(best_ns);
return best_ms; std.debug.print(" {s:<20} => {d:>7.1}{s}\n", .{ name, t.value, t.unit });
return best_ns;
} }
// Benchmark compiled templates (zero parse overhead) - Best of 5 runs // Benchmark compiled templates (zero parse overhead) - Best of 5 runs
// Returns nanoseconds for consistent comparison
fn benchCompiled( fn benchCompiled(
name: []const u8, name: []const u8,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
@@ -287,7 +310,7 @@ fn benchCompiled(
var arena = std.heap.ArenaAllocator.init(allocator); var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit(); defer arena.deinit();
var best_ms: f64 = std.math.inf(f64); var best_ns: f64 = std.math.inf(f64);
for (0..runs) |_| { for (0..runs) |_| {
_ = arena.reset(.retain_capacity); _ = arena.reset(.retain_capacity);
@@ -299,10 +322,11 @@ fn benchCompiled(
return 0; return 0;
}; };
} }
const ms = @as(f64, @floatFromInt(timer.read())) / 1_000_000.0; const ns = @as(f64, @floatFromInt(timer.read()));
if (ms < best_ms) best_ms = ms; if (ns < best_ns) best_ns = ns;
} }
std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ name, best_ms }); const t = formatTime(best_ns);
return best_ms; std.debug.print(" {s:<20} => {d:>7.1}{s}\n", .{ name, t.value, t.unit });
return best_ns;
} }