From 26c0484297e893bcc454484ff6e2835544d6a4d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Tue, 25 Aug 2026 20:42:57 +0200 Subject: [PATCH] [Client/Server/Protocol] Surface live metrics in the Developer tab Extend Response_GetServerStats with live counters from the in-process MetricsRegistry: cards in games, event loop stall totals/worst, total commands processed, average command time, active command types, and game-start count/duration. Add a repeated CommandStats message carrying per-command breakdowns (kind, extension number, resolved protobuf name, count, total ms) for every type that has seen at least one sample. Server-side cmdGetServerStats() populates all new fields after the existing DB uptime snapshot query, resolving protobuf extension names via the descriptor pool for human-readable labels like session/Command_Ping. Expand TabDeveloper with two tables: an overview section (existing DB stats plus the new live metrics) and a per-command breakdown table (Command / Count / Total ms / Avg ms) sorted by total_ms descending so the hottest commands surface first. Took 55 minutes Took 47 seconds --- .../interface/widgets/tabs/tab_developer.cpp | 97 ++++++++++++++++++- .../interface/widgets/tabs/tab_developer.h | 3 + .../pb/response_get_server_stats.proto | 22 +++++ servatrice/src/serversocketinterface.cpp | 2 +- 4 files changed, 120 insertions(+), 4 deletions(-) diff --git a/cockatrice/src/interface/widgets/tabs/tab_developer.cpp b/cockatrice/src/interface/widgets/tabs/tab_developer.cpp index b8e6a8033..5456b86b0 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_developer.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_developer.cpp @@ -7,11 +7,13 @@ #include "tab_developer.h" #include +#include #include #include #include #include #include +#include #include #include #include @@ -21,12 +23,25 @@ TabDeveloper::TabDeveloper(TabSupervisor *_tabSupervisor, AbstractClient *_clien : Tab(_tabSupervisor), client(_client) { statsTable = new QTableWidget(0, 2); - statsTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + statsTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); statsTable->setEditTriggers(QAbstractItemView::NoEditTriggers); statsTable->setSelectionBehavior(QAbstractItemView::SelectRows); statsTable->setSelectionMode(QAbstractItemView::SingleSelection); - statsTable->horizontalHeader()->setStretchLastSection(true); statsTable->verticalHeader()->setVisible(false); + statsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Interactive); + statsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Interactive); + statsTable->horizontalHeader()->setStretchLastSection(true); + + commandTable = new QTableWidget(0, 4); + commandTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + commandTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + commandTable->setSelectionBehavior(QAbstractItemView::SelectRows); + commandTable->setSelectionMode(QAbstractItemView::SingleSelection); + commandTable->verticalHeader()->setVisible(false); + commandTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Interactive); + commandTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Interactive); + commandTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Interactive); + commandTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Interactive); statusLabel = new QLabel; @@ -38,8 +53,12 @@ TabDeveloper::TabDeveloper(TabSupervisor *_tabSupervisor, AbstractClient *_clien buttonLayout->addWidget(statusLabel, 1, Qt::AlignLeft); buttonLayout->addWidget(refreshButton, 0, Qt::AlignRight); + auto *tableLayout = new QHBoxLayout; + tableLayout->addWidget(statsTable, 1); + tableLayout->addWidget(commandTable, 2); + auto *mainLayout = new QVBoxLayout; - mainLayout->addWidget(statsTable); + mainLayout->addLayout(tableLayout, 1); mainLayout->addLayout(buttonLayout); auto *central = new QWidget; @@ -53,6 +72,7 @@ void TabDeveloper::retranslateUi() { refreshButton->setText(tr("Refresh server stats")); statsTable->setHorizontalHeaderLabels(QString(tr("Statistic;Value")).split(";")); + commandTable->setHorizontalHeaderLabels(QString(tr("Command;Count;Total ms;Avg ms")).split(";")); if (statsTable->rowCount() == 0) { statusLabel->clear(); } @@ -75,6 +95,14 @@ QString TabDeveloper::formatBytes(quint64 bytes) return tr("%1 bytes").arg(bytes); } +QString TabDeveloper::formatDurationMs(qint64 ms) +{ + if (ms >= 1000) { + return tr("%1 s").arg(QString::number(ms / 1000.0, 'f', 2)); + } + return tr("%1 ms").arg(ms); +} + void TabDeveloper::appendStatRow(const QString &name, const QString &value) { const int row = statsTable->rowCount(); @@ -83,6 +111,19 @@ void TabDeveloper::appendStatRow(const QString &name, const QString &value) statsTable->setItem(row, 1, new QTableWidgetItem(value)); } +void TabDeveloper::appendSeparatorRow(const QString §ionTitle) +{ + const int row = statsTable->rowCount(); + statsTable->insertRow(row); + auto *labelItem = new QTableWidgetItem(sectionTitle); + auto font = labelItem->font(); + font.setBold(true); + labelItem->setFont(font); + labelItem->setFlags(labelItem->flags() & ~Qt::ItemIsSelectable); + statsTable->setItem(row, 0, labelItem); + statsTable->setItem(row, 1, new QTableWidgetItem(QString())); +} + void TabDeveloper::refreshClicked() { Command_GetServerStats cmd; @@ -101,6 +142,8 @@ void TabDeveloper::serverStatsResponse(const Response &resp) const Response_GetServerStats &response = resp.GetExtension(Response_GetServerStats::ext); statsTable->setRowCount(0); + + // Overview section appendStatRow(tr("Registered users online"), QString::number(response.users_count())); appendStatRow(tr("Moderators online"), QString::number(response.mods_count())); appendStatRow(tr("Games running"), QString::number(response.games_count())); @@ -117,6 +160,54 @@ void TabDeveloper::serverStatsResponse(const Response &resp) const QDateTime snapshotTime = QDateTime::fromSecsSinceEpoch(static_cast(response.timest())); appendStatRow(tr("Snapshot taken"), snapshotTime.toLocalTime().toString("yyyy-MM-dd HH:mm")); + // Live metrics section + appendSeparatorRow(tr("Live Metrics")); + appendStatRow(tr("Cards in live games"), QString::number(response.cards_in_games())); + appendStatRow(tr("Total commands processed"), QString::number(response.total_commands())); + + if (response.total_commands() > 0) { + const double avgMs = static_cast(response.total_command_time_ms()) / response.total_commands(); + appendStatRow(tr("Avg command time"), QString::number(avgMs, 'f', 2) + " ms"); + } + appendStatRow(tr("Active command types"), QString::number(response.active_command_types())); + + appendStatRow(tr("Event loop stalls"), QString::number(response.eventloop_stalls_total())); + appendStatRow(tr("Last stall overshoot"), formatDurationMs(response.eventloop_last_stall_ms())); + appendStatRow(tr("Worst stall overshoot"), formatDurationMs(response.eventloop_max_stall_ms())); + + if (response.game_start_count() > 0) { + appendStatRow(tr("Game starts"), QString::number(response.game_start_count())); + const double avgStartMs = static_cast(response.game_start_total_ms()) / response.game_start_count(); + appendStatRow(tr("Avg game start time"), QString::number(avgStartMs, 'f', 1) + " ms"); + } + + // Per-command breakdown table + QList sortedStats(response.command_stats().begin(), response.command_stats().end()); + std::sort(sortedStats.begin(), sortedStats.end(), + [](const auto &a, const auto &b) { return a.total_ms() > b.total_ms(); }); + + commandTable->setRowCount(0); + for (const auto &cs : sortedStats) { + const int row = commandTable->rowCount(); + commandTable->insertRow(row); + commandTable->setItem(row, 0, new QTableWidgetItem(QString::fromStdString(cs.command_name()))); + + auto *countItem = new QTableWidgetItem(QString::number(cs.count())); + countItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); + commandTable->setItem(row, 1, countItem); + + auto *totalItem = new QTableWidgetItem(QString::number(cs.total_ms())); + totalItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); + commandTable->setItem(row, 2, totalItem); + + const double avg = cs.count() > 0 ? static_cast(cs.total_ms()) / cs.count() : 0.0; + auto *avgItem = new QTableWidgetItem(QString::number(avg, 'f', 2)); + avgItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); + commandTable->setItem(row, 3, avgItem); + } + commandTable->resizeColumnsToContents(); statsTable->resizeColumnsToContents(); + commandTable->resizeColumnsToContents(); + statusLabel->setText(tr("Updated %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm"))); } diff --git a/cockatrice/src/interface/widgets/tabs/tab_developer.h b/cockatrice/src/interface/widgets/tabs/tab_developer.h index 501b14e3e..fa142ae03 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_developer.h +++ b/cockatrice/src/interface/widgets/tabs/tab_developer.h @@ -21,11 +21,14 @@ class TabDeveloper : public Tab private: AbstractClient *client; QTableWidget *statsTable; + QTableWidget *commandTable; QPushButton *refreshButton; QLabel *statusLabel; void appendStatRow(const QString &name, const QString &value); + void appendSeparatorRow(const QString §ionTitle); static QString formatBytes(quint64 bytes); + static QString formatDurationMs(qint64 ms); private slots: void refreshClicked(); diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_server_stats.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_server_stats.proto index fb8a0cae2..8788834ea 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_server_stats.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_server_stats.proto @@ -1,6 +1,14 @@ syntax = "proto2"; import "response.proto"; +message CommandStats { + optional uint32 kind_index = 1; // 0=session, 1=room, 2=game, 3=moderator, 4=admin + optional uint32 extension_number = 2; // protobuf extension number within the kind + optional string command_name = 3; // e.g. "session/Command_Ping" + optional uint64 count = 4; // number of times observed + optional uint64 total_ms = 5; // cumulative processing milliseconds +} + message Response_GetServerStats { extend Response { optional Response_GetServerStats ext = 1220; @@ -16,4 +24,18 @@ message Response_GetServerStats { optional uint64 uptime_secs = 6; optional uint64 timest = 7; // unix timestamp of the snapshot + + // Live metrics from MetricsRegistry (reset on server restart) + optional uint64 cards_in_games = 8; + optional uint64 eventloop_stalls_total = 9; + optional uint64 eventloop_last_stall_ms = 10; + optional uint64 eventloop_max_stall_ms = 11; + optional uint64 total_commands = 12; + optional uint64 total_command_time_ms = 13; + optional int32 active_command_types = 14; + optional uint64 game_start_count = 15; + optional uint64 game_start_total_ms = 16; + + // Per-command breakdown (only types with count > 0) + repeated CommandStats command_stats = 20; } diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 55c9716c7..fbacc827f 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -1772,7 +1772,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetServerStats(const Co message ? pool->FindExtensionByNumber(message, number) : nullptr; if (extension) { label = QString::fromLatin1(MetricsRegistry::KindNames[kind]) + QStringLiteral("/") + - QString::fromStdString(std::string(extension->name())); + QString::fromStdString(std::string(extension->message_type()->name())); } } if (label.isEmpty()) {