perf(store): seek message pages by timestamp before testing arrival IDs

Merged
JL
5d ago
+581-20

Reading a deep message page made SQLite rescan newer rows because the lexicographic OR cursor constrained only the account/chat prefix of idx_messages_chat_time. Bound the timestamp range explicitly, then test the arrival-ID tiebreak. The forward reader uses the inverse bound. The accepted rows and order remain equivalent for the non-null stored keys, including late backfills. No new index, migration, cache, application dependency or public API change.

Paired release measurements on bundled SQLite 3.53.2 used 200,000 synthetic target messages plus 200,000 rows belonging to another account. Seven paired samples per cursor/direction scenario, with excluded warmups, retain 140 observations. Timestamp-skipping cases improved 42–90x: 256 queries fell from 943–1,981 ms to 21–31 ms, with query-loop CPU falling from 940–1,980 ms to 20–30 ms. An all-timestamps-equal backward case improved 11%; its tied-run scan remains linear. Shallow cases changed little. A row-value alternative was rejected after it regressed the tied-run case on the bundled engine.

The workload uses production SQL and MessageRow decoding, with setup outside the interval and correctness assertions on every result. It excludes async pool acquisition, alias resolution/folding, protobuf/quote hydration, disk I/O and connected sessions. No RSS, live-heap or end-to-end startup gain is claimed. No allocation instrumentation, external profiling service, credentials or root. Reproduction, ranges and limitations are in docs/sqlite-pagination-performance.md; raw data is in docs/measurements/sqlite-pages.csv.

Validation: 455 all-feature chat-store tests passed, including pagination, aliases, unread, quotes, edits, search and account isolation; five profiling/doc workloads ignored in ordinary runs. Clippy for all chat-store targets/features, formatting and diff checks passed. New tests require a timestamp index bound in Diesel's actual SQL and compare cursor results against lexicographic ordering across integer limits, ties, account scopes and one/two chat keys.

This is the exclusive storage front. Application changes are confined to crates/chat-store/src/queries.rs. No changes to #265 or #273. Codex reviewed final head a5c7c3e without findings. CI remains queued, so this PR stays draft.


Summary by cubic

Makes message page reads bound the timestamp range before testing the arrival-ID tiebreak, so SQLite seeks directly through idx_messages_chat_time instead of rescanning rows that sort after the cursor. Deep timestamp-skipping pages improve 42–90x (256 queries drop from 943–1,981 ms to 21–31 ms); an all-timestamps-equal backward case improves 11% and keeps its linear tied-run scan. Shallow pages are unchanged.

The forward reader gets the inverse bound. Accepted rows and order stay equivalent for the non-null stored keys, including late backfills, with no index, migration, cache, dependency, or public API change. A row-value alternative was rejected after it regressed the tied-run case on bundled SQLite 3.53.2. Benchmark reproduction, ranges, and limitations are in docs/sqlite-pagination-performance.md, with raw data in docs/measurements/sqlite-pages.csv and a runner in scripts/measure-sqlite-pages.py.

Validation — All 455 chat-store tests pass, including pagination, aliases, unread, quotes, edits, search, and account isolation; clippy, formatting, and diff checks pass. New tests require a timestamp index bound in the SQL Diesel renders and compare cursor results against lexicographic order across integer limits, ties, account scopes, and one or two chat keys.