diff --git a/po/POTFILES.in b/po/POTFILES.in index 74689c48..0e91036d 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -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 diff --git a/src/client/meson.build b/src/client/meson.build index 1f3f15a7..fcfebf8b 100644 --- a/src/client/meson.build +++ b/src/client/meson.build @@ -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', diff --git a/src/client/util/util-cache.vala b/src/client/util/util-cache.vala new file mode 100644 index 00000000..3161fd39 --- /dev/null +++ b/src/client/util/util-cache.vala @@ -0,0 +1,122 @@ +/* + * Copyright 2019 Michael Gratton + * + * 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 : Geary.BaseObject { + + + private class CacheEntry { + + + public static int lru_compare(CacheEntry a, CacheEntry 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> cache = + new Gee.HashMap>(); + private Gee.SortedSet> ordering = + new Gee.TreeSet>(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 entry = new CacheEntry(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? 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? 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(); + } + +} diff --git a/test/client/util/util-cache-test.vala b/test/client/util/util-cache-test.vala new file mode 100644 index 00000000..1a40b7a9 --- /dev/null +++ b/test/client/util/util-cache-test.vala @@ -0,0 +1,71 @@ +/* + * Copyright 2019 Michael Gratton + * + * 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 test_article = new Lru(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 test_article = new Lru(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); + } + +} diff --git a/test/meson.build b/test/meson.build index 3414a95f..d93cd883 100644 --- a/test/meson.build +++ b/test/meson.build @@ -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', diff --git a/test/test-client.vala b/test/test-client.vala index 1852ba6e..fe408374 100644 --- a/test/test-client.vala +++ b/test/test-client.vala @@ -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");