aboutsummaryrefslogtreecommitdiff
path: root/mobile
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-25 10:45:28 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-25 10:45:28 +0200
commitf110c338718a6eea3693fb6afb4f7fcc5b77003b (patch)
tree52b6adb382445a0762f63fb862361d7fbd8637f2 /mobile
parentfd3eda0da228d2417b8b7f9007533e214d7a5e80 (diff)
parentb24702cc9b8d7f2e408bb92032e8f7fa2aaa836b (diff)
downloadlectio-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 'mobile')
-rw-r--r--mobile/mobile.go16
-rw-r--r--mobile/mobile_bench_test.go30
-rw-r--r--mobile/mobile_test.go1
3 files changed, 44 insertions, 3 deletions
diff --git a/mobile/mobile.go b/mobile/mobile.go
index 7e4c109..bc380d2 100644
--- a/mobile/mobile.go
+++ b/mobile/mobile.go
@@ -78,7 +78,12 @@ func Day(date, form, version, lang string) string {
// Day identity (name/season/colour) in the interface language.
cfgID := config.Config{UILanguage: lang, Lectionary: lect, All: true}
- if _, info, err := readings.Load(cfgID, readings.Options{Date: date, All: true}); err == nil {
+ // cfgID and cfgR below differ only in UILanguage/ReadingVersion, never in
+ // Lectionary or Use -- the two inputs Prepare's cost depends on -- so one
+ // Prepare (keyed off cfgID; either config would do) serves both Load
+ // calls. See readings.Prepared's doc comment.
+ p := readings.Prepare(cfgID)
+ if _, info, err := readings.LoadWith(p, cfgID, readings.Options{Date: date, All: true}); err == nil {
out.Name = info.Name
out.Season = info.Season
out.Colour = info.Colour
@@ -92,7 +97,7 @@ func Day(date, form, version, lang string) string {
// language localizes the structural labels (Heading) and citation dialect;
// the corpus (version) alone decides the scripture text.
cfgR := config.Config{UILanguage: lang, Lectionary: lect, ReadingVersion: version, All: true}
- secs, _, err := readings.Load(cfgR, readings.Options{Date: date, All: true})
+ secs, _, err := readings.LoadWith(p, cfgR, readings.Options{Date: date, All: true})
if err != nil {
if out.Error == "" {
out.Error = err.Error()
@@ -166,12 +171,17 @@ func Days(start string, count int, form, lang string) string {
lect := lectByForm(form)
ui := i18n.Get(lang)
cfg := config.Config{UILanguage: lang, Lectionary: lect, All: true}
+ // cfg is identical for every date in the loop below, so the layer stack
+ // and book table it implies are too -- Prepare once here rather than
+ // letting each readings.Load call re-stack/re-parse them. See
+ // readings.Prepared's doc comment.
+ p := readings.Prepare(cfg)
out := make([]daySummary, 0, count)
for i := 0; i < count; i++ {
date := d.AddDate(0, 0, i).Format("2006-01-02")
row := daySummary{Date: date, Parts: []summaryPart{}}
- secs, info, err := readings.Load(cfg, readings.Options{Date: date, All: true})
+ secs, info, err := readings.LoadWith(p, cfg, readings.Options{Date: date, All: true})
if err != nil {
row.Error = err.Error()
} else {
diff --git a/mobile/mobile_bench_test.go b/mobile/mobile_bench_test.go
new file mode 100644
index 0000000..7e95744
--- /dev/null
+++ b/mobile/mobile_bench_test.go
@@ -0,0 +1,30 @@
+package mobile
+
+import "testing"
+
+// BenchmarkDaysWeek (mobile_test.go) already covers the OF 7-day view this
+// perf fix targets -- see internal/readings/offline.go's offlineLoad, which
+// currently re-stacks the calendar layers and re-parses the book table once
+// per day inside the loop Days drives. The benchmarks below add the two
+// comparisons that let that fix's win be measured: the same view in the
+// traditional form (a different, larger embedded calendar layer), and a
+// single Day call, the per-day unit of work Days repeats.
+
+// BenchmarkDays7EF is the 7-day view in the traditional (1962) form, whose
+// calendar layer is a different embedded file (Tridentine vs Universal).
+func BenchmarkDays7EF(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ Days("2026-07-27", 7, "ef", "pl")
+ }
+}
+
+// BenchmarkDay1 measures a single Day call, the unit of work Days repeats.
+// Comparing this to BenchmarkDaysWeek/7 shows how much of each day's cost is
+// the date-independent setup this fix hoists out.
+func BenchmarkDay1(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ Day("2026-07-27", "of", "vul", "pl")
+ }
+}
diff --git a/mobile/mobile_test.go b/mobile/mobile_test.go
index 85de500..f497c56 100644
--- a/mobile/mobile_test.go
+++ b/mobile/mobile_test.go
@@ -130,6 +130,7 @@ func TestDaysAcceptsUpperBoundCount(t *testing.T) {
}
func BenchmarkDaysWeek(b *testing.B) {
+ b.ReportAllocs()
for i := 0; i < b.N; i++ {
Days("2026-07-27", 7, "of", "pl")
}