gui(forums): coalesce full-forum reloads to stop sync-burst UI freezes

handleEvent_main_thread() ran a full updateDisplay(true) (updateForum ->
getForumPostsHierarchy -> setPosts full reset -> proxy re-sort/re-filter ->
tree rebuild) for every incoming GXS event. A sync batch delivers many
NEW_MESSAGE events back to back, so the forum was reloaded once per post,
freezing the UI for seconds and resetting the model out from under the user's
current selection (and racing the async read-status persistence, making a
just-read post pop back to bold).

Route those events through a single-shot QTimer (300 ms) that is restarted on
each event, so a whole burst collapses into one reload once the events settle.
An explicit reload (forum switch) cancels any pending deferred one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jolavillette 2026-07-24 20:40:13 +02:00
parent 78ff5d66e6
commit 839293f84d
2 changed files with 32 additions and 1 deletions

View File

@ -23,6 +23,7 @@
#include <QKeyEvent>
#include <QScrollBar>
#include <QPainter>
#include <QTimer>
#include "util/qtthreadsutils.h"
#include "util/misc.h"
@ -374,6 +375,12 @@ GxsForumThreadWidget::GxsForumThreadWidget(const RsGxsGroupId &forumId, QWidget
ui->threadTreeWidget->enableColumnCustomize(true);
#endif
// Single-shot timer used to coalesce the full-forum reloads requested by
// incoming GXS events (see scheduleForumReload()).
mDeferredReloadTimer = new QTimer(this);
mDeferredReloadTimer->setSingleShot(true);
connect(mDeferredReloadTimer, &QTimer::timeout, this, [this]() { updateDisplay(true); });
mEventHandlerId = 0;
// Needs to be asynced because this function is called by another thread!
rsEvents->registerEventsHandler(
@ -402,7 +409,7 @@ void GxsForumThreadWidget::handleEvent_main_thread(std::shared_ptr<const RsEvent
case RsForumEventCode::PINNED_POSTS_CHANGED:
case RsForumEventCode::SYNC_PARAMETERS_UPDATED:
if(e->mForumGroupId == mForumGroup.mMeta.mGroupId)
updateDisplay(true);
scheduleForumReload();
break;
case RsForumEventCode::SUBSCRIBE_STATUS_CHANGED:
@ -424,6 +431,16 @@ void GxsForumThreadWidget::handleEvent_main_thread(std::shared_ptr<const RsEvent
}
}
void GxsForumThreadWidget::scheduleForumReload()
{
// Coalesce bursts of incoming events into a single reload. 300 ms is short
// enough to feel immediate yet long enough to absorb a whole sync batch, so
// the expensive updateForum()/setPosts() cycle runs once instead of once per
// post. Restarting the timer on every event pushes the reload back until the
// events settle.
mDeferredReloadTimer->start(300);
}
void GxsForumThreadWidget::showForumInfo()
{
mThreadId.clear();
@ -596,6 +613,10 @@ void GxsForumThreadWidget::updateDisplay(bool complete)
}
if(complete) // need to update the group data, reload the messages etc.
{
// We are reloading now, so drop any reload still pending in the coalescing
// timer (e.g. queued by events for a forum we just switched away from).
mDeferredReloadTimer->stop();
saveExpandedItems(mSavedExpandedMessages);
if(groupId() != mThreadModel->currentGroupId())

View File

@ -29,6 +29,7 @@
#include "util/FontSizeHandler.h"
class QSortFilterProxyModel;
class QTimer;
class QTreeWidgetItem;
class RSTreeWidgetItemCompareRole;
class GxsForumsFillThread;
@ -197,6 +198,13 @@ private:
void handleEvent_main_thread(std::shared_ptr<const RsEvent> event);
// Coalesce the full-forum reloads triggered by incoming GXS events: a sync
// burst delivers many NEW_MESSAGE events in a row, and reloading the whole
// forum for each one froze the UI for seconds and reset the model out from
// under the user's selection. Restart a single-shot timer instead so a burst
// results in one reload once the events settle.
void scheduleForumReload();
private:
void setForumDescriptionLoading();
void clearForumDescription();
@ -239,6 +247,8 @@ private:
Ui::GxsForumThreadWidget *ui;
RsEventsHandlerId_t mEventHandlerId;
QTimer *mDeferredReloadTimer;
};
#endif // GXSFORUMTHREADWIDGET_H