From dbe487e0f4dc7ee8d635f42f41aca026d3f66e50 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Mon, 1 Jun 2026 08:29:56 -0700 Subject: [PATCH] use arena for refresh loop --- src/main.zig | 51 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/src/main.zig b/src/main.zig index 51bc063..c6c4e26 100644 --- a/src/main.zig +++ b/src/main.zig @@ -508,27 +508,31 @@ fn writeNewPortfolio(io: std.Io, allocator: std.mem.Allocator, path: []const u8, // ── Refresh command ────────────────────────────────────────── -fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process.Environ.Map) !void { - var config = zfin.Config.fromEnv(io, allocator, environ); +fn refresh( + io: std.Io, + gpa: std.mem.Allocator, + environ: *const std.process.Environ.Map, +) !void { + var config = zfin.Config.fromEnv(io, gpa, environ); defer config.deinit(); - var svc = zfin.DataService.init(io, allocator, config); + var svc = zfin.DataService.init(io, gpa, config); defer svc.deinit(); const portfolio_path = environ.get("ZFIN_PORTFOLIO") orelse "portfolio.srf"; - const data = std.Io.Dir.cwd().readFileAlloc(io, portfolio_path, allocator, .limited(10 * 1024 * 1024)) catch { + const data = std.Io.Dir.cwd().readFileAlloc(io, portfolio_path, gpa, .limited(10 * 1024 * 1024)) catch { log.err("failed to read portfolio: {s}", .{portfolio_path}); return error.ReadFailed; }; - defer allocator.free(data); + defer gpa.free(data); - var portfolio = zfin.cache.deserializePortfolio(allocator, data) catch { + var portfolio = zfin.cache.deserializePortfolio(gpa, data) catch { log.err("failed to parse portfolio", .{}); return error.ParseFailed; }; defer portfolio.deinit(); - var symbols = std.StringHashMap(void).init(allocator); + var symbols = std.StringHashMap(void).init(gpa); defer symbols.deinit(); for (portfolio.lots) |lot| { if (lot.security_type != .stock and lot.security_type != .watch) continue; @@ -577,6 +581,25 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process try stdout.flush(); } + // Per-iteration scratch arena. Wraps the gpa so iteration-scoped + // allocations get freed in bulk via `reset(.retain_capacity)` at + // the bottom of each loop iteration — no per-allocation defers, + // no manual `free` mistakes. Capacity is retained so we're not + // re-acquiring pages every symbol. + // + // What goes through this arena: any per-iteration scratch we + // allocate ourselves (CIK dupes, future symbol-derived strings). + // NOT the `FetchResult` payloads — those are owned by the + // service's gpa and freed via `result.deinit()` at their own + // call sites. + // + // We don't use juicy-main's `init.arena` because that's + // process-lifetime; resetting it mid-run would clobber other + // startup allocations. + var iter_arena_state = std.heap.ArenaAllocator.init(gpa); + defer iter_arena_state.deinit(); + const iter_arena = iter_arena_state.allocator(); + var it = symbols.iterator(); while (it.next()) |entry| { const sym = entry.key_ptr.*; @@ -637,14 +660,16 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process // had it — used to chain into entity_facts below. // NotFound is logged as `n/a` (symbol genuinely has no // Wikidata entry) and doesn't flip sym_ok. - var cik_buf: ?[]u8 = null; - defer if (cik_buf) |b| allocator.free(b); + // + // The CIK dupe goes through `iter_arena`. No defer-free + // needed — the arena is reset at end of iteration. + var cik_str: ?[]const u8 = null; try printRateLimitWait(&svc, stdout); if (svc.getClassification(sym, .{})) |result| { defer result.deinit(); if (result.data.len > 0) { if (result.data[0].cik) |cik| { - cik_buf = allocator.dupe(u8, cik) catch null; + cik_str = iter_arena.dupe(u8, cik) catch null; } } try stdout.print(", classification ok", .{}); @@ -678,7 +703,7 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process // have an EDGAR CIK from the ticker map (production // zfin chains entity_facts off Wikidata's CIK, so the // server warms the cache the same way). - if (cik_buf) |cik| { + if (cik_str) |cik| { try printRateLimitWait(&svc, stdout); if (svc.getEntityFacts(cik, .{})) |result| { result.deinit(); @@ -696,6 +721,10 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process try stdout.flush(); if (sym_ok) success_count += 1 else fail_count += 1; + + // Reset per-iteration scratch. Retains capacity so the + // next iteration's allocations reuse the same pages. + _ = iter_arena_state.reset(.retain_capacity); } try stdout.print("\nRefresh complete: {d} ok, {d} failed\n", .{ success_count, fail_count });