185 lines
5.7 KiB
C++
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, ®istered, &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();
|
|
}
|