aboutsummaryrefslogtreecommitdiff
path: root/mobile
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 /mobile
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 '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")
}