Compare commits

...

4 Commits

Author SHA1 Message Date
8bba456b14
Cache thumbnails across reloads
This will speed up sort changes, as well as simple reloads,
at the cost of an extra hash map from URIs to Cairo surface references.

It seems unnecessary to provide an explicit option to flush this cache,
as it may be cleared by changing either the directory or the current
thumbnail size.
2022-06-04 16:37:25 +02:00
bb97445a96
Attach mtime to the browser's rescaled thumbnails 2022-06-04 16:37:25 +02:00
e2adac72cc
Use the model's mtime for validating thumbnails
Saves a syscall, generalizes fiv_thumbnail_lookup(),
wastes a tiny bit of memory per entry.
2022-06-04 16:37:25 +02:00
3ddb0cf205
Expose the mtime of the model's entries 2022-06-04 14:50:56 +02:00
7 changed files with 98 additions and 63 deletions

View File

@ -72,6 +72,8 @@ struct _FivBrowser {
double drag_begin_x; ///< Viewport start X coordinate or -1
double drag_begin_y; ///< Viewport start Y coordinate or -1
GHashTable *thumbnail_cache; ///< [URI]cairo_surface_t, for item_size
Thumbnailer *thumbnailers; ///< Parallelized thumbnailers
size_t thumbnailers_len; ///< Thumbnailers array size
GList *thumbnailers_queue; ///< Queued up Entry pointers
@ -84,8 +86,12 @@ struct _FivBrowser {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// The "last modified" timestamp of source images for thumbnails.
static cairo_user_data_key_t fiv_browser_key_mtime_msec;
struct entry {
char *uri; ///< GIO URI
gint64 mtime_msec; ///< Modification time in milliseconds
cairo_surface_t *thumbnail; ///< Prescaled thumbnail
GIcon *icon; ///< If no thumbnail, use this icon
};
@ -427,18 +433,34 @@ entry_add_thumbnail(gpointer data, gpointer user_data)
g_clear_pointer(&self->thumbnail, cairo_surface_destroy);
FivBrowser *browser = FIV_BROWSER(user_data);
GFile *file = g_file_new_for_uri(self->uri);
self->thumbnail = rescale_thumbnail(
fiv_thumbnail_lookup(file, browser->item_size), browser->item_height);
if (self->thumbnail)
goto out;
cairo_surface_t *cached =
g_hash_table_lookup(browser->thumbnail_cache, self->uri);
if (cached &&
(intptr_t) cairo_surface_get_user_data(
cached, &fiv_browser_key_mtime_msec) == self->mtime_msec) {
self->thumbnail = cairo_surface_reference(cached);
} else {
cairo_surface_t *found = fiv_thumbnail_lookup(
self->uri, self->mtime_msec, browser->item_size);
self->thumbnail = rescale_thumbnail(found, browser->item_height);
}
if (self->thumbnail) {
// This choice of mtime favours unnecessary thumbnail reloading.
cairo_surface_set_user_data(self->thumbnail,
&fiv_browser_key_mtime_msec, (void *) (intptr_t) self->mtime_msec,
NULL);
return;
}
// Fall back to symbolic icons, though there's only so much we can do
// in parallel--GTK+ isn't thread-safe.
GFile *file = g_file_new_for_uri(self->uri);
GFileInfo *info = g_file_query_info(file,
G_FILE_ATTRIBUTE_STANDARD_NAME
"," G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON,
G_FILE_QUERY_INFO_NONE, NULL, NULL);
g_object_unref(file);
if (info) {
GIcon *icon = g_file_info_get_symbolic_icon(info);
if (icon)
@ -449,8 +471,6 @@ entry_add_thumbnail(gpointer data, gpointer user_data)
// The GVfs backend may not be friendly.
if (!self->icon)
self->icon = g_icon_new_for_string("text-x-generic-symbolic", NULL);
out:
g_object_unref(file);
}
static void
@ -506,8 +526,18 @@ reload_thumbnails(FivBrowser *self)
g_thread_pool_push(pool, &g_array_index(self->entries, Entry, i), NULL);
g_thread_pool_free(pool, FALSE, TRUE);
for (guint i = 0; i < self->entries->len; i++)
materialize_icon(self, &g_array_index(self->entries, Entry, i));
// Once a URI disappears from the model, its thumbnail is forgotten.
g_hash_table_remove_all(self->thumbnail_cache);
for (guint i = 0; i < self->entries->len; i++) {
Entry *entry = &g_array_index(self->entries, Entry, i);
if (entry->thumbnail) {
g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->uri),
cairo_surface_reference(entry->thumbnail));
}
materialize_icon(self, entry);
}
gtk_widget_queue_resize(GTK_WIDGET(self));
}
@ -529,6 +559,11 @@ thumbnailer_reprocess_entry(FivBrowser *self, GBytes *output, Entry *entry)
fiv_io_deserialize(output), self->item_height))) {
entry_add_thumbnail(entry, self);
materialize_icon(self, entry);
} else {
// This choice of mtime favours unnecessary thumbnail reloading.
cairo_surface_set_user_data(entry->thumbnail,
&fiv_browser_key_mtime_msec, (void *) (intptr_t) entry->mtime_msec,
NULL);
}
gtk_widget_queue_resize(GTK_WIDGET(self));
@ -914,6 +949,8 @@ fiv_browser_finalize(GObject *gobject)
g_clear_object(&self->model);
}
g_hash_table_destroy(self->thumbnail_cache);
cairo_surface_destroy(self->glow);
g_clear_object(&self->pointer);
@ -958,6 +995,8 @@ set_item_size(FivBrowser *self, FivThumbnailSize size)
if (size != self->item_size) {
self->item_size = size;
self->item_height = fiv_thumbnail_sizes[self->item_size].size;
g_hash_table_remove_all(self->thumbnail_cache);
reload_thumbnails(self);
thumbnailers_start(self);
@ -1674,6 +1713,9 @@ fiv_browser_init(FivBrowser *self)
g_array_set_clear_func(self->layouted_rows, (GDestroyNotify) row_free);
abort_button_tracking(self);
self->thumbnail_cache = g_hash_table_new_full(g_str_hash, g_str_equal,
g_free, (GDestroyNotify) cairo_surface_destroy);
self->thumbnailers_len = g_get_num_processors();
self->thumbnailers =
g_malloc0_n(self->thumbnailers_len, sizeof *self->thumbnailers);
@ -1689,6 +1731,7 @@ fiv_browser_init(FivBrowser *self)
// --- Public interface --------------------------------------------------------
// TODO(p): Later implement any arguments of this FivIoModel signal.
static void
on_model_files_changed(FivIoModel *model, FivBrowser *self)
{
@ -1698,18 +1741,16 @@ on_model_files_changed(FivIoModel *model, FivBrowser *self)
if (self->selected)
selected_uri = g_strdup(self->selected->uri);
// TODO(p): Later implement arguments.
thumbnailers_abort(self);
g_array_set_size(self->entries, 0);
g_array_set_size(self->layouted_rows, 0);
GPtrArray *files = fiv_io_model_get_files(self->model);
for (guint i = 0; i < files->len; i++) {
g_array_append_val(self->entries,
((Entry) {.thumbnail = NULL, .uri = files->pdata[i]}));
files->pdata[i] = NULL;
gsize len = 0;
const FivIoModelEntry *files = fiv_io_model_get_files(self->model, &len);
for (gsize i = 0; i < len; i++) {
g_array_append_val(self->entries, ((Entry) {.thumbnail = NULL,
.uri = g_strdup(files[i].uri), .mtime_msec = files[i].mtime_msec}));
}
g_ptr_array_free(files, TRUE);
fiv_browser_select(self, selected_uri);
g_free(selected_uri);

View File

@ -2795,14 +2795,8 @@ fiv_io_deserialize(GBytes *bytes)
#include <fnmatch.h>
typedef struct {
gchar *uri; ///< GIO URI
gchar *collate_key; ///< Collate key for the filename
gint64 mtime_msec; ///< Modification time in milliseconds
} ModelEntry;
static void
model_entry_finalize(ModelEntry *entry)
model_entry_finalize(FivIoModelEntry *entry)
{
g_free(entry->uri);
g_free(entry->collate_key);
@ -2867,8 +2861,9 @@ model_supports(FivIoModel *self, const gchar *filename)
}
static inline int
model_compare_entries(FivIoModel *self, const ModelEntry *entry1, GFile *file1,
const ModelEntry *entry2, GFile *file2)
model_compare_entries(FivIoModel *self,
const FivIoModelEntry *entry1, GFile *file1,
const FivIoModelEntry *entry2, GFile *file2)
{
if (g_file_has_prefix(file1, file2))
return +1;
@ -2894,8 +2889,8 @@ model_compare_entries(FivIoModel *self, const ModelEntry *entry1, GFile *file1,
static gint
model_compare(gconstpointer a, gconstpointer b, gpointer user_data)
{
const ModelEntry *entry1 = a;
const ModelEntry *entry2 = b;
const FivIoModelEntry *entry1 = a;
const FivIoModelEntry *entry2 = b;
GFile *file1 = g_file_new_for_uri(entry1->uri);
GFile *file2 = g_file_new_for_uri(entry2->uri);
int result = model_compare_entries(user_data, entry1, file1, entry2, file2);
@ -2939,7 +2934,7 @@ model_reload(FivIoModel *self, GError **error)
if (self->filtering && g_file_info_get_is_hidden(info))
continue;
ModelEntry entry = {.uri = g_file_get_uri(child)};
FivIoModelEntry entry = {.uri = g_file_get_uri(child)};
GDateTime *mtime = g_file_info_get_modification_date_time(info);
if (mtime) {
entry.mtime_msec = g_date_time_to_unix(mtime) * 1000 +
@ -3076,8 +3071,8 @@ fiv_io_model_init(FivIoModel *self)
self->supported_globs = extract_mime_globs((const char **) types);
g_strfreev(types);
self->files = g_array_new(FALSE, TRUE, sizeof(ModelEntry));
self->subdirs = g_array_new(FALSE, TRUE, sizeof(ModelEntry));
self->files = g_array_new(FALSE, TRUE, sizeof(FivIoModelEntry));
self->subdirs = g_array_new(FALSE, TRUE, sizeof(FivIoModelEntry));
g_array_set_clear_func(
self->subdirs, (GDestroyNotify) model_entry_finalize);
g_array_set_clear_func(
@ -3107,24 +3102,18 @@ fiv_io_model_get_location(FivIoModel *self)
return self->directory;
}
GPtrArray *
fiv_io_model_get_files(FivIoModel *self)
const FivIoModelEntry *
fiv_io_model_get_files(FivIoModel *self, gsize *len)
{
GPtrArray *a = g_ptr_array_new_full(self->files->len, g_free);
for (guint i = 0; i < self->files->len; i++)
g_ptr_array_add(
a, g_strdup(g_array_index(self->files, ModelEntry, i).uri));
return a;
*len = self->files->len;
return (const FivIoModelEntry *) self->files->data;
}
GPtrArray *
fiv_io_model_get_subdirectories(FivIoModel *self)
const FivIoModelEntry *
fiv_io_model_get_subdirs(FivIoModel *self, gsize *len)
{
GPtrArray *a = g_ptr_array_new_full(self->subdirs->len, g_free);
for (guint i = 0; i < self->subdirs->len; i++)
g_ptr_array_add(
a, g_strdup(g_array_index(self->subdirs, ModelEntry, i).uri));
return a;
*len = self->subdirs->len;
return (const FivIoModelEntry *) self->subdirs->data;
}
// --- Export ------------------------------------------------------------------

View File

@ -122,8 +122,14 @@ gboolean fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error);
/// There is no ownership transfer, and the object may be NULL.
GFile *fiv_io_model_get_location(FivIoModel *self);
GPtrArray *fiv_io_model_get_files(FivIoModel *self);
GPtrArray *fiv_io_model_get_subdirectories(FivIoModel *self);
typedef struct {
char *uri; ///< GIO URI
char *collate_key; ///< Collate key for the filename
gint64 mtime_msec; ///< Modification time in milliseconds
} FivIoModelEntry;
const FivIoModelEntry *fiv_io_model_get_files(FivIoModel *self, gsize *len);
const FivIoModelEntry *fiv_io_model_get_subdirs(FivIoModel *self, gsize *len);
// --- Export ------------------------------------------------------------------

View File

@ -241,14 +241,15 @@ update_location(FivSidebar *self)
if ((row = create_row(self, location, "circle-filled-symbolic")))
gtk_container_add(GTK_CONTAINER(self->listbox), row);
GPtrArray *subdirs = fiv_io_model_get_subdirectories(self->model);
for (guint i = 0; i < subdirs->len; i++) {
GFile *file = g_file_new_for_uri(subdirs->pdata[i]);
gsize len = 0;
const FivIoModelEntry *subdirs =
fiv_io_model_get_subdirs(self->model, &len);
for (gsize i = 0; i < len; i++) {
GFile *file = g_file_new_for_uri(subdirs[i].uri);
if ((row = create_row(self, file, "go-down-symbolic")))
gtk_container_add(GTK_CONTAINER(self->listbox), row);
g_object_unref(file);
}
g_ptr_array_free(subdirs, TRUE);
}
static void

View File

@ -579,18 +579,11 @@ fail_init:
}
cairo_surface_t *
fiv_thumbnail_lookup(GFile *target, FivThumbnailSize size)
fiv_thumbnail_lookup(char *uri, gint64 mtime_msec, FivThumbnailSize size)
{
g_return_val_if_fail(size >= FIV_THUMBNAIL_SIZE_MIN &&
size <= FIV_THUMBNAIL_SIZE_MAX, NULL);
// Local files only, at least for now.
GStatBuf st = {};
const gchar *path = g_file_peek_path(target);
if (!path || g_stat(path, &st))
return NULL;
gchar *uri = g_file_get_uri(target);
gchar *sum = g_compute_checksum_for_string(G_CHECKSUM_MD5, uri, -1);
gchar *thumbnails_dir = fiv_thumbnail_get_root();
@ -605,7 +598,7 @@ fiv_thumbnail_lookup(GFile *target, FivThumbnailSize size)
const char *name = fiv_thumbnail_sizes[use].thumbnail_spec_name;
gchar *wide =
g_strdup_printf("%s/wide-%s/%s.webp", thumbnails_dir, name, sum);
result = read_wide_thumbnail(wide, uri, st.st_mtim.tv_sec, &error);
result = read_wide_thumbnail(wide, uri, mtime_msec / 1000, &error);
if (error) {
g_debug("%s: %s", wide, error->message);
g_clear_error(&error);
@ -621,7 +614,7 @@ fiv_thumbnail_lookup(GFile *target, FivThumbnailSize size)
gchar *path =
g_strdup_printf("%s/%s/%s.png", thumbnails_dir, name, sum);
result = read_spng_thumbnail(path, uri, st.st_mtim.tv_sec, &error);
result = read_spng_thumbnail(path, uri, mtime_msec / 1000, &error);
if (error) {
g_debug("%s: %s", path, error->message);
g_clear_error(&error);
@ -639,7 +632,6 @@ fiv_thumbnail_lookup(GFile *target, FivThumbnailSize size)
g_free(thumbnails_dir);
g_free(sum);
g_free(uri);
return result;
}

View File

@ -62,7 +62,8 @@ gboolean fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size,
/// Retrieves a thumbnail of the most appropriate quality and resolution
/// for the target file.
cairo_surface_t *fiv_thumbnail_lookup(GFile *target, FivThumbnailSize size);
cairo_surface_t *fiv_thumbnail_lookup(
char *uri, gint64 mtime_msec, FivThumbnailSize size);
/// Invalidate the wide thumbnail cache. May write to standard streams.
void fiv_thumbnail_invalidate(void);

7
fiv.c
View File

@ -702,8 +702,13 @@ on_model_files_changed(FivIoModel *model, G_GNUC_UNUSED gpointer user_data)
{
g_return_if_fail(model == g.model);
gsize len = 0;
const FivIoModelEntry *files = fiv_io_model_get_files(g.model, &len);
g_ptr_array_free(g.files, TRUE);
g.files = fiv_io_model_get_files(g.model);
g.files = g_ptr_array_new_full(len, g_free);
for (gsize i = 0; i < len; i++)
g_ptr_array_add(g.files, g_strdup(files[i].uri));
update_files_index();
gtk_widget_set_sensitive(