diff --git a/.clang-format b/.clang-format
index 4d3a14a..83f47b5 100644
--- a/.clang-format
+++ b/.clang-format
@@ -1,96 +1,17 @@
----
-Language: Cpp
-# BasedOnStyle: LLVM
-AccessModifierOffset: -4
-AlignAfterOpenBracket: Align
+BasedOnStyle: Mozilla
+IndentWidth: 4
+ColumnLimit: 120
+Standard: c++20
+IndentExternBlock: NoIndent
+AlwaysBreakAfterDefinitionReturnType: None
+BreakAfterReturnType: None
+SpaceAfterTemplateKeyword: true
+AllowShortFunctionsOnASingleLine: All
AlignConsecutiveAssignments: true
+AlignConsecutiveBitFields: true
AlignConsecutiveDeclarations: true
+AlignConsecutiveMacros: true
+AlignConsecutiveShortCaseStatements: { Enabled: true }
AlignEscapedNewlines: Left
-AlignOperands: true
AlignTrailingComments: true
-AllowAllParametersOfDeclarationOnNextLine: false
-AllowShortBlocksOnASingleLine: Never
-AllowShortCaseLabelsOnASingleLine: false
-AllowShortFunctionsOnASingleLine: Inline
-AllowShortIfStatementsOnASingleLine: Never
-AllowShortLoopsOnASingleLine: false
-AlwaysBreakAfterDefinitionReturnType: None
-AlwaysBreakAfterReturnType: None
-AlwaysBreakBeforeMultilineStrings: false
-AlwaysBreakTemplateDeclarations: Yes
-BinPackArguments: false
-BinPackParameters: false
-BraceWrapping:
- AfterCaseLabel: true
- AfterClass: true
- AfterControlStatement: true
- AfterEnum: true
- AfterFunction: true
- AfterNamespace: true
- AfterStruct: true
- AfterUnion: true
- BeforeCatch: true
- BeforeElse: true
- IndentBraces: false
- SplitEmptyFunction: false
- SplitEmptyRecord: false
- SplitEmptyNamespace: false
- AfterExternBlock: false # Keeps the contents un-indented.
-BreakBeforeBinaryOperators: None
-BreakBeforeBraces: Custom
-BreakBeforeTernaryOperators: true
-BreakConstructorInitializers: AfterColon
-# BreakInheritanceList: AfterColon
-BreakStringLiterals: true
-ColumnLimit: 120
-CommentPragmas: '^ (coverity|pragma:)'
-CompactNamespaces: false
-ConstructorInitializerAllOnOneLineOrOnePerLine: true
-ConstructorInitializerIndentWidth: 4
-ContinuationIndentWidth: 4
-Cpp11BracedListStyle: true
-DerivePointerAlignment: false
-DisableFormat: false
-ExperimentalAutoDetectBinPacking: false
-FixNamespaceComments: true
-ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ]
-IncludeBlocks: Preserve
-IndentCaseLabels: false
-IndentPPDirectives: AfterHash
-IndentWidth: 4
-IndentWrappedFunctionNames: false
-KeepEmptyLinesAtTheStartOfBlocks: false
-MacroBlockBegin: ''
-MacroBlockEnd: ''
-MaxEmptyLinesToKeep: 1
-NamespaceIndentation: None
-PenaltyBreakAssignment: 2
-PenaltyBreakBeforeFirstCallParameter: 10000 # Raised intentionally; prefer breaking all
-PenaltyBreakComment: 300
-PenaltyBreakFirstLessLess: 120
-PenaltyBreakString: 1000
-PenaltyExcessCharacter: 1000000
-PenaltyReturnTypeOnItsOwnLine: 10000 # Raised intentionally because it hurts readability
-PointerAlignment: Left
-ReflowComments: true
-SortIncludes: CaseInsensitive
-SortUsingDeclarations: false
-SpaceAfterCStyleCast: true
-SpaceAfterTemplateKeyword: true
-SpaceBeforeAssignmentOperators: true
-SpaceBeforeCpp11BracedList: false
-SpaceBeforeInheritanceColon: true
-SpaceBeforeParens: ControlStatements
-SpaceBeforeCtorInitializerColon: true
-SpaceBeforeRangeBasedForLoopColon: true
-SpaceInEmptyParentheses: false
-SpacesBeforeTrailingComments: 2
-SpacesInAngles: false
-SpacesInCStyleCastParentheses: false
-SpacesInContainerLiterals: false
-SpacesInParentheses: false
-SpacesInSquareBrackets: false
-Standard: c++20
-TabWidth: 4
-UseTab: Never
-...
+SortIncludes: false
diff --git a/.clang-tidy b/.clang-tidy
index e6de58c..e10533e 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -20,6 +20,7 @@ Checks: >-
-llvm-header-guard,
-modernize-concat-nested-namespaces,
-modernize-type-traits,
+ -modernize-use-auto,
-modernize-use-constraints,
-modernize-use-default-member-init,
-modernize-use-nodiscard,
@@ -30,6 +31,7 @@ Checks: >-
-bugprone-suspicious-include,
-misc-include-cleaner,
-cppcoreguidelines-pro-type-vararg,
+ -hicpp-use-auto,
-hicpp-vararg,
CheckOptions:
- key: readability-function-cognitive-complexity.Threshold
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index be737bb..d332ddd 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -20,6 +20,7 @@ jobs:
- name: Install Dependencies
run: |
+ sudo apt update
sudo apt install gcc-multilib g++-multilib clang-tidy
g++ --version
clang-tidy --version
@@ -58,7 +59,10 @@ jobs:
- uses: actions/checkout@v4
- name: Install Dependencies
- run: sudo apt install gcc-multilib g++-multilib
+ run: |
+ sudo apt update
+ sudo apt install gcc-multilib g++-multilib
+ g++ --version
- name: Configure CMake
run: >
@@ -82,9 +86,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: DoozyX/clang-format-lint-action@v0.18.1
+ - uses: DoozyX/clang-format-lint-action@v0.20
with:
source: '.'
exclude: './lib'
extensions: 'c,h,cpp,hpp'
- clangFormatVersion: 18
+ clangFormatVersion: 20
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..1bab117
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,47 @@
+# Repository Guidelines
+
+## Project Structure & Module Organization
+- `include/olga_scheduler/`: public single-file headers.
+- `tests/`: GoogleTest suites (`test_*.cpp`, `test_*.c`) plus demos.
+- `lib/cavl/`: vendored CAVL header-only dependency used by the scheduler.
+- `CMakeLists.txt`: build, test, formatting, static-analysis, and coverage wiring.
+
+## Build, Test, and Development Commands
+Common CMake workflow:
+```sh
+cmake -S . -B build
+cmake --build build
+ctest --test-dir build --output-on-failure
+```
+Format sources (requires `clang-format`):
+```sh
+cmake --build build --target format
+```
+Static analysis is enabled by default and requires `clang-tidy`. Disable with:
+```sh
+cmake -S . -B build -DNO_STATIC_ANALYSIS=1
+```
+Coverage build (requires `gcovr` and GCC/Clang):
+```sh
+cmake -S . -B build-coverage -DOLGA_ENABLE_COVERAGE=ON
+cmake --build build-coverage --target coverage
+```
+Build the C demo:
+```sh
+cmake --build build --target olga_scheduler_c_demo
+```
+
+## Coding Style & Naming Conventions
+- C99 for C code and C++20 for C++ code.
+- Formatting is defined in `.clang-format` (Mozilla base, 4-space indent, 120 column limit). Use `clang-format` via the `format` target.
+- `clang-tidy` is enforced (warnings as errors). Keep code warning-free; the project builds with `-Wall -Wextra -Werror -pedantic`.
+- Tests are named `test_*.c` / `test_*.cpp`. Headers stay in `include/olga_scheduler/`.
+
+## Testing Guidelines
+- Tests use GoogleTest (fetched via CMake `FetchContent`).
+- Run with `ctest` from the build directory (see commands above).
+- Coverage target enforces 100% line and branch coverage for `include/olga_scheduler/olga_scheduler.h` (C API only), so keep coverage updates in sync with header changes.
+
+## Commit & Pull Request Guidelines
+- Commit history on feature branches is irrelevant as we use squash merging only.
+- PRs should clearly describe behavioral changes, list tests run (e.g., `ctest --test-dir build`), and link related issues/PRs when applicable.
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index 46e7ce4..0000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Changelog
-
-## v0.1 - August 2024
-
-Initial release.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5691a65..50337b5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -22,7 +22,8 @@ if (NOT clang_format)
message(STATUS "Could not locate clang-format")
else ()
file(GLOB format_files
- ${CMAKE_CURRENT_SOURCE_DIR}/include/*.[ch]pp
+ ${CMAKE_CURRENT_SOURCE_DIR}/include/*/*.h*
+ ${CMAKE_CURRENT_SOURCE_DIR}/tests/*.[ch]*
)
message(STATUS "Using clang-format: ${clang_format}; files: ${format_files}")
add_custom_target(format COMMAND ${clang_format} -i -fallback-style=none -style=file --verbose ${format_files})
@@ -47,6 +48,36 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsign-conversion -Wcast-align -Wmissing
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wtype-limits -Wnon-virtual-dtor -Woverloaded-virtual")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-attributes")
+option(OLGA_ENABLE_COVERAGE "Enable coverage instrumentation and reporting" OFF)
+if (OLGA_ENABLE_COVERAGE)
+ # Coverage builds are optimized for instrumentation rather than static analysis.
+ set(CMAKE_CXX_CLANG_TIDY "")
+
+ if ((CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") OR (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang"))
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -g --coverage -DNDEBUG")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g --coverage -DNDEBUG")
+ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
+ else ()
+ message(WARNING "Coverage is enabled but the compiler may not support --coverage flags.")
+ endif ()
+
+ find_program(gcovr_path NAMES gcovr)
+ if (NOT gcovr_path)
+ message(FATAL_ERROR "gcovr not found; install it or set OLGA_ENABLE_COVERAGE=OFF.")
+ endif ()
+ add_custom_target(coverage
+ COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
+ COMMAND ${gcovr_path}
+ -r ${CMAKE_CURRENT_SOURCE_DIR}
+ --filter ${CMAKE_CURRENT_SOURCE_DIR}/include/olga_scheduler/olga_scheduler\\.h
+ --exclude ${CMAKE_CURRENT_SOURCE_DIR}/include/olga_scheduler/olga_scheduler\\.hpp
+ --txt-metric branch
+ --fail-under-line 100
+ --fail-under-branch 100
+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+ )
+endif ()
+
add_executable(test_olga_scheduler_cpp20 ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_olga_scheduler.cpp)
set_target_properties(test_olga_scheduler_cpp20 PROPERTIES CXX_STANDARD 20)
target_include_directories(test_olga_scheduler_cpp20 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/olga_scheduler)
@@ -58,3 +89,19 @@ target_link_libraries(test_olga_scheduler_cpp20
include(GoogleTest)
gtest_discover_tests(test_olga_scheduler_cpp20)
+
+add_executable(test_olga_scheduler_capi ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_olga_scheduler_c.cpp)
+set_target_properties(test_olga_scheduler_capi PROPERTIES CXX_STANDARD 20)
+target_include_directories(test_olga_scheduler_capi PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/olga_scheduler)
+target_include_directories(test_olga_scheduler_capi SYSTEM PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/lib/cavl)
+target_link_libraries(test_olga_scheduler_capi
+ PRIVATE
+ GTest::gmock_main
+)
+gtest_discover_tests(test_olga_scheduler_capi)
+
+add_executable(olga_scheduler_c_demo ${CMAKE_CURRENT_SOURCE_DIR}/tests/olga_scheduler_c_demo.c)
+set_target_properties(olga_scheduler_c_demo PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED YES C_EXTENSIONS OFF)
+target_include_directories(olga_scheduler_c_demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/olga_scheduler)
+target_include_directories(olga_scheduler_c_demo SYSTEM PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/lib/cavl)
+target_compile_options(olga_scheduler_c_demo PRIVATE -Wall -Wextra -Werror -pedantic)
diff --git a/README.md b/README.md
index eb4881e..811c8b6 100644
--- a/README.md
+++ b/README.md
@@ -2,11 +2,11 @@
[](https://github.com/zubax/olga_scheduler/actions/workflows/main.yml)
-Generic single-file implementation of scheduler suitable for deeply embedded systems.
+Generic single-file implementation of an EDF scheduler suitable for deeply embedded systems.
"OLGa" is a reference to the fact that it has a logarithmic asymptotic complexity.
-**Simply copy `olga_scheduler.hpp` into your project tree and you are ready to roll.**
-The only dependency is the CAVL (`cavl.hpp`) header-only library
-(>= [v3.1.0](https://github.com/pavel-kirienko/cavl/tree/3.1.0)).
+
+**Simply copy `olga_scheduler.hpp` (C++) or `olga_scheduler.h` (C) into your project tree and you are ready to roll.**
+The only dependency is the [CAVL header-only library](https://github.com/pavel-kirienko/cavl).
The usage instructions are provided in the comments.
The code is fully covered by manual tests with full state space exploration.
@@ -17,4 +17,92 @@ To release a new version, simply create a new tag.
-
\ No newline at end of file
+
+
+## Examples
+
+### C
+
+```c
+#include "olga_scheduler.h"
+
+#include
+#include
+#include
+#include
+
+static int64_t get_microseconds(olga_t* sched)
+{
+ (void)sched;
+ struct timespec ts;
+ (void)clock_gettime(CLOCK_MONOTONIC, &ts);
+ return ((int64_t)ts.tv_sec * 1000000) + (ts.tv_nsec / 1000);
+}
+
+static void handler(olga_t* sched, olga_event_t* event, int64_t now)
+{
+ uint64_t* counter = (uint64_t*)event->user;
+ ++*counter;
+ printf("counter=%" PRIu64 " now=%" PRId64 "\n", *counter, now);
+ // Keep events triggering exactly at 1 Hz.
+ olga_defer(sched, event->deadline + 1000000, event->user, handler, event);
+}
+
+int main(void)
+{
+ olga_t sched;
+ olga_init(&sched, NULL, get_microseconds);
+
+ uint64_t counter = 0;
+ olga_event_t event = OLGA_EVENT_INIT;
+ olga_defer(&sched, sched.now(&sched) + 1000000, &counter, handler, &event);
+
+ for (;;) {
+ olga_spin_result_t spin_result = olga_spin(&sched);
+ (void)spin_result; // Optional performance information here.
+ const struct timespec delay = { .tv_sec = 0, .tv_nsec = 1000 * 1000 };
+ (void)nanosleep(&delay, NULL); // Do something else here: IO multiplexing, update scheduler stats, etc.
+ if (counter > 10) {
+ olga_cancel(&sched, &event);
+ puts("Event canceled");
+ break;
+ }
+ }
+ return 0;
+}
+```
+
+### C++
+
+```cpp
+#include "olga_scheduler.hpp"
+
+#include
+#include
+#include
+#include
+
+int main()
+{
+ using namespace std::chrono_literals;
+
+ olga_scheduler::EventLoop loop;
+ std::uint64_t counter = 0;
+
+ auto evt = loop.repeat(1s, [&](const auto& arg) {
+ ++counter;
+ std::cout << "counter=" << counter
+ << " now=" << arg.approx_now.time_since_epoch().count()
+ << '\n';
+ if (counter > 10) {
+ arg.event.cancel();
+ }
+ });
+
+ while (!loop.isEmpty()) {
+ (void)loop.spin();
+ std::this_thread::sleep_for(1ms); // Do something else here.
+ }
+ return 0;
+}
+```
diff --git a/include/olga_scheduler/olga_scheduler.h b/include/olga_scheduler/olga_scheduler.h
new file mode 100644
index 0000000..20788cf
--- /dev/null
+++ b/include/olga_scheduler/olga_scheduler.h
@@ -0,0 +1,181 @@
+/// Source: https://github.com/zubax/olga_scheduler
+///
+/// A single-file header-only EDF scheduler for embedded applications.
+///
+/// Copyright (c) Zubax Robotics
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+/// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
+/// and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in all copies or substantial portions of
+/// the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+/// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+/// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+/// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+#pragma once
+
+#include // Add to your include paths: https://github.com/pavel-kirienko/cavl
+
+#include
+#include
+
+#ifdef __cplusplus
+// This is, strictly speaking, useless because we do not define any functions with external linkage here,
+// but it tells static analyzers that what follows should be interpreted as C code rather than C++.
+extern "C"
+{
+#endif
+
+typedef struct olga_t olga_t;
+typedef struct olga_event_t olga_event_t;
+
+/// Represents a user-handled future event.
+/// When the handler is invoked, the event is already removed from the scheduler.
+/// If necessary, it can be re-inserted immediately from within the handler with a new deadline.
+/// It is safe to destroy the event inside the handler.
+/// The time units can be arbitrary.
+struct olga_event_t
+{
+ CAVL2_T base;
+ int64_t deadline;
+ uint64_t seqno;
+ void (*handler)(olga_t*, olga_event_t*, int64_t now);
+ void* user;
+};
+
+// Convenience initializer for a fresh event (all fields zeroed, base pointers NULL).
+#ifdef __cplusplus
+#define OLGA_EVENT_INIT \
+ olga_event_t {}
+#else
+#define OLGA_EVENT_INIT ((olga_event_t){ 0 })
+#endif
+
+/// The main scheduler type.
+struct olga_t
+{
+ CAVL2_T* events;
+ uint64_t next_seqno; ///< Monotonic sequence number for FIFO ordering of equal-deadline events.
+ int64_t (*now)(olga_t*); ///< Time provider; receives the scheduler to access user data if needed.
+ void* user;
+};
+
+/// Current state assessment returned from olga_spin().
+/// The `now` field contains the last sampled time from the user-provided clock, or INT64_MIN if not sampled.
+typedef struct olga_spin_result_t
+{
+ int64_t next_deadline;
+ int64_t worst_lateness;
+ int64_t now;
+} olga_spin_result_t;
+
+/// To deinitialize, simply cancel all events; nothing else needs to be done.
+/// The time units can be arbitrary.
+static inline void olga_init(olga_t* const self, void* const user, int64_t (*const now)(olga_t* sched))
+{
+ assert(self != NULL);
+ assert(now != NULL);
+ self->events = NULL;
+ self->next_seqno = 0U;
+ self->now = now;
+ self->user = user;
+}
+
+// INTERNAL USE ONLY.
+static inline CAVL2_RELATION olga_private_compare(const void* user, const CAVL2_T* event)
+{
+ const olga_event_t* const outer = (const olga_event_t*)user;
+ const olga_event_t* const inner = (const olga_event_t*)event;
+ if (outer->deadline != inner->deadline) {
+ return (outer->deadline > inner->deadline) ? +1 : -1; // Later deadlines go to the right.
+ }
+ if (outer->seqno != inner->seqno) {
+ return (outer->seqno > inner->seqno) ? +1 : -1; // Later insertion goes to the right.
+ }
+ return 0;
+}
+
+/// Schedule a one-time event.
+/// The handler will be invoked at or asap after the deadline; the actual invocation time will be provided.
+/// The scheduler pointer is passed to allow rescheduling from within the callback.
+/// The event pointer provides access to the user data and deadline.
+/// If the event is already scheduled, it will be automatically rescheduled with the new deadline.
+/// The event must be either zero-initialized using OLGA_EVENT_INIT or have been used at least once.
+/// Events are already canceled prior to handler invocation, so it is safe to re-register immediately from the handler.
+/// The complexity is logarithmic in the number of pending events.
+static inline void olga_defer(olga_t* const self,
+ const int64_t deadline,
+ void* const user,
+ void (*const handler)(olga_t*, olga_event_t*, int64_t now),
+ olga_event_t* const out_event)
+{
+ assert(self != NULL);
+ assert(handler != NULL);
+ assert(out_event != NULL);
+ (void)cavl2_remove_if(&self->events, &out_event->base);
+ out_event->deadline = deadline;
+ out_event->seqno = self->next_seqno++;
+ out_event->handler = handler;
+ out_event->user = user;
+ (void)cavl2_find_or_insert(&self->events, out_event, olga_private_compare, out_event, cavl2_trivial_factory);
+}
+
+/// No effect if the event has already been completed.
+/// It is safe to cancel a freshly created event if it has been initialized with OLGA_EVENT_INIT.
+/// The complexity is logarithmic in the number of pending events.
+static inline void olga_cancel(olga_t* const self, olga_event_t* const event)
+{
+ assert(self != NULL);
+ assert(event != NULL);
+ (void)cavl2_remove_if(&self->events, &event->base);
+ event->deadline = INT64_MIN;
+}
+
+/// True if the event is currently pending in the scheduler.
+static inline bool olga_is_pending(const olga_t* const self, const olga_event_t* const event)
+{
+ assert(self != NULL);
+ assert(event != NULL);
+ return cavl2_is_inserted(self->events, &event->base);
+}
+
+/// Execute pending events strictly in the order of their deadlines until there are no pending events left.
+/// Events with the same deadline are executed in the FIFO order.
+/// The handler receives a freshly sampled `now` taken immediately before invocation.
+/// This method should be invoked regularly to pump the event loop.
+static inline olga_spin_result_t olga_spin(olga_t* const self)
+{
+ assert(self != NULL);
+ olga_spin_result_t out = { .next_deadline = INT64_MAX, .worst_lateness = 0, .now = INT64_MIN };
+ for (;;) { // GCOVR_EXCL_LINE
+ olga_event_t* const event = (olga_event_t*)cavl2_min(self->events);
+ if (event == NULL) {
+ out.next_deadline = INT64_MAX;
+ break;
+ }
+ const int64_t deadline = event->deadline;
+ out.now = self->now(self);
+ if (out.now < deadline) {
+ out.next_deadline = deadline;
+ break;
+ }
+ cavl2_remove(&self->events, &event->base);
+ event->handler(self, event, out.now);
+ // event is no longer valid -- may be destroyed in the handler.
+
+ const int64_t lateness = out.now - deadline; // Non-negative because now >= deadline.
+ if (lateness > out.worst_lateness) {
+ out.worst_lateness = lateness;
+ }
+ }
+ return out;
+}
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/include/olga_scheduler/olga_scheduler.hpp b/include/olga_scheduler/olga_scheduler.hpp
index 885cfab..bca1993 100644
--- a/include/olga_scheduler/olga_scheduler.hpp
+++ b/include/olga_scheduler/olga_scheduler.hpp
@@ -26,8 +26,7 @@
#include
#include
-namespace olga_scheduler
-{
+namespace olga_scheduler {
template
class Event;
@@ -66,10 +65,10 @@ concept Callback = std::invocable&>;
template
class Event : private cavl::Node>
{
- friend class cavl::Node; // This friendship is required for the AVL tree implementation,
- friend class cavl::Tree; // otherwise, we would have to use public inheritance, which is undesirable.
+ friend class cavl::Node; // This friendship is required for the AVL tree implementation,
+ friend class cavl::Tree; // otherwise, we would have to use public inheritance, which is undesirable.
-public:
+ public:
Event(const Event&) = delete;
Event& operator=(const Event&) = delete;
Event& operator=(Event&& other) noexcept = delete;
@@ -81,8 +80,7 @@ class Event : private cavl::Node>
void cancel() noexcept
{
/// It is guaranteed that while an event resides in the tree, it has a valid deadline set.
- if (deadline_ != TimePoint::min())
- {
+ if (deadline_ != TimePoint::min()) {
// Removing a non-existent node from the tree is an UB that may lead to memory corruption,
// so we have to check first if the event is actually registered.
remove();
@@ -100,10 +98,10 @@ class Event : private cavl::Node>
// This method is necessary to store an Event in cetl::unbounded_variant
static constexpr std::array _get_type_id_() noexcept
{
- return {0xB6, 0x87, 0x48, 0xA6, 0x7A, 0xDB, 0x4D, 0xF1, 0xB3, 0x1D, 0xA9, 0x8D, 0x50, 0xA7, 0x82, 0x47};
+ return { 0xB6, 0x87, 0x48, 0xA6, 0x7A, 0xDB, 0x4D, 0xF1, 0xB3, 0x1D, 0xA9, 0x8D, 0x50, 0xA7, 0x82, 0x47 };
}
-protected:
+ protected:
using Tree = cavl::Tree;
using cavl::Node::remove;
@@ -120,10 +118,11 @@ class Event : private cavl::Node>
(this->getParentNode() == nullptr));
}
- Event(Event&& other) noexcept :
- cavl::Node{std::move(static_cast&>(other))},
- deadline_{std::exchange(other.deadline_, TimePoint::min())}
- {}
+ Event(Event&& other) noexcept
+ : cavl::Node{ std::move(static_cast&>(other)) }
+ , deadline_{ std::exchange(other.deadline_, TimePoint::min()) }
+ {
+ }
/// Ensure the event is in the tree and set the deadline to the specified absolute time point.
/// If the event is already scheduled, the old deadline is overwritten.
@@ -131,17 +130,17 @@ class Event : private cavl::Node>
void schedule(const TimePoint dead, Tree& tree) noexcept
{
cancel();
- deadline_ = dead; // The deadline shall be set before the event is inserted into the tree.
+ deadline_ = dead; // The deadline shall be set before the event is inserted into the tree.
const auto ptr_existing = tree.search(
- [dead](const Event& other) {
- /// No two deadlines compare equal, which allows us to have multiple nodes with the same deadline in
- /// the tree. With two nodes sharing the same deadline, the one added later is considered to be later.
- return (dead >= other.deadline_) ? +1 : -1;
- },
- [this] { return this; });
+ [dead](const Event& other) {
+ /// No two deadlines compare equal, which allows us to have multiple nodes with the same deadline in
+ /// the tree. With two nodes sharing the same deadline, the one added later is considered to be later.
+ return (dead >= other.deadline_) ? +1 : -1;
+ },
+ [this] { return this; });
assert(std::get<0>(ptr_existing) == this);
assert(!std::get<1>(ptr_existing));
- (void) ptr_existing;
+ (void)ptr_existing;
}
/// The execution handler shall either reschedule or cancel the event
@@ -150,7 +149,7 @@ class Event : private cavl::Node>
/// because the user may choose to cancel the event from the callback.
virtual void execute(const Arg& args, Tree& tree) = 0;
-private:
+ private:
TimePoint deadline_ = TimePoint::min();
};
@@ -183,22 +182,22 @@ struct SpinResult final
template
class EventLoop final
{
-public:
+ public:
using time_point = typename Clock::time_point;
using duration = typename Clock::duration;
-private:
+ private:
/// This proxy is needed to expose the protected execute() method to the event loop.
/// An alternative would be to make this class a friend of the Event class,
/// or to make the execute() method public, which is undesirable.
class EventProxy : public Event
{
- public:
+ public:
using Event::execute;
using Event::_get_type_id_;
};
-public:
+ public:
EventLoop() = default;
EventLoop(const EventLoop&) = delete;
@@ -220,18 +219,20 @@ class EventLoop final
{
class Impl final : public EventProxy
{
- public:
- Impl(EventLoop& owner, const duration per, Fun&& fun) : period_{per}, handler_{std::move(fun)}
+ public:
+ Impl(EventLoop& owner, const duration per, Fun&& fun)
+ : period_{ per }
+ , handler_{ std::move(fun) }
{
this->schedule(Clock::now() + period_, owner.tree_);
}
using EventProxy::_get_type_id_;
- private:
+ private:
void execute(const Arg& args, Tree& tree) override
{
- this->schedule(args.deadline + period_, tree); // Strict period advancement, no phase error growth.
+ this->schedule(args.deadline + period_, tree); // Strict period advancement, no phase error growth.
handler_(args);
}
@@ -239,7 +240,7 @@ class EventLoop final
const Fun handler_;
};
assert(period > duration::zero());
- return Impl{*this, period, std::forward(handler)};
+ return Impl{ *this, period, std::forward(handler) };
}
/// This is like repeat() with one crucial difference: the next deadline is defined not as (deadline+period),
@@ -253,18 +254,20 @@ class EventLoop final
{
class Impl final : public EventProxy
{
- public:
- Impl(EventLoop& owner, const duration per, Fun&& fun) : min_period_{per}, handler_{std::move(fun)}
+ public:
+ Impl(EventLoop& owner, const duration per, Fun&& fun)
+ : min_period_{ per }
+ , handler_{ std::move(fun) }
{
this->schedule(Clock::now() + min_period_, owner.tree_);
}
using EventProxy::_get_type_id_;
- private:
+ private:
void execute(const Arg& args, Tree& tree) override
{
- this->schedule(args.approx_now + min_period_, tree); // Accumulate phase error intentionally.
+ this->schedule(args.approx_now + min_period_, tree); // Accumulate phase error intentionally.
handler_(args);
}
@@ -272,7 +275,7 @@ class EventLoop final
const Fun handler_;
};
assert(min_period > duration::zero());
- return Impl{*this, min_period, std::forward(handler)};
+ return Impl{ *this, min_period, std::forward(handler) };
}
/// Like repeat() but the handler will be invoked only once and the event is canceled afterward.
@@ -282,15 +285,16 @@ class EventLoop final
{
class Impl final : public EventProxy
{
- public:
- Impl(EventLoop& owner, const time_point deadline, Fun&& fun) : handler_{std::move(fun)}
+ public:
+ Impl(EventLoop& owner, const time_point deadline, Fun&& fun)
+ : handler_{ std::move(fun) }
{
this->schedule(deadline, owner.tree_);
}
using EventProxy::_get_type_id_;
- private:
+ private:
void execute(const Arg& args, Tree&) override
{
this->cancel();
@@ -299,7 +303,7 @@ class EventLoop final
const Fun handler_;
};
- return Impl{*this, deadline, std::forward(handler)};
+ return Impl{ *this, deadline, std::forward(handler) };
}
/// Execute pending events strictly in the order of their deadlines until there are no pending events left.
@@ -311,35 +315,33 @@ class EventLoop final
template
[[nodiscard]] SpinResult spin()
{
- SpinResult result{.next_deadline = time_point::max(),
- .worst_lateness = duration::zero(),
- .approx_now = time_point::min()};
- if (tree_.empty()) [[unlikely]]
- {
+ SpinResult result{ .next_deadline = time_point::max(),
+ .worst_lateness = duration::zero(),
+ .approx_now = time_point::min() };
+ if (tree_.empty()) [[unlikely]] {
result.approx_now = Clock::now();
return result;
}
- while (auto* const evt = static_cast(tree_.min()))
- {
+ while (auto* const evt = static_cast(tree_.min())) {
// The deadline is guaranteed to be set because it is in the tree.
const auto deadline = evt->getDeadline();
- if (result.approx_now < deadline) // Too early -- either we need to sleep or the time sample is obsolete.
+ if (result.approx_now < deadline) // Too early -- either we need to sleep or the time sample is obsolete.
{
- result.approx_now = Clock::now(); // The number of calls to Clock::now() is minimized.
- if (result.approx_now < deadline) // Nope, even with the latest time sample we are still early -- exit.
+ result.approx_now = Clock::now(); // The number of calls to Clock::now() is minimized.
+ if (result.approx_now < deadline) // Nope, even with the latest time sample we are still early -- exit.
{
result.next_deadline = deadline;
break;
}
}
{
- ExecutionMonitor monitor{}; // RAII indication of the start and end of the event execution.
+ ExecutionMonitor monitor{}; // RAII indication of the start and end of the event execution.
// Execution will remove the event from the tree and then possibly re-insert it with a new deadline.
- evt->execute({.event = *evt, .deadline = deadline, .approx_now = result.approx_now}, tree_);
- (void) monitor;
+ evt->execute({ .event = *evt, .deadline = deadline, .approx_now = result.approx_now }, tree_);
+ (void)monitor;
}
- result.next_deadline = time_point::max(); // Reset the next deadline to the maximum possible value.
+ result.next_deadline = time_point::max(); // Reset the next deadline to the maximum possible value.
result.worst_lateness = std::max(result.worst_lateness, result.approx_now - deadline);
}
@@ -355,11 +357,11 @@ class EventLoop final
/// The nodes are ordered such that the earliest deadline is on the left.
const auto& getTree() const noexcept { return tree_; }
-private:
+ private:
using Tree = cavl::Tree>;
Tree tree_;
-}; // EventLoop
+}; // EventLoop
-} // namespace olga_scheduler
+} // namespace olga_scheduler
diff --git a/lib/cavl/cavl2.h b/lib/cavl/cavl2.h
new file mode 100644
index 0000000..1b83eaf
--- /dev/null
+++ b/lib/cavl/cavl2.h
@@ -0,0 +1,584 @@
+/// Source: https://github.com/pavel-kirienko/cavl
+///
+/// Cavl is a single-header C library providing an implementation of AVL tree suitable for deeply embedded systems.
+/// To integrate it into your project, simply copy this file into your source tree.
+/// You can define build option macros before including the header to customize the behavior.
+/// All definitions are prefixed with cavl2 to avoid collisions with other major versions of the library.
+/// Read the API docs below.
+///
+/// See also O1Heap -- a deterministic memory manager for hard-real-time
+/// high-integrity embedded systems.
+///
+/// Version history:
+///
+/// - v1.0: initial release.
+/// - v2.0:
+/// - Simplify the API and improve naming.
+/// - The header file now bears the major version number, which simplifies vendoring: a project now can safely
+/// depend on cavl without the risk of version compatibility issues.
+/// - For the same reason as above, all definitions are now prefixed with cavl2 instead of cavl.
+/// - Add optional CAVL2_T macro to allow overriding the cavl2_t type. This is needed for libudpard/libcanard/etc
+/// and is generally useful because it allows library vendors to avoid exposing cavl via the library API.
+/// Also add CAVL2_RELATION to simplify comparator implementations.
+/// - Add the trivial factory definition because it is needed in nearly every application using cavl.
+/// - New traversal function cavl2_next_greater() offering the same time complexity but without recursion/callbacks.
+///
+/// -------------------------------------------------------------------------------------------------------------------
+///
+/// Copyright (c) Pavel Kirienko
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+/// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
+/// and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in all copies or substantial portions of
+/// the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+/// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+/// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+/// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+// ReSharper disable CppCStyleCast CppZeroConstantCanBeReplacedWithNullptr CppTooWideScopeInitStatement
+// ReSharper disable CppRedundantElaboratedTypeSpecifier CppRedundantInlineSpecifier
+#pragma once
+
+#include
+#include
+#include
+
+/// If Cavl is used in throughput-critical code, then it is recommended to disable assertion checks as they may
+/// be costly in terms of execution time.
+#ifndef CAVL2_ASSERT
+#if defined(CAVL2_NO_ASSERT) && CAVL2_NO_ASSERT
+#define CAVL2_ASSERT(x) (void)0
+#else
+#include
+#define CAVL2_ASSERT(x) assert(x)
+#endif
+#endif
+
+#ifdef __cplusplus
+// This is, strictly speaking, useless because we do not define any functions with external linkage here,
+// but it tells static analyzers that what follows should be interpreted as C code rather than C++.
+extern "C"
+{
+#endif
+
+// ---------------------------------------- PUBLIC API SECTION ----------------------------------------
+
+/// CAVL2_T can be defined before including this header to provide a custom struct type for the node element.
+/// The custom type must have the same fields as the default struct cavl2_t.
+/// This option is useful if Cavl is integrated into a library without exposing it through the library API.
+#ifndef CAVL2_T
+/// The tree node/root. The user data is to be added through composition/inheritance.
+/// The memory layout of this type is compatible with void*[4], which is useful if this type cannot be exposed in API.
+/// Per standard convention, nodes that compare smaller are put on the left.
+/// Usage example:
+/// struct my_user_type_t {
+/// struct cavl2_t base; ///< Tree node. Should be the first element, otherwise, offsetof() will be needed.
+/// ... user data ...
+/// };
+struct cavl2_t
+{
+ struct cavl2_t* up; ///< Parent node, NULL in the root.
+ struct cavl2_t* lr[2]; ///< Left child (lesser), right child (greater).
+ int_fast8_t bf; ///< Balance factor is positive when right-heavy. Allowed values are {-1, 0, +1}.
+};
+#define CAVL2_T struct cavl2_t
+#endif
+
+#if defined(static_assert) || defined(__cplusplus)
+static_assert(sizeof(CAVL2_T) <= sizeof(void* [4]), "Bad size");
+#endif
+
+/// The comparator result can be overridden to simplify comparator functions.
+/// The type shall be a signed integer type.
+/// Only three possible states of the result are considered: negative, zero, and positive; the magnitude is ignored.
+#ifndef CAVL2_RELATION
+#define CAVL2_RELATION ptrdiff_t
+#endif
+/// Returns POSITIVE if the search target is GREATER than the provided node, negative if smaller, zero on match (found).
+typedef CAVL2_RELATION (*cavl2_comparator_t)(const void* user, const CAVL2_T* node);
+
+/// If provided, the factory will be invoked when the sought node does not exist in the tree.
+/// It is expected to return a new node that will be inserted immediately (without the need to traverse the tree again).
+/// If the factory returns NULL or is not provided, the tree is not modified.
+typedef CAVL2_T* (*cavl2_factory_t)(void* user);
+
+/// Look for a node in the tree using the specified comparator. The worst-case complexity is O(log n).
+/// - If the node is found (i.e., zero comparison result), return it.
+/// - If the node is not found and the factory is NULL, return NULL.
+/// - Otherwise, construct a new node using the factory; if the result is not NULL, insert it; return the result.
+/// The user_comparator is passed into the comparator unmodified.
+/// The user_factory is passed into the factory unmodified.
+/// The root node may be replaced in the process iff the factory is not NULL and it returns a new node;
+/// otherwise, the root node will not be modified.
+/// If comparator is NULL, returns NULL.
+static inline CAVL2_T* cavl2_find_or_insert(CAVL2_T** const root,
+ const void* const user_comparator,
+ const cavl2_comparator_t comparator,
+ void* const user_factory,
+ const cavl2_factory_t factory);
+
+/// A convenience wrapper over cavl2_find_or_insert() that passes NULL factory, so the tree is never modified.
+/// Since the tree is not modified, the root pointer is passed by value, unlike in the mutating version.
+static inline CAVL2_T* cavl2_find(CAVL2_T* root, const void* const user_comparator, const cavl2_comparator_t comparator)
+{
+ return cavl2_find_or_insert(&root, user_comparator, comparator, NULL, NULL);
+}
+
+/// Remove the specified node from its tree. The root node may be replaced in the process.
+/// The worst-case complexity is O(log n).
+/// The function has no effect if either of the pointers are NULL.
+/// If the node is not in the tree, the behavior is undefined; it may create cycles in the tree which is deadly.
+/// It is safe to pass the result of cavl2_find/cavl2_find_or_insert directly as the second argument:
+/// cavl2_remove(&root, cavl2_find(&root, user, search_comparator));
+/// The removed node will have all of its pointers set to NULL.
+static inline void cavl2_remove(CAVL2_T** const root, CAVL2_T* const node);
+
+/// Replace the specified node with another node without rebalancing.
+/// This is useful when you want to replace a node with an equivalent one (same key ordering).
+/// The new node takes over the position (parent, children, balance factor) of the old node.
+/// The old node will have all of its pointers set to NULL.
+/// The new node must not already be in the tree; if it is, the behavior is undefined.
+/// The new node's fields (up, lr, bf) will be overwritten to match the old node's position in the tree.
+/// The complexity is O(1).
+/// The function has no effect if any of the pointers are NULL.
+/// If the old node is not in the tree, the behavior is undefined.
+static inline void cavl2_replace(CAVL2_T** const root, CAVL2_T* const old_node, CAVL2_T* const new_node);
+
+/// True iff the node is in the tree. The complexity is O(1).
+/// Returns false if the node is NULL.
+/// Assumes that the node pointers are NULL when it is not inserted (this is ensured by the removal function).
+static inline bool cavl2_is_inserted(const CAVL2_T* const root, const CAVL2_T* const node)
+{
+ bool out = false;
+ if (node != NULL) {
+ out = (node->up != NULL) || (node->lr[0] != NULL) || (node->lr[1] != NULL) || (node == root);
+ }
+ return out;
+}
+
+/// Remove the specified node if it is inserted in the tree; otherwise, do nothing.
+/// This is a convenience wrapper that combines cavl2_is_inserted() and cavl2_remove().
+/// Returns true if the node was inserted and has been removed, false otherwise.
+static inline bool cavl2_remove_if(CAVL2_T** const root, CAVL2_T* const node)
+{
+ bool removed = false;
+ if ((root != NULL) && cavl2_is_inserted(*root, node)) {
+ cavl2_remove(root, node);
+ removed = true;
+ }
+ return removed;
+}
+
+/// Return the min-/max-valued node stored in the tree, depending on the flag. This is an extremely fast query.
+/// Returns NULL iff the argument is NULL (i.e., the tree is empty). The worst-case complexity is O(log n).
+static inline CAVL2_T* cavl2_extremum(CAVL2_T* const root, const bool maximum)
+{
+ CAVL2_T* result = NULL;
+ CAVL2_T* c = root;
+ while (c != NULL) {
+ result = c;
+ c = c->lr[maximum];
+ }
+ return result;
+}
+
+// clang-format off
+/// Convenience wrappers for cavl2_extremum().
+static inline CAVL2_T* cavl2_min(CAVL2_T* const root) { return cavl2_extremum(root, false); }
+static inline CAVL2_T* cavl2_max(CAVL2_T* const root) { return cavl2_extremum(root, true); }
+// clang-format on
+
+/// Returns the next greater node in the in-order traversal of the tree.
+/// Does nothing and returns NULL if the argument is NULL. Behavior undefined if the node is not in the tree.
+/// To use it, first invoke cavl2_min() to get the first node, then call this function repeatedly until it returns NULL:
+/// for (CAVL2_T* p = cavl2_min(root); p != NULL; p = cavl2_next_greater(p)) {
+/// ...
+/// }
+/// The asymptotic complexity for traversing the entire tree is O(n), identical to the traditional recursive traversal.
+static inline CAVL2_T* cavl2_next_greater(CAVL2_T* const node)
+{
+ CAVL2_T* c = NULL;
+ if (node != NULL) {
+ if (node->lr[1] != NULL) {
+ c = cavl2_min(node->lr[1]);
+ } else {
+ const CAVL2_T* n = node;
+ CAVL2_T* p = node->up;
+ while ((p != NULL) && (p->lr[1] == n)) {
+ n = p;
+ p = p->up;
+ }
+ c = p;
+ }
+ }
+ return c;
+}
+
+/// Find and return the root of the tree given an arbitrary node.
+/// Returns NULL if the argument is NULL. The worst-case complexity is O(log n).
+static inline CAVL2_T* cavl2_root(CAVL2_T* node)
+{
+ if (node != NULL) {
+ while (node->up != NULL) {
+ node = node->up;
+ }
+ }
+ return node;
+}
+
+/// Find the smallest node whose value is greater than or equal to the search target, in O(log n).
+/// Returns the first node for which the comparator returns a non-positive result.
+/// If no such node exists (all nodes compare less than target), returns NULL.
+/// The comparator returns: positive if target>candidate, zero if target==candidate, negative if target 5; target=5 => 5; target=8 => NULL.
+static inline CAVL2_T* cavl2_lower_bound(CAVL2_T* const root,
+ const void* const user,
+ const cavl2_comparator_t comparator)
+{
+ CAVL2_T* result = NULL;
+ if ((root != NULL) && (comparator != NULL)) {
+ CAVL2_T* n = root;
+ while (n != NULL) {
+ const CAVL2_RELATION cmp = comparator(user, n);
+ if (cmp <= 0) {
+ result = n;
+ n = n->lr[0];
+ } else {
+ n = n->lr[1];
+ }
+ }
+ }
+ return result;
+}
+
+/// Find the smallest node whose value is strictly greater than the search target (upper bound).
+/// Returns the first node for which the comparator returns a negative result.
+/// See cavl2_lower_bound() for details.
+/// Example: tree={1,3,5,7}, target=4 => 5; target=5 => 7; target=7 => NULL.
+static inline CAVL2_T* cavl2_upper_bound(CAVL2_T* const root,
+ const void* const user,
+ const cavl2_comparator_t comparator)
+{
+ CAVL2_T* result = NULL;
+ if ((root != NULL) && (comparator != NULL)) {
+ CAVL2_T* n = root;
+ while (n != NULL) {
+ const CAVL2_RELATION cmp = comparator(user, n);
+ if (cmp < 0) {
+ result = n;
+ n = n->lr[0];
+ } else {
+ n = n->lr[1];
+ }
+ }
+ }
+ return result;
+}
+
+/// Find the largest node whose value is less than or equal to the search target, in O(log n).
+/// Returns the last node for which the comparator returns a non-negative result.
+/// See cavl2_lower_bound() for details.
+/// Example: tree={1,3,5,7}, target=4 => 3; target=5 => 5; target=0 => NULL.
+static inline CAVL2_T* cavl2_predecessor(CAVL2_T* const root,
+ const void* const user,
+ const cavl2_comparator_t comparator)
+{
+ CAVL2_T* result = NULL;
+ if ((root != NULL) && (comparator != NULL)) {
+ CAVL2_T* n = root;
+ while (n != NULL) {
+ const CAVL2_RELATION cmp = comparator(user, n);
+ if (cmp >= 0) {
+ result = n;
+ n = n->lr[1];
+ } else {
+ n = n->lr[0];
+ }
+ }
+ }
+ return result;
+}
+
+/// The successor counterpart of cavl2_predecessor() is an alias of cavl2_lower_bound(), provided for completeness only.
+/// Example: tree={1,3,5,7}, target=4 => 5; target=5 => 5; target=8 => NULL.
+static inline CAVL2_T* cavl2_successor(CAVL2_T* const root, const void* const user, const cavl2_comparator_t comparator)
+{
+ return cavl2_lower_bound(root, user, comparator);
+}
+
+/// The trivial factory is useful in most applications. It simply returns the user pointed converted to CAVL2_T.
+/// It is meant for use with cavl2_find_or_insert().
+static inline CAVL2_T* cavl2_trivial_factory(void* const user)
+{
+ return (CAVL2_T*)user;
+}
+
+/// A convenience macro for use when a struct is a member of multiple AVL trees. For example:
+///
+/// struct my_type_t {
+/// struct cavl2_t tree_a;
+/// struct cavl2_t tree_b;
+/// ...
+/// };
+///
+/// If we only have tree_a, we don't need this helper because the C standard guarantees that the address of a struct
+/// equals the address of its first member, always, so simply casting a tree node to (struct my_type_t*) yields
+/// a valid pointer to the struct. However, if we have more than one tree nodes in a struct, for the other ones
+/// we will need to subtract the offset of the tree node field from the address of the tree node to get to the owner.
+/// This macro does exactly that. Example:
+///
+/// struct cavl2_t* tree_node_b = cavl2_find(...); // whatever
+/// if (tree_node_b == NULL) { ... } // do something else
+/// struct my_type_t* my_struct = CAVL2_TO_OWNER(tree_node_b, struct my_type_t, tree_b);
+#define CAVL2_TO_OWNER(tree_node_ptr, owner_type, owner_tree_node_field) \
+ ((owner_type*)cavl2_impl_to_owner_helper((tree_node_ptr), offsetof(owner_type, owner_tree_node_field))) // NOLINT
+
+// ---------------------------------------- END OF PUBLIC API SECTION ----------------------------------------
+// ---------------------------------------- POLICE LINE DO NOT CROSS ----------------------------------------
+
+/// INTERNAL USE ONLY.
+static inline void* cavl2_impl_to_owner_helper(const void* const tree_node_ptr, const size_t offset)
+{
+ return (tree_node_ptr == NULL) ? NULL : (void*)((char*)tree_node_ptr - offset);
+}
+
+/// INTERNAL USE ONLY. Makes the '!r' child of node 'x' its parent; i.e., rotates 'x' toward 'r'.
+static inline void cavl2_impl_rotate(CAVL2_T* const x, const bool r)
+{
+ CAVL2_ASSERT((x != NULL) && (x->lr[!r] != NULL) && ((x->bf >= -1) && (x->bf <= +1)));
+ CAVL2_T* const z = x->lr[!r];
+ if (x->up != NULL) {
+ x->up->lr[x->up->lr[1] == x] = z;
+ }
+ z->up = x->up;
+ x->up = z;
+ x->lr[!r] = z->lr[r];
+ if (x->lr[!r] != NULL) {
+ x->lr[!r]->up = x;
+ }
+ z->lr[r] = x;
+}
+
+/// INTERNAL USE ONLY.
+/// Accepts a node and how its balance factor needs to be changed -- either +1 or -1.
+/// Returns the new node to replace the old one if tree rotation took place, same node otherwise.
+static inline CAVL2_T* cavl2_impl_adjust_balance(CAVL2_T* const x, const bool increment)
+{
+ CAVL2_ASSERT((x != NULL) && ((x->bf >= -1) && (x->bf <= +1)));
+ CAVL2_T* out = x;
+ const int_fast8_t new_bf = (int_fast8_t)(x->bf + (increment ? +1 : -1));
+ if ((new_bf < -1) || (new_bf > 1)) {
+ const bool r = new_bf < 0; // bf<0 if left-heavy --> right rotation is needed.
+ const int_fast8_t sign = r ? +1 : -1; // Positive if we are rotating right.
+ CAVL2_T* const z = x->lr[!r];
+ CAVL2_ASSERT(z != NULL); // Heavy side cannot be empty. NOLINTNEXTLINE(clang-analyzer-core.NullDereference)
+ if ((z->bf * sign) <= 0) { // Parent and child are heavy on the same side or the child is balanced.
+ out = z;
+ cavl2_impl_rotate(x, r);
+ if (0 == z->bf) {
+ x->bf = (int_fast8_t)(-sign);
+ z->bf = (int_fast8_t)(+sign);
+ } else {
+ x->bf = 0;
+ z->bf = 0;
+ }
+ } else { // Otherwise, the child needs to be rotated in the opposite direction first.
+ CAVL2_T* const y = z->lr[r];
+ CAVL2_ASSERT(y != NULL); // Heavy side cannot be empty.
+ out = y;
+ cavl2_impl_rotate(z, !r);
+ cavl2_impl_rotate(x, r);
+ if ((y->bf * sign) < 0) {
+ x->bf = (int_fast8_t)(+sign);
+ y->bf = 0;
+ z->bf = 0;
+ } else if ((y->bf * sign) > 0) {
+ x->bf = 0;
+ y->bf = 0;
+ z->bf = (int_fast8_t)(-sign);
+ } else {
+ x->bf = 0;
+ z->bf = 0;
+ }
+ }
+ } else {
+ x->bf = new_bf; // Balancing not needed, just update the balance factor and call it a day.
+ }
+ return out;
+}
+
+/// INTERNAL USE ONLY.
+/// Takes the culprit node (the one that is added); returns NULL or the root of the tree (possibly new one).
+/// When adding a new node, set its balance factor to zero and call this function to propagate the changes upward.
+static inline CAVL2_T* cavl2_impl_retrace_on_growth(CAVL2_T* const added)
+{
+ CAVL2_ASSERT((added != NULL) && (0 == added->bf));
+ CAVL2_T* c = added; // Child
+ CAVL2_T* p = added->up; // Parent
+ while (p != NULL) {
+ const bool r = p->lr[1] == c; // c is the right child of parent
+ CAVL2_ASSERT(p->lr[r] == c);
+ c = cavl2_impl_adjust_balance(p, r);
+ p = c->up;
+ if (0 == c->bf) { // The height change of the subtree made this parent balanced (as all things should be),
+ break; // hence, the height of the outer subtree is unchanged, so upper balance factors are unchanged.
+ }
+ }
+ CAVL2_ASSERT(c != NULL);
+ return (NULL == p) ? c : NULL; // New root or nothing.
+}
+
+static inline CAVL2_T* cavl2_find_or_insert(CAVL2_T** const root,
+ const void* const user_comparator,
+ const cavl2_comparator_t comparator,
+ void* const user_factory,
+ const cavl2_factory_t factory)
+{
+ CAVL2_T* out = NULL;
+ if ((root != NULL) && (comparator != NULL)) {
+ CAVL2_T* up = *root;
+ CAVL2_T** n = root;
+ while (*n != NULL) {
+ const CAVL2_RELATION cmp = comparator(user_comparator, *n);
+ if (0 == cmp) {
+ out = *n;
+ break;
+ }
+ up = *n;
+ n = &(*n)->lr[cmp > 0];
+ CAVL2_ASSERT((NULL == *n) || ((*n)->up == up));
+ }
+ if (NULL == out) {
+ out = (NULL == factory) ? NULL : factory(user_factory);
+ if (out != NULL) {
+ *n = out; // Overwrite the pointer to the new node in the parent node.
+ out->lr[0] = NULL;
+ out->lr[1] = NULL;
+ out->up = up;
+ out->bf = 0;
+ CAVL2_T* const rt = cavl2_impl_retrace_on_growth(out);
+ if (rt != NULL) {
+ *root = rt;
+ }
+ }
+ }
+ }
+ return out;
+}
+
+static inline void cavl2_remove(CAVL2_T** const root, CAVL2_T* const node)
+{
+ if ((root != NULL) && (node != NULL)) {
+ CAVL2_ASSERT(*root != NULL); // Otherwise, the node would have to be NULL.
+ CAVL2_ASSERT((node->up != NULL) || (node == *root));
+ CAVL2_T* p = NULL; // The lowest parent node that suffered a shortening of its subtree.
+ bool r = false; // Which side of the above was shortened.
+ // The first step is to update the topology and remember the node where to start the retracing from later.
+ // Balancing is not performed yet so we may end up with an unbalanced tree.
+ if ((node->lr[0] != NULL) && (node->lr[1] != NULL)) {
+ CAVL2_T* const re = cavl2_extremum(node->lr[1], false);
+ CAVL2_ASSERT((re != NULL) && (NULL == re->lr[0]) && (re->up != NULL));
+ re->bf = node->bf;
+ re->lr[0] = node->lr[0];
+ re->lr[0]->up = re;
+ if (re->up != node) {
+ p = re->up; // Retracing starts with the ex-parent of our replacement node.
+ CAVL2_ASSERT(p->lr[0] == re);
+ p->lr[0] = re->lr[1]; // Reducing the height of the left subtree here.
+ if (p->lr[0] != NULL) {
+ p->lr[0]->up = p;
+ }
+ re->lr[1] = node->lr[1];
+ re->lr[1]->up = re;
+ r = false;
+ } else { // In this case, we are reducing the height of the right subtree, so r=1.
+ p = re; // Retracing starts with the replacement node itself as we are deleting its parent.
+ r = true; // The right child of the replacement node remains the same so we don't bother relinking it.
+ }
+ re->up = node->up;
+ if (re->up != NULL) {
+ re->up->lr[re->up->lr[1] == node] = re; // Replace link in the parent of node.
+ } else {
+ *root = re;
+ }
+ } else { // Either or both of the children are NULL.
+ p = node->up;
+ const bool rr = node->lr[1] != NULL;
+ if (node->lr[rr] != NULL) {
+ node->lr[rr]->up = p;
+ }
+ if (p != NULL) {
+ r = p->lr[1] == node;
+ p->lr[r] = node->lr[rr];
+ if (p->lr[r] != NULL) {
+ p->lr[r]->up = p;
+ }
+ } else {
+ *root = node->lr[rr];
+ }
+ }
+ // Now that the topology is updated, perform the retracing to restore balance. We climb up adjusting the
+ // balance factors until we reach the root or a parent whose balance factor becomes plus/minus one, which
+ // means that that parent was able to absorb the balance delta; in other words, the height of the outer
+ // subtree is unchanged, so upper balance factors shall be kept unchanged.
+ if (p != NULL) {
+ CAVL2_T* c = NULL;
+ for (;;) {
+ c = cavl2_impl_adjust_balance(p, !r);
+ p = c->up;
+ if ((c->bf != 0) || (NULL == p)) { // Reached the root or the height difference is absorbed by c.
+ break;
+ }
+ r = p->lr[1] == c;
+ }
+ if (NULL == p) {
+ CAVL2_ASSERT(c != NULL);
+ *root = c;
+ }
+ }
+ // Invalidate the node's pointers to indicate it is no longer in the tree.
+ node->up = NULL;
+ node->lr[0] = NULL;
+ node->lr[1] = NULL;
+ }
+}
+
+static inline void cavl2_replace(CAVL2_T** const root, CAVL2_T* const old_node, CAVL2_T* const new_node)
+{
+ if ((root != NULL) && (old_node != NULL) && (new_node != NULL)) {
+ CAVL2_ASSERT(*root != NULL); // Otherwise, old_node would have to be NULL.
+ CAVL2_ASSERT((old_node->up != NULL) || (old_node == *root)); // old_node must be in the tree.
+ CAVL2_ASSERT((new_node->up == NULL) && (new_node->lr[0] == NULL) && (new_node->lr[1] == NULL));
+ // Copy the structural data from the old node to the new node.
+ new_node->up = old_node->up;
+ new_node->lr[0] = old_node->lr[0];
+ new_node->lr[1] = old_node->lr[1];
+ new_node->bf = old_node->bf;
+ // Update the parent to point to the new node.
+ if (old_node->up != NULL) {
+ old_node->up->lr[old_node->up->lr[1] == old_node] = new_node;
+ } else {
+ *root = new_node;
+ }
+ // Update the children to point to the new parent.
+ if (old_node->lr[0] != NULL) {
+ old_node->lr[0]->up = new_node;
+ }
+ if (old_node->lr[1] != NULL) {
+ old_node->lr[1]->up = new_node;
+ }
+ // Invalidate the old node's pointers to indicate it is no longer in the tree.
+ old_node->up = NULL;
+ old_node->lr[0] = NULL;
+ old_node->lr[1] = NULL;
+ }
+}
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/tests/olga_scheduler_c_demo.c b/tests/olga_scheduler_c_demo.c
new file mode 100644
index 0000000..037255b
--- /dev/null
+++ b/tests/olga_scheduler_c_demo.c
@@ -0,0 +1,48 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "olga_scheduler.h"
+
+#include
+#include
+#include
+#include
+
+static int64_t get_microseconds(olga_t* sched)
+{
+ (void)sched;
+ struct timespec ts;
+ (void)clock_gettime(CLOCK_MONOTONIC, &ts);
+ return ((int64_t)ts.tv_sec * 1000000) + (ts.tv_nsec / 1000);
+}
+
+static void handler(olga_t* sched, olga_event_t* event, int64_t now)
+{
+ uint64_t* counter = (uint64_t*)event->user;
+ ++*counter;
+ printf("counter=%" PRIu64 " now=%" PRId64 "\n", *counter, now);
+ // Keep events triggering exactly at 1 Hz.
+ olga_defer(sched, event->deadline + 1000000, event->user, handler, event);
+}
+
+int main(void)
+{
+ olga_t sched;
+ olga_init(&sched, NULL, get_microseconds);
+
+ uint64_t counter = 0;
+ olga_event_t event = OLGA_EVENT_INIT;
+ olga_defer(&sched, sched.now(&sched) + 1000000, &counter, handler, &event);
+
+ for (;;) {
+ olga_spin_result_t spin_result = olga_spin(&sched);
+ (void)spin_result; // Optional performance information here.
+ const struct timespec delay = { .tv_sec = 0, .tv_nsec = 1000 * 1000 };
+ (void)nanosleep(&delay, NULL); // Do something else here: IO multiplexing, update scheduler stats, etc.
+ if (counter > 10) {
+ olga_cancel(&sched, &event);
+ puts("Event canceled");
+ break;
+ }
+ }
+ return 0;
+}
diff --git a/tests/test_olga_scheduler.cpp b/tests/test_olga_scheduler.cpp
index 691f9f0..039d51e 100644
--- a/tests/test_olga_scheduler.cpp
+++ b/tests/test_olga_scheduler.cpp
@@ -16,6 +16,7 @@
/// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include "olga_scheduler.hpp"
+#include "olga_scheduler.h"
#include
#include
@@ -28,29 +29,28 @@
#include
#include
+using testing::ElementsAre;
+using testing::ElementsAreArray;
using testing::Gt;
+using testing::IsEmpty;
+using testing::IsNull;
using testing::Le;
using testing::Ne;
-using testing::IsNull;
-using testing::IsEmpty;
-using testing::ElementsAre;
-using testing::ElementsAreArray;
// NOLINTBEGIN(bugprone-unchecked-optional-access)
// NOLINTBEGIN(readability-function-cognitive-complexity, misc-const-correctness)
// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers)
-namespace
-{
+namespace {
/// Expected type ID for the type returned from repeat(), poll() and defer().
-constexpr std::array event_type_id =
- {0xB6, 0x87, 0x48, 0xA6, 0x7A, 0xDB, 0x4D, 0xF1, 0xB3, 0x1D, 0xA9, 0x8D, 0x50, 0xA7, 0x82, 0x47};
+constexpr std::array event_type_id = { 0xB6, 0x87, 0x48, 0xA6, 0x7A, 0xDB, 0x4D, 0xF1,
+ 0xB3, 0x1D, 0xA9, 0x8D, 0x50, 0xA7, 0x82, 0x47 };
/// This clock has to keep global state to implement the TrivialClock trait.
class SteadyClockMock final
{
-public:
+ public:
using rep = std::int64_t;
using period = std::ratio<1, 1'000>;
using duration = std::chrono::duration;
@@ -73,10 +73,18 @@ class SteadyClockMock final
}
};
-} // namespace
+} // namespace
-namespace olga_scheduler::verification
+namespace olga_scheduler::verification {
+
+TEST(TestOlgaScheduler, OlgaEventInitCpp)
{
+ const olga_event_t event = OLGA_EVENT_INIT;
+ EXPECT_EQ(event.deadline, 0);
+ EXPECT_EQ(event.seqno, 0U);
+ EXPECT_EQ(event.user, nullptr);
+ EXPECT_EQ(event.handler, nullptr);
+}
TEST(TestOlgaScheduler, EventLoopBasic)
{
@@ -95,7 +103,7 @@ TEST(TestOlgaScheduler, EventLoopBasic)
std::optional c;
std::optional d;
- auto out = evl.spin(); // Nothing to do.
+ auto out = evl.spin(); // Nothing to do.
EXPECT_THAT(out.next_deadline, SteadyClockMock::time_point::max());
EXPECT_THAT(out.worst_lateness, SteadyClockMock::duration::zero());
EXPECT_THAT(out.approx_now.time_since_epoch(), 10'000ms);
@@ -111,7 +119,7 @@ TEST(TestOlgaScheduler, EventLoopBasic)
// Check the type ID of return type repeat().
EXPECT_THAT(decltype(evt_a)::_get_type_id_(), ElementsAreArray(event_type_id));
- auto evt_b = evl.repeat(100ms, // Smaller deadline goes on the left.
+ auto evt_b = evl.repeat(100ms, // Smaller deadline goes on the left.
[&](const auto& arg) { b.emplace(arg); });
EXPECT_THAT(evl.getTree()[0U]->getDeadline().time_since_epoch(), 10'100ms);
EXPECT_THAT(evl.getTree()[1U]->getDeadline().time_since_epoch(), 11'000ms);
@@ -122,18 +130,18 @@ TEST(TestOlgaScheduler, EventLoopBasic)
EXPECT_THAT(evl.getTree()[0U]->getDeadline().time_since_epoch(), 10'100ms);
EXPECT_THAT(evl.getTree()[1U]->getDeadline().time_since_epoch(), 11'000ms);
const auto* const f3 = evl.getTree()[2U];
- EXPECT_THAT(f3->getDeadline().time_since_epoch(), 12'000ms); // New entry.
+ EXPECT_THAT(f3->getDeadline().time_since_epoch(), 12'000ms); // New entry.
EXPECT_THAT(evl.getTree()[3U], IsNull());
- auto evt_d = evl.defer(SteadyClockMock::now() + 2000ms, // Same deadline!
+ auto evt_d = evl.defer(SteadyClockMock::now() + 2000ms, // Same deadline!
[&](const auto& arg) { d.emplace(arg); });
// EXPECT_THAT(evt_d, NotNull());
EXPECT_THAT(evl.getTree()[0U]->getDeadline().time_since_epoch(), 10'100ms);
EXPECT_THAT(evl.getTree()[1U]->getDeadline().time_since_epoch(), 11'000ms);
- EXPECT_THAT(evl.getTree()[2U], f3); // Entry added before this one.
+ EXPECT_THAT(evl.getTree()[2U], f3); // Entry added before this one.
const auto* const f4 = evl.getTree()[3U];
EXPECT_THAT(f3, Ne(f4));
- EXPECT_THAT(f4->getDeadline().time_since_epoch(), 12'000ms); // New entry, same deadline added later.
+ EXPECT_THAT(f4->getDeadline().time_since_epoch(), 12'000ms); // New entry, same deadline added later.
EXPECT_THAT(evl.getTree()[4U], IsNull());
// Poll but there are no pending Events yet.
@@ -173,7 +181,7 @@ TEST(TestOlgaScheduler, EventLoopBasic)
EXPECT_THAT(out.worst_lateness, 800ms);
EXPECT_THAT(out.approx_now.time_since_epoch(), 12'000ms);
EXPECT_THAT(evl.getTree()[0U]->getDeadline().time_since_epoch(), 12'100ms);
- EXPECT_THAT(evl.getTree().size(), 2); // C&D have left us.
+ EXPECT_THAT(evl.getTree().size(), 2); // C&D have left us.
EXPECT_TRUE(a);
EXPECT_THAT(a.value().deadline.time_since_epoch(), 12'000ms);
EXPECT_TRUE(b);
@@ -197,17 +205,17 @@ TEST(TestOlgaScheduler, EventLoopBasic)
EXPECT_THAT(SteadyClockMock::now().time_since_epoch(), 13'050ms);
EXPECT_THAT(evt_b.getDeadline(), Gt(Loop::time_point::min()));
evt_b.cancel();
- EXPECT_THAT(evt_b.getDeadline(), Loop::time_point::min()); // Unregistered, cleared.
- EXPECT_THAT(evl.getTree().size(), 1); // Freed already.
- evt_b.cancel(); // Idempotency.
- EXPECT_THAT(evt_b.getDeadline(), Loop::time_point::min()); // Ditto.
- EXPECT_THAT(evl.getTree().size(), 1); // Ditto.
+ EXPECT_THAT(evt_b.getDeadline(), Loop::time_point::min()); // Unregistered, cleared.
+ EXPECT_THAT(evl.getTree().size(), 1); // Freed already.
+ evt_b.cancel(); // Idempotency.
+ EXPECT_THAT(evt_b.getDeadline(), Loop::time_point::min()); // Ditto.
+ EXPECT_THAT(evl.getTree().size(), 1); // Ditto.
out = evl.spin();
- EXPECT_THAT(out.next_deadline.time_since_epoch(), 14'000ms); // B removed so the next one is A.
+ EXPECT_THAT(out.next_deadline.time_since_epoch(), 14'000ms); // B removed so the next one is A.
EXPECT_THAT(out.worst_lateness, 50ms);
EXPECT_THAT(out.approx_now.time_since_epoch(), 13'050ms);
EXPECT_THAT(evl.getTree()[0U]->getDeadline().time_since_epoch(), 14'000ms);
- EXPECT_THAT(1, evl.getTree().size()); // Second dropped.
+ EXPECT_THAT(1, evl.getTree().size()); // Second dropped.
EXPECT_TRUE(a);
EXPECT_THAT(a.value().deadline.time_since_epoch(), 13'000ms);
EXPECT_FALSE(b);
@@ -217,7 +225,7 @@ TEST(TestOlgaScheduler, EventLoopBasic)
// Nothing to do yet.
out = evl.spin();
- EXPECT_THAT(out.next_deadline.time_since_epoch(), 14'000ms); // Same up.
+ EXPECT_THAT(out.next_deadline.time_since_epoch(), 14'000ms); // Same up.
EXPECT_THAT(out.worst_lateness, 0ms);
EXPECT_THAT(out.approx_now.time_since_epoch(), 13'050ms);
EXPECT_FALSE(a);
@@ -251,7 +259,7 @@ TEST(TestOlgaScheduler, EventLoopTotalOrdering)
EXPECT_THAT(c, Le(b));
});
SteadyClockMock::advance(50ms);
- (void) evl.spin();
+ (void)evl.spin();
EXPECT_THAT(a, 5);
EXPECT_THAT(b, 5);
EXPECT_THAT(c, 5);
@@ -277,19 +285,19 @@ TEST(TestOlgaScheduler, EventLoopPoll)
SteadyClockMock::advance(30ms);
EXPECT_THAT(SteadyClockMock::now().time_since_epoch(), 130ms);
- (void) evl.spin();
+ (void)evl.spin();
EXPECT_TRUE(last_tp);
EXPECT_THAT(last_tp.value().time_since_epoch(), 110ms);
last_tp.reset();
- EXPECT_THAT(evl.getTree()[0U]->getDeadline().time_since_epoch(), 140ms); // Skipped ahead!
+ EXPECT_THAT(evl.getTree()[0U]->getDeadline().time_since_epoch(), 140ms); // Skipped ahead!
SteadyClockMock::advance(70ms);
EXPECT_THAT(SteadyClockMock::now().time_since_epoch(), 200ms);
- (void) evl.spin();
+ (void)evl.spin();
EXPECT_TRUE(last_tp);
EXPECT_THAT(last_tp.value().time_since_epoch(), 140ms);
last_tp.reset();
- EXPECT_THAT(evl.getTree()[0U]->getDeadline().time_since_epoch(), 210ms); // Skipped ahead!
+ EXPECT_THAT(evl.getTree()[0U]->getDeadline().time_since_epoch(), 210ms); // Skipped ahead!
}
TEST(TestOlgaScheduler, EventLoopDefer_single_overdue)
@@ -322,13 +330,13 @@ TEST(TestOlgaScheduler, EventLoopDefer_long_running_callback)
std::vector> calls;
- auto evt_a = evl.defer(SteadyClockMock::now() + 0ms, [&](const auto& arg) { //
+ auto evt_a = evl.defer(SteadyClockMock::now() + 0ms, [&](const auto& arg) { //
// Emulate that it took whole 100ms to execute "a" callback,
// so it will be already overdue for the next "b" event - should be executed as well.
calls.emplace_back("a", arg.deadline.time_since_epoch(), arg.approx_now.time_since_epoch());
SteadyClockMock::advance(100ms);
});
- auto evt_b = evl.defer(SteadyClockMock::now() + 20ms, [&](const auto& arg) { //
+ auto evt_b = evl.defer(SteadyClockMock::now() + 20ms, [&](const auto& arg) { //
calls.emplace_back("b", arg.deadline.time_since_epoch(), arg.approx_now.time_since_epoch());
});
@@ -338,7 +346,7 @@ TEST(TestOlgaScheduler, EventLoopDefer_long_running_callback)
EXPECT_THAT(out.approx_now.time_since_epoch(), 100ms);
EXPECT_THAT(calls,
- ElementsAre(std::make_tuple("a", 0ms, 0ms), //
+ ElementsAre(std::make_tuple("a", 0ms, 0ms), //
std::make_tuple("b", 20ms, 100ms)));
}
@@ -366,12 +374,12 @@ TEST(TestOlgaScheduler, HandleMovement)
SteadyClockMock::advance(1000ms);
EXPECT_TRUE(a);
- a.reset(); // Destroy a
+ a.reset(); // Destroy a
EXPECT_FALSE(a);
- (void) evl.spin();
+ (void)evl.spin();
EXPECT_THAT(evl.getTree().size(), 2);
EXPECT_THAT(calls,
- ElementsAre(std::make_tuple("b", 333ms * 1, 1000ms), //
+ ElementsAre(std::make_tuple("b", 333ms * 1, 1000ms), //
std::make_tuple("c", 337ms * 1, 1000ms),
std::make_tuple("b", 333ms * 2, 1000ms),
std::make_tuple("c", 337ms * 2, 1000ms),
@@ -379,12 +387,12 @@ TEST(TestOlgaScheduler, HandleMovement)
calls.clear();
EXPECT_TRUE(b);
- auto d = std::move(b); // b moved into d
+ auto d = std::move(b); // b moved into d
EXPECT_TRUE(d);
SteadyClockMock::advance(1000ms);
- (void) evl.spin();
- EXPECT_THAT(evl.getTree().size(), 2); // No change -- references moved but inferiors are kept alive.
+ (void)evl.spin();
+ EXPECT_THAT(evl.getTree().size(), 2); // No change -- references moved but inferiors are kept alive.
EXPECT_THAT(calls,
ElementsAre(std::make_tuple("c", 337ms * 3, 2000ms),
std::make_tuple("b", 333ms * 4, 2000ms),
@@ -395,12 +403,12 @@ TEST(TestOlgaScheduler, HandleMovement)
calls.clear();
EXPECT_TRUE(c);
- c.reset(); // Destroy c
+ c.reset(); // Destroy c
EXPECT_FALSE(c);
SteadyClockMock::advance(1000ms);
- (void) evl.spin();
- EXPECT_THAT(evl.getTree().size(), 1); // c destroyed, only b left alive (now in d).
+ (void)evl.spin();
+ EXPECT_THAT(evl.getTree().size(), 1); // c destroyed, only b left alive (now in d).
EXPECT_THAT(calls,
ElementsAre(std::make_tuple("b", 333ms * 7, 3000ms),
std::make_tuple("b", 333ms * 8, 3000ms),
@@ -409,12 +417,12 @@ TEST(TestOlgaScheduler, HandleMovement)
d.reset();
SteadyClockMock::advance(1000ms);
- (void) evl.spin();
+ (void)evl.spin();
EXPECT_THAT(evl.getTree().size(), 0);
EXPECT_THAT(calls, IsEmpty());
}
-} // namespace olga_scheduler::verification
+} // namespace olga_scheduler::verification
// NOLINTEND(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers)
// NOLINTEND(readability-function-cognitive-complexity, misc-const-correctness)
diff --git a/tests/test_olga_scheduler_c.cpp b/tests/test_olga_scheduler_c.cpp
new file mode 100644
index 0000000..df29922
--- /dev/null
+++ b/tests/test_olga_scheduler_c.cpp
@@ -0,0 +1,395 @@
+/// Source: https://github.com/zubax/olga_scheduler
+///
+/// Copyright (c) 2024 Zubax Robotics
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+/// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
+/// and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in all copies or substantial portions of
+/// the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+/// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+/// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+/// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+#include "olga_scheduler.h"
+
+#include
+
+#include
+#include
+
+// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers)
+
+namespace {
+
+struct TestClock final
+{
+ int64_t now;
+};
+
+int64_t clock_now(olga_t* const sched) { return static_cast(sched->user)->now; }
+
+void clock_advance(TestClock* const clock, const int64_t delta) { clock->now += delta; }
+
+struct CallLog final
+{
+ std::vector ids;
+ std::vector times;
+};
+
+struct CallbackCtx final
+{
+ CallLog* log;
+ int id;
+ int64_t expected_deadline;
+ TestClock* clock;
+ int64_t advance_by;
+};
+
+void record_handler(olga_t* const sched, olga_event_t* const event, const int64_t now)
+{
+ (void)sched;
+ auto* const ctx = static_cast(event->user);
+ if (ctx->log != nullptr) {
+ ctx->log->ids.push_back(ctx->id);
+ ctx->log->times.push_back(now);
+ }
+ if (ctx->expected_deadline != INT64_MIN) {
+ EXPECT_EQ(event->deadline, ctx->expected_deadline);
+ EXPECT_GE(now, ctx->expected_deadline);
+ }
+ if ((ctx->advance_by != 0) && (ctx->clock != nullptr)) {
+ ctx->clock->now += ctx->advance_by;
+ }
+}
+
+} // namespace
+
+TEST(OlgaSchedulerC, EmptySpin)
+{
+ TestClock clock{ .now = 10'000 };
+ olga_t sched;
+ olga_init(&sched, &clock, clock_now);
+
+ const olga_spin_result_t out = olga_spin(&sched);
+ EXPECT_EQ(out.next_deadline, INT64_MAX);
+ EXPECT_EQ(out.worst_lateness, 0);
+ EXPECT_EQ(out.now, INT64_MIN);
+}
+
+TEST(OlgaSchedulerC, ComparatorOrdering)
+{
+ olga_event_t a{};
+ olga_event_t b{};
+
+ a.deadline = 10;
+ a.seqno = 1;
+ b.deadline = 20;
+ b.seqno = 0;
+ EXPECT_LT(olga_private_compare(&a, &b.base), 0);
+ EXPECT_GT(olga_private_compare(&b, &a.base), 0);
+
+ b.deadline = 10;
+ b.seqno = 2;
+ EXPECT_LT(olga_private_compare(&a, &b.base), 0);
+
+ b.seqno = 1;
+ EXPECT_EQ(olga_private_compare(&a, &b.base), 0);
+}
+
+TEST(OlgaSchedulerC, BasicOrdering)
+{
+ TestClock clock{ .now = 10'000 };
+ olga_t sched;
+ olga_init(&sched, &clock, clock_now);
+
+ CallLog log{};
+
+ CallbackCtx ctx_a{ .log = &log, .id = 1, .expected_deadline = 11'000, .clock = &clock, .advance_by = 0 };
+ CallbackCtx ctx_b{ .log = &log, .id = 2, .expected_deadline = 10'100, .clock = &clock, .advance_by = 0 };
+ CallbackCtx ctx_c{ .log = &log, .id = 3, .expected_deadline = 12'000, .clock = &clock, .advance_by = 0 };
+ CallbackCtx ctx_d{ .log = &log, .id = 4, .expected_deadline = 12'000, .clock = &clock, .advance_by = 0 };
+
+ olga_event_t evt_a = OLGA_EVENT_INIT;
+ olga_event_t evt_b = OLGA_EVENT_INIT;
+ olga_event_t evt_c = OLGA_EVENT_INIT;
+ olga_event_t evt_d = OLGA_EVENT_INIT;
+
+ olga_defer(&sched, ctx_a.expected_deadline, &ctx_a, record_handler, &evt_a);
+ olga_defer(&sched, ctx_b.expected_deadline, &ctx_b, record_handler, &evt_b);
+ olga_defer(&sched, ctx_c.expected_deadline, &ctx_c, record_handler, &evt_c);
+ olga_defer(&sched, ctx_d.expected_deadline, &ctx_d, record_handler, &evt_d);
+
+ olga_spin_result_t out = olga_spin(&sched);
+ EXPECT_EQ(out.next_deadline, 10'100);
+ EXPECT_EQ(out.worst_lateness, 0);
+ EXPECT_EQ(out.now, 10'000);
+ EXPECT_TRUE(log.ids.empty());
+
+ clock_advance(&clock, 1'100);
+ out = olga_spin(&sched);
+ EXPECT_EQ(out.next_deadline, 12'000);
+ EXPECT_EQ(out.worst_lateness, 1'000);
+ EXPECT_EQ(out.now, 11'100);
+ EXPECT_EQ(log.ids, (std::vector{ 2, 1 }));
+ EXPECT_EQ(log.times, (std::vector{ 11'100, 11'100 }));
+ log.ids.clear();
+ log.times.clear();
+
+ clock_advance(&clock, 900);
+ out = olga_spin(&sched);
+ EXPECT_EQ(out.next_deadline, INT64_MAX);
+ EXPECT_EQ(out.worst_lateness, 0);
+ EXPECT_EQ(out.now, 12'000);
+ EXPECT_EQ(log.ids, (std::vector{ 3, 4 }));
+ EXPECT_EQ(log.times, (std::vector{ 12'000, 12'000 }));
+}
+
+TEST(OlgaSchedulerC, FifoSameDeadline)
+{
+ TestClock clock{ .now = 0 };
+ olga_t sched;
+ olga_init(&sched, &clock, clock_now);
+
+ CallLog log{};
+ CallbackCtx ctx_a{ .log = &log, .id = 1, .expected_deadline = 1'000, .clock = &clock, .advance_by = 0 };
+ CallbackCtx ctx_b{ .log = &log, .id = 2, .expected_deadline = 1'000, .clock = &clock, .advance_by = 0 };
+ CallbackCtx ctx_c{ .log = &log, .id = 3, .expected_deadline = 1'000, .clock = &clock, .advance_by = 0 };
+
+ olga_event_t evt_a = OLGA_EVENT_INIT;
+ olga_event_t evt_b = OLGA_EVENT_INIT;
+ olga_event_t evt_c = OLGA_EVENT_INIT;
+
+ olga_defer(&sched, ctx_a.expected_deadline, &ctx_a, record_handler, &evt_a);
+ olga_defer(&sched, ctx_b.expected_deadline, &ctx_b, record_handler, &evt_b);
+ olga_defer(&sched, ctx_c.expected_deadline, &ctx_c, record_handler, &evt_c);
+
+ clock.now = 1'000;
+ const olga_spin_result_t out = olga_spin(&sched);
+ EXPECT_EQ(out.next_deadline, INT64_MAX);
+ EXPECT_EQ(out.worst_lateness, 0);
+ EXPECT_EQ(out.now, 1'000);
+ EXPECT_EQ(log.ids, (std::vector{ 1, 2, 3 }));
+}
+
+TEST(OlgaSchedulerC, Cancel)
+{
+ TestClock clock{ .now = 0 };
+ olga_t sched;
+ olga_init(&sched, &clock, clock_now);
+
+ CallLog log{};
+ CallbackCtx ctx_a{ .log = &log, .id = 1, .expected_deadline = 100, .clock = &clock, .advance_by = 0 };
+ CallbackCtx ctx_b{ .log = &log, .id = 2, .expected_deadline = 200, .clock = &clock, .advance_by = 0 };
+
+ olga_event_t evt_a = OLGA_EVENT_INIT;
+ olga_event_t evt_b = OLGA_EVENT_INIT;
+
+ olga_defer(&sched, ctx_a.expected_deadline, &ctx_a, record_handler, &evt_a);
+ olga_defer(&sched, ctx_b.expected_deadline, &ctx_b, record_handler, &evt_b);
+
+ olga_cancel(&sched, &evt_a);
+
+ clock.now = 200;
+ const olga_spin_result_t out = olga_spin(&sched);
+ EXPECT_EQ(out.next_deadline, INT64_MAX);
+ EXPECT_EQ(out.worst_lateness, 0);
+ EXPECT_EQ(out.now, 200);
+ EXPECT_EQ(log.ids, (std::vector{ 2 }));
+}
+
+TEST(OlgaSchedulerC, IsPending)
+{
+ TestClock clock{ .now = 0 };
+ olga_t sched;
+ olga_init(&sched, &clock, clock_now);
+
+ CallbackCtx ctx{ .log = nullptr, .id = 0, .expected_deadline = INT64_MIN, .clock = &clock, .advance_by = 0 };
+ olga_event_t evt = OLGA_EVENT_INIT;
+
+ EXPECT_FALSE(olga_is_pending(&sched, &evt));
+
+ olga_defer(&sched, 100, &ctx, record_handler, &evt);
+ EXPECT_TRUE(olga_is_pending(&sched, &evt));
+
+ olga_cancel(&sched, &evt);
+ EXPECT_FALSE(olga_is_pending(&sched, &evt));
+
+ olga_defer(&sched, 100, &ctx, record_handler, &evt);
+ EXPECT_TRUE(olga_is_pending(&sched, &evt));
+
+ clock.now = 100;
+ (void)olga_spin(&sched);
+ EXPECT_FALSE(olga_is_pending(&sched, &evt));
+}
+
+TEST(OlgaSchedulerC, OverdueSingle)
+{
+ TestClock clock{ .now = 0 };
+ olga_t sched;
+ olga_init(&sched, &clock, clock_now);
+
+ CallLog log{};
+ CallbackCtx ctx{ .log = &log, .id = 1, .expected_deadline = 1'000, .clock = &clock, .advance_by = 0 };
+ olga_event_t evt = OLGA_EVENT_INIT;
+
+ olga_defer(&sched, ctx.expected_deadline, &ctx, record_handler, &evt);
+
+ clock.now = 1'030;
+ const olga_spin_result_t out = olga_spin(&sched);
+ EXPECT_EQ(out.next_deadline, INT64_MAX);
+ EXPECT_EQ(out.worst_lateness, 30);
+ EXPECT_EQ(out.now, 1'030);
+ EXPECT_EQ(log.ids, (std::vector{ 1 }));
+ EXPECT_EQ(log.times, (std::vector{ 1'030 }));
+ EXPECT_EQ(evt.deadline, ctx.expected_deadline);
+}
+
+TEST(OlgaSchedulerC, LongRunningCallback)
+{
+ TestClock clock{ .now = 0 };
+ olga_t sched;
+ olga_init(&sched, &clock, clock_now);
+
+ CallLog log{};
+ CallbackCtx ctx_a{ .log = &log, .id = 1, .expected_deadline = 0, .clock = &clock, .advance_by = 100 };
+ CallbackCtx ctx_b{ .log = &log, .id = 2, .expected_deadline = 20, .clock = &clock, .advance_by = 0 };
+
+ olga_event_t evt_a = OLGA_EVENT_INIT;
+ olga_event_t evt_b = OLGA_EVENT_INIT;
+
+ olga_defer(&sched, ctx_a.expected_deadline, &ctx_a, record_handler, &evt_a);
+ olga_defer(&sched, ctx_b.expected_deadline, &ctx_b, record_handler, &evt_b);
+
+ const olga_spin_result_t out = olga_spin(&sched);
+ EXPECT_EQ(out.next_deadline, INT64_MAX);
+ EXPECT_EQ(out.worst_lateness, 80);
+ EXPECT_EQ(out.now, 100);
+ EXPECT_EQ(log.ids, (std::vector{ 1, 2 }));
+ EXPECT_EQ(log.times, (std::vector{ 0, 100 }));
+}
+
+TEST(OlgaSchedulerC, WorstLatenessKeepsMax)
+{
+ TestClock clock{ .now = 100 };
+ olga_t sched;
+ olga_init(&sched, &clock, clock_now);
+
+ CallLog log{};
+ CallbackCtx ctx_a{ .log = &log, .id = 1, .expected_deadline = 0, .clock = &clock, .advance_by = 0 };
+ CallbackCtx ctx_b{ .log = &log, .id = 2, .expected_deadline = 60, .clock = &clock, .advance_by = 0 };
+
+ olga_event_t evt_a{};
+ olga_event_t evt_b{};
+
+ olga_defer(&sched, ctx_a.expected_deadline, &ctx_a, record_handler, &evt_a);
+ olga_defer(&sched, ctx_b.expected_deadline, &ctx_b, record_handler, &evt_b);
+
+ const olga_spin_result_t out = olga_spin(&sched);
+ EXPECT_EQ(out.next_deadline, INT64_MAX);
+ EXPECT_EQ(out.worst_lateness, 100);
+ EXPECT_EQ(out.now, 100);
+ EXPECT_EQ(log.ids, (std::vector{ 1, 2 }));
+ EXPECT_EQ(log.times, (std::vector{ 100, 100 }));
+}
+
+TEST(OlgaSchedulerC, RescheduleAlreadyScheduled)
+{
+ TestClock clock{ .now = 0 };
+ olga_t sched;
+ olga_init(&sched, &clock, clock_now);
+
+ CallLog log{};
+ CallbackCtx ctx_a{ .log = &log, .id = 1, .expected_deadline = INT64_MIN, .clock = &clock, .advance_by = 0 };
+ CallbackCtx ctx_b{ .log = &log, .id = 2, .expected_deadline = INT64_MIN, .clock = &clock, .advance_by = 0 };
+
+ olga_event_t evt_a = OLGA_EVENT_INIT;
+ olga_event_t evt_b = OLGA_EVENT_INIT;
+
+ // Schedule event A at 100 and event B at 200
+ olga_defer(&sched, 100, &ctx_a, record_handler, &evt_a);
+ olga_defer(&sched, 200, &ctx_b, record_handler, &evt_b);
+
+ // Reschedule event A to 300 (later than B)
+ olga_defer(&sched, 300, &ctx_a, record_handler, &evt_a);
+
+ // Now spin at time 250 - only B should fire (at 200)
+ clock.now = 250;
+ olga_spin_result_t out = olga_spin(&sched);
+ EXPECT_EQ(out.next_deadline, 300); // A is rescheduled to 300
+ EXPECT_EQ(out.worst_lateness, 50);
+ EXPECT_EQ(out.now, 250);
+ EXPECT_EQ(log.ids, (std::vector{ 2 })); // Only B fired
+ EXPECT_EQ(log.times, (std::vector{ 250 }));
+ log.ids.clear();
+ log.times.clear();
+
+ // Advance to 350 and spin - now A should fire
+ clock.now = 350;
+ out = olga_spin(&sched);
+ EXPECT_EQ(out.next_deadline, INT64_MAX);
+ EXPECT_EQ(out.worst_lateness, 50);
+ EXPECT_EQ(out.now, 350);
+ EXPECT_EQ(log.ids, (std::vector{ 1 })); // A fired
+ EXPECT_EQ(log.times, (std::vector{ 350 }));
+}
+
+TEST(OlgaSchedulerC, RescheduleBeforeAndAfter)
+{
+ TestClock clock{ .now = 0 };
+ olga_t sched;
+ olga_init(&sched, &clock, clock_now);
+
+ CallLog log{};
+ CallbackCtx ctx{ .log = &log, .id = 1, .expected_deadline = INT64_MIN, .clock = &clock, .advance_by = 0 };
+
+ olga_event_t evt = OLGA_EVENT_INIT;
+
+ // Schedule event at 500
+ olga_defer(&sched, 500, &ctx, record_handler, &evt);
+
+ // Reschedule to earlier time (100)
+ olga_defer(&sched, 100, &ctx, record_handler, &evt);
+
+ // Spin at 150 - event should fire at 100
+ clock.now = 150;
+ const olga_spin_result_t out = olga_spin(&sched);
+ EXPECT_EQ(out.next_deadline, INT64_MAX);
+ EXPECT_EQ(out.worst_lateness, 50);
+ EXPECT_EQ(out.now, 150);
+ EXPECT_EQ(log.ids, (std::vector{ 1 }));
+ EXPECT_EQ(log.times, (std::vector{ 150 }));
+}
+
+TEST(OlgaSchedulerC, MultipleReschedules)
+{
+ TestClock clock{ .now = 0 };
+ olga_t sched;
+ olga_init(&sched, &clock, clock_now);
+
+ CallLog log{};
+ CallbackCtx ctx{ .log = &log, .id = 1, .expected_deadline = INT64_MIN, .clock = &clock, .advance_by = 0 };
+
+ olga_event_t evt = OLGA_EVENT_INIT;
+
+ // Schedule, reschedule multiple times
+ olga_defer(&sched, 100, &ctx, record_handler, &evt);
+ olga_defer(&sched, 200, &ctx, record_handler, &evt);
+ olga_defer(&sched, 150, &ctx, record_handler, &evt);
+ olga_defer(&sched, 300, &ctx, record_handler, &evt);
+
+ // Spin at 350 - event should fire once at 300
+ clock.now = 350;
+ const olga_spin_result_t out = olga_spin(&sched);
+ EXPECT_EQ(out.next_deadline, INT64_MAX);
+ EXPECT_EQ(out.worst_lateness, 50);
+ EXPECT_EQ(out.now, 350);
+ EXPECT_EQ(log.ids, (std::vector{ 1 })); // Fired once
+ EXPECT_EQ(log.times, (std::vector{ 350 }));
+}
+
+// NOLINTEND(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers)