From b6ee8f09ad2c0bc9e83394e2427de861eb1e2a72 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Mon, 24 Aug 2026 16:35:01 +0200 Subject: perf(readings): stack calendar layers and load the book table once per Days call mobile.Days (dlectio's 7-day calendar view) called readings.Load once per date. Load's offlineLoad re-ran caldata.Stack (parses the embedded calendar INI, plus any user calendar layer) and bible.LoadBookTable (parses the embedded books.ini, plus any user override) on every call, even though neither depends on the date -- only on cfg.Selection().Form and cfg.Use, which mobile.Days holds fixed across its whole loop. Verified before changing anything: isolated benchmarks put Stack+ LoadBookTable at ~1.1ms/call (OF) and ~1.6ms/call (EF) -- real INI parsing and, for a user override, file I/O -- and for the OF form that redundant work was ~67% of a 7-day view's total time. Load gains a Prepared value (the layer stack + book table) and a Prepare/LoadWith pair: Prepare builds a Prepared once, LoadWith reuses it across dates. Load itself is unchanged, still calling Prepare on every invocation -- every existing caller keeps its current behaviour untouched. mobile.Days now Prepares once and loops LoadWith; mobile.Day picks up the same fix for its own two same-cfg Load calls. Benchmarked (interleaved before/after pairs, to control for machine thermal drift): the OF 7-day view (BenchmarkDaysWeek) drops from ~11.5ms to ~6.4ms/op (allocs 53359 -> 16347, -69%), an EF 7-day view (BenchmarkDays7EF) from ~74ms to ~66-73ms/op (allocs 299204 -> 253815, -15%; the EF form's per-day calendar computation dominates its total cost far more than OF's does, so the fix's share of the win is smaller there), and a single Day call (BenchmarkDay1) from ~4.3ms to ~2.9ms/op (allocs 15357 -> 9192, -40%). Output identity verified separately (not part of this diff): a 288-case sweep of mobile.Day/mobile.Days across both forms, both UI languages, all four corpora, a leap day, the Sacred Triduum, a Requiem day, and windows spanning Christmas/Pentecost/Assumption/All Souls produced byte-identical (SHA-256-equal) JSON before and after this change. New TestLoadWithAgreesWithLoad asserts LoadWith(Prepare(cfg), cfg, opts) equals Load(cfg, opts) for every case, so the fast path cannot silently drift from the slow one. go test ./... and make ci (both build tags, oracle/differential suite included) are green. --- internal/readings/readings.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'internal/readings/readings.go') diff --git a/internal/readings/readings.go b/internal/readings/readings.go index d0d7bf9..29eff5a 100644 --- a/internal/readings/readings.go +++ b/internal/readings/readings.go @@ -25,8 +25,22 @@ type Options struct { // liturgical colour -- see liturgy.DayInfo) for the configured form // (cfg.Lectionary: "traditional" or "new") and applies part filtering. Every // reading is resolved offline from the embedded calendar and lectionary data. +// +// Load is LoadWith(Prepare(cfg), cfg, opts) -- a single call's worth of +// convenience. A caller resolving several dates against the same cfg (a +// week/month view) should call Prepare once and use LoadWith directly instead +// of paying Prepare's cost on every date; see Prepared's doc comment +// (internal/readings/offline.go). func Load(cfg config.Config, opts Options) ([]liturgy.Section, liturgy.DayInfo, error) { - secs, info, err := offlineLoad(cfg, opts.Date) + return LoadWith(Prepare(cfg), cfg, opts) +} + +// LoadWith is Load, given an already-built Prepared (see Prepare) instead of +// building its own. Reuse one Prepared across every date resolved against the +// same cfg to skip re-stacking the calendar layers and re-parsing the book +// table per date -- the fast path mobile.Days's multi-day loop uses. +func LoadWith(p Prepared, cfg config.Config, opts Options) ([]liturgy.Section, liturgy.DayInfo, error) { + secs, info, err := offlineLoadWith(p, cfg, opts.Date) if err != nil { return nil, liturgy.DayInfo{}, err } -- cgit v1.3