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