| #include "NVMeCache.hpp" |
| |
| #include <valgrind/valgrind.h> |
| |
| #include <boost/asio.hpp> |
| #include <boost/asio/spawn.hpp> |
| |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| const int valgrindTimeScaler = []() { |
| return (RUNNING_ON_VALGRIND != 0U) ? 10 : 1; |
| }(); |
| |
| class TestNVMeCache : public testing::Test |
| { |
| public: |
| boost::asio::io_context io; |
| std::shared_ptr<Scheduler<std::chrono::steady_clock>> scheduler; |
| |
| TestNVMeCache() |
| { |
| scheduler = std::make_shared<Scheduler<std::chrono::steady_clock>>(io); |
| } |
| }; |
| |
| class MetricMock : public MetricBase<std::chrono::steady_clock> |
| { |
| public: |
| template <class Duration> |
| MetricMock(std::shared_ptr<Scheduler<std::chrono::steady_clock>> scheduler, |
| Duration interval) : |
| MetricBase<std::chrono::steady_clock>( |
| std::move(scheduler), |
| std::chrono::duration_cast<std::chrono::steady_clock::duration>( |
| interval)) |
| {} |
| ~MetricMock() override |
| { |
| std::cerr << "MetricMock destroy\n"; |
| } |
| |
| MOCK_METHOD( |
| void, readDevice, |
| (std::function<void(std::error_code ec, size_t size, bool complete)> && |
| cb), |
| (noexcept, override)); |
| |
| MOCK_METHOD((std::string_view), getIdentifier, (), |
| (const, noexcept, override)); |
| |
| MOCK_METHOD(bool, isCacheValid, (), (const, noexcept, override)); |
| |
| MOCK_METHOD((std::tuple<std::chrono::time_point<ClockType>, |
| std::chrono::time_point<ClockType>, |
| std::span<const uint8_t>>), |
| getCache, (), (const, noexcept, override)); |
| }; |
| |
| /** |
| * Simulate the scheduling sequence for |
| * * NVMe subsystem intiation |
| * * Metric Instance creation (after subsystem init) |
| * * Refresh/PartialRefresh |
| * The scheduling process should comply to the flowchart: |
| * go/nvme-mi-cache-time-scheduling#heading=h.iolas4v1li7i |
| */ |
| TEST_F(TestNVMeCache, SechuduleSequence) |
| { |
| // blocks for partial refresh |
| static constexpr unsigned refreshStep = 4; |
| |
| boost::asio::spawn(io, [this](boost::asio::yield_context yield) { |
| /* Test: metric creation within system init procedure */ |
| // metric 1 with 200 ms refresh rate |
| auto mtx = std::make_shared<MetricMock>(this->scheduler, |
| std::chrono::milliseconds(200) * |
| valgrindTimeScaler); |
| ON_CALL(*mtx, readDevice) |
| .WillByDefault( |
| [this](std::function<void(std::error_code ec, size_t size, |
| bool complete)>&& cb) { |
| boost::asio::spawn(io, |
| [this, cb](boost::asio::yield_context yield) { |
| static unsigned i = 0; |
| static std::chrono::steady_clock::time_point prev{ |
| std::chrono::steady_clock::time_point::min()}; |
| std::chrono::steady_clock::time_point current = |
| std::chrono::steady_clock::now(); |
| if (prev != std::chrono::steady_clock::time_point::min()) |
| { |
| // the refresh rate should be faster than the deginated |
| // internal + device reading delay + some buffer. |
| EXPECT_LE(current - prev, |
| std::chrono::duration_cast< |
| std::chrono::steady_clock::duration>( |
| std::chrono::milliseconds( |
| static_cast<unsigned>((200 + 10) * 1.1)) * |
| valgrindTimeScaler)); |
| } |
| prev = current; |
| |
| // similate a device delay |
| boost::asio::steady_timer timer(this->io); |
| timer.expires_after(std::chrono::milliseconds(10) * |
| valgrindTimeScaler); |
| timer.async_wait(yield); |
| |
| bool complete = (++i % refreshStep) == 0U; |
| cb({}, 409, complete); |
| }); |
| }); |
| |
| // the cache data fetch should not be happening until subsystem |
| // initiation complete |
| EXPECT_CALL(*mtx, readDevice).Times(0); |
| |
| // force refresh for metric initialization |
| mtx->refresh([mtx](std::error_code ec) { |
| if (ec) |
| { |
| std::cerr << "refresh cb1: error returned\n"; |
| return; |
| } |
| // the backgound refresh should continue |
| EXPECT_CALL(*mtx, readDevice).Times(::testing::AtLeast(1)); |
| }); |
| |
| // simulate a subsystem initiate delay |
| boost::asio::spawn(io, [this](boost::asio::yield_context yield) { |
| boost::asio::steady_timer timer(this->io); |
| timer.expires_after(std::chrono::milliseconds(200) * |
| valgrindTimeScaler); |
| timer.async_wait(yield); |
| }); |
| |
| /* subsystem initiation done */ |
| |
| // should fetch data for 4 times for metric 1 |
| EXPECT_CALL(*mtx, readDevice).Times(refreshStep); |
| this->scheduler->start(); |
| this->scheduler->dequeue(); |
| |
| /* Test: metric created after system initiation */ |
| boost::asio::steady_timer timer(this->io); |
| timer.expires_after(std::chrono::milliseconds(100) * |
| valgrindTimeScaler); |
| timer.async_wait(yield); |
| |
| auto mtx2 = std::make_shared<MetricMock>( |
| this->scheduler, |
| std::chrono::milliseconds(300) * valgrindTimeScaler); |
| ON_CALL(*mtx2, readDevice) |
| .WillByDefault( |
| [this](std::function<void(std::error_code ec, size_t size, |
| bool complete)>&& cb) { |
| boost::asio::spawn(io, |
| [this, cb](boost::asio::yield_context yield) { |
| static unsigned i = 0; |
| // similate a device delay |
| boost::asio::steady_timer timer(this->io); |
| timer.expires_after(std::chrono::milliseconds(10) * |
| valgrindTimeScaler); |
| timer.async_wait(yield); |
| |
| bool complete = (++i % refreshStep) == 0U; |
| cb({}, 409, complete); |
| }); |
| }); |
| |
| // the force refresh should start immediately |
| EXPECT_CALL(*mtx2, readDevice).Times(refreshStep); |
| // force refresh for metric initialization |
| mtx2->refresh([mtx2](std::error_code ec) { |
| if (ec) |
| { |
| std::cerr << "refresh cb2: error returned\n"; |
| return; |
| } |
| // the backgound refresh should continue |
| EXPECT_CALL(*mtx2, readDevice).Times(::testing::AtLeast(1)); |
| }); |
| |
| /* Test: refresh and partial refresh */ |
| timer.expires_after(std::chrono::seconds(1) * valgrindTimeScaler); |
| timer.async_wait(yield); |
| |
| mtx2->refresh([mtx2](std::error_code ec) { |
| if (ec) |
| { |
| std::cerr << "refresh cb3: error returned\n"; |
| return; |
| } |
| // the backgound refresh should continue |
| EXPECT_CALL(*mtx2, readDevice).Times(::testing::AtLeast(1)); |
| }); |
| mtx2->partialRefresh([mtx2](std::error_code ec) { |
| if (ec) |
| { |
| std::cerr << "partial refresh cb1: error returned\n"; |
| return; |
| } |
| // refresh should continue until completion |
| EXPECT_CALL(*mtx2, readDevice) |
| .Times(::testing::AtMost(refreshStep - 1)); |
| }); |
| // read device only once for partial refresh |
| EXPECT_CALL(*mtx2, readDevice).Times(1); |
| |
| /* Test: Metric deletion */ |
| timer.expires_after(std::chrono::seconds(1) * valgrindTimeScaler); |
| timer.async_wait(yield); |
| bool exitWithCancel = false; |
| mtx2->refresh([&exitWithCancel](std::error_code ec) { |
| if (ec) |
| { |
| exitWithCancel = true; |
| return; |
| } |
| }); |
| // slightly wait to push the task into queue |
| timer.expires_after(std::chrono::milliseconds(1) * valgrindTimeScaler); |
| timer.async_wait(yield); |
| // destroy the metric |
| mtx2.reset(); |
| // slightly wait until the async destroy complete |
| timer.expires_after(std::chrono::milliseconds(1) * valgrindTimeScaler); |
| timer.async_wait(yield); |
| // |
| EXPECT_EQ(exitWithCancel, true); |
| |
| // keep the subsystem running |
| timer.expires_after(std::chrono::seconds(1) * valgrindTimeScaler); |
| timer.async_wait(yield); |
| }); |
| |
| io.run(); |
| } |
| |
| /** |
| * Simulate a cucurrency of caller for the cache refresh task. |
| * Make sure the callback are issued in the expected behavior as the example: |
| * |
| * go/nvme-mi-cache-time-scheduling#heading=h.xnlsabh6sfwj |
| */ |
| TEST_F(TestNVMeCache, ConcurrentInvoker) |
| { |
| boost::asio::spawn(io, [this](boost::asio::yield_context yield) { |
| /* create the schedule and metric*/ |
| // metric with infinity refresh rate to make sure no background refresh |
| auto mtx = std::make_shared<MetricMock>( |
| this->scheduler, std::chrono::steady_clock::duration::max()); |
| |
| // metric parameters |
| static constexpr unsigned refreshStep = 6; |
| static const auto deviceDelay = std::chrono::milliseconds(10) * |
| valgrindTimeScaler; |
| static unsigned currentStep = 0; |
| static std::chrono::steady_clock::time_point prevTime{ |
| std::chrono::steady_clock::time_point::min()}; |
| |
| // default readDevice should return a very large data size to block the |
| // background schduling for next partialRefresh |
| ON_CALL(*mtx, readDevice) |
| .WillByDefault( |
| [this](std::function<void(std::error_code ec, size_t size, |
| bool complete)>&& cb) { |
| boost::asio::spawn(io, |
| [this, cb](boost::asio::yield_context yield) { |
| std::chrono::steady_clock::time_point currentTime = |
| std::chrono::steady_clock::now(); |
| |
| prevTime = currentTime; |
| |
| std::cerr << "step: " << currentStep << ", current time: " |
| << currentTime.time_since_epoch().count() << '\n'; |
| |
| // similate a device delay |
| boost::asio::steady_timer timer(this->io); |
| timer.expires_after(deviceDelay); |
| timer.async_wait(yield); |
| |
| std::cerr << "step: " << currentStep << '\n'; |
| std::cerr << "previous time: " |
| << currentTime.time_since_epoch().count() << '\n'; |
| bool complete = (++currentStep % refreshStep) == 0U; |
| if (currentStep == refreshStep) |
| { |
| currentStep = 0; |
| } |
| |
| std::cerr << "complete: " << complete << '\n'; |
| cb({}, 4096000000, complete); |
| }); |
| }); |
| |
| // the cache data fetch should not be happening until subsystem |
| // initiation complete |
| EXPECT_CALL(*mtx, readDevice).Times(0); |
| |
| this->scheduler->start(); |
| this->scheduler->dequeue(); |
| |
| // caller A |
| std::cerr << "caller A\n"; |
| EXPECT_CALL(*mtx, readDevice).Times(1); |
| mtx->partialRefresh([mtx](std::error_code ec) { |
| EXPECT_FALSE(ec); |
| std::cerr << "notify A\n"; |
| // the backgound refresh should continue |
| EXPECT_CALL(*mtx, readDevice).Times(0); |
| }); |
| |
| boost::asio::steady_timer timer(this->io); |
| timer.expires_after(deviceDelay * 2); |
| timer.async_wait(yield); |
| |
| // move forward to step 1 |
| EXPECT_EQ(currentStep, 1); |
| |
| /* caller B to caller G */ |
| // check by currentStep in the future. |
| EXPECT_CALL(*mtx, readDevice).Times(::testing::AtLeast(1)); |
| // caller B |
| std::cerr << "caller B\n"; |
| mtx->refresh([mtx](std::error_code ec) { |
| EXPECT_FALSE(ec); |
| std::cerr << "notify B\n"; |
| EXPECT_EQ(currentStep, 0); |
| // The schuduler should stop until next caller (G) |
| EXPECT_CALL(*mtx, readDevice).Times(0); |
| }); |
| |
| // move forward to step 2 |
| timer.expires_after(deviceDelay + deviceDelay / 10); |
| timer.async_wait(yield); |
| EXPECT_EQ(currentStep, 2); |
| |
| // caller C |
| std::cerr << "caller C\n"; |
| mtx->refresh([mtx](std::error_code ec) { |
| EXPECT_FALSE(ec); |
| std::cerr << "notify C\n"; |
| EXPECT_EQ(currentStep, 0); |
| // The schuduler should stop until next caller (G) |
| EXPECT_CALL(*mtx, readDevice).Times(0); |
| }); |
| |
| // metric should continue to step 3 |
| timer.expires_after(deviceDelay); |
| timer.async_wait(yield); |
| EXPECT_EQ(currentStep, 3); |
| |
| // caller D |
| std::cerr << "caller D\n"; |
| mtx->partialRefresh([mtx](std::error_code ec) { |
| EXPECT_FALSE(ec); |
| std::cerr << "notify D\n"; |
| EXPECT_EQ(currentStep, 4); |
| }); |
| |
| // caller E |
| std::cerr << "caller E\n"; |
| mtx->partialRefresh([mtx](std::error_code ec) { |
| EXPECT_FALSE(ec); |
| std::cerr << "notify E\n"; |
| EXPECT_EQ(currentStep, 4); |
| }); |
| |
| // move forward to next cycle (step 0) |
| timer.expires_after(deviceDelay * 10); |
| timer.async_wait(yield); |
| EXPECT_EQ(currentStep, 0); |
| |
| // caller G |
| std::cerr << "caller G\n"; |
| EXPECT_CALL(*mtx, readDevice).Times(1); |
| mtx->partialRefresh([mtx](std::error_code ec) { |
| EXPECT_FALSE(ec); |
| std::cerr << "notify G\n"; |
| // no more data fetch until next caller |
| EXPECT_CALL(*mtx, readDevice).Times(0); |
| }); |
| |
| // move forward to setp 1 |
| timer.expires_after(deviceDelay * 2); |
| timer.async_wait(yield); |
| |
| EXPECT_EQ(currentStep, 1); |
| |
| // caller H |
| std::cerr << "caller H\n"; |
| EXPECT_CALL(*mtx, readDevice).Times(::testing::AnyNumber()); |
| mtx->partialRefresh([mtx](std::error_code ec) { |
| EXPECT_FALSE(ec); |
| std::cerr << "notify H\n"; |
| EXPECT_EQ(currentStep, 2); |
| }); |
| |
| // move forward slightly |
| timer.expires_after(deviceDelay / 4); |
| timer.async_wait(yield); |
| |
| // caller I |
| std::cerr << "caller I\n"; |
| bool completeI = false; |
| mtx->refresh([&completeI](std::error_code ec) { |
| // should be cancelled |
| EXPECT_TRUE(ec); |
| std::cerr << "notify I\n"; |
| // either step 3 or 4 depends on if the async readDevice is |
| // compeleted or not |
| EXPECT_GE(currentStep, 3); |
| EXPECT_LE(currentStep, 4); |
| |
| completeI = true; |
| }); |
| |
| // move forward to step 3 |
| timer.expires_after(deviceDelay * 2); |
| timer.async_wait(yield); |
| |
| // caller J |
| std::cerr << "caller J\n"; |
| bool completeJ = false; |
| mtx->partialRefresh([&completeJ](std::error_code ec) { |
| // should be cancelled |
| EXPECT_TRUE(ec); |
| std::cerr << "notify J\n"; |
| // either step 3 or 4 depends on if the async readDevice is |
| // compeleted or not |
| EXPECT_GE(currentStep, 3); |
| EXPECT_LE(currentStep, 4); |
| |
| completeJ = true; |
| }); |
| |
| // matrix Closure |
| mtx.reset(); |
| |
| // the notify I/J should be executed immediately after the Closure |
| timer.expires_after(std::chrono::microseconds(1)); |
| timer.async_wait(yield); |
| |
| EXPECT_TRUE(completeI); |
| EXPECT_TRUE(completeJ); |
| |
| timer.expires_after(deviceDelay * 10); |
| timer.async_wait(yield); |
| |
| this->scheduler.reset(); |
| }); |
| |
| io.run(); |
| } |
| |
| int main(int argc, char** argv) |
| { |
| ::testing::InitGoogleTest(&argc, argv); |
| return RUN_ALL_TESTS(); |
| } |