aboutsummaryrefslogtreecommitdiff
path: root/internal/readings/offline.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-24 16:35:01 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-24 16:35:01 +0200
commitb6ee8f09ad2c0bc9e83394e2427de861eb1e2a72 (patch)
tree9dd1c143c5052de2d79e6a6bf9303f1dab06563e /internal/readings/offline.go
parentfd3eda0da228d2417b8b7f9007533e214d7a5e80 (diff)
downloadlectio-b6ee8f09ad2c0bc9e83394e2427de861eb1e2a72.tar.gz
lectio-b6ee8f09ad2c0bc9e83394e2427de861eb1e2a72.zip
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.
Diffstat (limited to 'internal/readings/offline.go')
-rw-r--r--internal/readings/offline.go49
1 files changed, 43 insertions, 6 deletions
diff --git a/internal/readings/offline.go b/internal/readings/offline.go
index b2c16df..546d3b0 100644
--- a/internal/readings/offline.go
+++ b/internal/readings/offline.go
@@ -13,6 +13,33 @@ import (
"github.com/lukaszkasprzak/lectio/internal/naming"
)
+// Prepared holds the date-independent setup offlineLoad otherwise redoes on
+// every call: the stacked calendar layers (caldata.Stack) and the book table
+// (bible.LoadBookTable). Both depend only on cfg -- never on the date -- so a
+// caller resolving many dates against the same cfg (mobile.Days's 7-day loop,
+// an eventual month view) should build one Prepared with Prepare and reuse it
+// via LoadWith for every date, instead of paying Stack's INI parsing and
+// LoadBookTable's file read + parse once per date. See Prepare and LoadWith.
+type Prepared struct {
+ layers []calendar.Layer
+ tbl *bible.BookTable
+}
+
+// Prepare builds a Prepared for cfg: the layer stack for cfg.Selection().Form
+// stacked with cfg.Use (caldata.Stack), and the book table for the user's
+// books.ini override, if any (bible.LoadBookTable). Both calls already
+// tolerate their own failure (Stack falls back to the embedded calendar on a
+// bad user layer; LoadBookTable falls back to the embedded book table on a
+// bad user override) exactly as offlineLoad always has -- Prepare changes
+// only when this work happens, never what it computes or how it degrades.
+func Prepare(cfg config.Config) Prepared {
+ sel := cfg.Selection()
+ dir, _ := config.CalendarsDir()
+ layers, _ := caldata.Stack(sel.Form, dir, cfg.Use) // Stack falls back to embedded data on error
+ tbl, _ := bible.LoadBookTable(config.UserBooksINI()) // nil on error -> citations shown as authored
+ return Prepared{layers: layers, tbl: tbl}
+}
+
// offlineLoad resolves a day's readings entirely from the embedded calendar
// engine and lectionary data -- no network. It returns the same source-agnostic
// liturgy.Section / liturgy.DayInfo the CLI/TUI/web already render, so the daily
@@ -20,18 +47,28 @@ import (
// lectio's English-canonical authored form; the render localises each one to
// the chosen corpus's Psalter and the user's sigla dialect (see
// render.GatherVersion, bible.OFRef).
+//
+// offlineLoad is Prepare(cfg) followed by offlineLoadWith -- a single call's
+// worth of convenience for Load, which has no date to amortize Prepare's cost
+// over. A caller with several dates should call Prepare once and use
+// offlineLoadWith/LoadWith directly instead (see Prepared's doc comment).
func offlineLoad(cfg config.Config, date string) ([]liturgy.Section, liturgy.DayInfo, error) {
+ return offlineLoadWith(Prepare(cfg), cfg, date)
+}
+
+// offlineLoadWith is offlineLoad, given an already-built Prepared instead of
+// building its own. Computing the day itself (calendar.Compute, the readings
+// it resolves) still happens once per call, exactly as before -- only the
+// layer stack and book table are reused.
+func offlineLoadWith(p Prepared, cfg config.Config, date string) ([]liturgy.Section, liturgy.DayInfo, error) {
d, err := time.Parse("2006-01-02", date)
if err != nil {
return nil, liturgy.DayInfo{}, fmt.Errorf("bad date %q (want YYYY-MM-DD)", date)
}
sel := cfg.Selection()
- dir, _ := config.CalendarsDir()
- layers, _ := caldata.Stack(sel.Form, dir, cfg.Use) // Stack falls back to embedded data on error
- day := calendar.Compute(d.UTC(), sel, layers)
- rs := caldata.Readings(sel, layers, d.UTC(), day)
- tbl, _ := bible.LoadBookTable(config.UserBooksINI()) // nil on error -> citations shown as authored
- return sectionsFor(rs, sel.Form, cfg.UILanguage, cfg.SiglaLang(), tbl), dayInfo(cfg, day), nil
+ day := calendar.Compute(d.UTC(), sel, p.layers)
+ rs := caldata.Readings(sel, p.layers, d.UTC(), day)
+ return sectionsFor(rs, sel.Form, cfg.UILanguage, cfg.SiglaLang(), p.tbl), dayInfo(cfg, day), nil
}
// citationForms renders a reading's authored (English) citation into its