mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
[Client] Show latency in status bar and server tab indicator (#7154)
* [Client] Show live connection latency in the status bar Add a permanent status-bar label fed by ConnectionController's pingStatsUpdated: shows the latest round-trip time, hides while disconnected or without samples, and carries a tooltip with Last/Median/95th percentile/Maximum over the rolling sample window (mirrored into the accessible description). The server tab gets the same stats as its tooltip. Took 2 minutes Took 2 minutes Took 1 minute Took 48 seconds Took 25 seconds Took 4 minutes * [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 * [Client] Show latency details when clicking the ping display Clicking the status bar ping area opens a popup with a larger instance of the latency graph plus the numeric statistics, selectable and mirrored into the accessible name. Qt::Popup closes it on any outside click; contents refresh live while open. Took 33 seconds * Fixup from core commit Took 6 minutes Took 5 seconds Took 5 minutes * Lint. Took 12 minutes * Consolidate. Took 6 minutes --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
fc0199d3db
commit
8a5723c0b5
9 changed files with 289 additions and 1 deletions
|
|
@ -15,6 +15,8 @@ set(cockatrice_SOURCES
|
||||||
src/client/network/update/client/client_update_checker.cpp
|
src/client/network/update/client/client_update_checker.cpp
|
||||||
src/client/network/update/client/release_channel.cpp
|
src/client/network/update/client/release_channel.cpp
|
||||||
src/client/network/update/card_spoiler/spoiler_background_updater.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/lag_monitor.cpp
|
||||||
src/client/sound_engine.cpp
|
src/client/sound_engine.cpp
|
||||||
src/client/settings/cache_settings.cpp
|
src/client/settings/cache_settings.cpp
|
||||||
|
|
|
||||||
51
cockatrice/src/client/latency_graph_widget.cpp
Normal file
51
cockatrice/src/client/latency_graph_widget.cpp
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
/**
|
||||||
|
* @file latency_graph_widget.cpp
|
||||||
|
* @ingroup Client
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "latency_graph_widget.h"
|
||||||
|
|
||||||
|
#include <QPainter>
|
||||||
|
|
||||||
|
LatencyGraphWidget::LatencyGraphWidget(QWidget *parent) : QWidget(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void LatencyGraphWidget::setSamples(const QList<int> &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<qint64>(sample));
|
||||||
|
}
|
||||||
|
|
||||||
|
const qreal widthPerBar = static_cast<qreal>(width()) / samples.size();
|
||||||
|
for (int i = 0; i < samples.size(); ++i) {
|
||||||
|
const qreal heightRatio = qBound(0.0, static_cast<double>(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<double>(samples.at(i)) / ColorScaleMs, 1.0);
|
||||||
|
QColor color;
|
||||||
|
color.setHsv(qRound(120.0 * (1.0 - colorRatio)), 255, 255);
|
||||||
|
|
||||||
|
const QRectF bar(static_cast<qreal>(i) * widthPerBar + 1.0, static_cast<qreal>(height()) - barHeight,
|
||||||
|
qMax(1.0, widthPerBar - 2.0), barHeight);
|
||||||
|
painter.fillRect(bar, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
cockatrice/src/client/latency_graph_widget.h
Normal file
43
cockatrice/src/client/latency_graph_widget.h
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* @file latency_graph_widget.h
|
||||||
|
* @ingroup Client
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef LATENCY_GRAPH_WIDGET_H
|
||||||
|
#define LATENCY_GRAPH_WIDGET_H
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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<int> &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<int> samples;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
111
cockatrice/src/client/latency_status_widget.cpp
Normal file
111
cockatrice/src/client/latency_status_widget.cpp
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
/**
|
||||||
|
* @file latency_status_widget.cpp
|
||||||
|
* @ingroup Client
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "latency_status_widget.h"
|
||||||
|
|
||||||
|
#include "latency_graph_widget.h"
|
||||||
|
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Clicking anywhere in the area opens the detail view.
|
||||||
|
for (QObject *child : QList<QObject *>{pingLabel, latencyGraph}) {
|
||||||
|
child->installEventFilter(this);
|
||||||
|
}
|
||||||
|
setCursor(Qt::PointingHandCursor);
|
||||||
|
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LatencyStatusWidget::updateData(const LatencyTracker::Stats &stats, const QList<int> &samplesMs)
|
||||||
|
{
|
||||||
|
latestSamples = samplesMs;
|
||||||
|
latencyGraph->setSamples(samplesMs);
|
||||||
|
if (popup && popup->isVisible() && detailGraph) {
|
||||||
|
detailGraph->setSamples(samplesMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.sampleCount == 0) {
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString statsStr = statsText(stats);
|
||||||
|
|
||||||
|
pingLabel->setText(tr("Ping: %1 ms").arg(stats.lastMs));
|
||||||
|
pingLabel->setToolTip(statsStr);
|
||||||
|
pingLabel->setAccessibleDescription(statsStr);
|
||||||
|
if (popup && popup->isVisible() && detailLabel) {
|
||||||
|
detailLabel->setText(statsStr);
|
||||||
|
}
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LatencyStatusWidget::eventFilter(QObject *watched, QEvent *event)
|
||||||
|
{
|
||||||
|
if ((watched == pingLabel || watched == latencyGraph) && event->type() == QEvent::MouseButtonPress) {
|
||||||
|
togglePopup();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return QWidget::eventFilter(watched, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LatencyStatusWidget::togglePopup()
|
||||||
|
{
|
||||||
|
if (!popup) {
|
||||||
|
popup = new QWidget(this, Qt::Popup | Qt::FramelessWindowHint);
|
||||||
|
auto *layout = new QVBoxLayout(popup);
|
||||||
|
layout->setContentsMargins(8, 8, 8, 8);
|
||||||
|
|
||||||
|
detailLabel = new QLabel(popup);
|
||||||
|
detailLabel->setAccessibleName(tr("Connection latency details"));
|
||||||
|
detailLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||||
|
|
||||||
|
detailGraph = new LatencyGraphWidget(popup);
|
||||||
|
detailGraph->setFixedSize(280, 80);
|
||||||
|
|
||||||
|
layout->addWidget(detailLabel, 0, Qt::AlignLeft);
|
||||||
|
layout->addWidget(detailGraph, 0, Qt::AlignHCenter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (popup->isVisible()) {
|
||||||
|
popup->hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Qt::Popup closes itself on any outside click, so just position and show.
|
||||||
|
if (latestSamples.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
detailGraph->setSamples(latestSamples);
|
||||||
|
detailLabel->setText(pingLabel->toolTip());
|
||||||
|
popup->adjustSize();
|
||||||
|
const QPoint anchor = mapToGlobal(QPoint(width() / 2, 0));
|
||||||
|
popup->move(anchor.x() - popup->width() / 2, anchor.y() - popup->height() - 6);
|
||||||
|
popup->show();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString LatencyStatusWidget::statsText(const LatencyTracker::Stats &stats) const
|
||||||
|
{
|
||||||
|
return tr("Connection quality over the last %n sample(s):", "", stats.sampleCount) + "\n" +
|
||||||
|
tr("Last: %1 ms").arg(stats.lastMs) + "\n" + tr("Median: %1 ms").arg(stats.medianMs) + "\n" +
|
||||||
|
tr("95th percentile: %1 ms").arg(stats.p95Ms) + "\n" + tr("Maximum: %1 ms").arg(stats.maxMs);
|
||||||
|
}
|
||||||
50
cockatrice/src/client/latency_status_widget.h
Normal file
50
cockatrice/src/client/latency_status_widget.h
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/**
|
||||||
|
* @file latency_status_widget.h
|
||||||
|
* @ingroup Client
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef LATENCY_STATUS_WIDGET_H
|
||||||
|
#define LATENCY_STATUS_WIDGET_H
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QWidget>
|
||||||
|
#include <libcockatrice/network/client/abstract/latency_tracker.h>
|
||||||
|
|
||||||
|
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. Clicking anywhere in the area opens
|
||||||
|
* a popup with a larger graph and the numeric statistics. It closes on any
|
||||||
|
* outside click. Stays hidden while disconnected or before any samples exist.
|
||||||
|
* Owns all latency display state so MainWindow only needs to forward one
|
||||||
|
* signal here.
|
||||||
|
*/
|
||||||
|
class LatencyStatusWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit LatencyStatusWidget(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void updateData(const LatencyTracker::Stats &stats, const QList<int> &samplesMs);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void togglePopup();
|
||||||
|
QString statsText(const LatencyTracker::Stats &stats) const;
|
||||||
|
|
||||||
|
QLabel *pingLabel = nullptr;
|
||||||
|
LatencyGraphWidget *latencyGraph = nullptr;
|
||||||
|
QWidget *popup = nullptr;
|
||||||
|
LatencyGraphWidget *detailGraph = nullptr;
|
||||||
|
QLabel *detailLabel = nullptr;
|
||||||
|
QList<int> latestSamples;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -141,6 +141,7 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *
|
||||||
connect(client, &AbstractClient::gameJoinedEventReceived, this, &TabSupervisor::gameJoined);
|
connect(client, &AbstractClient::gameJoinedEventReceived, this, &TabSupervisor::gameJoined);
|
||||||
connect(client, &AbstractClient::userMessageEventReceived, this, &TabSupervisor::processUserMessageEvent);
|
connect(client, &AbstractClient::userMessageEventReceived, this, &TabSupervisor::processUserMessageEvent);
|
||||||
connect(client, &AbstractClient::maxPingTime, this, &TabSupervisor::updatePingTime);
|
connect(client, &AbstractClient::maxPingTime, this, &TabSupervisor::updatePingTime);
|
||||||
|
connect(client, &AbstractClient::pingStatsUpdated, this, &TabSupervisor::updateLatencyTooltip);
|
||||||
connect(client, &AbstractClient::notifyUserEventReceived, this, &TabSupervisor::processNotifyUserEvent);
|
connect(client, &AbstractClient::notifyUserEventReceived, this, &TabSupervisor::processNotifyUserEvent);
|
||||||
|
|
||||||
// create tabs menu actions
|
// create tabs menu actions
|
||||||
|
|
@ -883,6 +884,23 @@ void TabSupervisor::updatePingTime(int value, int max)
|
||||||
setTabIcon(indexOf(tabServer), QIcon(PingPixmapGenerator::generatePixmap(15, value, max)));
|
setTabIcon(indexOf(tabServer), QIcon(PingPixmapGenerator::generatePixmap(15, value, max)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TabSupervisor::updateLatencyTooltip(const LatencyTracker::Stats &stats)
|
||||||
|
{
|
||||||
|
if (!tabServer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.sampleCount == 0) {
|
||||||
|
setTabToolTip(indexOf(tabServer), QString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTabToolTip(indexOf(tabServer),
|
||||||
|
tr("Connection quality over the last %n sample(s):", "", stats.sampleCount) + "\n" +
|
||||||
|
tr("Last: %1 ms").arg(stats.lastMs) + "\n" + tr("Median: %1 ms").arg(stats.medianMs) + "\n" +
|
||||||
|
tr("95th percentile: %1 ms").arg(stats.p95Ms) + "\n" + tr("Maximum: %1 ms").arg(stats.maxMs));
|
||||||
|
}
|
||||||
|
|
||||||
void TabSupervisor::gameJoined(const Event_GameJoined &event)
|
void TabSupervisor::gameJoined(const Event_GameJoined &event)
|
||||||
{
|
{
|
||||||
QMap<int, QString> roomGameTypes;
|
QMap<int, QString> roomGameTypes;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QProxyStyle>
|
#include <QProxyStyle>
|
||||||
#include <QTabWidget>
|
#include <QTabWidget>
|
||||||
|
#include <libcockatrice/network/client/abstract/latency_tracker.h>
|
||||||
|
|
||||||
class TabCardArtRules;
|
class TabCardArtRules;
|
||||||
inline Q_LOGGING_CATEGORY(TabSupervisorLog, "tab_supervisor");
|
inline Q_LOGGING_CATEGORY(TabSupervisorLog, "tab_supervisor");
|
||||||
|
|
@ -220,6 +221,7 @@ private slots:
|
||||||
|
|
||||||
void updateCurrent(int index);
|
void updateCurrent(int index);
|
||||||
void updatePingTime(int value, int max);
|
void updatePingTime(int value, int max);
|
||||||
|
void updateLatencyTooltip(const LatencyTracker::Stats &stats);
|
||||||
void gameJoined(const Event_GameJoined &event);
|
void gameJoined(const Event_GameJoined &event);
|
||||||
void localGameJoined(const Event_GameJoined &event);
|
void localGameJoined(const Event_GameJoined &event);
|
||||||
void gameLeft(TabGame *tab);
|
void gameLeft(TabGame *tab);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
#include "window_main.h"
|
#include "window_main.h"
|
||||||
|
|
||||||
|
#include "../client/latency_status_widget.h"
|
||||||
#include "../client/network/update/client/client_update_checker.h"
|
#include "../client/network/update/client/client_update_checker.h"
|
||||||
#include "../client/network/update/client/release_channel.h"
|
#include "../client/network/update/client/release_channel.h"
|
||||||
#include "../client/settings/cache_settings.h"
|
#include "../client/settings/cache_settings.h"
|
||||||
|
|
@ -56,6 +57,7 @@
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
|
#include <QLabel>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QMenuBar>
|
#include <QMenuBar>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
|
@ -535,6 +537,12 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
[this](bool show) { statusBar()->setVisible(show); });
|
[this](bool show) { statusBar()->setVisible(show); });
|
||||||
statusBar()->setVisible(SettingsCache::instance().userInterface().getShowStatusBar());
|
statusBar()->setVisible(SettingsCache::instance().userInterface().getShowStatusBar());
|
||||||
|
|
||||||
|
latencyStatus = new LatencyStatusWidget(this);
|
||||||
|
statusBar()->addPermanentWidget(latencyStatus);
|
||||||
|
|
||||||
|
connect(connectionController, &ConnectionController::pingStatsUpdated, latencyStatus,
|
||||||
|
&LatencyStatusWidget::updateData);
|
||||||
|
|
||||||
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
|
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
|
||||||
&MainWindow::refreshShortcuts);
|
&MainWindow::refreshShortcuts);
|
||||||
refreshShortcuts();
|
refreshShortcuts();
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ class GameReplay;
|
||||||
class HandlePublicServers;
|
class HandlePublicServers;
|
||||||
class LocalClient;
|
class LocalClient;
|
||||||
class LocalServer;
|
class LocalServer;
|
||||||
|
class QLabel;
|
||||||
|
class LatencyStatusWidget;
|
||||||
class QThread;
|
class QThread;
|
||||||
class RemoteClient;
|
class RemoteClient;
|
||||||
class ServerInfo_User;
|
class ServerInfo_User;
|
||||||
|
|
@ -146,7 +148,8 @@ private:
|
||||||
WndSets *wndSets;
|
WndSets *wndSets;
|
||||||
ConnectionController *connectionController;
|
ConnectionController *connectionController;
|
||||||
LocalServer *localServer;
|
LocalServer *localServer;
|
||||||
LagMonitor lagMonitor; ///< watches the main thread for event loop stalls
|
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;
|
bool bHasActivated, askedForDbUpdater;
|
||||||
QProcess *cardUpdateProcess;
|
QProcess *cardUpdateProcess;
|
||||||
DlgViewLog *logviewDialog;
|
DlgViewLog *logviewDialog;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue