From a44ad0b7d03faefa598498f689013362fe03aac6 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Thu, 23 Apr 2026 06:38:09 -0700 Subject: [PATCH] use arena allocator for cli commands --- src/main.zig | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/main.zig b/src/main.zig index 11bff4d..abcac16 100644 --- a/src/main.zig +++ b/src/main.zig @@ -145,12 +145,15 @@ fn resolveUserPath( } pub fn main() !u8 { + // Long-lived allocator for things that span the whole process. Only + // actually used for the early argsAlloc and the TUI path — CLI + // commands run under a per-invocation arena (see below). var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); - const allocator = gpa.allocator(); + const gpa_alloc = gpa.allocator(); - const args = try std.process.argsAlloc(allocator); - defer std.process.argsFree(allocator, args); + const args = try std.process.argsAlloc(gpa_alloc); + defer std.process.argsFree(gpa_alloc, args); // Single buffered writer for all stdout output var stdout_buf: [4096]u8 = undefined; @@ -194,19 +197,38 @@ pub fn main() !u8 { const color = @import("format.zig").shouldUseColor(globals.no_color); - var config = zfin.Config.fromEnv(allocator); - defer config.deinit(); - const command = args[globals.cursor]; const cmd_args = args[globals.cursor + 1 ..]; - // Interactive TUI -- delegates to the TUI module (owns its own DataService). + // Interactive TUI: long-lived, per-frame allocations benefit from a + // real (non-arena) allocator. Runs against `gpa` directly. if (std.mem.eql(u8, command, "interactive") or std.mem.eql(u8, command, "i")) { + var tui_config = zfin.Config.fromEnv(gpa_alloc); + defer tui_config.deinit(); try out.flush(); - try tui.run(allocator, config, globals.portfolio_path, globals.watchlist_path, cmd_args); + try tui.run(gpa_alloc, tui_config, globals.portfolio_path, globals.watchlist_path, cmd_args); return 0; } + // ── Per-invocation arena ───────────────────────────────────── + // + // CLI commands do a batch of work then exit. Almost every allocation + // they make has the same lifetime (the invocation). An arena matched + // to that unit gives us three wins: skip per-allocation bookkeeping, + // ignore all the per-object `defer X.deinit()` calls (they become + // no-ops but remain correct code if the function is ever called from + // a non-arena context), and avoid gpa's leak-checking overhead for + // ephemeral state we're about to discard anyway. + // + // See models/portfolio.zig for the "match the arena to the unit of + // work" principle. Here the unit is one `zfin `. + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var config = zfin.Config.fromEnv(allocator); + defer config.deinit(); + // Version: doesn't need DataService; uses build_info + Config paths. if (std.mem.eql(u8, command, "version")) { commands.version.run(config, cmd_args, out) catch |err| switch (err) {