Files
move_base2/test/runtime_stats_test.cpp
2026-08-03 22:41:32 +07:00

185 lines
5.7 KiB
C++

/**
* @file runtime_stats_test.cpp
* @brief Kiểm @ref move_base2::RuntimeStats.
*
* Ba tính chất được khoá lại ở đây, đều là thứ mà một lỗi ở chúng sẽ làm bảng thống kê nói dối chứ
* không làm chương trình chết:
*
* 1. **Tắt là tắt hẳn** — `period <= 0` thì không đăng ký được đoạn nào, không đo, không in.
* Telemetry bật ngoài ý muốn trên robot thật nghĩa là log chen vào vòng điều khiển.
* 2. **Cửa sổ được reset sau mỗi lần in** — số liệu là của cửa sổ vừa qua, không phải tích luỹ từ
* lúc khởi động. Cộng dồn mãi thì mọi đỉnh tức thời sẽ bị pha loãng và không bao giờ thấy lại.
* 3. **Thread tự tạo được gán nhãn qua cửa sổ chụp** — đây là cơ chế duy nhất gọi tên được thread
* của costmap mà không phải sửa gói costmap.
*/
#include <move_base2/io/runtime_stats.h>
#include <atomic>
#include <chrono>
#include <thread>
#include <gtest/gtest.h>
namespace
{
using move_base2::RuntimeStats;
using move_base2::ScopedSection;
/// Đốt CPU thật trong khoảng @p ms — sleep không làm tăng bộ đếm CPU của thread.
void burnCpu(int ms)
{
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(ms);
volatile double sink = 0.0;
while (std::chrono::steady_clock::now() < deadline)
{
for (int i = 0; i < 1000; ++i)
{
sink += static_cast<double>(i) * 1.000001;
}
}
(void)sink;
}
TEST(RuntimeStatsTest, DisabledMeansNoWork)
{
RuntimeStats stats(0.0);
EXPECT_FALSE(stats.enabled());
EXPECT_EQ(stats.section("anything"), RuntimeStats::kInvalidSection);
EXPECT_FALSE(stats.tick()) << "period = 0 yet it still printed to the terminal";
// Không được crash dù chỉ số không hợp lệ.
stats.record(RuntimeStats::kInvalidSection, 1000);
stats.registerCurrentThread("must not be recorded");
const std::string table = stats.render();
EXPECT_EQ(table.find("anything"), std::string::npos);
}
TEST(RuntimeStatsTest, NegativePeriodDisables)
{
RuntimeStats stats(-1.0);
EXPECT_FALSE(stats.enabled());
}
TEST(RuntimeStatsTest, SectionAccumulatesAndResetsPerWindow)
{
RuntimeStats stats(3600.0); // chu kỳ dài: chỉ render() thủ công mới đóng cửa sổ
ASSERT_TRUE(stats.enabled());
const RuntimeStats::SectionId id = stats.section("test.section");
ASSERT_NE(id, RuntimeStats::kInvalidSection);
stats.record(id, 1'000'000); // 1 ms
stats.record(id, 3'000'000); // 3 ms
const std::string first = stats.render();
EXPECT_NE(first.find("test.section"), std::string::npos);
EXPECT_NE(first.find("2.00"), std::string::npos) << "average must be 2.00 ms:\n" << first;
EXPECT_NE(first.find("3.00"), std::string::npos) << "peak must be 3.00 ms:\n" << first;
// Cửa sổ mới: đoạn vẫn còn trong bảng nhưng số liệu về 0.
const std::string second = stats.render();
EXPECT_NE(second.find("test.section"), std::string::npos);
EXPECT_NE(second.find("0.00"), std::string::npos) << "a new window must reset:\n" << second;
}
TEST(RuntimeStatsTest, SameNameReturnsSameId)
{
RuntimeStats stats(3600.0);
EXPECT_EQ(stats.section("loop"), stats.section("loop"));
}
TEST(RuntimeStatsTest, TickOnlyPrintsWhenPeriodElapsed)
{
RuntimeStats stats(3600.0);
EXPECT_FALSE(stats.tick()) << "printed before the period elapsed";
RuntimeStats fast(0.001); // [s]
std::this_thread::sleep_for(std::chrono::milliseconds(5));
EXPECT_TRUE(fast.tick());
EXPECT_FALSE(fast.tick()) << "the window must be reopened right after printing";
}
TEST(RuntimeStatsTest, RegisteredThreadAppearsWithCpuTime)
{
RuntimeStats stats(3600.0);
std::atomic<bool> registered{ false };
std::atomic<bool> stop{ false };
std::thread worker([&stats, &registered, &stop]() {
stats.registerCurrentThread("test/worker");
registered.store(true);
while (!stop.load())
{
burnCpu(5);
}
});
while (!registered.load())
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
burnCpu(60);
const std::string table = stats.render();
stop.store(true);
worker.join();
EXPECT_NE(table.find("test/worker"), std::string::npos) << table;
EXPECT_NE(table.find("(unregistered)"), std::string::npos)
<< "CPU outside the registered threads must show up, otherwise the table hides the culprit:\n"
<< table;
}
TEST(RuntimeStatsTest, ThreadCaptureLabelsThreadsCreatedInsideWindow)
{
RuntimeStats stats(3600.0);
std::atomic<bool> stop{ false };
std::thread created;
stats.beginThreadCapture();
created = std::thread([&stop]() {
while (!stop.load())
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
});
// Thread phải thực sự tồn tại trong /proc trước khi đóng cửa sổ chụp.
std::this_thread::sleep_for(std::chrono::milliseconds(20));
stats.endThreadCapture("simulated/costmap");
const std::string table = stats.render();
stop.store(true);
created.join();
#ifdef __linux__
EXPECT_NE(table.find("simulated/costmap"), std::string::npos) << table;
#else
GTEST_SKIP() << "thread capture windows rely on /proc, Linux only";
#endif
}
TEST(RuntimeStatsTest, ScopedSectionToleratesNullStats)
{
// Đây là đường đi bình thường của mọi test dùng cổng giả: runner không được gắn telemetry.
{
ScopedSection timer(nullptr, RuntimeStats::kInvalidSection);
}
RuntimeStats stats(0.0);
{
ScopedSection timer(&stats, stats.section("skipped"));
}
SUCCEED();
}
} // namespace
int main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}