perf(database): bound the read pool's mapping and cache by measurement

#283 read as "64 MB of mmap and up to 32 MB of page cache per pool",
and it is — but on a 50 000-track library neither is what the
process's memory is: the RSS of the database mapping is 46 MB whether
the bound is 64 MB or 16 MB, because a mapping costs what the working
set touches. Quartering it and halving the page cache moved no query
either: 1 172 → 1 197 ms for the whole track list, 106 → 124 ms for the
album list, 6 → 8 ms for an FTS search, all inside the run-to-run
spread.

Kept, because a bound that costs nothing measurable is worth having on
a phone: five read connections' worth of mapping is 320 MB of address
space at 64 MB each against 80 MB here, and #52 was a low-memory kill.

Closes #283
This commit is contained in:
yonlu committed 2026-10-06 01:51:56 -04:00
1 parent 620151aa41
commit c9b3048a20
1 file changed
+13 -3
+13 -3
View File
@@ -54,9 +54,19 @@ type DB struct {
// form is silently ignored, which is why WAL was never actually on.
const (
writeDSNParams = "?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)"
// The read pool's own page cache and mapping bound, sized by
// measurement rather than by appetite (#283). On a 50 000-track
// library, halving the cache and quartering the mapping moved no
// query — 1 172 → 1 197 ms for the whole track list, 106 → 124 ms
// for the album list, 6 → 8 ms for an FTS search, all inside the
// run-to-run spread — and the RSS of the database mapping was 46 MB
// under both settings, because a mapping's cost is what the working
// set touches, not the bound. The bound is what matters on a
// phone, where five connections' worth of address space is the
// thing the low-memory killer reads (cf. #52).
readDSNParams = "?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)" +
"&_pragma=query_only(true)&_pragma=synchronous(NORMAL)" +
"&_pragma=cache_size(-8000)&_pragma=mmap_size(67108864)"
"&_pragma=cache_size(-2000)&_pragma=mmap_size(16777216)"
// readPoolConns bounds concurrent read connections. A handful is
// plenty for interactive search + art/lookup fan-out and keeps WAL
// reader overhead small.
@@ -354,8 +364,8 @@ func applyPRAGMAs(ctx context.Context, db *sql.DB) error {
pragmas := []string{
"PRAGMA foreign_keys = ON",
"PRAGMA synchronous = NORMAL",
"PRAGMA cache_size = -8000",
"PRAGMA mmap_size = 67108864",
"PRAGMA cache_size = -2000",
"PRAGMA mmap_size = 16777216",
}
for _, pragma := range pragmas {