Skip to content

GXS: faster message loading, meta caches warmed up by a background scan - #359

Open
jolavillette wants to merge 4 commits into
RetroShare:masterfrom
jolavillette:perf/gxs-meta-single-scan
Open

GXS: faster message loading, meta caches warmed up by a background scan#359
jolavillette wants to merge 4 commits into
RetroShare:masterfrom
jolavillette:perf/gxs-meta-single-scan

Conversation

@jolavillette

Copy link
Copy Markdown
Contributor

Performance work on the GXS message loading path, measured on a real profile (gxsforums_db 235 MB, gxschannels_db 638 MB, SQLCipher):

  • batch message retrieval into IN(...) queries instead of one query per message, avoid per-batch group rescans, make message types movable, and only read the post versions that will be displayed;
  • warm up the per-group message meta caches with one sequential scan of the message table instead of one indexed query per group (one file pass instead of one seek per message);
  • run that scan on its own thread, in slices sized to a ~250 ms time budget, taking the DB mutex only for the duration of one slice. Cold, the full scan takes up to ~60 s on the database above: done synchronously it froze every reader of the service at startup; in the background nobody waits, and cold groups are served by the regular indexed query until it completes.

🤖 Generated with Claude Code

@jolavillette
jolavillette force-pushed the perf/gxs-meta-single-scan branch 4 times, most recently from 75f1d85 to 52ab2eb Compare August 10, 2026 07:21
jolavillette and others added 4 commits August 12, 2026 12:38
…ry per group

Reading the meta of a whole group runs SELECT ... WHERE grpId=?, which
INDEX_MESSAGES_GRPID serves with one row lookup per message. Since the
payload blob lives in the same row, those lookups are scattered over the
whole file: warming up the cache of N groups costs N passes of random
I/O over a database that is hundreds of megabytes.

When more than one group still needs a cold full read, read the meta of
every message in a single sequential scan instead and fill every
per-group cache from it. The file is then read in physical order, and
the cost no longer grows with the number of groups.

Measured on a synthetic database of the same shape and size as a real
gxsforums_db (235 MB, 23 KB rows, 20 groups), cold cache:

  20 per-group queries   29449 ms
  one sequential scan     2146 ms      13.7x

and the scan does not get more expensive as groups are added, where the
per-group path grows linearly with them. On a node subscribed to
hundreds of forums this is the difference between tens of seconds of
startup and a fixed couple of seconds.

Nothing else changes: same columns, same cache contents, same values
returned. Callers and public API are untouched, and no database schema
or format is modified.

Trade-off: the scan fills the cache for groups that were not requested
yet. That is the same memory the cache reaches as soon as those groups
are browsed, but it is reached up front rather than progressively.

The scan reports itself through the existing opt-in profiler, so the
gain is verifiable on a real profile rather than taken on trust:

    GXS-PROF loadAllMsgMetaInOneScan  db=gxsforums_db groups=571 metas=48213 in 2100ms

Stacked on the channel loading branch: both reshape the same function,
and this one reuses the profiler introduced there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed slices

The warm-up scan was triggered synchronously inside retrieveGxsMsgMetaData
by the second cold whole-group request, and ran under mDbMutex in one go.
Cold page cache, it was measured at up to 57 s on a real gxsforums_db
(235 MB): the caller -- possibly asking for a handful of metas from one
group -- and every other reader of the service froze for that long at
startup.

Keep the trigger and the sequential scan, but run it on a dedicated
thread in slices of 4096 rows by increasing rowid, taking mDbMutex only
for the duration of one slice so readers interleave. Until the scan
completes, cold groups keep being served by the indexed per-group query.
Messages stored while the scan runs are cached by storeMessage() itself,
so rowid reuse after deletions cannot leave a hole. The thread is joined
in the destructor before the DB is closed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One line per database (rows, slices, duration) so the background warm-up
can be observed and validated from the logs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A fixed 4096-row slice held mDbMutex for ~10 s on a cold large-row
database (23492 forum metas warmed in 6 slices of ~10 s each), stalling
single-group readers for that long -- the very stall the background scan
exists to avoid. Start at 256 rows and rescale each slice towards a
250 ms target, clamped to [64, 4096] rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jolavillette
jolavillette force-pushed the perf/gxs-meta-single-scan branch from 52ab2eb to 2eb3604 Compare August 12, 2026 10:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant