From 8a9635949ff0b82cffcfe9423d1f7a47fd3019b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sat, 22 Aug 2026 09:16:46 +0200 Subject: [PATCH] [Client] Detect main-thread event loop stalls LagMonitor ticks the GUI event loop every 500 ms and records gaps beyond 2 s as stalls, warning with their duration and keeping a bounded ring of recent records for diagnostics. Measurement uses a monotonic QElapsedTimer so wall-clock steps and suspend do not fabricate stalls; recorded timestamps stay in wall time for correlating with user reports. Took 1 minute Took 13 minutes Took 2 minutes --- cockatrice/CMakeLists.txt | 1 + cockatrice/src/client/lag_monitor.cpp | 43 +++++++++++++++++ cockatrice/src/client/lag_monitor.h | 66 ++++++++++++++++++++++++++ cockatrice/src/interface/window_main.h | 2 + 4 files changed, 112 insertions(+) create mode 100644 cockatrice/src/client/lag_monitor.cpp create mode 100644 cockatrice/src/client/lag_monitor.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index ed196e501..e3e88b70c 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -15,6 +15,7 @@ set(cockatrice_SOURCES src/client/network/update/client/client_update_checker.cpp src/client/network/update/client/release_channel.cpp src/client/network/update/card_spoiler/spoiler_background_updater.cpp + src/client/lag_monitor.cpp src/client/sound_engine.cpp src/client/settings/cache_settings.cpp src/client/settings/card_counter_settings.cpp diff --git a/cockatrice/src/client/lag_monitor.cpp b/cockatrice/src/client/lag_monitor.cpp new file mode 100644 index 000000000..f542df24c --- /dev/null +++ b/cockatrice/src/client/lag_monitor.cpp @@ -0,0 +1,43 @@ +#include "lag_monitor.h" + +#include + +LagMonitor::LagMonitor(QObject *parent) : QObject(parent) +{ + timer = new QTimer(this); + timer->setInterval(TickIntervalMs); + connect(timer, &QTimer::timeout, this, &LagMonitor::checkTick); + tickClock.start(); + timer->start(); +} + +QList LagMonitor::recentStalls() const +{ + return stalls; +} + +void LagMonitor::clearStalls() +{ + stalls.clear(); +} + +void LagMonitor::checkTick() +{ + const qint64 gap = tickClock.restart(); + + if (gap <= StallThresholdMs) { + return; + } + + StallRecord record; + record.timestampMsSinceEpoch = QDateTime::currentMSecsSinceEpoch(); // wall time, for log correlation + record.durationMs = gap; + + stalls.append(record); + while (stalls.size() > MaxRecordedStalls) { + stalls.removeFirst(); + } + + qCWarning(LagMonitorLog, "Event loop stalled for %lld ms (threshold: %d ms)", static_cast(gap), + StallThresholdMs); +} diff --git a/cockatrice/src/client/lag_monitor.h b/cockatrice/src/client/lag_monitor.h new file mode 100644 index 000000000..4476bc458 --- /dev/null +++ b/cockatrice/src/client/lag_monitor.h @@ -0,0 +1,66 @@ +/** + * @file lag_monitor.h + * @ingroup Client + */ + +#ifndef LAG_MONITOR_H +#define LAG_MONITOR_H + +#include +#include +#include +#include +#include + +inline Q_LOGGING_CATEGORY(LagMonitorLog, "lag_monitor"); + +class QTimer; + +/** + * @brief Detects main-thread event loop stalls ("UI freezes") from the inside. + * + * A timer is expected to fire every TickIntervalMs of wall time. When the + * observed gap greatly exceeds that interval, some other task blocked the + * event loop for roughly the overshooting duration. This is what separates + * "my client froze" from "the network is lagging" in user reports. + * + * Healthy operation costs one timer wakeup per tick and two integer + * comparisons. Allocations happen only when a stall is actually recorded. + */ +class LagMonitor : public QObject +{ + Q_OBJECT + +public: + struct StallRecord + { + qint64 timestampMsSinceEpoch = 0; ///< when the stalled period ended + qint64 durationMs = 0; ///< approximate length of the freeze + }; + + static constexpr int TickIntervalMs = 500; + static constexpr int StallThresholdMs = 2000; + static constexpr int MaxRecordedStalls = 32; + + explicit LagMonitor(QObject *parent = nullptr); + + /** + * @brief Stalls recorded during this session, oldest first. + * + * Intended consumers are log output and the diagnostics export. The list + * holds at most MaxRecordedStalls entries. + */ + QList recentStalls() const; + + void clearStalls(); + +private slots: + void checkTick(); + +private: + QTimer *timer; + QElapsedTimer tickClock; ///< monotonic clock, so wall clock steps and suspend do not fabricate stalls + QList stalls; +}; + +#endif diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index fc0791832..3c0cc6302 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -25,6 +25,7 @@ #ifndef WINDOW_H #define WINDOW_H +#include "../client/lag_monitor.h" #include "connection_controller/remote_connection_controller.h" #include "widgets/dialogs/dlg_local_game_options.h" @@ -145,6 +146,7 @@ private: WndSets *wndSets; ConnectionController *connectionController; LocalServer *localServer; + LagMonitor lagMonitor; ///< watches the main thread for event loop stalls bool bHasActivated, askedForDbUpdater; QProcess *cardUpdateProcess; DlgViewLog *logviewDialog;