mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
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>
229 lines
8.9 KiB
C++
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
|
|
}
|