diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3f4272e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[**.{cpp, c, hpp, h}] +indent_style = tab +trim_trailing_whitespace = true +end_of_line = lf +insert_final_newline = true diff --git a/.gitignore b/.gitignore index 259148f..0a1cddf 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,13 @@ *.exe *.out *.app + +# Build directories +.idea +.vs/ +build/ + +cmake-build-* +[Dd]ebug/ +[Rr]elease/ +[Bb]in/ diff --git a/.gitmodules b/.gitmodules index 8cf8b5e..49ff7da 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "googletest"] - path = googletest + path = src/test/googletest url = https://github.com/google/googletest.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..40aa5c5 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.8) +project(jessilib) + +#set(CMAKE_CXX_STANDARD 14) + +# Setup source files +add_subdirectory(src) diff --git a/googletest b/googletest deleted file mode 160000 index e5b88b2..0000000 --- a/googletest +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e5b88b227e6adfa7196575b1264384e718d16cab diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..c1672ee --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.8) + +# Setup source files +include_directories(include) +add_subdirectory(common) +add_subdirectory(test) diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt new file mode 100644 index 0000000..8628f4b --- /dev/null +++ b/src/common/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.8) + +# Setup source files +include_directories(../include) +set(SOURCE_FILES + timer.cpp) + +add_library(jessilib ${SOURCE_FILES}) diff --git a/src/common/timer.cpp b/src/common/timer.cpp new file mode 100644 index 0000000..e4b1685 --- /dev/null +++ b/src/common/timer.cpp @@ -0,0 +1,205 @@ +/** + * Copyright (C) 2017 Jessica James. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Written by Jessica James + */ + +#include "timer.hpp" +#include + +namespace jessilib { + +/** timer::manager */ + +timer::manager timer::s_manager; + +timer::manager::~manager() { + m_thread_active = false; + m_cvar.notify_one(); + m_thread.join(); +} + +void timer::manager::loop() { + // loop + while (m_thread_active) { + std::unique_lock lock(m_mutex); + auto itr{ m_timers.begin() }; + if (itr != m_timers.end()) { + // Wait until the next timer is ready to fire + if (m_cvar.wait_until(lock, (*itr)->next()) == std::cv_status::timeout && itr != m_timers.end()) { + // Set active + std::shared_ptr target = *itr; + m_active = target.get(); + + // Execute timer + target->m_callback(*target); + itr = m_timers.begin(); + if (itr != m_timers.end() && *itr == target) { + // Timer still exists; pop + m_timers.erase(itr); + + // reset timings + target->m_next = target->calc_next(); + + // push + m_timers.insert(std::move(target)); + } + + // Reset active + m_active = nullptr; + } + } + else { + // Wait until a timer is added + m_cvar.wait(lock); + } + } +} + +/** timer::node */ +timer::node::node(duration_t period, callback_t callback) + : m_period{ period }, + m_next{ calc_next() }, + m_callback{ callback } { + // Empty ctor body +} + +timer::time_point_t timer::node::next() const { + return m_next; +} + +timer::duration_t timer::node::period() const { + return m_period; +} + +timer::callback_t timer::node::function() const { + return m_callback; +} + +void timer::node::cancel() { + // Race condition: cancel() being called externally while active + if (s_manager.m_active == this && s_manager.m_thread.get_id() == std::this_thread::get_id()) { + s_manager.m_timers.erase(shared_from_this()); + return; + } + + { + std::lock_guard lock(s_manager.m_mutex); + s_manager.m_timers.erase(shared_from_this()); + } + s_manager.m_cvar.notify_one(); +} + +// Internals + +timer::time_point_t timer::node::calc_next() { + return std::chrono::steady_clock::now() + m_period; +} + +/** timer */ + +// Static functions +void timer::set(duration_t period, callback_t callback) { + // Emplace timer + { + std::lock_guard lock(s_manager.m_mutex); + s_manager.m_timers.emplace(new node(period, callback)); + } + + // Notify + s_manager.m_cvar.notify_one(); +} + +void timer::set(duration_t period, size_t iterations, callback_t callback) { + // Expand callback and forward to set + set(period, callback_with_iterations(iterations, callback)); +} + +// Constructors +timer::timer(duration_t period, callback_t callback) { + // Sanity check + assert(callback != nullptr); + + // Initialize timer + { + std::lock_guard lock(s_manager.m_mutex); + m_timer = *s_manager.m_timers.emplace(new node(period, callback)); + } + + // Notify + s_manager.m_cvar.notify_one(); +} + +timer::timer(duration_t period, size_t iterations, callback_t callback) + : timer{ period, callback_with_iterations(iterations, callback) } { + // Empty ctor body +} + +// Destructor +timer::~timer() { + if (auto my_timer = m_timer.lock()) { + my_timer->cancel(); + } +} + +// Functions +timer::time_point_t timer::next() const { + if (auto my_timer = m_timer.lock()) { + my_timer->next(); + } + + return {}; +} + + +timer::duration_t timer::period() const { + if (auto my_timer = m_timer.lock()) { + return my_timer->period(); + } + + return duration_t{ 0 }; +} + +timer::callback_t timer::function() const { + if (auto my_timer = m_timer.lock()) { + return my_timer->function(); + } + + return nullptr; +} + +void timer::cancel() { + if (auto my_timer = m_timer.lock()) { + my_timer->cancel(); + } +} + +bool timer::active() const { + return m_timer.lock() != nullptr; +} + +timer::callback_t timer::callback_with_iterations(size_t iterations, callback_t callback) { + assert(iterations > 0); + + return [iterations, callback](node& timer) mutable { + callback(timer); + + if (--iterations == 0) { + timer.cancel(); + } + }; +} + +} // namespace jessilib \ No newline at end of file diff --git a/src/include/timer.hpp b/src/include/timer.hpp new file mode 100644 index 0000000..834aa30 --- /dev/null +++ b/src/include/timer.hpp @@ -0,0 +1,139 @@ +/** + * Copyright (C) 2017 Jessica James. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Written by Jessica James + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +// TODO: add 'timer* m_parent' to timer::node; rename timer::node to timer::internals? + +namespace jessilib { + +/** timer */ + +class timer { +public: + class node; + + // Types + using callback_t = std::function; /** Function type called by the timer */ + using time_point_t = std::chrono::steady_clock::time_point; /** Type representing the point in time at which a timer will be called */ + using duration_t = std::chrono::steady_clock::duration; + + // Static functions + static void set(duration_t period, callback_t callback); + static void set(duration_t period, size_t iterations, callback_t callback); + + // Constructors + timer(duration_t period, callback_t callback); + timer(duration_t period, size_t iterations, callback_t callback); + + // Explicitly deleted constructors + timer() = delete; + timer(const timer&) = delete; + timer(timer&& in_timer) = delete; + + // Destructor + ~timer(); + + // Functions + time_point_t next() const; + duration_t period() const; + callback_t function() const; + void cancel(); + bool active() const; + +private: + // Weak pointer to referenced timer + std::weak_ptr m_timer; + + // Timer manager + class manager; + static manager s_manager; + + // Helper + static callback_t callback_with_iterations(size_t iterations, callback_t callback); +}; // timer + +/** timer::node */ + +class timer::node : public std::enable_shared_from_this { +public: + time_point_t next() const; + duration_t period() const; + callback_t function() const; + void cancel(); + +private: + // Internal constructor + node(duration_t period, callback_t callback); + + // Explicitly deleted constructors + node() = delete; + node(const node&) = delete; + node(node&& in_timer) = delete; + + // Internal helpers + time_point_t calc_next(); + + // Members + duration_t m_period; + callback_t m_callback; + time_point_t m_next; + + // Friends + friend class timer; +}; // class timer::node + + +/** timer::manager */ + +class timer::manager { + // Destructor + ~manager(); + + struct timer_sort { + bool operator()(const std::shared_ptr& lhs, const std::shared_ptr& rhs) const { + return lhs->next() < rhs->next(); + } + }; + + // Loop + void loop(); + + // Members + std::multiset, timer_sort> m_timers; + std::mutex m_mutex; + std::condition_variable m_cvar; + std::atomic m_active{ nullptr }; + std::atomic m_thread_active{ true }; + std::thread m_thread{ [this]() { + loop(); + }}; + + // Friends + friend class timer; +}; // class timer::manager + +}; // namespace jessilib \ No newline at end of file diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt new file mode 100644 index 0000000..d836d32 --- /dev/null +++ b/src/test/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.8) + +# Setup source files +include_directories(../include) +set(SOURCE_FILES + timer.cpp) + +# Setup gtest +add_subdirectory(googletest/googletest) +#include_directories(googletest/googletest) +#include_directories(googletest/googlemock) + +add_executable(jessilib_tests ${SOURCE_FILES}) + +# Link with gtest +target_link_libraries(jessilib_tests gtest gtest_main jessilib) diff --git a/src/test/googletest b/src/test/googletest new file mode 160000 index 0000000..d175c8b --- /dev/null +++ b/src/test/googletest @@ -0,0 +1 @@ +Subproject commit d175c8bf823e709d570772b038757fadf63bc632 diff --git a/src/test/timer.cpp b/src/test/timer.cpp new file mode 100644 index 0000000..fb29c33 --- /dev/null +++ b/src/test/timer.cpp @@ -0,0 +1,110 @@ +/** + * Copyright (C) 2017 Jessica James. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Written by Jessica James + */ + +#include +#include "gtest/gtest.h" +#include "timer.hpp" + +using namespace jessilib; +using namespace std::literals; + +constexpr size_t total_iterations{ 4 }; +constexpr std::chrono::steady_clock::duration period = 1ms; +constexpr std::chrono::steady_clock::duration timeout = 1s; + +TEST(TimerTest, set) { + size_t iterations{ 0 }; + std::promise promise; + + timer::set(period, [&iterations, &promise](timer::node& in_timer) { + if (++iterations == total_iterations) { + in_timer.cancel(); + promise.set_value(); + } + }); + + EXPECT_EQ(promise.get_future().wait_for(timeout), std::future_status::ready); +} + +TEST(TimerTest, scoped) { + size_t iterations{ 0 }; + std::promise promise; + + timer timer{ period, [&iterations, &promise](timer::node& in_timer) { + if (++iterations == total_iterations) { + in_timer.cancel(); + promise.set_value(); + } + } }; + + EXPECT_EQ(promise.get_future().wait_for(timeout), std::future_status::ready); +} + +TEST(TimerTest, setWithIterations) { + size_t iterations{ 0 }; + std::promise promise; + + timer::set(period, total_iterations, [&iterations, &promise](timer::node& in_timer) { + if (++iterations == total_iterations) { + promise.set_value(); + } + }); + + EXPECT_EQ(promise.get_future().wait_for(timeout), std::future_status::ready); +} + +TEST(TimerTest, scopedWithIterations) { + size_t iterations{ 0 }; + std::promise promise; + + timer timer{ period, total_iterations, [&iterations, &promise](timer::node& in_timer) { + if (++iterations == total_iterations) { + promise.set_value(); + } + } }; + + EXPECT_EQ(promise.get_future().wait_for(timeout), std::future_status::ready); +} + +TEST(TimerTest, setWithIterationsCancel) { + size_t iterations{ 0 }; + std::promise promise; + + timer::set(period, total_iterations, [&iterations, &promise](timer::node& in_timer) { + if (++iterations == total_iterations) { + in_timer.cancel(); + promise.set_value(); + } + }); + + EXPECT_EQ(promise.get_future().wait_for(timeout), std::future_status::ready); +} + +TEST(TimerTest, scopedWithIterationsCancel) { + size_t iterations{ 0 }; + std::promise promise; + + timer timer{ period, total_iterations, [&iterations, &promise](timer::node& in_timer) { + if (++iterations == total_iterations) { + in_timer.cancel(); + promise.set_value(); + } + } }; + + EXPECT_EQ(promise.get_future().wait_for(timeout), std::future_status::ready); +}