From 898eacc215525560070d3e3edea39d6593ea9554 Mon Sep 17 00:00:00 2001 From: martensitingale Date: Thu, 24 May 2018 19:52:59 +0000 Subject: [PATCH] implement image cache (pending good replacement policy) --- meson.build | 1 + src/Application.vala | 4 +- src/ImageCache.vala | 141 ++++++++++++++++++++++++++++++ src/Views/AccountView.vala | 2 +- src/Widgets/AccountsButton.vala | 2 +- src/Widgets/AttachmentWidget.vala | 4 +- src/Widgets/StatusWidget.vala | 2 +- 7 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 src/ImageCache.vala diff --git a/meson.build b/meson.build index baff2a7..eda3c98 100644 --- a/meson.build +++ b/meson.build @@ -22,6 +22,7 @@ executable( 'src/MainWindow.vala', 'src/SettingsManager.vala', 'src/AccountManager.vala', + 'src/ImageCache.vala', 'src/NetManager.vala', 'src/Utils.vala', 'src/Notificator.vala', diff --git a/src/Application.vala b/src/Application.vala index 90989ed..c7c9fd2 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -10,6 +10,7 @@ namespace Tootle{ public static SettingsManager settings; public static AccountManager accounts; public static NetManager network; + public static ImageCache image_cache; public class Application : Granite.Application { @@ -31,7 +32,8 @@ namespace Tootle{ settings = new SettingsManager (); accounts = new AccountManager (); network = new NetManager (); - + image_cache = new ImageCache (); + return app.run (args); } diff --git a/src/ImageCache.vala b/src/ImageCache.vala new file mode 100644 index 0000000..fee9d65 --- /dev/null +++ b/src/ImageCache.vala @@ -0,0 +1,141 @@ +using Soup; +using GLib; +using Gdk; +using Json; + +private struct CachedImage { + public string uri; + public int size; + + public CachedImage(string uri, int size) { this.uri=uri; this.size=size; } + + public static uint hash(CachedImage? c) { + assert(c != null); + assert(c.uri != null); + return GLib.int64_hash(c.size) ^ c.uri.hash(); + } + + public static bool equal(CachedImage? a, CachedImage? b) { + if (a == null || b == null) { return false; } + return a.size == b.size && a.uri == b.uri; + } +} + +public delegate void PixbufCallback (Gdk.Pixbuf pb); + +public class Tootle.ImageCache : GLib.Object { + private GLib.HashTable in_progress; + private GLib.HashTable pixbufs; + private uint total_size_est; + private uint size_limit; + + construct { + pixbufs = new GLib.HashTable(CachedImage.hash, CachedImage.equal); + in_progress = new GLib.HashTable(CachedImage.hash, CachedImage.equal); + total_size_est = 0; + Tootle.settings.changed.connect (on_settings_changed); + on_settings_changed (); + } + + public ImageCache() { + GLib.Object(); + } + + // adopts cache size limit from settings + private void on_settings_changed () { + // assume 32BPP (divide bytes by 4 to get # pixels) and raw, overhead-free storage + // cache_size setting is number of megabytes + size_limit = (1024 * 1024 * Tootle.settings.cache_size / 4); + enforce_size_limit (); + } + + // remove all cached images + public void remove_all () { + GLib.debug("image cache cleared"); + pixbufs.remove_all (); + total_size_est = 0; + } + + // remove any entry for the given uri and size from the cache + public void remove_one (string uri, int size) { + GLib.debug("image cache removing %s", uri); + CachedImage ci = CachedImage (uri, size); + bool removed = pixbufs.remove(ci); + if (removed) { + assert (total_size_est >= size * size); + total_size_est -= size * size; + GLib.debug("image cache removed %s; size est. is %zd", uri, total_size_est); + } + } + + // delete the pixbuf with the smallest reference count from the cache + private void remove_least_used () { + GLib.debug("image cache removing least-used"); + // for now, dummy implementation: just remove the first pixbuf + var keys = pixbufs.get_keys(); + if (keys.first() != null) { + var ci = keys.first().data; + remove_one(ci.uri, ci.size); + } + } + + private void enforce_size_limit () { + GLib.debug("image cache enforcing size limit (%zd/%zd)", total_size_est, size_limit); + while (total_size_est > size_limit && pixbufs.size() > 0) { + remove_least_used (); + } + assert (total_size_est <= size_limit); + } + + private void store_pixbuf (CachedImage ci, Gdk.Pixbuf pixbuf) { + GLib.debug("image cache inserting %s@%d", ci.uri, ci.size); + assert (!pixbufs.contains (ci)); + pixbufs.insert (ci, pixbuf); + in_progress.remove (ci); + total_size_est += ci.size * ci.size; + enforce_size_limit (); + } + + public async void get_image (string uri, int size, owned PixbufCallback? cb = null) { + CachedImage ci = CachedImage (uri, size); + Gdk.Pixbuf? pb = pixbufs.get(ci); + if (pb != null) { + cb (pb); + return; + } + GLib.debug("image cache miss for %s@%d", uri, size); + + Soup.Message? msg = in_progress.get(ci); + if (msg == null) { + msg = new Soup.Message("GET", uri); + msg.finished.connect(() => { + GLib.debug("image cache about to insert %s@%d", uri, size); + var data = msg.response_body.data; + var stream = new MemoryInputStream.from_data (data); + var pixbuf = new Gdk.Pixbuf.from_stream_at_scale (stream, size, size, true); + store_pixbuf(ci, pixbuf); + cb(pixbuf); + }); + in_progress[ci] = msg; + Tootle.network.queue (msg); + } else { + GLib.debug("found in-progress request for %s@%d", uri, size); + msg.finished.connect_after(() => { + GLib.debug("in-progress request finished for %s@%d", uri, size); + cb(pixbufs[ci]); + }); + } + } + + public void load_avatar (string uri, Granite.Widgets.Avatar avatar, int size = 32) { + get_image.begin(uri, size, (pixbuf) => avatar.pixbuf = pixbuf); + } + + public void load_image (string uri, Gtk.Image image) { + load_scaled_image(uri, image, -1); + } + + public void load_scaled_image (string uri, Gtk.Image image, int size = 64) { + get_image.begin(uri, size, image.set_from_pixbuf); + } +} diff --git a/src/Views/AccountView.vala b/src/Views/AccountView.vala index 01e9329..685c8d7 100644 --- a/src/Views/AccountView.vala +++ b/src/Views/AccountView.vala @@ -142,7 +142,7 @@ public class Tootle.AccountView : TimelineView { username.label = "@" + account.acct; note.label = Utils.simplify_html (account.note); button_follow.visible = !account.is_self (); - Tootle.network.load_avatar (account.avatar, avatar, 128); + Tootle.image_cache.load_avatar (account.avatar, avatar, 128); menu_edit.visible = account.is_self (); diff --git a/src/Widgets/AccountsButton.vala b/src/Widgets/AccountsButton.vala index 1ea6c26..cbb482d 100644 --- a/src/Widgets/AccountsButton.vala +++ b/src/Widgets/AccountsButton.vala @@ -91,7 +91,7 @@ public class Tootle.AccountsButton : Gtk.MenuButton{ Tootle.accounts.switched.connect (account => { if (account != null){ - Tootle.network.load_avatar (account.avatar, avatar, 24); + Tootle.image_cache.load_avatar (account.avatar, avatar, 24); default_account.display_name.label = ""+account.display_name+""; default_account.user.label = "@"+account.username; } diff --git a/src/Widgets/AttachmentWidget.vala b/src/Widgets/AttachmentWidget.vala index 2ebd873..261c3b8 100644 --- a/src/Widgets/AttachmentWidget.vala +++ b/src/Widgets/AttachmentWidget.vala @@ -49,9 +49,9 @@ public class Tootle.AttachmentWidget : Gtk.EventBox { image.valign = Gtk.Align.CENTER; image.show (); if (editable) - Tootle.network.load_scaled_image (attachment.preview_url, image); + Tootle.image_cache.load_scaled_image (attachment.preview_url, image); else - Tootle.network.load_image (attachment.preview_url, image); + Tootle.image_cache.load_image (attachment.preview_url, image); grid.attach (image, 0, 0); label.hide (); break; diff --git a/src/Widgets/StatusWidget.vala b/src/Widgets/StatusWidget.vala index 23b7993..f1816e8 100644 --- a/src/Widgets/StatusWidget.vala +++ b/src/Widgets/StatusWidget.vala @@ -222,7 +222,7 @@ public class Tootle.StatusWidget : Gtk.EventBox { reblog.tooltip_text = _("This post can't be boosted"); } - Tootle.network.load_avatar (formal.account.avatar, avatar, avatar_size); + Tootle.image_cache.load_avatar (formal.account.avatar, avatar, avatar_size); } public bool is_spoiler () {