From b11fb3d7ca4a94410830a1de916bf407545971fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sat, 22 Aug 2026 17:43:11 +0200 Subject: [PATCH] [Client] Graph connection latency history in the status bar Add LatencyGraphWidget, a size-agnostic bar sparkline over the rolling sample window: heights scale to the window's own range while colors map onto an absolute quality ramp, so a steady good ping stays green. Embed it in the new LatencyStatusWidget together with the textual ping readout and feed both through ConnectionController's forwarded signals; the whole area hides while disconnected or without samples. Took 9 minutes Took 14 seconds --- cockatrice/CMakeLists.txt | 2 + .../src/client/latency_graph_widget.cpp | 52 +++++++++++++++++++ cockatrice/src/client/latency_graph_widget.h | 43 +++++++++++++++ .../src/client/latency_status_widget.cpp | 50 ++++++++++++++++++ cockatrice/src/client/latency_status_widget.h | 38 ++++++++++++++ cockatrice/src/interface/window_main.cpp | 30 +++-------- cockatrice/src/interface/window_main.h | 4 +- 7 files changed, 195 insertions(+), 24 deletions(-) create mode 100644 cockatrice/src/client/latency_graph_widget.cpp create mode 100644 cockatrice/src/client/latency_graph_widget.h create mode 100644 cockatrice/src/client/latency_status_widget.cpp create mode 100644 cockatrice/src/client/latency_status_widget.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index e3e88b70c..1924d86bf 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -15,6 +15,8 @@ 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/latency_graph_widget.cpp + src/client/latency_status_widget.cpp src/client/lag_monitor.cpp src/client/sound_engine.cpp src/client/settings/cache_settings.cpp diff --git a/cockatrice/src/client/latency_graph_widget.cpp b/cockatrice/src/client/latency_graph_widget.cpp new file mode 100644 index 000000000..786de5cb6 --- /dev/null +++ b/cockatrice/src/client/latency_graph_widget.cpp @@ -0,0 +1,52 @@ +/** + * @file latency_graph_widget.cpp + * @ingroup Client + */ + +#include "latency_graph_widget.h" + +#include + +LatencyGraphWidget::LatencyGraphWidget(QWidget *parent) : QWidget(parent) +{ +} + +void LatencyGraphWidget::setSamples(const QList &samplesMs) +{ + samples = samplesMs; + update(); +} + +void LatencyGraphWidget::paintEvent(QPaintEvent * /* event */) +{ + if (samples.isEmpty()) { + return; + } + + QPainter painter(this); + + // Heights are relative to the window's own worst sample (floored at + // MinScaleMs) so the shape of the variance stays readable even when every + // value is small. + qint64 heightScaleMs = MinScaleMs; + for (int sample : samples) { + heightScaleMs = qMax(heightScaleMs, static_cast(sample)); + } + + const qreal widthPerBar = static_cast(width()) / samples.size(); + for (int i = 0; i < samples.size(); ++i) { + const qreal heightRatio = qBound(0.0, static_cast(samples.at(i)) / heightScaleMs, 1.0); + const qreal barHeight = heightRatio * height(); + + // Colors follow an absolute quality ramp: a steady good ping stays + // green no matter how uniform the window is. + const qreal colorRatio = qBound(0.0, static_cast(samples.at(i)) / ColorScaleMs, 1.0); + QColor color; + color.setHsv(qRound(120.0 * (1.0 - colorRatio)), 255, 255); + + const QRectF bar(static_cast(i) * widthPerBar + 1.0, + static_cast(height()) - barHeight, + qMax(1.0, widthPerBar - 2.0), barHeight); + painter.fillRect(bar, color); + } +} diff --git a/cockatrice/src/client/latency_graph_widget.h b/cockatrice/src/client/latency_graph_widget.h new file mode 100644 index 000000000..4f6f38f8b --- /dev/null +++ b/cockatrice/src/client/latency_graph_widget.h @@ -0,0 +1,43 @@ +/** + * @file latency_graph_widget.h + * @ingroup Client + */ + +#ifndef LATENCY_GRAPH_WIDGET_H +#define LATENCY_GRAPH_WIDGET_H + +#include +#include + +/** + * @brief Bar graph of recent network round-trip samples. + * + * Draws one bar per sample, oldest on the left. Bar height is relative to the + * window's own scale so the shape of the variance stays readable, while bar + * color maps each sample onto an absolute quality ramp (green at rest through + * red at ColorScaleMs) so a steady good ping never looks alarming. Size + * agnostic: the status bar embeds a small instance while the latency detail + * popup shows a large one. + */ +class LatencyGraphWidget : public QWidget +{ + Q_OBJECT +public: + explicit LatencyGraphWidget(QWidget *parent = nullptr); + + /// Sample in milliseconds that maps to a fully red bar. + static constexpr qint64 ColorScaleMs = 500; + + void setSamples(const QList &samplesMs); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + /// Floor of the vertical scale in milliseconds. Keeps small windows readable. + static constexpr qint64 MinScaleMs = 100; + + QList samples; +}; + +#endif diff --git a/cockatrice/src/client/latency_status_widget.cpp b/cockatrice/src/client/latency_status_widget.cpp new file mode 100644 index 000000000..a8d48d126 --- /dev/null +++ b/cockatrice/src/client/latency_status_widget.cpp @@ -0,0 +1,50 @@ +/** + * @file latency_status_widget.cpp + * @ingroup Client + */ + +#include "latency_status_widget.h" + +#include "latency_graph_widget.h" + +#include +#include + +LatencyStatusWidget::LatencyStatusWidget(QWidget *parent) : QWidget(parent) +{ + pingLabel = new QLabel(this); + pingLabel->setAccessibleName(tr("Ping")); + + latencyGraph = new LatencyGraphWidget(this); + latencyGraph->setFixedSize(90, 14); + + auto *layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(4); + layout->addWidget(latencyGraph); + layout->addWidget(pingLabel); + + hide(); +} + +void LatencyStatusWidget::updateStats(int lastMs, int medianMs, int p95Ms, int maxMs, int sampleCount) +{ + if (sampleCount == 0) { + hide(); + return; + } + + const QString stats = tr("Connection quality over the last %n sample(s):", "", sampleCount) + "\n" + + tr("Last: %1 ms").arg(lastMs) + "\n" + tr("Median: %1 ms").arg(medianMs) + "\n" + + tr("95th percentile: %1 ms").arg(p95Ms) + "\n" + tr("Maximum: %1 ms").arg(maxMs); + + pingLabel->setText(tr("Ping: %1 ms").arg(lastMs)); + pingLabel->setToolTip(stats); + pingLabel->setAccessibleDescription(stats); + show(); +} + +void LatencyStatusWidget::updateSamples(const QList &samplesMs) +{ + latencyGraph->setSamples(samplesMs); +} diff --git a/cockatrice/src/client/latency_status_widget.h b/cockatrice/src/client/latency_status_widget.h new file mode 100644 index 000000000..bb8824178 --- /dev/null +++ b/cockatrice/src/client/latency_status_widget.h @@ -0,0 +1,38 @@ +/** + * @file latency_status_widget.h + * @ingroup Client + */ + +#ifndef LATENCY_STATUS_WIDGET_H +#define LATENCY_STATUS_WIDGET_H + +#include +#include + +class QLabel; +class LatencyGraphWidget; + +/** + * @brief Status bar presentation of server round-trip health. + * + * Combines the textual "Ping" readout with a small LatencyGraphWidget + * sparkline of the rolling sample window. Stays hidden while disconnected or + * before any samples exist. Owns all latency display state so MainWindow only + * needs to forward two signals here. + */ +class LatencyStatusWidget : public QWidget +{ + Q_OBJECT +public: + explicit LatencyStatusWidget(QWidget *parent = nullptr); + +public slots: + void updateStats(int lastMs, int medianMs, int p95Ms, int maxMs, int sampleCount); + void updateSamples(const QList &samplesMs); + +private: + QLabel *pingLabel = nullptr; + LatencyGraphWidget *latencyGraph = nullptr; +}; + +#endif diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 45c7c0429..288fb6c3c 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -19,6 +19,7 @@ ***************************************************************************/ #include "window_main.h" +#include "../client/latency_status_widget.h" #include "../client/network/update/client/client_update_checker.h" #include "../client/network/update/client/release_channel.h" #include "../client/settings/cache_settings.h" @@ -145,23 +146,6 @@ void MainWindow::statusChanged(ClientStatus _status) } } -void MainWindow::updatePingDisplay(int lastMs, int medianMs, int p95Ms, int maxMs, int sampleCount) -{ - if (!pingLabel || sampleCount == 0) { - if (pingLabel) { - pingLabel->hide(); - } - return; - } - - pingLabel->setText(tr("Ping: %1 ms").arg(lastMs)); - pingLabel->setToolTip(tr("Connection quality over the last %n sample(s):", "", sampleCount) + "\n" + - tr("Last: %1 ms").arg(lastMs) + "\n" + tr("Median: %1 ms").arg(medianMs) + "\n" + - tr("95th percentile: %1 ms").arg(p95Ms) + "\n" + tr("Maximum: %1 ms").arg(maxMs)); - pingLabel->setAccessibleDescription(pingLabel->toolTip()); - pingLabel->show(); -} - // Actions void MainWindow::actConnect() @@ -553,11 +537,13 @@ MainWindow::MainWindow(QWidget *parent) [this](bool show) { statusBar()->setVisible(show); }); statusBar()->setVisible(SettingsCache::instance().userInterface().getShowStatusBar()); - pingLabel = new QLabel(this); - pingLabel->setAccessibleName(tr("Ping")); - pingLabel->hide(); - statusBar()->addPermanentWidget(pingLabel); - connect(connectionController, &ConnectionController::pingStatsUpdated, this, &MainWindow::updatePingDisplay); + latencyStatus = new LatencyStatusWidget(this); + statusBar()->addPermanentWidget(latencyStatus); + + connect(connectionController, &ConnectionController::pingStatsUpdated, latencyStatus, + &LatencyStatusWidget::updateStats); + connect(connectionController, &ConnectionController::pingSamplesUpdated, latencyStatus, + &LatencyStatusWidget::updateSamples); connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, &MainWindow::refreshShortcuts); diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index 42913ec27..47a373589 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -51,6 +51,7 @@ class HandlePublicServers; class LocalClient; class LocalServer; class QLabel; +class LatencyStatusWidget; class QThread; class RemoteClient; class ServerInfo_User; @@ -73,7 +74,6 @@ public slots: private slots: void updateTabMenu(const QList &newMenuList); void statusChanged(ClientStatus _status); - void updatePingDisplay(int lastMs, int medianMs, int p95Ms, int maxMs, int sampleCount); void localGameEnded(); void pixmapCacheSizeChanged(int newSizeInMBs); void actDisconnect(); @@ -148,8 +148,8 @@ private: WndSets *wndSets; ConnectionController *connectionController; LocalServer *localServer; - QLabel *pingLabel = nullptr; ///< status bar label with live round-trip stats LagMonitor lagMonitor; ///< watches the main thread for event loop stalls + LatencyStatusWidget *latencyStatus = nullptr; ///< status bar widget with live round-trip stats and history graph bool bHasActivated, askedForDbUpdater; QProcess *cardUpdateProcess; DlgViewLog *logviewDialog;