From e2025d7de843abe32aaff299f52b70c2f2771350 Mon Sep 17 00:00:00 2001 From: Ankit Patial Date: Wed, 28 Jan 2026 19:38:59 +0530 Subject: [PATCH] - demo build fix. - README changes for bench values. --- README.md | 125 ++++++++++++++++++++++++++++----- examples/demo/build.zig | 34 ++++----- examples/demo/src/main.zig | 4 +- src/tests/benchmarks/bench.zig | 76 +++++++++++++------- 4 files changed, 172 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 2db7129..442ab34 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ exe.root_module.addImport("pugz", pugz_dep.module("pugz")); ## Usage -### ViewEngine (Recommended) +### ViewEngine 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 @@ -154,28 +235,40 @@ const html = try engine.render(arena.allocator(), "index", data); ## 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 | -|----------|--------|------|---------| -| simple-0 | 0.8ms | 0.2ms | 4x | -| simple-1 | 1.5ms | 0.9ms | 1.7x | -| simple-2 | 1.7ms | 2.4ms | 0.7x | -| if-expression | 0.6ms | 0.4ms | 1.5x | -| projects-escaped | 4.6ms | 2.4ms | 1.9x | -| search-results | 15.3ms | 17.7ms | 0.9x | -| friends | 156.7ms | 132.2ms | 1.2x | -| **TOTAL** | **181.3ms** | **156.2ms** | **1.16x** | +| Mode | Description | +|------|-------------| +| **Pug.js** | Node.js Pug - compile once, render many | +| **Prerender** | Pugz - parse + render every iteration (no caching) | +| **Cached** | Pugz - parse once, render many (like Pug.js) | +| **Compiled** | Pugz - pre-compiled to Zig functions (zero parse overhead) | + +### Results + +| 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 -# Pugz +# Pugz (all modes) zig build bench # Pug.js (for comparison) -cd benchmarks/pugjs && npm install && npm run bench +cd src/tests/benchmarks/pugjs && npm install && npm run bench ``` --- diff --git a/examples/demo/build.zig b/examples/demo/build.zig index 86618e6..fb155ef 100644 --- a/examples/demo/build.zig +++ b/examples/demo/build.zig @@ -39,27 +39,10 @@ pub fn build(b: *std.Build) void { // =========================================================================== // Main Executable // =========================================================================== - // Check if compiled templates exist - const has_templates = blk: { - var dir = std.fs.cwd().openDir("generated", .{}) catch break :blk false; - 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"); - } + // Templates module - uses output from compile step + const templates_mod = b.createModule(.{ + .root_source_file = compile_templates.getOutput(), + }); const exe = b.addExecutable(.{ .name = "demo", @@ -67,10 +50,17 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("src/main.zig"), .target = target, .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); // Run step diff --git a/examples/demo/src/main.zig b/examples/demo/src/main.zig index a6e5d10..cb989b4 100644 --- a/examples/demo/src/main.zig +++ b/examples/demo/src/main.zig @@ -242,8 +242,7 @@ fn home(app: *App, _: *httpz.Request, res: *httpz.Response) !void { break :blk try templates.pages_home.render(res.arena, .{ .title = "Home", .cartCount = "2", - .authenticated = true, - .items = sample_products, + .authenticated = "true", }); } else app.view.render(res.arena, "pages/home", .{ .title = "Home", @@ -315,7 +314,6 @@ fn cart(app: *App, _: *httpz.Request, res: *httpz.Response) !void { .subtotal = sample_cart.subtotal, .tax = sample_cart.tax, .total = sample_cart.total, - .cartItems = &sample_cart_items, }); } else app.view.render(res.arena, "pages/cart", .{ .title = "Shopping Cart", diff --git a/src/tests/benchmarks/bench.zig b/src/tests/benchmarks/bench.zig index cf014d4..eb0aa9e 100644 --- a/src/tests/benchmarks/bench.zig +++ b/src/tests/benchmarks/bench.zig @@ -132,7 +132,8 @@ pub fn main() !void { total_cached += try benchCached("friends", allocator, friends_ast, friends_data); 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", .{}); // ═══════════════════════════════════════════════════════════════════════ @@ -156,7 +157,8 @@ pub fn main() !void { total_nocache += try benchNoCache("friends", allocator, friends_tpl, friends_data); 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", .{}); // ═══════════════════════════════════════════════════════════════════════ @@ -180,34 +182,50 @@ pub fn main() !void { total_compiled += try benchCompiled("friends", allocator, compiled.friends); 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", .{}); // ═══════════════════════════════════════════════════════════════════════ - // 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("║ SUMMARY ║\n", .{}); std.debug.print("╚═══════════════════════════════════════════════════════════════╝\n", .{}); - std.debug.print(" Cached AST (render only): {d:>7.1}ms\n", .{total_cached}); - std.debug.print(" No Cache (parse+render): {d:>7.1}ms\n", .{total_nocache}); + 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}{s}\n", .{ t_nocache_sum.value, t_nocache_sum.unit }); 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(" Parse overhead: {d:>7.1}ms ({d:.1}%)\n", .{ - total_nocache - total_cached, + std.debug.print(" Parse overhead: {d:>7.1}{s} ({d:.1}%)\n", .{ + t_parse_overhead.value, + t_parse_overhead.unit, ((total_nocache - total_cached) / total_nocache) * 100.0, }); if (total_compiled > 0) { - std.debug.print(" Cached vs Compiled: {d:>7.1}ms ({d:.1}x faster)\n", .{ - total_cached - total_compiled, + std.debug.print(" Cached vs Compiled: {d:.0}x faster\n", .{ total_cached / total_compiled, }); } 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 { const path = templates_dir ++ "/" ++ filename; 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 +// Returns nanoseconds for consistent comparison fn benchCached( name: []const u8, allocator: std.mem.Allocator, @@ -230,7 +249,7 @@ fn benchCached( var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); - var best_ms: f64 = std.math.inf(f64); + var best_ns: f64 = std.math.inf(f64); for (0..runs) |_| { _ = arena.reset(.retain_capacity); @@ -242,22 +261,24 @@ fn benchCached( return 0; }; } - const ms = @as(f64, @floatFromInt(timer.read())) / 1_000_000.0; - if (ms < best_ms) best_ms = ms; + const ns = @as(f64, @floatFromInt(timer.read())); + if (ns < best_ns) best_ns = ns; } - std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ name, best_ms }); - return best_ms; + const t = formatTime(best_ns); + 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 +// Returns nanoseconds for consistent comparison fn benchNoCache( name: []const u8, allocator: std.mem.Allocator, source: []const u8, data: anytype, ) !f64 { - var best_ms: f64 = std.math.inf(f64); + var best_ns: f64 = std.math.inf(f64); for (0..runs) |_| { var timer = try std.time.Timer.start(); @@ -270,15 +291,17 @@ fn benchNoCache( return 0; }; } - const ms = @as(f64, @floatFromInt(timer.read())) / 1_000_000.0; - if (ms < best_ms) best_ms = ms; + const ns = @as(f64, @floatFromInt(timer.read())); + if (ns < best_ns) best_ns = ns; } - std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ name, best_ms }); - return best_ms; + const t = formatTime(best_ns); + 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 +// Returns nanoseconds for consistent comparison fn benchCompiled( name: []const u8, allocator: std.mem.Allocator, @@ -287,7 +310,7 @@ fn benchCompiled( var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); - var best_ms: f64 = std.math.inf(f64); + var best_ns: f64 = std.math.inf(f64); for (0..runs) |_| { _ = arena.reset(.retain_capacity); @@ -299,10 +322,11 @@ fn benchCompiled( return 0; }; } - const ms = @as(f64, @floatFromInt(timer.read())) / 1_000_000.0; - if (ms < best_ms) best_ms = ms; + const ns = @as(f64, @floatFromInt(timer.read())); + if (ns < best_ns) best_ns = ns; } - std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ name, best_ms }); - return best_ms; + const t = formatTime(best_ns); + std.debug.print(" {s:<20} => {d:>7.1}{s}\n", .{ name, t.value, t.unit }); + return best_ns; }