move to srf iterator/bail early when possible

This commit is contained in:
Emil Lerch 2026-03-09 09:56:42 -07:00
parent 0a2dd47f3e
commit 44dfafd574
Signed by: lobo
GPG key ID: A7B62D657EF764F8
6 changed files with 62 additions and 65 deletions

View file

@ -13,8 +13,8 @@
.hash = "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ", .hash = "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ",
}, },
.srf = .{ .srf = .{
.url = "git+https://git.lerch.org/lobo/srf.git#95036e83e26bb885641c62aaf1e26dbfbb147ea9", .url = "git+https://git.lerch.org/lobo/srf.git#8e12b7396afc1bcbc4e2a3f19d8725a82b71b27e",
.hash = "srf-0.0.0-qZj575xZAQB4wzO6J8wf0hBFTZMDjCfFFCtHx6BCQifK", .hash = "srf-0.0.0-qZj573V9AQBJTR8ehcnA6KW_wb6cdkJZtFZGq87b8dAJ",
}, },
}, },
.paths = .{ .paths = .{

View file

@ -78,7 +78,7 @@ pub fn parseAccountsFile(allocator: std.mem.Allocator, data: []const u8) !Accoun
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData; const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
defer parsed.deinit(); defer parsed.deinit();
for (parsed.records.items) |record| { for (parsed.records) |record| {
const entry = record.to(AccountTaxEntry) catch continue; const entry = record.to(AccountTaxEntry) catch continue;
try entries.append(allocator, .{ try entries.append(allocator, .{
.account = try allocator.dupe(u8, entry.account), .account = try allocator.dupe(u8, entry.account),

115
src/cache/store.zig vendored
View file

@ -129,18 +129,19 @@ pub const Store = struct {
/// - Negative cache entries (# fetch_failed) are always fresh. /// - Negative cache entries (# fetch_failed) are always fresh.
/// - Data with `#!expires=` is fresh if the SRF library says so. /// - Data with `#!expires=` is fresh if the SRF library says so.
/// - Data without expiry metadata is considered stale (triggers re-fetch). /// - Data without expiry metadata is considered stale (triggers re-fetch).
/// Uses the SRF iterator to read only the header directives without parsing any records.
pub fn isFreshData(data: []const u8, allocator: std.mem.Allocator) bool { pub fn isFreshData(data: []const u8, allocator: std.mem.Allocator) bool {
// Negative cache entry -- always fresh // Negative cache entry -- always fresh
if (std.mem.indexOf(u8, data, "# fetch_failed")) |_| return true; if (std.mem.indexOf(u8, data, "# fetch_failed")) |_| return true;
var reader = std.Io.Reader.fixed(data); var reader = std.Io.Reader.fixed(data);
const parsed = srf.parse(&reader, allocator, .{}) catch return false; const it = srf.iterator(&reader, allocator, .{}) catch return false;
defer parsed.deinit(); defer it.deinit();
// No expiry directive stale (legacy file, trigger re-fetch + rewrite) // No expiry directive stale (legacy file, trigger re-fetch + rewrite)
if (parsed.expires == null) return false; if (it.expires == null) return false;
return parsed.isFresh(); return it.isFresh();
} }
/// Clear all cached data for a symbol. /// Clear all cached data for a symbol.
@ -232,11 +233,11 @@ pub const Store = struct {
errdefer candles.deinit(allocator); errdefer candles.deinit(allocator);
var reader = std.Io.Reader.fixed(data); var reader = std.Io.Reader.fixed(data);
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData; var it = srf.iterator(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
defer parsed.deinit(); defer it.deinit();
for (parsed.records.items) |record| { while (try it.next()) |fields| {
const candle = record.to(Candle) catch continue; const candle = fields.to(Candle) catch continue;
try candles.append(allocator, candle); try candles.append(allocator, candle);
} }
@ -262,13 +263,14 @@ pub const Store = struct {
} }
/// Deserialize candle metadata from SRF data. /// Deserialize candle metadata from SRF data.
/// Uses the SRF iterator to read only the first record without parsing the entire file.
pub fn deserializeCandleMeta(allocator: std.mem.Allocator, data: []const u8) !CandleMeta { pub fn deserializeCandleMeta(allocator: std.mem.Allocator, data: []const u8) !CandleMeta {
var reader = std.Io.Reader.fixed(data); var reader = std.Io.Reader.fixed(data);
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData; var it = srf.iterator(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
defer parsed.deinit(); defer it.deinit();
if (parsed.records.items.len == 0) return error.InvalidData; const fields = (try it.next()) orelse return error.InvalidData;
return parsed.records.items[0].to(CandleMeta) catch error.InvalidData; return fields.to(CandleMeta) catch error.InvalidData;
} }
/// Inline fetch metadata embedded as the first record in non-candle SRF files. /// Inline fetch metadata embedded as the first record in non-candle SRF files.
@ -278,14 +280,15 @@ pub const Store = struct {
}; };
/// Read the `fetched_at` timestamp from the first record of an SRF file. /// Read the `fetched_at` timestamp from the first record of an SRF file.
/// Uses the SRF iterator to read only the first record without parsing the entire file.
/// Returns null if the file has no FetchMeta record or cannot be parsed. /// Returns null if the file has no FetchMeta record or cannot be parsed.
pub fn readFetchedAt(allocator: std.mem.Allocator, data: []const u8) ?i64 { pub fn readFetchedAt(allocator: std.mem.Allocator, data: []const u8) ?i64 {
var reader = std.Io.Reader.fixed(data); var reader = std.Io.Reader.fixed(data);
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return null; var it = srf.iterator(&reader, allocator, .{ .alloc_strings = false }) catch return null;
defer parsed.deinit(); defer it.deinit();
if (parsed.records.items.len == 0) return null; const fields = (it.next() catch return null) orelse return null;
const meta = parsed.records.items[0].to(FetchMeta) catch return null; const meta = fields.to(FetchMeta) catch return null;
return meta.fetched_at; return meta.fetched_at;
} }
@ -323,12 +326,12 @@ pub const Store = struct {
} }
var reader = std.Io.Reader.fixed(data); var reader = std.Io.Reader.fixed(data);
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData; var it = srf.iterator(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
defer parsed.deinit(); defer it.deinit();
for (parsed.records.items) |record| { while (try it.next()) |fields| {
var div = record.to(Dividend) catch continue; var div = fields.to(Dividend) catch continue;
// Dupe owned strings before parsed.deinit() frees the backing buffer // Dupe owned strings before iterator.deinit() frees the backing buffer
if (div.currency) |c| { if (div.currency) |c| {
div.currency = allocator.dupe(u8, c) catch null; div.currency = allocator.dupe(u8, c) catch null;
} }
@ -344,11 +347,11 @@ pub const Store = struct {
errdefer splits.deinit(allocator); errdefer splits.deinit(allocator);
var reader = std.Io.Reader.fixed(data); var reader = std.Io.Reader.fixed(data);
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData; var it = srf.iterator(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
defer parsed.deinit(); defer it.deinit();
for (parsed.records.items) |record| { while (try it.next()) |fields| {
const split = record.to(Split) catch continue; const split = fields.to(Split) catch continue;
try splits.append(allocator, split); try splits.append(allocator, split);
} }
@ -373,11 +376,11 @@ pub const Store = struct {
errdefer events.deinit(allocator); errdefer events.deinit(allocator);
var reader = std.Io.Reader.fixed(data); var reader = std.Io.Reader.fixed(data);
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData; var it = srf.iterator(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
defer parsed.deinit(); defer it.deinit();
for (parsed.records.items) |record| { while (try it.next()) |fields| {
var ev = record.to(EarningsEvent) catch continue; var ev = fields.to(EarningsEvent) catch continue;
// Recompute surprise from actual/estimate // Recompute surprise from actual/estimate
if (ev.actual != null and ev.estimate != null) { if (ev.actual != null and ev.estimate != null) {
ev.surprise = ev.actual.? - ev.estimate.?; ev.surprise = ev.actual.? - ev.estimate.?;
@ -444,8 +447,8 @@ pub const Store = struct {
/// Deserialize ETF profile from SRF data. /// Deserialize ETF profile from SRF data.
pub fn deserializeEtfProfile(allocator: std.mem.Allocator, data: []const u8) !EtfProfile { pub fn deserializeEtfProfile(allocator: std.mem.Allocator, data: []const u8) !EtfProfile {
var reader = std.Io.Reader.fixed(data); var reader = std.Io.Reader.fixed(data);
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData; var it = srf.iterator(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
defer parsed.deinit(); defer it.deinit();
var profile = EtfProfile{ .symbol = "" }; var profile = EtfProfile{ .symbol = "" };
var sectors: std.ArrayList(SectorWeight) = .empty; var sectors: std.ArrayList(SectorWeight) = .empty;
@ -453,8 +456,8 @@ pub const Store = struct {
var holdings: std.ArrayList(Holding) = .empty; var holdings: std.ArrayList(Holding) = .empty;
errdefer holdings.deinit(allocator); errdefer holdings.deinit(allocator);
for (parsed.records.items) |record| { while (try it.next()) |fields| {
const etf_rec = record.to(EtfRecord) catch continue; const etf_rec = fields.to(EtfRecord) catch continue;
switch (etf_rec) { switch (etf_rec) {
.meta => |m| { .meta => |m| {
profile.expense_ratio = m.expense_ratio; profile.expense_ratio = m.expense_ratio;
@ -583,10 +586,12 @@ pub const Store = struct {
} }
/// Deserialize options chains from SRF data. /// Deserialize options chains from SRF data.
/// Chain headers appear before their contracts in the SRF file, so a single
/// pass can assign contracts to the correct chain as they are encountered.
pub fn deserializeOptions(allocator: std.mem.Allocator, data: []const u8) ![]OptionsChain { pub fn deserializeOptions(allocator: std.mem.Allocator, data: []const u8) ![]OptionsChain {
var reader = std.Io.Reader.fixed(data); var reader = std.Io.Reader.fixed(data);
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData; var it = srf.iterator(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
defer parsed.deinit(); defer it.deinit();
var chains: std.ArrayList(OptionsChain) = .empty; var chains: std.ArrayList(OptionsChain) = .empty;
errdefer { errdefer {
@ -601,26 +606,7 @@ pub const Store = struct {
var exp_map = std.AutoHashMap(i32, usize).init(allocator); var exp_map = std.AutoHashMap(i32, usize).init(allocator);
defer exp_map.deinit(); defer exp_map.deinit();
// First pass: collect chain headers // Accumulate contracts per chain
for (parsed.records.items) |record| {
const opt_rec = record.to(OptionsRecord) catch continue;
switch (opt_rec) {
.chain => |ch| {
const idx = chains.items.len;
try chains.append(allocator, .{
.underlying_symbol = try allocator.dupe(u8, ch.symbol),
.underlying_price = ch.price,
.expiration = ch.expiration,
.calls = &.{},
.puts = &.{},
});
try exp_map.put(ch.expiration.days, idx);
},
else => {},
}
}
// Second pass: collect contracts
var calls_map = std.AutoHashMap(usize, std.ArrayList(OptionContract)).init(allocator); var calls_map = std.AutoHashMap(usize, std.ArrayList(OptionContract)).init(allocator);
defer { defer {
var iter = calls_map.valueIterator(); var iter = calls_map.valueIterator();
@ -634,9 +620,21 @@ pub const Store = struct {
puts_map.deinit(); puts_map.deinit();
} }
for (parsed.records.items) |record| { // Single pass: chain headers and contracts arrive in order
const opt_rec = record.to(OptionsRecord) catch continue; while (try it.next()) |fields| {
const opt_rec = fields.to(OptionsRecord) catch continue;
switch (opt_rec) { switch (opt_rec) {
.chain => |ch| {
const idx = chains.items.len;
try chains.append(allocator, .{
.underlying_symbol = try allocator.dupe(u8, ch.symbol),
.underlying_price = ch.price,
.expiration = ch.expiration,
.calls = &.{},
.puts = &.{},
});
try exp_map.put(ch.expiration.days, idx);
},
.call => |cf| { .call => |cf| {
if (exp_map.get(cf.expiration.days)) |idx| { if (exp_map.get(cf.expiration.days)) |idx| {
const entry = try calls_map.getOrPut(idx); const entry = try calls_map.getOrPut(idx);
@ -651,7 +649,6 @@ pub const Store = struct {
try entry.value_ptr.append(allocator, fieldsToContract(cf, .put)); try entry.value_ptr.append(allocator, fieldsToContract(cf, .put));
} }
}, },
.chain => {},
} }
} }
@ -712,7 +709,7 @@ pub fn deserializePortfolio(allocator: std.mem.Allocator, data: []const u8) !Por
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData; const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
defer parsed.deinit(); defer parsed.deinit();
for (parsed.records.items) |record| { for (parsed.records) |record| {
var lot = Lot{ var lot = Lot{
.symbol = "", .symbol = "",
.shares = 0, .shares = 0,

View file

@ -60,7 +60,7 @@ pub fn parseClassificationFile(allocator: std.mem.Allocator, data: []const u8) !
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData; const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
defer parsed.deinit(); defer parsed.deinit();
for (parsed.records.items) |record| { for (parsed.records) |record| {
const entry = record.to(ClassificationEntry) catch continue; const entry = record.to(ClassificationEntry) catch continue;
try entries.append(allocator, .{ try entries.append(allocator, .{
.symbol = try allocator.dupe(u8, entry.symbol), .symbol = try allocator.dupe(u8, entry.symbol),

View file

@ -321,7 +321,7 @@ pub fn loadFromData(allocator: std.mem.Allocator, data: []const u8) ?KeyMap {
var bindings = std.ArrayList(Binding).empty; var bindings = std.ArrayList(Binding).empty;
for (parsed.records.items) |record| { for (parsed.records) |record| {
var action: ?Action = null; var action: ?Action = null;
var key: ?KeyCombo = null; var key: ?KeyCombo = null;

View file

@ -256,7 +256,7 @@ pub fn loadFromData(data: []const u8) ?Theme {
var theme = default_theme; var theme = default_theme;
for (parsed.records.items) |record| { for (parsed.records) |record| {
for (record.fields) |field| { for (record.fields) |field| {
if (field.value) |v| { if (field.value) |v| {
const str = switch (v) { const str = switch (v) {