Files
DarkflameServer/tests/dCommonTests/TrafficStatsTests.cpp
Aaron Kimbrell 13ad679df1 feat(net): count every server's packets and send a traffic report every 5 seconds
TrafficStats keeps packets and bytes in and out per second and the busiest
packet and game message types. dServer counts at its send and receive calls
and adds RakNet's connection statistics (datagrams, resends, ping); the report
goes to master as SERVER_TRAFFIC (appended), which passes it to the dashboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:01 -05:00

229 lines
8.9 KiB
C++

#include <gtest/gtest.h>
#include <chrono>
#include <cstring>
#include "TrafficStats.h"
#include "MessageIdentifiers.h"
#include "ServiceType.h"
#include "MessageType/Client.h"
#include "MessageType/Game.h"
#include "MessageType/World.h"
using namespace TrafficStats;
namespace {
// An LU packet header (and, for game messages, the target object and message ID)
std::vector<uint8_t> LuPacket(ServiceType service, uint32_t packet, int32_t gameMessage = -1, size_t pad = 0) {
std::vector<uint8_t> data{ ID_USER_PACKET_ENUM };
const auto s = static_cast<uint16_t>(service);
data.push_back(static_cast<uint8_t>(s));
data.push_back(static_cast<uint8_t>(s >> 8));
for (int i = 0; i < 4; i++) data.push_back(static_cast<uint8_t>(packet >> (8 * i)));
data.push_back(0);
if (gameMessage >= 0) {
for (int i = 0; i < 8; i++) data.push_back(0x11);
data.push_back(static_cast<uint8_t>(gameMessage));
data.push_back(static_cast<uint8_t>(gameMessage >> 8));
}
data.resize(data.size() + pad);
return data;
}
}
TEST(TrafficStatsTest, HistogramBucketsDoubleEveryThird) {
EXPECT_EQ(Histogram::UpperBound(0), 100u);
EXPECT_EQ(Histogram::UpperBound(3), 200u);
EXPECT_EQ(Histogram::UpperBound(30), 102400u);
EXPECT_EQ(Histogram::UpperBound(Histogram::BUCKETS - 1), UINT64_MAX);
EXPECT_EQ(Histogram::BucketFor(0), 0u);
EXPECT_EQ(Histogram::BucketFor(100), 0u);
EXPECT_EQ(Histogram::BucketFor(101), 1u);
EXPECT_EQ(Histogram::BucketFor(200), 3u);
EXPECT_EQ(Histogram::BucketFor(UINT64_MAX), Histogram::BUCKETS - 1);
for (size_t i = 1; i + 1 < Histogram::BUCKETS; i++) EXPECT_GT(Histogram::UpperBound(i), Histogram::UpperBound(i - 1));
}
TEST(TrafficStatsTest, PercentilesAreWithinABucket) {
Histogram h;
for (uint64_t ms = 1; ms <= 1000; ms++) h.Add(ms * 1000);
EXPECT_EQ(h.Count(), 1000u);
EXPECT_EQ(h.Sum(), 500500000u);
// A bucket spans 26%, so the answer is within that of the exact value
for (const auto [fraction, exact] : { std::pair{ 0.5, 500000.0 }, std::pair{ 0.95, 950000.0 }, std::pair{ 0.99, 990000.0 } }) {
const auto p = static_cast<double>(h.Percentile(fraction));
EXPECT_NEAR(p, exact, exact * 0.26) << fraction;
}
EXPECT_LE(h.Percentile(0.5), h.Percentile(0.95));
EXPECT_LE(h.Percentile(0.95), h.Percentile(0.99));
EXPECT_EQ(Histogram().Percentile(0.5), 0u);
}
TEST(TrafficStatsTest, SinglePercentileStaysInItsBucket) {
Histogram h;
h.Add(150);
const auto p = h.Percentile(0.99);
EXPECT_GT(p, Histogram::UpperBound(0));
EXPECT_LE(p, Histogram::UpperBound(Histogram::BucketFor(150)));
// Overflow reports its lower bound instead of infinity
Histogram slow;
slow.Add(3600ull * 1000000);
EXPECT_EQ(slow.Percentile(0.5), Histogram::UpperBound(Histogram::BUCKETS - 2));
}
TEST(TrafficStatsTest, HistogramsMergeAndSurviveSparse) {
Histogram a, b;
a.Add(500, 3);
b.Add(500);
b.Add(40000, 2);
a.Merge(b);
EXPECT_EQ(a.Count(), 6u);
EXPECT_EQ(a.Sum(), 500u * 4 + 80000u);
const auto sparse = a.Sparse();
ASSERT_EQ(sparse.size(), 2u);
const auto back = Histogram::FromSparse(sparse, a.Sum());
for (size_t i = 0; i < Histogram::BUCKETS; i++) EXPECT_EQ(back.At(i), a.At(i));
EXPECT_EQ(back.Sum(), a.Sum());
EXPECT_EQ(back.Percentile(0.5), a.Percentile(0.5));
}
TEST(TrafficStatsTest, StatusClasses) {
EXPECT_EQ(StatusClass(101), 0u);
EXPECT_EQ(StatusClass(200), 1u);
EXPECT_EQ(StatusClass(304), 2u);
EXPECT_EQ(StatusClass(404), 3u);
EXPECT_EQ(StatusClass(503), 4u);
EXPECT_EQ(StatusClass(0), 4u);
EXPECT_EQ(StatusClass(999), 4u);
}
TEST(TrafficStatsTest, KeysFromPackets) {
const auto world = LuPacket(ServiceType::WORLD, static_cast<uint32_t>(MessageType::World::POSITION_UPDATE), -1, 20);
auto key = KeyOf(world.data(), world.size(), false);
EXPECT_EQ(key.service, static_cast<uint16_t>(ServiceType::WORLD));
EXPECT_EQ(key.packet, static_cast<uint32_t>(MessageType::World::POSITION_UPDATE));
EXPECT_EQ(key.gameMessage, 0);
EXPECT_FALSE(key.outbound);
const auto gm = LuPacket(ServiceType::CLIENT, static_cast<uint32_t>(MessageType::Client::GAME_MSG), static_cast<int32_t>(MessageType::Game::REQUEST_USE));
key = KeyOf(gm.data(), gm.size(), true);
EXPECT_EQ(key.service, static_cast<uint16_t>(ServiceType::CLIENT));
EXPECT_EQ(key.gameMessage, static_cast<uint16_t>(MessageType::Game::REQUEST_USE));
EXPECT_TRUE(key.outbound);
// A game message cut short has no message ID; never reads past the end
key = KeyOf(gm.data(), 17, true);
EXPECT_EQ(key.gameMessage, 0);
const uint8_t replica[] = { ID_REPLICA_MANAGER_SERIALIZE, 1, 2 };
key = KeyOf(replica, sizeof(replica), true);
EXPECT_EQ(key.service, MessageKey::RAKNET);
EXPECT_EQ(key.packet, static_cast<uint32_t>(ID_REPLICA_MANAGER_SERIALIZE));
const uint8_t shortLu[] = { ID_USER_PACKET_ENUM, 4, 0 };
EXPECT_EQ(KeyOf(shortLu, sizeof(shortLu), false).service, MessageKey::RAKNET);
EXPECT_EQ(KeyOf(nullptr, 0, false).service, MessageKey::RAKNET);
}
TEST(TrafficStatsTest, KeysPackAndUnpack) {
for (const auto& key : { MessageKey{ true, 5, 12, 1234 }, MessageKey{ false, MessageKey::RAKNET, 36, 0 }, MessageKey{ false, 4, 0xFFFFFFFF, 0xFFFF } }) {
EXPECT_EQ(MessageKey::Unpack(key.Packed()), key);
}
EXPECT_NE((MessageKey{ true, 5, 12, 0 }).Packed(), (MessageKey{ false, 5, 12, 0 }).Packed());
}
TEST(TrafficStatsTest, RecorderFillsSilentSecondsAndKeepsTheCurrentOne) {
Recorder r;
const MessageKey in{ false, 4, 5, 0 };
const MessageKey out{ true, 5, 12, 0 };
r.Packet(1000, in, 100);
r.Packet(1000, in, 50);
r.Packet(1000, out, 30, 4); // a broadcast to four
r.Packet(1003, in, 10);
r.Packet(1005, in, 10); // the current second: stays
const auto report = r.Take(1005);
ASSERT_EQ(report.seconds.size(), 5u); // 1000..1004
EXPECT_EQ(report.seconds[0].time, 1000);
EXPECT_EQ(report.seconds[0].packetsIn, 2u);
EXPECT_EQ(report.seconds[0].bytesIn, 150u);
EXPECT_EQ(report.seconds[0].packetsOut, 4u);
EXPECT_EQ(report.seconds[0].bytesOut, 120u);
EXPECT_TRUE(report.seconds[1].Idle());
EXPECT_EQ(report.seconds[3].packetsIn, 1u);
EXPECT_EQ(report.seconds[4].time, 1004);
const auto next = r.Take(1008);
ASSERT_EQ(next.seconds.size(), 3u); // 1005..1007, no second twice
EXPECT_EQ(next.seconds[0].time, 1005);
EXPECT_EQ(next.seconds[0].packetsIn, 1u);
}
TEST(TrafficStatsTest, RecorderCapsLongSilences) {
Recorder r;
r.Packet(1000, MessageKey{}, 1);
r.Take(1001);
const auto report = r.Take(1001 + 10000);
EXPECT_EQ(report.seconds.size(), static_cast<size_t>(Recorder::MAX_GAP));
EXPECT_EQ(report.seconds.back().time, 1000 + 10000);
}
TEST(TrafficStatsTest, RecorderTopMessagesPerDirection) {
Recorder r;
for (uint32_t id = 0; id < 40; id++) {
for (uint32_t n = 0; n <= id; n++) {
r.Packet(1, MessageKey{ false, 4, id, 0 }, 10);
r.Packet(1, MessageKey{ true, 5, id, 0 }, 10);
}
}
const auto report = r.Take(2);
ASSERT_EQ(report.messages.size(), Recorder::TOP_MESSAGES * 2);
EXPECT_FALSE(report.messages.front().key.outbound);
EXPECT_EQ(report.messages.front().key.packet, 39u);
EXPECT_EQ(report.messages.front().count, 40u);
EXPECT_TRUE(report.messages[Recorder::TOP_MESSAGES].key.outbound);
EXPECT_TRUE(r.Take(3).messages.empty());
}
TEST(TrafficStatsTest, RecorderHttpAndRoutes) {
Recorder r;
r.Http(10, "GET /api/players", 200, 1500, 2000);
r.Http(10, "GET /api/players", 404, 300, 20);
r.Http(11, "POST /api/login", 500, 90000, 50);
for (size_t i = 0; i < Recorder::MAX_ROUTES + 5; i++) r.Http(11, "GET /r" + std::to_string(i), 200, 100, 1);
r.SetGauge("workers_busy", [] { return 3.0; });
const auto report = r.Take(12);
ASSERT_EQ(report.seconds.size(), 2u);
EXPECT_EQ(report.seconds[0].httpRequests, 2u);
EXPECT_EQ(report.seconds[0].httpStatus[1], 1u);
EXPECT_EQ(report.seconds[0].httpStatus[3], 1u);
EXPECT_EQ(report.seconds[0].httpBytesOut, 2020u);
EXPECT_EQ(report.seconds[0].httpLatency.Count(), 2u);
EXPECT_EQ(report.routes.size(), Recorder::MAX_ROUTES + 1); // the rest counted as "other"
const auto other = std::find_if(report.routes.begin(), report.routes.end(), [](const RouteStats& s) { return s.route == "other"; });
ASSERT_NE(other, report.routes.end());
EXPECT_EQ(other->count, 7u);
ASSERT_EQ(report.gauges.size(), 1u);
EXPECT_EQ(report.gauges[0].second, 3.0);
}
TEST(TrafficStatsTest, DueAfterTheInterval) {
Recorder r;
EXPECT_FALSE(r.Due(100, 5)); // starts the clock
EXPECT_FALSE(r.Due(104, 5));
EXPECT_TRUE(r.Due(105, 5));
r.Take(105);
EXPECT_FALSE(r.Due(106, 5));
}
// Counting is on every packet's path: keep it cheap
TEST(TrafficStatsTest, CountingIsCheap) {
Recorder r;
const auto packet = LuPacket(ServiceType::CLIENT, static_cast<uint32_t>(MessageType::Client::GAME_MSG), 100, 30);
constexpr int N = 1000000;
const auto start = std::chrono::steady_clock::now();
for (int i = 0; i < N; i++) r.Packet(Now(), KeyOf(packet.data(), packet.size(), (i & 1) != 0), packet.size());
const auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - start).count() / N;
std::printf("[ ] KeyOf + Recorder::Packet: %lld ns per packet\n", static_cast<long long>(ns));
EXPECT_LT(ns, 2000); // generous for slow CI machines and sanitizers
}