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
This commit is contained in:
Norman Feske 2024-03-07 13:30:03 +01:00 committed by Christian Helmuth
parent 3e46cf5664
commit 44e0d7003a
11 changed files with 559 additions and 0 deletions

View File

@ -0,0 +1 @@
Scenario that tests 'Genode::Alarm_registry'

View File

@ -0,0 +1,2 @@
_/src/init
_/src/test-alarm

View File

@ -0,0 +1 @@
2024-03-13 c282e266e79dc12297300db0f9eddbfff3d83d1c

View File

@ -0,0 +1,23 @@
<runtime ram="32M" caps="1000" binary="test-alarm">
<fail after_seconds="20"/>
<succeed>
[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.
</succeed>
<content>
<rom label="ld.lib.so"/>
<rom label="test-alarm"/>
</content>
<config/>
</runtime>

View File

@ -0,0 +1,2 @@
SRC_DIR = src/test/alarm src/include/base/internal
include $(GENODE_DIR)/repos/base/recipes/src/content.inc

View File

@ -0,0 +1 @@
2024-03-13 89f82089c7e23c62627f04af81fdc19ee17bfad3

View File

@ -0,0 +1 @@
base

View File

@ -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 <base/log.h>
namespace Genode { template <typename, typename> 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 '<unsigned>' numeric value
* where '<unsigned>' can be an unsigned integer of any byte width.
* The clock provides the following interface:
*
* ! static constexpr <unsigned> MASK = <lsb-bitmask>;
* ! <unsigned> 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 <typename T, typename CLOCK>
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<Clock, None>;
class Element : Avl_node<Element>
{
private:
Alarm_registry &_registry;
T &_obj;
friend class Alarm_registry;
friend class Avl_node<Element>;
friend class Avl_tree<Element>;
public:
Clock time;
Element(Alarm_registry &registry, 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<Element> _elements { };
static void _with_first(auto &registry, 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 &registry, 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_ */

View File

@ -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 <base/component.h>
#include <base/heap.h>
#include <base/log.h>
/* base-internal includes */
#include <base/internal/xoroshiro.h>
#include <base/internal/alarm_registry.h>
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<Alarm, Clock>;
struct Alarm : Alarms::Element
{
using Name = String<64>;
Name const name;
Alarm(auto &registry, 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<Alarm> 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.");
}

View File

@ -0,0 +1,4 @@
TARGET = test-alarm
SRC_CC = main.cc
LIBS = base
INC_DIR += $(REP_DIR)/src/include

View File

@ -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