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 @@ [![Main Workflow](https://github.com/zubax/olga_scheduler/actions/workflows/main.yml/badge.svg)](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.

Olga of Kiev -

\ 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)