Optimization: Parallelize GXS message deserialization using OpenMP (4x speedup) - #246
Optimization: Parallelize GXS message deserialization using OpenMP (4x speedup)#246jolavillette wants to merge 7 commits into
Conversation
|
openmp is a great tool for parallelizing (I use if often), but you need to make sure that
|
|
Note that in this PR only rsgenexchange is changed. So here is what Antigravity says: "I have reviewed the OpenMP parallelized code in libretroshare/src/gxs/rsgenexchange.cc (around line 1559) considering Cyril's feedback. Safety Analysis: No static variables involved below the parallel loop calls mSerialiser->deserialise(...). 100% Re-entrant: SQLCipher (Recursive/Simultaneous usage): Conclusion: The code appears to comply with the stated constraints and should not cause crashes or corruption related to SQLCipher or static variables. The parallelization here is strictly computational (deserialization). However, please ensure that all services using RsGenExchange (Forums, Channels, etc.) strictly use "clean" serializers (like the Forums one I verified), which follows the standard pattern in the current codebase." |
1f06ab1 to
dc46a88
Compare
|
remove debug messages |
1c04192 to
bc8693f
Compare
40ec79e to
5478934
Compare
5478934 to
93d9db0
Compare
ea2bfdb to
233016d
Compare
233016d to
6aa99e5
Compare
6aa99e5 to
7f4c1cd
Compare
0cb7ea0 to
ab28158
Compare
ab28158 to
eaee1be
Compare
42d6a9a to
8b21be6
Compare
cdcd4f7 to
216c9f9
Compare
cd1bd82 to
65ef595
Compare
5b6b7a0 to
9c67065
Compare
814fc13 to
91c9d47
Compare
b23c7c8 to
9985bd8
Compare
e21bd29 to
e554361
Compare
34eba18 to
8c6aa02
Compare
8c6aa02 to
9261454
Compare
…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>
9261454 to
e582783
Compare
…s published publishMsgs() builds a grpMetas map out of msgChangeMap and then calls RsDataService::retrieveGxsGrpMetaData() unconditionally. When no message was published - which is the case on virtually every tick - that map is empty, and an empty map means "retrieve ALL groups" for the data service: a full group table scan (with the associated decryption cost when the grp meta cache is cold). That happens with mGenMtx held, so the whole GXS service is frozen for the duration, and so is any GUI call that needs mGenMtx. Observed on a client with 878 forums: a msg meta retrieval held the data service mutex for 35 s, publishMsgs() blocked on it while holding mGenMtx, and the GUI thread hung 27 s inside RsGenExchange::getDefaultSyncPeriod() while building the forums help string. Guard the call the same way processRecvdMessages() already does.
The database work is finished when the loop starts; what remains is pure in-memory decoding, one item per message, independent of each other. Run it through an OpenMP parallel for: results land in a pre-sized array so the loop shares no mutable state, and are merged serially afterwards, which keeps the output order stable and reports deserialisation errors from a single thread instead of interleaving lines from the pool. mSerialiser is used concurrently and must remain stateless/re-entrant; its declaration now says so. Builds without OpenMP ignore the pragma and run the loop serially, unchanged. Restacked on top of perf/gxs-channel-loading, whose version filtering and move semantics reshape the same loop: the two changes are complementary (fewer items to decode, then decoded in parallel). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds -fopenmp where the qmake build compiles and links libretroshare. The previous version of this change also dropped rs_deep_forums_index from the xapian link condition in use_libretroshare.pri; that was unrelated and would have broken deep-forum-index builds, so it is not carried over. CMake builds do not set the flag yet: there the pragma is ignored and the loop runs serially, which is correct, just not parallel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e582783 to
3eae183
Compare
Code by Antigravity
This PR requires RetroShare pr/3136
Description
This PR significantly optimizes the loading performance of GXS services (Channels, Forums, Boards) by parallelizing the message deserialization process.
Profiling identified
RsGenExchange::getMsgDataas a major bottleneck during the loading phase, where deserialization was performed sequentially on a single thread. This PR introduces OpenMP to parallelize this workload across available CPU cores.Changes
Enabled
-fopenmpcompiler and linker flags globally for Linux and Windows (MSYS2) builds. This ensures consistent OpenMP support acrosslibretroshareand the GUI executable.libretroshare): Refactored the main loop inRsGenExchange::getMsgDatato use#pragma omp parallel for. This allows concurrent deserialization ofRsGxsMsgItemobjects.Performance Results
Benchmarks performed on an Intel Xeon E3-1230 v6 (4 cores / 8 threads) on Ubuntu 24.04, and on an Intel 4790K (4 cores / 8 threads) on Windows 10
Example: Deserialization time on Xeon:
| Dataset Size | Serial (Before) | Parallel (After) | Speedup |
| Large (~6000 items) | ~1900 ms | ~440 ms | 4.3x
| Medium (~3000 items) | ~265 ms | ~65 ms | 4.0x
| Small (~500 items) | ~388 ms | ~80 ms | 4.8x
Impact
Notes
std::sortoperations from the UI thread to a worker thread was tested but yielded negligible gains (~50ms) compared to Qt's rendering cost. Therefore, UI-specific changes were reverted to maintain code simplicity.