mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
312 lines
12 KiB
C++
312 lines
12 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
|
|
}
|
|
|
|
TEST(TrafficStatsTest, RecorderSplitsPacketsByPeer) {
|
|
Recorder r;
|
|
const MessageKey in{ false, 4, 5, 0 };
|
|
const MessageKey out{ true, 5, 12, 0 };
|
|
r.Packet(2000, in, 100); // clients by default
|
|
r.Packet(2000, out, 40, 3, Peer::CLIENTS);
|
|
r.Packet(2000, out, 20, 1, Peer::MASTER);
|
|
r.Packet(2000, in, 60, 1, Peer::MASTER);
|
|
r.Packet(2000, in, 8, 1, Peer::SERVERS);
|
|
const auto report = r.Take(2001);
|
|
EXPECT_TRUE(report.peerSplit);
|
|
ASSERT_EQ(report.seconds.size(), 1u);
|
|
const auto& s = report.seconds[0];
|
|
EXPECT_EQ(s.peers[0], (PeerCounts{ 1, 3, 100, 120 }));
|
|
EXPECT_EQ(s.peers[1], (PeerCounts{ 1, 1, 60, 20 }));
|
|
EXPECT_EQ(s.peers[2], (PeerCounts{ 1, 0, 8, 0 }));
|
|
// The split adds up to the totals
|
|
uint64_t packetsIn = 0, bytesOut = 0;
|
|
for (const auto& p : s.peers) { packetsIn += p.packetsIn; bytesOut += p.bytesOut; }
|
|
EXPECT_EQ(packetsIn, s.packetsIn);
|
|
EXPECT_EQ(bytesOut, s.bytesOut);
|
|
|
|
Second merged = s;
|
|
merged.Merge(s);
|
|
EXPECT_EQ(merged.peers[1], (PeerCounts{ 2, 2, 120, 40 }));
|
|
}
|
|
|
|
TEST(TrafficStatsTest, RecorderSplitsHttpByWhoAsked) {
|
|
Recorder r;
|
|
r.Http(3000, "GET /api/a", 200, 100, 1000);
|
|
r.Http(3000, "GET /api/a", 200, 100, 500, true);
|
|
r.HttpOut(3000, 700);
|
|
const auto report = r.Take(3001);
|
|
ASSERT_EQ(report.seconds.size(), 1u);
|
|
EXPECT_EQ(report.seconds[0].httpRequests, 2u);
|
|
EXPECT_EQ(report.seconds[0].httpFromServers, 1u);
|
|
EXPECT_EQ(report.seconds[0].httpFromServersBytesOut, 500u);
|
|
EXPECT_EQ(report.seconds[0].httpOutRequests, 1u);
|
|
EXPECT_EQ(report.seconds[0].httpOutBytesIn, 700u);
|
|
}
|
|
|
|
TEST(TrafficStatsTest, HttpClientsApartBySignedInAccount) {
|
|
Recorder r;
|
|
r.HttpClient("203.0.113.5", false, 100, 1000, 7, "alice");
|
|
r.HttpClient("203.0.113.5", false, 100, 1000, 7, "alice");
|
|
r.HttpClient("203.0.113.5", false, 50, 500, 9, "bob");
|
|
r.HttpClient("203.0.113.5", false, 10, 20); // not signed in (the sign-in page)
|
|
const auto report = r.Take(1);
|
|
ASSERT_EQ(report.connections.size(), 3u);
|
|
for (const auto& c : report.connections) {
|
|
EXPECT_EQ(c.address, "203.0.113.5");
|
|
EXPECT_TRUE(c.http);
|
|
if (c.accountId == 7) {
|
|
EXPECT_EQ(c.account, "alice");
|
|
EXPECT_EQ(c.packetsIn, 2u);
|
|
EXPECT_EQ(c.bytesOut, 2000u);
|
|
} else if (c.accountId == 9) {
|
|
EXPECT_EQ(c.account, "bob");
|
|
} else {
|
|
EXPECT_EQ(c.accountId, 0u);
|
|
EXPECT_TRUE(c.account.empty());
|
|
EXPECT_EQ(c.bytesIn, 10u);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(TrafficStatsTest, ApiKeyTrafficIsNamedAfterTheKey) {
|
|
Recorder r;
|
|
r.HttpClient("203.0.113.5", false, 100, 1000, 7, "alice"); // alice's browser
|
|
r.HttpClient("203.0.113.5", false, 50, 500, 7, "alice", "status bot"); // alice's API key
|
|
r.HttpClient("203.0.113.5", false, 50, 500, 7, "alice", "status bot");
|
|
const auto report = r.Take(1);
|
|
ASSERT_EQ(report.connections.size(), 2u);
|
|
bool sawKey = false, sawBrowser = false;
|
|
for (const auto& c : report.connections) {
|
|
EXPECT_EQ(c.accountId, 7u);
|
|
if (c.account == "alice (API key \"status bot\")") { sawKey = true; EXPECT_EQ(c.packetsIn, 2u); }
|
|
else if (c.account == "alice") sawBrowser = true;
|
|
}
|
|
EXPECT_TRUE(sawKey);
|
|
EXPECT_TRUE(sawBrowser);
|
|
}
|