mirror of
https://github.com/genodelabs/genode.git
synced 2025-01-18 02:40:08 +00:00
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:
parent
3e46cf5664
commit
44e0d7003a
1
repos/base/recipes/pkg/test-alarm/README
Normal file
1
repos/base/recipes/pkg/test-alarm/README
Normal file
@ -0,0 +1 @@
|
||||
Scenario that tests 'Genode::Alarm_registry'
|
2
repos/base/recipes/pkg/test-alarm/archives
Normal file
2
repos/base/recipes/pkg/test-alarm/archives
Normal file
@ -0,0 +1,2 @@
|
||||
_/src/init
|
||||
_/src/test-alarm
|
1
repos/base/recipes/pkg/test-alarm/hash
Normal file
1
repos/base/recipes/pkg/test-alarm/hash
Normal file
@ -0,0 +1 @@
|
||||
2024-03-13 c282e266e79dc12297300db0f9eddbfff3d83d1c
|
23
repos/base/recipes/pkg/test-alarm/runtime
Normal file
23
repos/base/recipes/pkg/test-alarm/runtime
Normal 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>
|
2
repos/base/recipes/src/test-alarm/content.mk
Normal file
2
repos/base/recipes/src/test-alarm/content.mk
Normal file
@ -0,0 +1,2 @@
|
||||
SRC_DIR = src/test/alarm src/include/base/internal
|
||||
include $(GENODE_DIR)/repos/base/recipes/src/content.inc
|
1
repos/base/recipes/src/test-alarm/hash
Normal file
1
repos/base/recipes/src/test-alarm/hash
Normal file
@ -0,0 +1 @@
|
||||
2024-03-13 89f82089c7e23c62627f04af81fdc19ee17bfad3
|
1
repos/base/recipes/src/test-alarm/used_apis
Normal file
1
repos/base/recipes/src/test-alarm/used_apis
Normal file
@ -0,0 +1 @@
|
||||
base
|
281
repos/base/src/include/base/internal/alarm_registry.h
Normal file
281
repos/base/src/include/base/internal/alarm_registry.h
Normal 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 ®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<Element> _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_ */
|
242
repos/base/src/test/alarm/main.cc
Normal file
242
repos/base/src/test/alarm/main.cc
Normal 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 ®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<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.");
|
||||
}
|
4
repos/base/src/test/alarm/target.mk
Normal file
4
repos/base/src/test/alarm/target.mk
Normal file
@ -0,0 +1,4 @@
|
||||
TARGET = test-alarm
|
||||
SRC_CC = main.cc
|
||||
LIBS = base
|
||||
INC_DIR += $(REP_DIR)/src/include
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user