From 44e0d7003a54f0eb2cdb87257b925851583436e2 Mon Sep 17 00:00:00 2001 From: Norman Feske Date: Thu, 7 Mar 2024 13:30:03 +0100 Subject: [PATCH] base: Alarm_registry data structure This data structure uses an AVL tree to maintain a time-sorted set of alarm objects. It supports the use of circular clocks of an bit width. Issue #5138 --- repos/base/recipes/pkg/test-alarm/README | 1 + repos/base/recipes/pkg/test-alarm/archives | 2 + repos/base/recipes/pkg/test-alarm/hash | 1 + repos/base/recipes/pkg/test-alarm/runtime | 23 ++ repos/base/recipes/src/test-alarm/content.mk | 2 + repos/base/recipes/src/test-alarm/hash | 1 + repos/base/recipes/src/test-alarm/used_apis | 1 + .../include/base/internal/alarm_registry.h | 281 ++++++++++++++++++ repos/base/src/test/alarm/main.cc | 242 +++++++++++++++ repos/base/src/test/alarm/target.mk | 4 + repos/gems/run/depot_autopilot.run | 1 + 11 files changed, 559 insertions(+) create mode 100644 repos/base/recipes/pkg/test-alarm/README create mode 100644 repos/base/recipes/pkg/test-alarm/archives create mode 100644 repos/base/recipes/pkg/test-alarm/hash create mode 100644 repos/base/recipes/pkg/test-alarm/runtime create mode 100644 repos/base/recipes/src/test-alarm/content.mk create mode 100644 repos/base/recipes/src/test-alarm/hash create mode 100644 repos/base/recipes/src/test-alarm/used_apis create mode 100644 repos/base/src/include/base/internal/alarm_registry.h create mode 100644 repos/base/src/test/alarm/main.cc create mode 100644 repos/base/src/test/alarm/target.mk diff --git a/repos/base/recipes/pkg/test-alarm/README b/repos/base/recipes/pkg/test-alarm/README new file mode 100644 index 0000000000..6cf8638c7b --- /dev/null +++ b/repos/base/recipes/pkg/test-alarm/README @@ -0,0 +1 @@ +Scenario that tests 'Genode::Alarm_registry' diff --git a/repos/base/recipes/pkg/test-alarm/archives b/repos/base/recipes/pkg/test-alarm/archives new file mode 100644 index 0000000000..a39876e781 --- /dev/null +++ b/repos/base/recipes/pkg/test-alarm/archives @@ -0,0 +1,2 @@ +_/src/init +_/src/test-alarm diff --git a/repos/base/recipes/pkg/test-alarm/hash b/repos/base/recipes/pkg/test-alarm/hash new file mode 100644 index 0000000000..7218c0b90c --- /dev/null +++ b/repos/base/recipes/pkg/test-alarm/hash @@ -0,0 +1 @@ +2024-03-13 c282e266e79dc12297300db0f9eddbfff3d83d1c diff --git a/repos/base/recipes/pkg/test-alarm/runtime b/repos/base/recipes/pkg/test-alarm/runtime new file mode 100644 index 0000000000..06a4794ba8 --- /dev/null +++ b/repos/base/recipes/pkg/test-alarm/runtime @@ -0,0 +1,23 @@ + + + + + [init] in range [1...3]: a1 + [init] in range [1...3]: a2 + [init] in range [1...3]: a3 + [init] in range [3...1]: a3 + [init] in range [3...1]: a4 + [init] in range [3...1]: a0 + [init] in range [3...1]: a1* + [init] soonest(5) -> 0* + [init] Test succeeded. + + + + + + + + + + diff --git a/repos/base/recipes/src/test-alarm/content.mk b/repos/base/recipes/src/test-alarm/content.mk new file mode 100644 index 0000000000..2d461e96a4 --- /dev/null +++ b/repos/base/recipes/src/test-alarm/content.mk @@ -0,0 +1,2 @@ +SRC_DIR = src/test/alarm src/include/base/internal +include $(GENODE_DIR)/repos/base/recipes/src/content.inc diff --git a/repos/base/recipes/src/test-alarm/hash b/repos/base/recipes/src/test-alarm/hash new file mode 100644 index 0000000000..acdbbba24c --- /dev/null +++ b/repos/base/recipes/src/test-alarm/hash @@ -0,0 +1 @@ +2024-03-13 89f82089c7e23c62627f04af81fdc19ee17bfad3 diff --git a/repos/base/recipes/src/test-alarm/used_apis b/repos/base/recipes/src/test-alarm/used_apis new file mode 100644 index 0000000000..df967b96a5 --- /dev/null +++ b/repos/base/recipes/src/test-alarm/used_apis @@ -0,0 +1 @@ +base diff --git a/repos/base/src/include/base/internal/alarm_registry.h b/repos/base/src/include/base/internal/alarm_registry.h new file mode 100644 index 0000000000..4c1eb8c241 --- /dev/null +++ b/repos/base/src/include/base/internal/alarm_registry.h @@ -0,0 +1,281 @@ +/* + * \brief Registry of time-sorted alarms + * \author Norman Feske + * \date 2024-03-06 + */ + +/* + * Copyright (C) 2024 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _INCLUDE__BASE__INTERNAL__ALARM_REGISTRY_H_ +#define _INCLUDE__BASE__INTERNAL__ALARM_REGISTRY_H_ + +/* Genode includes */ +#include + +namespace Genode { template class Alarm_registry; } + + +/** + * Registry of schedules alarm objects + * + * \param T alarm type, must be derived from 'Alarm_registry::Element' + * \param CLOCK type representing a circular clock + * + * The registry represents a set of scheduled alarms. An alarm object is + * scheduled upon creation and de-scheduled on destruction. + * + * The 'CLOCK' type must be constructible with an '' numeric value + * where '' can be an unsigned integer of any byte width. + * The clock provides the following interface: + * + * ! static constexpr MASK = ; + * ! value() const; + * ! void print(Output &) const; + * + * 'MASK' defines the limit of the circular clock. + * The 'value()' method returns a number between 0 and MASK. + * The 'print' method is needed only when using 'Alarm_registry::print'. + * In this case, the alarm type must also provide a 'print' method. + */ +template +class Genode::Alarm_registry : Noncopyable +{ + private: + + using Clock = CLOCK; + + struct Range + { + Clock start, end; /* range [start,end] where 'start' >= 'end' */ + + void with_intersection(Range const other, auto const &fn) const + { + auto const f = max(start.value(), other.start.value()); + auto const t = min(end.value(), other.end.value()); + + if (f <= t) + fn(Range { Clock { f }, Clock { t } }); + } + + bool contains(Clock const time) const + { + return (time.value() >= start.value()) + && (time.value() <= end.value()); + } + + void print(Output &out) const + { + Genode::print(out, "[", start.value(), "...", end.value(), "]"); + } + }; + + public: + + struct None { }; + using Soonest_result = Attempt; + + class Element : Avl_node + { + private: + + Alarm_registry &_registry; + + T &_obj; + + friend class Alarm_registry; + friend class Avl_node; + friend class Avl_tree; + + public: + + Clock time; + + Element(Alarm_registry ®istry, T &obj, Clock time) + : + _registry(registry), _obj(obj), time(time) + { + _registry._elements.insert(this); + } + + ~Element() + { + _registry._elements.remove(this); + } + + /** + * Avl_node ordering, allow duplicated keys + */ + bool higher(Element const * const other) const + { + return time.value() <= other->time.value(); + } + + void print(Output &out) const + { + Genode::print(out, _obj, ": time=", time); + } + + private: + + static void _with_child(auto &element, bool side, auto const &fn) + { + if (element.child(side)) + fn(*element.child(side)); + } + + void _for_each(Range const range, auto const &fn) const + { + _with_child(*this, this->LEFT, [&] (Element const &child) { + range.with_intersection({ Clock { }, time }, [&] (Range l_range) { + child._for_each(l_range, fn); }); }); + + if (range.contains(time)) + fn(_obj); + + _with_child(*this, this->RIGHT, [&] (Element const &child) { + range.with_intersection({ time, Clock { Clock::MASK }}, [&] (Range r_range) { + child._for_each(r_range, fn); }); }); + } + + Element *_find_any(Range const range) + { + if (range.contains(time)) + return this; + + Element *result = nullptr; + + _with_child(*this, this->LEFT, [&] (Element &child) { + range.with_intersection({ Clock { }, time }, [&] (Range l_range) { + result = child._find_any(l_range); }); }); + + if (result) + return result; + + _with_child(*this, this->RIGHT, [&] (Element &child) { + range.with_intersection({ time, Clock { Clock::MASK }}, [&] (Range r_range) { + result = child._find_any(r_range); }); }); + + return result; + } + + Soonest_result _soonest(Clock const now) const + { + Soonest_result result = None { }; + + if (time.value() >= now.value()) { + result = time; + _with_child(*this, this->LEFT, [&] (Element const &child) { + child._soonest(now).with_result( + [&] (Clock left_soonest) { + if (time.value() > left_soonest.value()) + result = left_soonest; }, + [&] (None) { }); }); + } else { + _with_child(*this, this->RIGHT, [&] (Element const &child) { + result = child._soonest(now); }); + } + + return result; + } + }; + + private: + + Avl_tree _elements { }; + + static void _with_first(auto ®istry, auto const &fn) + { + if (registry._elements.first()) + fn(*registry._elements.first()); + } + + /* + * Call 'fn' up to two times, with the first element and a search range + * as argument. + */ + static void _for_each_search_range(auto ®istry, Clock start, Clock end, + auto const &fn) + { + _with_first(registry, [&] (auto &first) { + + if (start.value() <= end.value()) { + fn(first, Range { start, end }); + + } else if (start.value() > end.value()) { + fn(first, Range { start, Clock { Clock::MASK } }); + fn(first, Range { Clock { }, end }); + } + }); + } + + public: + + /** + * Return soonest alarm time from 'now' + */ + Soonest_result soonest(Clock const now) const + { + Soonest_result result = None { }; + + _with_first(*this, [&] (auto &first) { + first._soonest(now).with_result( + [&] (Clock soonest) { + result = soonest; }, + [&] (None) { /* clock wrapped, search from beginning */ + result = first._soonest(Clock { }); }); }); + + return result; + } + + /** + * Call 'fn' for each alarm scheduled between 'start' and 'end' + * + * The 'start' and 'end' values may wrap. + */ + void for_each_in_range(Clock const start, Clock const end, auto const &fn) const + { + _for_each_search_range(*this, start, end, [&] (Element const &e, Range range) { + e._for_each(range, fn); }); + } + + /** + * Call 'fn' with any alarm scheduled between 'start' and 'end' + * + * \return true if 'fn' was called + * + * The found alarm is passed to 'fn' as non-const reference, which + * allows the caller to modify or destroy it. + * + * The return value is handy for calling 'with_any_in_range' as a + * condition of a while loop, purging all alarms within the time + * window. + */ + bool with_any_in_range(Clock const start, Clock const end, auto const &fn) + { + Element *found_ptr = nullptr; + _for_each_search_range(*this, start, end, [&] (Element &e, Range range) { + if (!found_ptr) + found_ptr = e._find_any(range); }); + + if (found_ptr) + fn(found_ptr->_obj); + + return found_ptr != nullptr; + } + + void print(Output &out) const + { + bool first = true; + for_each_in_range(Clock { }, Clock { Clock::MASK }, [&] (Element const &e) { + Genode::print(out, first ? "" : "\n", e); + first = false; + }); + } +}; + +#endif /* _INCLUDE__BASE__INTERNAL__ALARM_REGISTRY_H_ */ diff --git a/repos/base/src/test/alarm/main.cc b/repos/base/src/test/alarm/main.cc new file mode 100644 index 0000000000..35d1b80e81 --- /dev/null +++ b/repos/base/src/test/alarm/main.cc @@ -0,0 +1,242 @@ +/* + * \brief Alarm data-structure test + * \author Norman Feske + * \date 2024-03-06 + */ + +/* + * Copyright (C) 2024 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +/* Genode includes */ +#include +#include +#include + +/* base-internal includes */ +#include +#include + + +namespace Test { + + using namespace Genode; + + struct Clock + { + unsigned _value; + + static constexpr unsigned LIMIT_LOG2 = 4, + LIMIT = 1 << LIMIT_LOG2, + MASK = LIMIT - 1; + + unsigned value() const { return _value & MASK; } + + void print(Genode::Output &out) const { Genode::print(out, _value); } + }; + + struct Alarm; + using Alarms = Alarm_registry; + + struct Alarm : Alarms::Element + { + using Name = String<64>; + + Name const name; + + Alarm(auto ®istry, Name const &name, Clock time) + : + Alarms::Element(registry, *this, time), + name(name) + { } + + void print(Output &out) const + { + Genode::print(out, name); + } + }; +} + + +void Component::construct(Genode::Env &env) +{ + using namespace Test; + + struct Panic { }; + + Xoroshiro_128_plus random { 0 }; + + Alarms alarms { }; + + /* + * Test searching alarms defined for a circular clock, and the + * searching for the alarm scheduled next from a given time. + */ + { + Alarm a0 { alarms, "a0", Clock { 0 } }, + a1 { alarms, "a1", Clock { 1 } }, + a2 { alarms, "a2", Clock { 2 } }, + a3 { alarms, "a3", Clock { 3 } }; + + log(alarms); + + { + Alarm a4 { alarms, "a4", Clock { 4 } }; + log(alarms); + + alarms.for_each_in_range({ 1 }, { 3 }, [&] (Alarm const &alarm) { + log("in range [1...3]: ", alarm); }); + + alarms.for_each_in_range({ 3 }, { 1 }, [&] (Alarm const &alarm) { + log("in range [3...1]: ", alarm); }); + + for (unsigned i = 0; i < 6; i++) + alarms.soonest(Clock { i }).with_result( + [&] (Clock const &time) { + log("soonest(", i, ") -> ", time); }, + [&] (Alarms::None) { + log("soonest(", i, ") -> none"); + } + ); + + /* a4 removed */ + } + log(alarms); + + /* a0...a3 removed */ + } + + auto check_no_alarms_present = [&] + { + alarms.soonest(Clock { }).with_result( + [&] (Clock const &time) { + error("soonest exepectedly returned ", time); }, + [&] (Alarms::None) { + log("soonest expectedly returned None"); + } + ); + }; + + check_no_alarms_present(); + + /* + * Create random alarms, in particular featuring the same time values. + * This stress-tests the AVL tree's ability to handle duplicated keys. + */ + { + unsigned const N = 100; + Constructible array[N] { }; + + auto check_consistency = [&] (unsigned const expected_count) + { + Clock time { }; + unsigned count = 0; + alarms.for_each_in_range({ 0 }, { Clock::MASK }, [&] (Alarm const &alarm) { + count++; + if (alarm.time.value() < time.value()) { + error("alarms are unexpectedly not ordered"); + throw Panic { }; + } + time = alarm.time; + }); + + if (count != expected_count) { + error("foreach visited ", count, " alarms, expected ", expected_count); + throw Panic { }; + } + }; + + /* construct alarms with random times */ + for (unsigned total = 0; total < N; ) { + Clock const time { unsigned(random.value()) % Clock::MASK }; + array[total++].construct(alarms, Alarm::Name("a", total), time); + check_consistency(total); + } + + log(alarms); + + /* destruct alarms in random order */ + for (unsigned total = N; total > 0; total--) { + + check_consistency(total); + + /* pick Nth still existing element */ + unsigned const nth = (total*uint16_t(random.value())) >> 16; + + for (unsigned count = 0, i = 0; i < N; i++) { + if (array[i].constructed()) { + if (count == nth) { + array[i].destruct(); + break; + } + count++; + } + } + } + + check_no_alarms_present(); + } + + /* + * Test the purging of all alarms in a given time window + */ + { + Heap heap { env.ram(), env.rm() }; + + unsigned const N = 1000; + + /* schedule alarms for the whole time range */ + for (unsigned total = 0; total < N; total++) { + Clock const time { unsigned(random.value()) % Clock::MASK }; + new (heap) Alarm(alarms, Alarm::Name("a", total), time); + } + + auto histogram_of_scheduled_alarms = [&] (unsigned expected_total) + { + unsigned total = 0; + for (unsigned i = 0; i < Clock::MASK; i++) { + unsigned count = 0; + alarms.for_each_in_range({ i }, { i }, [&] (Alarm const &) { + count++; }); + log("time ", i, ": ", count, " alarms"); + total += count; + } + if (total != expected_total) { + error("total number of ", total, " alarms, expected ", expected_total); + throw Panic { }; + } + }; + + histogram_of_scheduled_alarms(N); + + unsigned triggered = 0; + while (alarms.with_any_in_range({ 12 }, { 3 }, [&] (Alarm &alarm) { + triggered++; + destroy(heap, &alarm); + })); + + log("after purging all alarms in time window 12...3:"); + histogram_of_scheduled_alarms(N - triggered); + + /* check absence of any alarms in purged range */ + { + unsigned count = 0; + alarms.for_each_in_range({ 12 }, { 3 }, [&] (Alarm const &) { + count++; }); + + if (count != 0) { + error("range of purged alarms unexpectedly not empty"); + throw Panic { }; + } + } + + /* clear up heap */ + while (alarms.with_any_in_range({ 0 }, { Clock::MASK }, [&] (Alarm &alarm) { + destroy(heap, &alarm); })); + } + + log("Test succeeded."); +} diff --git a/repos/base/src/test/alarm/target.mk b/repos/base/src/test/alarm/target.mk new file mode 100644 index 0000000000..d11b1141a3 --- /dev/null +++ b/repos/base/src/test/alarm/target.mk @@ -0,0 +1,4 @@ +TARGET = test-alarm +SRC_CC = main.cc +LIBS = base +INC_DIR += $(REP_DIR)/src/include diff --git a/repos/gems/run/depot_autopilot.run b/repos/gems/run/depot_autopilot.run index e3a877c1f7..cc36c43847 100644 --- a/repos/gems/run/depot_autopilot.run +++ b/repos/gems/run/depot_autopilot.run @@ -649,6 +649,7 @@ set default_test_pkgs { test-spark test-spark_exception test-spark_secondary_stack + test-alarm test-black_hole test-clipboard test-depot_query_index