mirror of
https://github.com/genodelabs/genode.git
synced 2025-04-07 19:34:56 +00:00
gems: utility for implementing LRU-based caches
This patch extracts the LRU cache implementation from the 'Cached_font' so that it becomes reusable for other applications. Fixes #3117
This commit is contained in:
parent
6b4d76739e
commit
ece5cbbbb5
@ -11,11 +11,10 @@
|
||||
* under the terms of the GNU Affero General Public License version 3.
|
||||
*/
|
||||
|
||||
#ifndef _INCLUDE__GEMS__CACHED_FONT_T_
|
||||
#define _INCLUDE__GEMS__CACHED_FONT_T_
|
||||
#ifndef _INCLUDE__GEMS__CACHED_FONT_H_
|
||||
#define _INCLUDE__GEMS__CACHED_FONT_H_
|
||||
|
||||
#include <base/output.h>
|
||||
#include <base/allocator.h>
|
||||
#include <gems/lru_cache.h>
|
||||
#include <nitpicker_gfx/text_painter.h>
|
||||
|
||||
namespace Genode { class Cached_font; }
|
||||
@ -23,178 +22,103 @@ namespace Genode { class Cached_font; }
|
||||
|
||||
class Genode::Cached_font : public Text_painter::Font
|
||||
{
|
||||
public:
|
||||
|
||||
struct Stats
|
||||
{
|
||||
unsigned misses;
|
||||
unsigned hits;
|
||||
unsigned consumed_bytes;
|
||||
|
||||
void print(Output &out) const
|
||||
{
|
||||
Genode::print(out, "used: ", consumed_bytes/1024, " KiB, "
|
||||
"hits: ", hits, ", misses: ", misses);
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
typedef Text_painter::Area Area;
|
||||
typedef Text_painter::Font Font;
|
||||
typedef Text_painter::Glyph Glyph;
|
||||
|
||||
struct Time { unsigned value; };
|
||||
|
||||
Allocator &_alloc;
|
||||
Font const &_font;
|
||||
size_t const _limit;
|
||||
Time mutable _now { 0 };
|
||||
Stats mutable _stats { };
|
||||
|
||||
class Cached_glyph : Avl_node<Cached_glyph>
|
||||
struct Cached_glyph : Glyph, Noncopyable
|
||||
{
|
||||
private:
|
||||
Glyph::Opacity _values[];
|
||||
|
||||
friend class Avl_node<Cached_glyph>;
|
||||
friend class Avl_tree<Cached_glyph>;
|
||||
friend class Cached_font;
|
||||
/*
|
||||
* The number of values is not statically known but runtime-
|
||||
* dependent. The values are stored directly after the
|
||||
* 'Cached_glyph' object.
|
||||
*/
|
||||
|
||||
Codepoint const _codepoint;
|
||||
Glyph const _glyph;
|
||||
Time _last_used;
|
||||
|
||||
Glyph::Opacity _values[];
|
||||
|
||||
bool _higher(Codepoint const other) const
|
||||
{
|
||||
return _codepoint.value > other.value;
|
||||
}
|
||||
|
||||
unsigned _importance(Time now) const
|
||||
{
|
||||
return now.value - _last_used.value;
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
Cached_glyph(Codepoint c, Glyph const &glyph, Time now)
|
||||
:
|
||||
_codepoint(c),
|
||||
_glyph({ .width = glyph.width,
|
||||
.height = glyph.height,
|
||||
.vpos = glyph.vpos,
|
||||
.advance = glyph.advance,
|
||||
.values = _values }),
|
||||
_last_used(now)
|
||||
{
|
||||
for (unsigned i = 0; i < glyph.num_values(); i++)
|
||||
_values[i] = glyph.values[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Avl_node interface
|
||||
*/
|
||||
bool higher(Cached_glyph *c) { return _higher(c->_codepoint); }
|
||||
|
||||
Cached_glyph *find_by_codepoint(Codepoint requested)
|
||||
{
|
||||
if (_codepoint.value == requested.value) return this;
|
||||
|
||||
Cached_glyph *c = Avl_node<Cached_glyph>::child(_higher(requested));
|
||||
|
||||
return c ? c->find_by_codepoint(requested) : nullptr;
|
||||
}
|
||||
|
||||
Cached_glyph *find_least_recently_used(Time now)
|
||||
{
|
||||
Cached_glyph *result = this;
|
||||
|
||||
for (unsigned i = 0; i < 2; i++) {
|
||||
Cached_glyph *c = Avl_node<Cached_glyph>::child(i);
|
||||
if (c && c->_importance(now) > result->_importance(now))
|
||||
result = c;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void mark_as_used(Time now) { _last_used = now; }
|
||||
|
||||
void apply(Font::Apply_fn const &fn) const { fn.apply(_glyph); }
|
||||
Cached_glyph(Glyph const &glyph)
|
||||
:
|
||||
Glyph({ .width = glyph.width,
|
||||
.height = glyph.height,
|
||||
.vpos = glyph.vpos,
|
||||
.advance = glyph.advance,
|
||||
.values = _values })
|
||||
{
|
||||
for (unsigned i = 0; i < glyph.num_values(); i++)
|
||||
_values[i] = glyph.values[i];
|
||||
}
|
||||
};
|
||||
|
||||
Avl_tree<Cached_glyph> mutable _avl_tree { };
|
||||
Font const &_font;
|
||||
|
||||
size_t const _opacity_values_size = 4*_font.bounding_box().count();
|
||||
|
||||
/**
|
||||
* Size of one cache entry in bytes
|
||||
* Allocator wrapper that inflates each allocation with a byte padding
|
||||
*/
|
||||
size_t const _alloc_size = sizeof(Cached_glyph)
|
||||
+ 4*_font.bounding_box().count();
|
||||
struct Padding_allocator : Allocator
|
||||
{
|
||||
size_t const _padding_bytes;
|
||||
|
||||
Allocator &_alloc;
|
||||
|
||||
size_t _consumed_bytes = 0;
|
||||
|
||||
size_t _padded(size_t size) const { return size + _padding_bytes; }
|
||||
|
||||
Padding_allocator(Allocator &alloc, size_t padding_bytes)
|
||||
: _padding_bytes(padding_bytes), _alloc(alloc) { }
|
||||
|
||||
size_t consumed_bytes() const { return _consumed_bytes; }
|
||||
|
||||
bool alloc(size_t size, void **out_addr) override
|
||||
{
|
||||
size = _padded(size);
|
||||
|
||||
bool const result = _alloc.alloc(size, out_addr);
|
||||
|
||||
if (result) {
|
||||
memset(*out_addr, 0, size);
|
||||
_consumed_bytes += size + overhead(size);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t consumed() const override { return _alloc.consumed(); }
|
||||
|
||||
size_t overhead(size_t size) const override { return _alloc.overhead(size); };
|
||||
|
||||
void free(void *addr, size_t size) override
|
||||
{
|
||||
size = _padded(size);
|
||||
|
||||
_alloc.free(addr, size);
|
||||
_consumed_bytes -= size + overhead(size);
|
||||
}
|
||||
|
||||
bool need_size_for_free() const override { return _alloc.need_size_for_free(); }
|
||||
};
|
||||
|
||||
Padding_allocator _padding_alloc;
|
||||
|
||||
typedef Lru_cache<Codepoint, Cached_glyph> Cache;
|
||||
|
||||
Cache mutable _cache;
|
||||
|
||||
/**
|
||||
* Add cache entry for the given glyph
|
||||
*
|
||||
* \throw Out_of_ram
|
||||
* \throw Out_of_caps
|
||||
* Return number of cache elements that fit in 'avail_bytes'
|
||||
*/
|
||||
void _insert(Codepoint codepoint, Glyph const &glyph)
|
||||
Cache::Size _cache_size(size_t const avail_bytes)
|
||||
{
|
||||
auto const cached_glyph_ptr = (Cached_glyph *)_alloc.alloc(_alloc_size);
|
||||
size_t const element_size = Cache::element_size() + _opacity_values_size;
|
||||
|
||||
_stats.consumed_bytes += _alloc_size;
|
||||
size_t const bytes_per_element = element_size + _padding_alloc.overhead(element_size);
|
||||
|
||||
memset(cached_glyph_ptr, 0, _alloc_size);
|
||||
|
||||
construct_at<Cached_glyph>(cached_glyph_ptr, codepoint, glyph, _now);
|
||||
|
||||
_avl_tree.insert(cached_glyph_ptr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict glyph from cache
|
||||
*/
|
||||
void _remove(Cached_glyph &glyph)
|
||||
{
|
||||
_avl_tree.remove(&glyph);
|
||||
|
||||
glyph.~Cached_glyph();
|
||||
|
||||
_alloc.free(&glyph, _alloc_size);
|
||||
|
||||
_stats.consumed_bytes -= _alloc_size;
|
||||
}
|
||||
|
||||
Cached_glyph *_find_by_codepoint(Codepoint codepoint)
|
||||
{
|
||||
if (!_avl_tree.first())
|
||||
return nullptr;
|
||||
|
||||
return _avl_tree.first()->find_by_codepoint(codepoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evice least recently used glyph from cache
|
||||
*
|
||||
* \return true if a glyph was released
|
||||
*/
|
||||
bool _remove_least_recently_used()
|
||||
{
|
||||
if (!_avl_tree.first())
|
||||
return false;
|
||||
|
||||
Cached_glyph *glyph = _avl_tree.first()->find_least_recently_used(_now);
|
||||
if (!glyph)
|
||||
return false; /* this should never happen */
|
||||
|
||||
_remove(*glyph);
|
||||
_stats.misses++;
|
||||
return true;
|
||||
}
|
||||
|
||||
void _remove_all()
|
||||
{
|
||||
while (Cached_glyph *glyph_ptr = _avl_tree.first())
|
||||
_remove(*glyph_ptr);
|
||||
/* bytes_per_element can never be zero */
|
||||
return Cache::Size { avail_bytes / bytes_per_element };
|
||||
}
|
||||
|
||||
public:
|
||||
@ -210,44 +134,38 @@ class Genode::Cached_font : public Text_painter::Font
|
||||
*/
|
||||
Cached_font(Allocator &alloc, Font const &font, Limit limit)
|
||||
:
|
||||
_alloc(alloc), _font(font), _limit(limit.value)
|
||||
_font(font),
|
||||
_padding_alloc(alloc, _opacity_values_size),
|
||||
_cache(_padding_alloc, _cache_size(limit.value))
|
||||
{ }
|
||||
|
||||
~Cached_font() { _remove_all(); }
|
||||
struct Stats
|
||||
{
|
||||
Cache::Stats cache_stats;
|
||||
size_t consumed_bytes;
|
||||
|
||||
Stats stats() const { return _stats; }
|
||||
void print(Output &out) const
|
||||
{
|
||||
Genode::print(out, "used: ", consumed_bytes/1024, " KiB, ", cache_stats);
|
||||
}
|
||||
};
|
||||
|
||||
Stats stats() const
|
||||
{
|
||||
return Stats { _cache.stats(), _padding_alloc.consumed_bytes() };
|
||||
}
|
||||
|
||||
void _apply_glyph(Codepoint c, Apply_fn const &fn) const override
|
||||
{
|
||||
_now.value++;
|
||||
|
||||
/*
|
||||
* Try to lookup glyph from the cache. If it is missing, fill cache
|
||||
* with requested glyph and repeat the lookup. When under memory
|
||||
* pressure, flush least recently used glyphs from cache.
|
||||
*
|
||||
* Even though '_apply_glyph' is a const method, the internal cache
|
||||
* and stats must of course be mutated. Hence the 'const_cast'.
|
||||
*/
|
||||
Cached_font &mutable_this = const_cast<Cached_font &>(*this);
|
||||
|
||||
/* retry once after handling a cache miss */
|
||||
for (int i = 0; i < 2; i++) {
|
||||
|
||||
if (Cached_glyph *glyph_ptr = mutable_this._find_by_codepoint(c)) {
|
||||
glyph_ptr->apply(fn);
|
||||
glyph_ptr->mark_as_used(_now);
|
||||
_stats.hits += (i == 0);
|
||||
return;
|
||||
}
|
||||
|
||||
while (_stats.consumed_bytes + _alloc_size > _limit)
|
||||
if (!mutable_this._remove_least_recently_used())
|
||||
break;
|
||||
auto hit_fn = [&] (Glyph const &glyph) { fn.apply(glyph); };
|
||||
|
||||
auto miss_fn = [&] (Cache::Missing_element &missing_element)
|
||||
{
|
||||
_font.apply_glyph(c, [&] (Glyph const &glyph) {
|
||||
mutable_this._insert(c, glyph); });
|
||||
}
|
||||
missing_element.construct(glyph); });
|
||||
};
|
||||
|
||||
(void)_cache.try_apply(c, hit_fn, miss_fn);
|
||||
}
|
||||
|
||||
Advance_info advance_info(Codepoint c) const override
|
||||
@ -255,7 +173,6 @@ class Genode::Cached_font : public Text_painter::Font
|
||||
unsigned width = 0;
|
||||
Text_painter::Fixpoint_number advance { 0 };
|
||||
|
||||
/* go through the '_apply_glyph' cache-fill mechanism */
|
||||
Font::apply_glyph(c, [&] (Glyph const &glyph) {
|
||||
width = glyph.width, advance = glyph.advance; });
|
||||
|
||||
@ -267,4 +184,4 @@ class Genode::Cached_font : public Text_painter::Font
|
||||
Area bounding_box() const override { return _font.bounding_box(); }
|
||||
};
|
||||
|
||||
#endif /* _INCLUDE__GEMS__CACHED_FONT_T_ */
|
||||
#endif /* _INCLUDE__GEMS__CACHED_FONT_H_ */
|
||||
|
307
repos/gems/include/gems/lru_cache.h
Normal file
307
repos/gems/include/gems/lru_cache.h
Normal file
@ -0,0 +1,307 @@
|
||||
/*
|
||||
* \brief Cache with a least-recently-used eviction policy
|
||||
* \author Norman Feske
|
||||
* \date 2019-01-12
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2019 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__GEMS__LRU_CACHE_H_
|
||||
#define _INCLUDE__GEMS__LRU_CACHE_H_
|
||||
|
||||
#include <base/output.h>
|
||||
#include <base/allocator.h>
|
||||
|
||||
|
||||
namespace Genode { template <typename, typename> class Lru_cache; }
|
||||
|
||||
|
||||
template <typename KEY, typename ELEM>
|
||||
class Genode::Lru_cache : Noncopyable
|
||||
{
|
||||
public:
|
||||
|
||||
struct Stats
|
||||
{
|
||||
unsigned hits, evictions;
|
||||
|
||||
void print(Output &out) const
|
||||
{
|
||||
Genode::print(out, "hits: ", hits, ", evictions: ", evictions);
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
struct Time { unsigned value; };
|
||||
|
||||
Allocator &_alloc;
|
||||
unsigned const _max_elements;
|
||||
unsigned _used_elements = 0;
|
||||
Time _now { 0 };
|
||||
Stats _stats { };
|
||||
|
||||
class Tag
|
||||
{
|
||||
protected:
|
||||
|
||||
KEY const _key;
|
||||
|
||||
Time _last_used;
|
||||
|
||||
Tag(KEY const &key, Time now) : _key(key), _last_used(now) { }
|
||||
};
|
||||
|
||||
/*
|
||||
* The '_key' and '_last_used' attributes are supplemented as the 'Tag'
|
||||
* base class to the 'Element'instead of being 'Element' member
|
||||
* variables to allow 'ELEM' to be at the trailing end of the object.
|
||||
* This way, 'ELEM' can be a variable-length type (using a flexible
|
||||
* array member).
|
||||
*/
|
||||
|
||||
class Element : private Tag, private Avl_node<Element>, public ELEM
|
||||
{
|
||||
private:
|
||||
|
||||
friend class Avl_node<Element>;
|
||||
friend class Avl_tree<Element>;
|
||||
friend class Lru_cache;
|
||||
|
||||
bool _higher(KEY const &other) const
|
||||
{
|
||||
return Tag::_key.value > other.value;
|
||||
}
|
||||
|
||||
unsigned _importance(Time now) const
|
||||
{
|
||||
return now.value - Tag::_last_used.value;
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
template <typename... ARGS>
|
||||
Element(KEY key, Time now, ARGS &&... args)
|
||||
: Tag(key, now), ELEM(args...) { }
|
||||
|
||||
/**
|
||||
* Avl_node interface
|
||||
*/
|
||||
bool higher(Element *e) { return _higher(e->_key); }
|
||||
|
||||
template <typename FN>
|
||||
bool try_apply(KEY const &key, FN const &fn)
|
||||
{
|
||||
if (Tag::_key.value == key.value) {
|
||||
fn(*this);
|
||||
return true;
|
||||
}
|
||||
|
||||
Element * const e = Avl_node<Element>::child(_higher(key));
|
||||
|
||||
return e && e->try_apply(key, fn);
|
||||
}
|
||||
|
||||
template <typename FN>
|
||||
void with_least_recently_used(Time now, FN const fn)
|
||||
{
|
||||
Element *result = this;
|
||||
|
||||
for (unsigned i = 0; i < 2; i++) {
|
||||
Element *e = Avl_node<Element>::child(i);
|
||||
if (e && e->_importance(now) > result->_importance(now))
|
||||
result = e;
|
||||
}
|
||||
|
||||
if (result)
|
||||
fn(*result);
|
||||
}
|
||||
|
||||
void mark_as_used(Time now) { Tag::_last_used = now; }
|
||||
};
|
||||
|
||||
Avl_tree<Element> mutable _avl_tree { };
|
||||
|
||||
/**
|
||||
* Add cache entry for the given key
|
||||
*
|
||||
* \param ARGS constructor arguments passed to new ELEM
|
||||
*
|
||||
* \throw Out_of_ram
|
||||
* \throw Out_of_caps
|
||||
*/
|
||||
template <typename... ARGS>
|
||||
void _insert(KEY key, ARGS &... args)
|
||||
{
|
||||
auto const element_ptr = (Element *)_alloc.alloc(sizeof(Element));
|
||||
|
||||
_used_elements++;
|
||||
|
||||
construct_at<Element>(element_ptr, key, _now, args...);
|
||||
|
||||
_avl_tree.insert(element_ptr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict element from cache
|
||||
*/
|
||||
void _remove(Element &element)
|
||||
{
|
||||
_avl_tree.remove(&element);
|
||||
|
||||
element.~Element();
|
||||
|
||||
_alloc.free(&element, sizeof(Element));
|
||||
|
||||
_used_elements--;
|
||||
_stats.evictions++;
|
||||
}
|
||||
|
||||
template <typename FN>
|
||||
bool _try_apply(KEY const &key, FN const &fn)
|
||||
{
|
||||
if (!_avl_tree.first())
|
||||
return false;
|
||||
|
||||
return _avl_tree.first()->try_apply(key, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict least recently used element from cache
|
||||
*
|
||||
* \return true if an element was released
|
||||
*/
|
||||
bool _remove_least_recently_used()
|
||||
{
|
||||
if (!_avl_tree.first())
|
||||
return false;
|
||||
|
||||
_avl_tree.first()->with_least_recently_used(_now, [&] (Element &e) {
|
||||
_remove(e); });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void _remove_all()
|
||||
{
|
||||
while (Element *element_ptr = _avl_tree.first())
|
||||
_remove(*element_ptr);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
struct Size { size_t value; };
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* \param alloc backing store for cache elements
|
||||
* \param size maximum number of cache elements
|
||||
*/
|
||||
Lru_cache(Allocator &alloc, Size size)
|
||||
: _alloc(alloc), _max_elements(size.value) { }
|
||||
|
||||
~Lru_cache() { _remove_all(); }
|
||||
|
||||
/**
|
||||
* Return size of a single cache entry including the meta data
|
||||
*
|
||||
* The returned value is useful for cache-dimensioning calculations.
|
||||
*/
|
||||
static constexpr size_t element_size() { return sizeof(Element); }
|
||||
|
||||
/**
|
||||
* Return usage stats
|
||||
*/
|
||||
Stats stats() const { return _stats; }
|
||||
|
||||
/**
|
||||
* Interface presented to the cache-miss handler to construct an
|
||||
* element
|
||||
*/
|
||||
class Missing_element : Noncopyable
|
||||
{
|
||||
private:
|
||||
|
||||
friend class Lru_cache;
|
||||
|
||||
Lru_cache &_cache;
|
||||
KEY const &_key;
|
||||
|
||||
Missing_element(Lru_cache &cache, KEY const &key)
|
||||
: _cache(cache), _key(key) { }
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Populate cache with new element
|
||||
*
|
||||
* \param ARGS arguments passed to the 'ELEM' constructor
|
||||
*/
|
||||
template <typename... ARGS>
|
||||
void construct(ARGS &... args) { _cache._insert(_key, args...); }
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply functor 'hit_fn' to element with matching 'key'
|
||||
*
|
||||
* \param miss_fn cache-miss handler
|
||||
*
|
||||
* \return true if 'hit_fn' got executed
|
||||
*
|
||||
* If the requested 'key' is not present in the cache, the cache-miss
|
||||
* handler is called with the interface 'Missing_element &' as
|
||||
* argument. By calling 'Missing_element::construct', the handler is
|
||||
* able to fill the cache with the missing element. After resolving a
|
||||
* cache miss, 'hit_fn' is called for the freshly inserted element.
|
||||
*
|
||||
* If an occurring cache miss is not handled, 'hit_fn' is not called.
|
||||
*/
|
||||
template <typename HIT_FN, typename MISS_FN>
|
||||
bool try_apply(KEY key, HIT_FN const &hit_fn, MISS_FN const &miss_fn)
|
||||
{
|
||||
_now.value++;
|
||||
|
||||
/*
|
||||
* Try to look up element from the cache. If it is missing, fill
|
||||
* cache with requested element and repeat the lookup. When under
|
||||
* memory pressure, evict the least recently used element from the
|
||||
* cache.
|
||||
*/
|
||||
|
||||
/* retry once after handling a cache miss */
|
||||
for (unsigned i = 0; i < 2; i++) {
|
||||
|
||||
bool const hit = _try_apply(key, [&] (Element &element) {
|
||||
hit_fn(element);
|
||||
element.mark_as_used(_now);
|
||||
_stats.hits += (i == 0);
|
||||
});
|
||||
|
||||
if (hit)
|
||||
return true;
|
||||
|
||||
/*
|
||||
* Handle cache miss
|
||||
*/
|
||||
|
||||
/* evict element if the cache is fully populated */
|
||||
while (_used_elements >= _max_elements)
|
||||
if (!_remove_least_recently_used())
|
||||
break;
|
||||
|
||||
/* fetch missing element into the cache */
|
||||
Missing_element missing_element { *this, key };
|
||||
miss_fn(missing_element);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
#endif /* _INCLUDE__GEMS__LRU_CACHE_H_ */
|
Loading…
x
Reference in New Issue
Block a user