Cockatrice/tests/metrics_registry_test.cpp
Lukas Brübach a542831aad [Server] Instrument command processing, game starts, and event loops
Add a lock-free MetricsRegistry that accumulates per-command processing
times in preallocated histogram slots (one per protobuf command type,
bucketed at 1/5/10/25/50/100/250/500/1000/2500/5000 ms +Inf). The
hot-path observeCommand() uses only relaxed atomic adds — no locks,
no allocations, no cache-line ping-pong beyond the unavoidable counter
updates.

Wire the registry into AbstractServerSocketInterface::processCommandContainer()
so every processed command is attributed with its container's wall-clock
time. When a container exceeds metrics/slow_command_ms (default 500),
a warning is logged including the connected username.

Add an EventLoopWatchdog heartbeat that runs on every socket pool thread.
If a heartbeat overshoots metrics/stall_warn_ms (default 2000 ms), the
overshoot is recorded in atomic counters and a warning is logged. Both
thresholds are configurable in servatrice.ini; setting stall_warn_ms to 0
disables the watchdogs entirely.

Track game-start durations via a separate histogram in MetricsRegistry.
Server_Game::startGameNow() measures the time from zone creation through
player materialization and reports it via Server::observeGameStartDurationMs().

Add a live card-count gauge: Server_Game exposes getCardsInGame() and
Servatrice::getCardsInGamesTotal() sums across all running games under
the appropriate read locks.

Include a standalone metrics_registry_test (Google Test) that validates
empty registries, single/multi-sample histograms, kind encoding,
overflow-slot collapse, negative-duration clamping, gauge rendering,
and the game-start histogram separation.

Took 10 minutes
2026-09-11 09:01:55 +02:00

131 lines
4.7 KiB
C++

#include <QRegularExpression>
#include <gtest/gtest.h>
#include <metrics_registry.h>
TEST(MetricsRegistryTest, EmptyRegistryProducesNoHistogramLines)
{
MetricsRegistry registry;
EXPECT_EQ(0, registry.totalCommands());
EXPECT_EQ(0, registry.totalTimeMs());
EXPECT_EQ(0, registry.activeTypeCount());
const QString text = registry.toPrometheusText([](int) { return QString("x"); }, {});
// The family TYPE declaration may stand alone. What must not exist is a
// histogram sample without data behind it.
EXPECT_FALSE(text.contains(QRegularExpression("servatrice_commands_duration_ms_(bucket|sum|count)")));
}
TEST(MetricsRegistryTest, SingleSampleIsRecordedInTotalsAndBuckets)
{
MetricsRegistry registry;
registry.observeCommand(MetricsRegistry::typeIdFor(0, 1000), 7);
EXPECT_EQ(1, registry.totalCommands());
EXPECT_EQ(7, registry.totalTimeMs());
EXPECT_EQ(1, registry.activeTypeCount());
const QString text = registry.toPrometheusText(
[](int typeId) {
return QString("%1/%2").arg(typeId / MetricsRegistry::KindStride).arg(typeId % MetricsRegistry::KindStride);
},
{});
// 7ms falls into the le="10" bucket. Smaller buckets stay empty
EXPECT_TRUE(text.contains("# TYPE servatrice_commands_duration_ms histogram\n"));
EXPECT_TRUE(text.contains(",le=\"10\"} 1"));
EXPECT_TRUE(text.contains(",le=\"5\"} 0"));
EXPECT_TRUE(text.contains("_sum{command=\"0/1000\"} 7"));
EXPECT_TRUE(text.contains("_count{command=\"0/1000\"} 1"));
}
TEST(MetricsRegistryTest, BucketsAreCumulative)
{
MetricsRegistry registry;
registry.observeCommand(MetricsRegistry::typeIdFor(0, 1000), 2);
registry.observeCommand(MetricsRegistry::typeIdFor(0, 1000), 30);
const QString text = registry.toPrometheusText([](int) { return QString("cmd"); }, {});
// cumulative counts: <=25 -> 1 sample, <=50 -> 2 samples
EXPECT_TRUE(text.contains(",le=\"25\"} 1\n"));
EXPECT_TRUE(text.contains(",le=\"50\"} 2\n"));
EXPECT_TRUE(text.contains(",le=\"+Inf\"} 2\n"));
}
TEST(MetricsRegistryTest, KindEncodingSeparatesSameExtensionNumber)
{
MetricsRegistry registry;
const int sessionPing = MetricsRegistry::typeIdFor(0, 1000);
const int roomLeaveRoom = MetricsRegistry::typeIdFor(1, 1000);
ASSERT_NE(sessionPing, roomLeaveRoom);
registry.observeCommand(sessionPing, 1);
registry.observeCommand(roomLeaveRoom, 5000);
EXPECT_EQ(2, registry.activeTypeCount());
}
TEST(MetricsRegistryTest, OutOfRangeIdsLandInOverflowSlot)
{
MetricsRegistry registry;
registry.observeCommand(-1, 4);
registry.observeCommand(MetricsRegistry::MaxTypes + 12345, 4);
EXPECT_EQ(2, registry.totalCommands());
EXPECT_EQ(1, registry.activeTypeCount()); // both collapsed into one slot
EXPECT_EQ(8, registry.totalTimeMs());
}
TEST(MetricsRegistryTest, NegativeDurationsAreClamped)
{
MetricsRegistry registry;
registry.observeCommand(MetricsRegistry::typeIdFor(0, 1000), -50);
EXPECT_EQ(0, registry.totalTimeMs());
}
TEST(MetricsRegistryTest, GaugesAndLabelEscapingAreRendered)
{
MetricsRegistry registry;
QHash<QString, qint64> gauges;
gauges.insert("servatrice_users_current", 42);
const QString text = registry.toPrometheusText(nullptr, gauges);
EXPECT_TRUE(text.contains("# TYPE servatrice_users_current gauge\n"));
EXPECT_TRUE(text.contains("servatrice_users_current 42\n"));
}
TEST(MetricsRegistryTest, UnnamedTypesFallBackToNumericLabel)
{
MetricsRegistry registry;
registry.observeCommand(MetricsRegistry::typeIdFor(2, 1042), 9);
const QString text = registry.toPrometheusText(nullptr, {});
EXPECT_TRUE(text.contains("{command=\"" + QString::number(MetricsRegistry::typeIdFor(2, 1042)) + "\"}"));
}
TEST(MetricsRegistryTest, GameStartHistogramOnlyAppearsAfterSamples)
{
MetricsRegistry registry;
EXPECT_FALSE(registry.toPrometheusText(nullptr, {}).contains("servatrice_game_start_duration_ms"));
registry.observeGameStartDurationMs(120);
const QString text = registry.toPrometheusText(nullptr, {});
// 120ms falls into the le="250" bucket
EXPECT_TRUE(text.contains("# TYPE servatrice_game_start_duration_ms histogram\n"));
EXPECT_TRUE(text.contains(",le=\"100\"} 0\n"));
EXPECT_TRUE(text.contains(",le=\"250\"} 1\n"));
EXPECT_TRUE(text.contains("servatrice_game_start_duration_ms_sum 120\n"));
EXPECT_TRUE(text.contains("servatrice_game_start_duration_ms_count 1\n"));
}
TEST(MetricsRegistryTest, GameStartHistogramIsSeparateFromCommandTotals)
{
MetricsRegistry registry;
registry.observeGameStartDurationMs(10);
EXPECT_EQ(0, registry.totalCommands());
EXPECT_EQ(0, registry.totalTimeMs());
EXPECT_EQ(0, registry.activeTypeCount());
}