Add new generic LRU cache to the client

This commit is contained in:
Michael Gratton 2019-03-17 13:48:15 +11:00 committed by Michael James Gratton
parent 41d061c8ff
commit 73159ba091
6 changed files with 197 additions and 0 deletions

View file

@ -91,6 +91,7 @@ src/client/sidebar/sidebar-count-cell-renderer.vala
src/client/sidebar/sidebar-entry.vala
src/client/sidebar/sidebar-tree.vala
src/client/util/util-avatar.vala
src/client/util/util-cache.vala
src/client/util/util-date.vala
src/client/util/util-email.vala
src/client/util/util-files.vala

View file

@ -96,6 +96,7 @@ geary_client_vala_sources = files(
'sidebar/sidebar-tree.vala',
'util/util-avatar.vala',
'util/util-cache.vala',
'util/util-date.vala',
'util/util-email.vala',
'util/util-files.vala',

View file

@ -0,0 +1,122 @@
/*
* Copyright 2019 Michael Gratton <mike@vee.net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/** A simple least-recently-used cache. */
public class Util.Cache.Lru<T> : Geary.BaseObject {
private class CacheEntry<T> {
public static int lru_compare(CacheEntry<T> a, CacheEntry<T> b) {
return (a.key == b.key)
? 0 : (int) (a.last_used - b.last_used);
}
public string key;
public T value;
public int64 last_used;
public CacheEntry(string key, T value, int64 last_used) {
this.key = key;
this.value = value;
this.last_used = last_used;
}
}
/** Specifies the maximum number of cache entries to be stored. */
public uint max_size { get; set; }
/** Determines if the cache has any entries or not. */
public bool is_empty {
get { return this.cache.is_empty; }
}
/** Determines the current number of cache entries. */
public uint size {
get { return this.cache.size; }
}
private Gee.Map<string,CacheEntry<T>> cache =
new Gee.HashMap<string,CacheEntry<T>>();
private Gee.SortedSet<CacheEntry<T>> ordering =
new Gee.TreeSet<CacheEntry<T>>(CacheEntry.lru_compare);
/**
* Creates a new least-recently-used cache with the given size.
*/
public Lru(uint max_size) {
this.max_size = max_size;
}
/**
* Sets an entry in the cache, replacing any existing entry.
*
* The entry is added to the back of the removal queue. If adding
* the entry causes the size of the cache to exceed the maximum,
* the entry at the front of the queue will be evicted.
*/
public void set_entry(string key, T value) {
int64 now = GLib.get_monotonic_time();
CacheEntry<T> entry = new CacheEntry<T>(key, value, now);
this.cache.set(key, entry);
this.ordering.add(entry);
// Prune if needed
if (this.cache.size > this.max_size) {
CacheEntry oldest = this.ordering.first();
this.cache.unset(oldest.key);
this.ordering.remove(oldest);
}
}
/**
* Returns the entry from the cache, if found.
*
* If the entry was found, it is move to the back of the removal
* queue.
*/
public T get_entry(string key) {
int64 now = GLib.get_monotonic_time();
CacheEntry<T>? entry = this.cache.get(key);
T value = null;
if (entry != null) {
value = entry.value;
// Need to remove the entry from the ordering before
// updating the last used time since doing so changes the
// ordering
this.ordering.remove(entry);
entry.last_used = now;
this.ordering.add(entry);
}
return value;
}
/** Removes an entry from the cache and returns it, if found. */
public T remove_entry(string key) {
CacheEntry<T>? entry = null;
T value = null;
this.cache.unset(key, out entry);
if (entry != null) {
this.ordering.remove(entry);
value = entry.value;
}
return value;
}
/** Evicts all entries in the cache. */
public void clear() {
this.cache.clear();
this.ordering.clear();
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright 2019 Michael Gratton <mike@vee.net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Util.Cache.Test : TestCase {
public Test() {
base("UtilCacheTest");
add_test("lru_insertion", lru_insertion);
add_test("lru_eviction", lru_eviction);
}
public void lru_insertion() throws GLib.Error {
const string A = "a";
const string B = "b";
const string C = "c";
const string D = "d";
Lru<string> test_article = new Lru<string>(2);
assert_true(test_article.is_empty);
assert_uint(0, test_article.size);
assert_true(test_article.get_entry(A) == null);
test_article.set_entry(A, A);
assert_string(A, test_article.get_entry(A));
assert_false(test_article.is_empty);
assert_uint(1, test_article.size);
test_article.set_entry(B, B);
assert_string(B, test_article.get_entry(B));
assert_uint(2, test_article.size);
test_article.set_entry(C, C);
assert_string(C, test_article.get_entry(C));
assert_uint(2, test_article.size);
assert_true(test_article.get_entry(A) == null);
test_article.set_entry(D, D);
assert_string(D, test_article.get_entry(D));
assert_uint(2, test_article.size);
assert_true(test_article.get_entry(B) == null);
test_article.clear();
assert_true(test_article.is_empty);
assert_uint(0, test_article.size);
}
public void lru_eviction() throws GLib.Error {
const string A = "a";
const string B = "b";
const string C = "c";
Lru<string> test_article = new Lru<string>(2);
test_article.set_entry(A, A);
test_article.set_entry(B, B);
test_article.get_entry(A);
test_article.set_entry(C, C);
assert_string(C, test_article.get_entry(C));
assert_string(A, test_article.get_entry(A));
assert_true(test_article.get_entry(B) == null);
}
}

View file

@ -78,6 +78,7 @@ geary_test_client_sources = [
'client/components/client-web-view-test-case.vala',
'client/composer/composer-web-view-test.vala',
'client/util/util-avatar-test.vala',
'client/util/util-cache-test.vala',
'client/util/util-email-test.vala',
'js/client-page-state-test.vala',

View file

@ -44,6 +44,7 @@ int main(string[] args) {
client.add_suite(new ComposerWebViewTest().get_suite());
client.add_suite(new ConfigurationTest().get_suite());
client.add_suite(new Util.Avatar.Test().get_suite());
client.add_suite(new Util.Cache.Test().get_suite());
client.add_suite(new Util.Email.Test().get_suite());
TestSuite js = new TestSuite("js");