aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuqian Yang <crupest@crupest.life>2025-11-03 14:54:51 +0800
committerYuqian Yang <crupest@crupest.life>2025-11-03 15:29:07 +0800
commit9c897e8727d90345c2db7f36f52ab678778db936 (patch)
treee4eeb7454677a40603785cab77061aaaf6f7c66e
parent95d061e4fca2f7903ac903a2426cb5ad30c737f7 (diff)
downloadcru-9c897e8727d90345c2db7f36f52ab678778db936.tar.gz
cru-9c897e8727d90345c2db7f36f52ab678778db936.tar.bz2
cru-9c897e8727d90345c2db7f36f52ab678778db936.zip
Add TimerRegistry.
-rw-r--r--CMakeLists.txt2
-rw-r--r--include/cru/base/Timer.h123
-rw-r--r--test/base/CMakeLists.txt1
-rw-r--r--test/base/TimerTest.cpp43
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);
+}