room-read-path-scale-budget
Measure Room/SQLite read paths at realistic data volume (years of user data) and set a regression budget from the measurements rather than a round number — covers bulk-seeding an isolated database, ti
SKILL.mdskills/managed/room-read-path-scale-budget/SKILL.md
GitHubScale budget for Room read paths
Most data tests use three rows. That proves correctness and says nothing about the size a real user reaches. An accidental N+1 or a scan-per-row is invisible at three rows and a freeze at three thousand.
1. Pick the volume from the product, not a round number
Derive it: "four sessions a week for five years" → 1,000 sessions × 5 sets. State the derivation in the test, so the next reader can argue with the premise rather than the number.
2. Seed in bulk, on an isolated database
Insert through the DAO, not through the domain flow — going through
completeSession() a thousand times measures writes and buries the read you
care about. Use a test-only database name; never the app's live file.
db = MonarchDatabase.create(context, TEST_DB) // isolated
repeat(SESSIONS) { i ->
val id = db.sessionDao().insertSession(...)
db.sessionDao().insertSets( (0 until SETS).map { ... } ) // one batch call
}
3. Time only what loads on a UI thread
Journal/history, any aggregate a screen header shows, and export. Collect into a map so one failure reports every number:
val timings = LinkedHashMap<String, Long>()
suspend fun time(label: String, block: suspend () -> Int) {
val t0 = System.nanoTime()
val size = block()
timings[label] = (System.nanoTime() - t0) / 1_000_000
assertTrue("$label returned nothing", size > 0) // guards a vacuous zero
}
The size > 0 check matters: a query that returns an empty list is fast and
proves nothing.
4. Get the numbers BEFORE choosing the budget
Set the budget to 1L, run, read the assertion message. That single step does
two jobs — it reports the real timings and it proves the check can fail.
read paths slower than 1ms at 1000 sessions:
{journal=76, titles ledger=122, export=131, completed count=0}
Then set the budget about an order of magnitude above the slowest measurement and record the measurements in a KDoc beside it.
A budget with 30x headroom is decoration. If the slowest path is 131ms, a 4,000ms budget can never fail; 1,500ms still tolerates slower hardware while catching a regression that lands in seconds.
5. What this is and is not
It is a guard against an order-of-magnitude regression. It is not a benchmark: an emulator on a software renderer is far slower than a phone, and the numbers move with the host. Say so in the test, or someone will later "fix" it by tightening the budget until it flakes.
Traps
- An instrumented gate run uninstalls the app; a device sweep straight after one drives an empty launcher.
runTestmakesdelayvirtual — userunBlockingwhen anything must wait in real time.- Assert the fixture size first (
assertEquals(SESSIONS, completedCount())), otherwise a seeding failure reads as excellent performance.