diff options
| author | Yuqian Yang <crupest@crupest.life> | 2025-11-03 14:54:51 +0800 |
|---|---|---|
| committer | Yuqian Yang <crupest@crupest.life> | 2025-11-03 15:29:07 +0800 |
| commit | 9c897e8727d90345c2db7f36f52ab678778db936 (patch) | |
| tree | e4eeb7454677a40603785cab77061aaaf6f7c66e | |
| parent | 95d061e4fca2f7903ac903a2426cb5ad30c737f7 (diff) | |
| download | cru-9c897e8727d90345c2db7f36f52ab678778db936.tar.gz cru-9c897e8727d90345c2db7f36f52ab678778db936.tar.bz2 cru-9c897e8727d90345c2db7f36f52ab678778db936.zip | |
Add TimerRegistry.
| -rw-r--r-- | CMakeLists.txt | 2 | ||||
| -rw-r--r-- | include/cru/base/Timer.h | 123 | ||||
| -rw-r--r-- | test/base/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | test/base/TimerTest.cpp | 43 |
4 files changed, 168 insertions, 1 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index a5315b73..1a8da2e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.21) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/output) diff --git a/include/cru/base/Timer.h b/include/cru/base/Timer.h new file mode 100644 index 00000000..80929f17 --- /dev/null +++ b/include/cru/base/Timer.h @@ -0,0 +1,123 @@ +#pragma once + +#include "Base.h" +#include "Exception.h" + +#include <algorithm> +#include <chrono> +#include <list> +#include <mutex> +#include <optional> +#include <ranges> + +namespace cru { +template <typename D> +class TimerRegistry : public Object2 { + private: + struct TimerData { + int id; + D data; + std::chrono::steady_clock::time_point created; + std::chrono::steady_clock::time_point last_check; + std::chrono::milliseconds interval; + bool repeat; + + TimerData(int id, std::chrono::milliseconds interval, bool repeat, + std::chrono::steady_clock::time_point created, D data) + : id(id), + created(created), + last_check(created), + interval(interval), + repeat(repeat), + data(std::move(data)) {} + + std::chrono::milliseconds NextTimeout( + std::chrono::steady_clock::time_point now) const { + return std::chrono::duration_cast<std::chrono::milliseconds>( + interval - (now - created) % interval); + } + + bool Update(std::chrono::steady_clock::time_point now) { + auto next_trigger = + last_check - (last_check - created) % interval + interval; + if (now >= next_trigger) { + last_check = next_trigger; + return true; + } else { + last_check = now; + return false; + } + } + }; + + public: + struct UpdateResult { + int id; + D data; + + bool operator==(const UpdateResult&) const = default; + }; + + public: + TimerRegistry() : next_id_(1) {} + + int Add(D data, std::chrono::milliseconds interval, bool repeat, + std::chrono::steady_clock::time_point created = + std::chrono::steady_clock::now()) { + if (interval < std::chrono::milliseconds::zero()) { + throw Exception("Timer interval can't be negative."); + } + if (repeat && interval == std::chrono::milliseconds::zero()) { + throw Exception("Repeat timer interval can't be 0."); + } + + std::unique_lock lock(mutex_); + auto id = next_id_++; + timers_.emplace_back(id, interval, repeat, created, std::move(data)); + return id; + } + + void Remove(int id) { + std::unique_lock lock(mutex_); + timers_.remove_if([id](const TimerData& timer) { return timer.id == id; }); + } + + /** + * Returns 0 if there is no timer. + */ + std::chrono::milliseconds NextTimeout( + std::chrono::steady_clock::time_point now) { + std::unique_lock lock(mutex_); + + if (timers_.empty()) return std::chrono::milliseconds::zero(); + + return std::ranges::min( + timers_ | std::views::transform([now](const TimerData& timer) { + return timer.NextTimeout(now); + })); + } + + std::optional<UpdateResult> Update( + std::chrono::steady_clock::time_point new_time) { + std::unique_lock lock(mutex_); + for (auto iter = timers_.begin(); iter != timers_.end(); ++iter) { + auto& timer = *iter; + if (timer.Update(new_time)) { + if (timer.repeat) { + return UpdateResult{timer.id, timer.data}; + } else { + D data(std::move(timer.data)); + timers_.erase(iter); // We will return, so it's safe to erase here. + return UpdateResult{timer.id, std::move(data)}; + } + } + } + return std::nullopt; + } + + private: + std::mutex mutex_; + int next_id_; + std::list<TimerData> timers_; +}; +} // namespace cru diff --git a/test/base/CMakeLists.txt b/test/base/CMakeLists.txt index b008e1b3..39895c3e 100644 --- a/test/base/CMakeLists.txt +++ b/test/base/CMakeLists.txt @@ -5,6 +5,7 @@ add_executable(CruBaseTest SelfResolvableTest.cpp StringUtilTest.cpp SubProcessTest.cpp + TimerTest.cpp ) target_link_libraries(CruBaseTest PRIVATE CruBase CruTestBase) diff --git a/test/base/TimerTest.cpp b/test/base/TimerTest.cpp new file mode 100644 index 00000000..ca42f01f --- /dev/null +++ b/test/base/TimerTest.cpp @@ -0,0 +1,43 @@ +#include "cru/base/Timer.h" + +#include <catch2/catch_test_macros.hpp> +#include <chrono> + +using cru::TimerRegistry; +using std::chrono::milliseconds; +using namespace std::chrono_literals; + +TEST_CASE("TimerRegistry Works", "[timer]") { + TimerRegistry<int> registry; + + REQUIRE_THROWS(registry.Add(1, -1ms, false)); + REQUIRE_THROWS(registry.Add(1, 0ms, true)); + + auto mock_now = std::chrono::steady_clock::now(); + + auto timer1 = registry.Add(1, 50ms, false, mock_now); + auto timer2 = registry.Add(2, 100ms, false, mock_now); + auto timer3 = registry.Add(3, 60ms, true, mock_now); + auto timer4 = registry.Add(4, 100ms, true, mock_now); + + mock_now += 20ms; + REQUIRE(registry.Update(mock_now) == std::nullopt); + REQUIRE(registry.NextTimeout(mock_now) == 30ms); + + mock_now += 30ms; + REQUIRE(registry.Update(mock_now) == + TimerRegistry<int>::UpdateResult{timer1, 1}); + REQUIRE(registry.Update(mock_now) == std::nullopt); + REQUIRE(registry.NextTimeout(mock_now) == 10ms); + + mock_now += 20ms; + REQUIRE(registry.Update(mock_now) == + TimerRegistry<int>::UpdateResult{timer3, 3}); + REQUIRE(registry.Update(mock_now) == std::nullopt); + REQUIRE(registry.NextTimeout(mock_now) == 30ms); + + mock_now += 200ms; + int count = 0; + while (registry.Update(mock_now)) { count++;} + REQUIRE(count == 6); +} |
