diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-25 10:45:28 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-25 10:45:28 +0200 |
| commit | f110c338718a6eea3693fb6afb4f7fcc5b77003b (patch) | |
| tree | 52b6adb382445a0762f63fb862361d7fbd8637f2 /internal/readings/offline.go | |
| parent | fd3eda0da228d2417b8b7f9007533e214d7a5e80 (diff) | |
| parent | b24702cc9b8d7f2e408bb92032e8f7fa2aaa836b (diff) | |
| download | lectio-f110c338718a6eea3693fb6afb4f7fcc5b77003b.tar.gz lectio-f110c338718a6eea3693fb6afb4f7fcc5b77003b.zip | |
merge: make the calendar engine stop repeating whole-year work per day
A seven-day view resolved the year seven times. Each readings.Load stacked
the calendar data layers and re-read the Bible book table -- both
date-independent -- and the EF path rebuilt the year's entire transfer plan
and its occupancy scan on every single day.
Three logic-neutral fixes: hoist the date-independent setup out of the Days
loop; memoise the EF transfer plan on (year, Selection, content hash of the
merged sanctoral); and build the occupancy index once alongside it rather
than scanning per candidate.
EF's seven-day view goes 74ms to 16ms, its multiple over OF from ~13x to
~3x. OF is unchanged, as it never touched the EF paths.
Output is byte-identical throughout, proven by SHA-256 over a 492-case
sweep spanning both forms, both languages, four corpora, the Triduum, a
Requiem day and the three Joseph/Annunciation collision years. The caches
are keyed on content, not identity, so a user overlay invalidates them --
asserted by a test that varies the overlay and requires the answer to
change, itself mutation-proved by dropping the content hash and watching it
fail.
Diffstat (limited to 'internal/readings/offline.go')
| -rw-r--r-- | internal/readings/offline.go | 49 |
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 |
