[gnome-shell/wip/chergert/app-cache: 16/16] app-cache: add ShellAppCache for GAppInfo caching



commit c6fdaf47f873ec9fa5f3e1a249b34880768df8e6
Author: Christian Hergert <chergert redhat com>
Date:   Thu Feb 27 19:36:14 2020 -0800

    app-cache: add ShellAppCache for GAppInfo caching
    
    This caches GAppInfo so that the compositor thread does not have to perform
    costly disk access to load them. Instead, they are loaded from a worker
    thread and the ShellAppCache notifies of changes.
    
    To simplify maintenance, ShellAppCache manages this directly and the
    existing ShellAppSystem wraps the cache. We may want to graft these
    together in the future, but now it provides the easiest way to backport
    changes to older Shell releases.
    
    Another source of compositor thread disk access was in determining the
    name for an application directory. Translations are provided via GKeyFile
    installed in "desktop-directories". Each time we would build the name
    for a label (or update it) we would have to load all of these files.
    
    Instead, the ShellAppCache caches that information and updates the cache
    in bulk when those change. We can reduce this in the future to do less
    work, but chances are these will come together anyway so that is probably
    worth fixing if we ever come across it.
    
    https://gitlab.gnome.org/GNOME/gnome-shell/issues/2282

 js/ui/appDisplay.js           |  24 +--
 src/meson.build               |   5 +-
 src/shell-app-cache-private.h |  19 ++
 src/shell-app-cache.c         | 405 ++++++++++++++++++++++++++++++++++++++++++
 src/shell-app-system.c        |  32 ++--
 src/shell-util.c              |  16 ++
 src/shell-util.h              |   2 +
 7 files changed, 467 insertions(+), 36 deletions(-)
---
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index cdd1756c2c..3f11c88de4 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -71,15 +71,9 @@ function _getFolderName(folder) {
     let name = folder.get_string('name');
 
     if (folder.get_boolean('translate')) {
-        let keyfile = new GLib.KeyFile();
-        let path = 'desktop-directories/%s'.format(name);
-
-        try {
-            keyfile.load_from_data_dirs(path, GLib.KeyFileFlags.NONE);
-            name = keyfile.get_locale_string('Desktop Entry', 'Name', null);
-        } catch (e) {
-            return name;
-        }
+        let translated = Shell.util_get_translated_folder_name(name);
+        if (translated != null)
+            return translated;
     }
 
     return name;
@@ -120,15 +114,9 @@ function _findBestFolderName(apps) {
     }, commonCategories);
 
     for (let category of commonCategories) {
-        let keyfile = new GLib.KeyFile();
-        let path = 'desktop-directories/%s.directory'.format(category);
-
-        try {
-            keyfile.load_from_data_dirs(path, GLib.KeyFileFlags.NONE);
-            return keyfile.get_locale_string('Desktop Entry', 'Name', null);
-        } catch (e) {
-            continue;
-        }
+        let translated = Shell.util_get_translated_folder_name(category);
+        if (translated != null)
+            return translated;
     }
 
     return null;
diff --git a/src/meson.build b/src/meson.build
index 0d607698aa..8e18e30805 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -115,6 +115,7 @@ endif
 
 libshell_private_headers = [
   'shell-app-private.h',
+  'shell-app-cache-private.h',
   'shell-app-system-private.h',
   'shell-global-private.h',
   'shell-window-tracker-private.h',
@@ -154,7 +155,9 @@ if have_networkmanager
   libshell_sources += 'shell-network-agent.c'
 endif
 
-libshell_private_sources = []
+libshell_private_sources = [
+  'shell-app-cache.c',
+]
 
 if enable_recorder
     libshell_sources += ['shell-recorder.c']
diff --git a/src/shell-app-cache-private.h b/src/shell-app-cache-private.h
new file mode 100644
index 0000000000..b73094ab18
--- /dev/null
+++ b/src/shell-app-cache-private.h
@@ -0,0 +1,19 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+#ifndef __SHELL_APP_CACHE_PRIVATE_H__
+#define __SHELL_APP_CACHE_PRIVATE_H__
+
+#include <gio/gio.h>
+#include <gio/gdesktopappinfo.h>
+
+#define SHELL_TYPE_APP_CACHE (shell_app_cache_get_type())
+
+G_DECLARE_FINAL_TYPE (ShellAppCache, shell_app_cache, SHELL, APP_CACHE, GObject)
+
+ShellAppCache   *shell_app_cache_get_default      (void);
+GList           *shell_app_cache_get_all          (ShellAppCache *cache);
+GDesktopAppInfo *shell_app_cache_get_info         (ShellAppCache *cache,
+                                                   const char    *id);
+char            *shell_app_cache_translate_folder (ShellAppCache *cache,
+                                                   const char    *name);
+
+#endif /* __SHELL_APP_CACHE_PRIVATE_H__ */
diff --git a/src/shell-app-cache.c b/src/shell-app-cache.c
new file mode 100644
index 0000000000..8ad226bbf6
--- /dev/null
+++ b/src/shell-app-cache.c
@@ -0,0 +1,405 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+#include "config.h"
+
+#include "shell-app-cache-private.h"
+
+/**
+ * SECTION:shell-app-cache
+ * @title: ShellAppCache
+ * @short_description: application information cache
+ *
+ * The #ShellAppCache is responsible for caching information about #GAppInfo
+ * to ensure that the compositor thread never needs to perform disk reads to
+ * access them. All of the work is done off-thread. When the new data has
+ * been loaded, a #ShellAppCache::changed signal is emitted.
+ *
+ * Additionally, the #ShellAppCache caches information about translations for
+ * directories. This allows translation provided in [Desktop Entry] GKeyFiles
+ * to be available when building StLabel and other elements without performing
+ * costly disk reads.
+ *
+ * Various monitors are used to keep this information up to date while the
+ * Shell is running.
+ */
+
+#define DEFAULT_TIMEOUT_SECONDS 5
+
+struct _ShellAppCache
+{
+  GObject          parent_instance;
+
+  GAppInfoMonitor *monitor;
+  GPtrArray       *dir_monitors;
+  GHashTable      *folders;
+  GCancellable    *cancellable;
+  GList           *app_infos;
+
+  guint            queued_update;
+};
+
+typedef struct
+{
+  GList      *app_infos;
+  GHashTable *folders;
+} CacheState;
+
+G_DEFINE_TYPE (ShellAppCache, shell_app_cache, G_TYPE_OBJECT)
+
+enum {
+  CHANGED,
+  N_SIGNALS
+};
+
+static guint signals [N_SIGNALS];
+
+static void
+cache_state_free (CacheState *state)
+{
+  g_clear_pointer (&state->folders, g_hash_table_unref);
+  g_list_free_full (state->app_infos, g_object_unref);
+  g_slice_free (CacheState, state);
+}
+
+static CacheState *
+cache_state_new (void)
+{
+  CacheState *state;
+
+  state = g_slice_new0 (CacheState);
+  state->folders = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+  return g_steal_pointer (&state);
+}
+
+/**
+ * shell_app_cache_get_default:
+ *
+ * Gets the default #ShellAppCache.
+ *
+ * Returns: (transfer none): a #ShellAppCache
+ */
+ShellAppCache *
+shell_app_cache_get_default (void)
+{
+  static ShellAppCache *instance;
+
+  if (instance == NULL)
+    {
+      instance = g_object_new (SHELL_TYPE_APP_CACHE, NULL);
+      g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance);
+    }
+
+  return instance;
+}
+
+static void
+load_folder (GHashTable *folders,
+             const char *path)
+{
+  g_autoptr(GDir) dir = NULL;
+  const char *name;
+
+  g_assert (folders != NULL);
+  g_assert (path != NULL);
+
+  dir = g_dir_open (path, 0, NULL);
+  if (dir == NULL)
+    return;
+
+  while ((name = g_dir_read_name (dir)))
+    {
+      g_autofree gchar *filename = NULL;
+      g_autoptr(GKeyFile) keyfile = NULL;
+
+      /* First added wins */
+      if (g_hash_table_contains (folders, name))
+        continue;
+
+      filename = g_build_filename (path, name, NULL);
+      keyfile = g_key_file_new ();
+
+      if (g_key_file_load_from_file (keyfile, filename, G_KEY_FILE_NONE, NULL))
+        {
+          gchar *translated;
+
+          translated = g_key_file_get_locale_string (keyfile,
+                                                     "Desktop Entry", "Name",
+                                                     NULL, NULL);
+
+          if (translated != NULL)
+            g_hash_table_insert (folders, g_strdup (name), translated);
+        }
+    }
+}
+
+static void
+load_folders (GHashTable *folders)
+{
+  const char * const *dirs;
+  g_autofree gchar *userdir = NULL;
+  guint i;
+
+  g_assert (folders != NULL);
+
+  userdir = g_build_filename (g_get_user_data_dir (), "desktop-directories", NULL);
+  load_folder (folders, userdir);
+
+  dirs = g_get_system_data_dirs ();
+  for (i = 0; dirs[i] != NULL; i++)
+    {
+      g_autofree gchar *sysdir = g_build_filename (dirs[i], "desktop-directories", NULL);
+      load_folder (folders, sysdir);
+    }
+}
+
+static void
+shell_app_cache_worker (GTask        *task,
+                        gpointer      source_object,
+                        gpointer      task_data,
+                        GCancellable *cancellable)
+{
+  CacheState *state;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (SHELL_IS_APP_CACHE (source_object));
+
+  state = cache_state_new ();
+  state->app_infos = g_app_info_get_all ();
+  load_folders (state->folders);
+
+  g_task_return_pointer (task, state, (GDestroyNotify) cache_state_free);
+}
+
+static void
+apply_update_cb (GObject      *object,
+                 GAsyncResult *result,
+                 gpointer      user_data)
+{
+  ShellAppCache *cache = (ShellAppCache *)object;
+  g_autoptr(GError) error = NULL;
+  CacheState *state;
+
+  g_assert (SHELL_IS_APP_CACHE (cache));
+  g_assert (G_IS_TASK (result));
+  g_assert (user_data == NULL);
+
+  state = g_task_propagate_pointer (G_TASK (result), &error);
+
+  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+    return;
+
+  g_list_free_full (cache->app_infos, g_object_unref);
+  cache->app_infos = g_steal_pointer (&state->app_infos);
+
+  g_clear_pointer (&cache->folders, g_hash_table_unref);
+  cache->folders = g_steal_pointer (&state->folders);
+
+  g_signal_emit (cache, signals[CHANGED], 0);
+
+  cache_state_free (state);
+}
+
+static gboolean
+shell_app_cache_do_update (gpointer user_data)
+{
+  ShellAppCache *cache = user_data;
+  g_autoptr(GTask) task = NULL;
+
+  cache->queued_update = 0;
+
+  /* Reset the cancellable state so we don't race with
+   * two updates coming back overlapped and applying the
+   * information in the wrong order.
+   */
+  g_cancellable_cancel (cache->cancellable);
+  g_clear_object (&cache->cancellable);
+  cache->cancellable = g_cancellable_new ();
+
+  task = g_task_new (cache, cache->cancellable, apply_update_cb, NULL);
+  g_task_set_source_tag (task, shell_app_cache_do_update);
+  g_task_run_in_thread (task, shell_app_cache_worker);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+shell_app_cache_queue_update (ShellAppCache *self)
+{
+  g_assert (SHELL_IS_APP_CACHE (self));
+
+  if (self->queued_update != 0)
+    g_source_remove (self->queued_update);
+
+  self->queued_update = g_timeout_add_seconds (DEFAULT_TIMEOUT_SECONDS,
+                                               shell_app_cache_do_update,
+                                               self);
+}
+
+static void
+monitor_data_dir (ShellAppCache *self,
+                  const gchar   *directory)
+{
+  g_autofree gchar *subdir = NULL;
+  g_autoptr(GFile) file = NULL;
+  g_autoptr(GFileMonitor) monitor = NULL;
+
+  g_assert (SHELL_IS_APP_CACHE (self));
+
+  if (directory == NULL)
+    return;
+
+  subdir = g_build_filename (directory, "desktop-directories", NULL);
+  file = g_file_new_for_path (subdir);
+  monitor = g_file_monitor_directory (file, G_FILE_MONITOR_NONE, NULL, NULL);
+
+  if (monitor != NULL)
+    {
+      g_file_monitor_set_rate_limit (monitor, DEFAULT_TIMEOUT_SECONDS * 1000);
+      g_signal_connect_object (monitor,
+                               "changed",
+                               G_CALLBACK (shell_app_cache_queue_update),
+                               self,
+                               G_CONNECT_SWAPPED);
+      g_ptr_array_add (self->dir_monitors, g_steal_pointer (&monitor));
+    }
+}
+
+static void
+shell_app_cache_finalize (GObject *object)
+{
+  ShellAppCache *self = (ShellAppCache *)object;
+
+  g_clear_object (&self->monitor);
+
+  if (self->queued_update)
+    {
+      g_source_remove (self->queued_update);
+      self->queued_update = 0;
+    }
+
+  g_clear_pointer (&self->dir_monitors, g_ptr_array_unref);
+  g_clear_pointer (&self->folders, g_hash_table_unref);
+  g_list_free_full (self->app_infos, g_object_unref);
+
+  G_OBJECT_CLASS (shell_app_cache_parent_class)->finalize (object);
+}
+
+static void
+shell_app_cache_class_init (ShellAppCacheClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = shell_app_cache_finalize;
+
+  /**
+   * ShellAppCache::changed:
+   *
+   * The "changed" signal is emitted when the cache has updated
+   * information about installed applications.
+   */
+  signals [CHANGED] =
+    g_signal_new ("changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL,
+                  G_TYPE_NONE, 0);
+}
+
+static void
+shell_app_cache_init (ShellAppCache *self)
+{
+  const gchar * const *sysdirs;
+  guint i;
+
+  /* Monitor directories for translation changes */
+  self->dir_monitors = g_ptr_array_new_with_free_func (g_object_unref);
+  monitor_data_dir (self, g_get_user_data_dir ());
+  sysdirs = g_get_system_data_dirs ();
+  for (i = 0; sysdirs[i] != NULL; i++)
+    monitor_data_dir (self, sysdirs[i]);
+
+  /* Load translated directory names immediately */
+  self->folders = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+  load_folders (self->folders);
+
+  /* Setup AppMonitor to track changes */
+  self->monitor = g_app_info_monitor_get ();
+  g_signal_connect_object (self->monitor,
+                           "changed",
+                           G_CALLBACK (shell_app_cache_queue_update),
+                           self,
+                           G_CONNECT_SWAPPED);
+  self->app_infos = g_app_info_get_all ();
+}
+
+/**
+ * shell_app_cache_get_all:
+ * @cache: (nullable): a #ShellAppCache or %NULL
+ *
+ * Like g_app_info_get_all() but always returns a
+ * cached set of application info so the caller can be
+ * sure that I/O will not happen on the current thread.
+ *
+ * Returns: (transfer none) (element-type GAppInfo):
+ *   a #GList of references to #GAppInfo.
+ */
+GList *
+shell_app_cache_get_all (ShellAppCache *cache)
+{
+  g_return_val_if_fail (SHELL_IS_APP_CACHE (cache), NULL);
+
+  return cache->app_infos;
+}
+
+/**
+ * shell_app_cache_get_info:
+ * @cache: (nullable): a #ShellAppCache or %NULL
+ * @id: the application id
+ *
+ * A replacement for g_desktop_app_info_new() that will lookup the
+ * information from the cache instead of (re)loading from disk.
+ *
+ * Returns: (nullable) (transfer none): a #GDesktopAppInfo or %NULL
+ */
+GDesktopAppInfo *
+shell_app_cache_get_info (ShellAppCache *cache,
+                          const char    *id)
+{
+  const GList *iter;
+
+  g_return_val_if_fail (SHELL_IS_APP_CACHE (cache), NULL);
+
+  for (iter = cache->app_infos; iter != NULL; iter = iter->next)
+    {
+      GAppInfo *info = iter->data;
+
+      if (g_strcmp0 (id, g_app_info_get_id (info)) == 0)
+        return G_DESKTOP_APP_INFO (info);
+    }
+
+  return NULL;
+}
+
+/**
+ * shell_app_cache_translate_folder:
+ * @cache: (nullable): a #ShellAppCache or %NULL
+ * @name: the folder name
+ *
+ * Gets the translated folder name for @name if any exists. Otherwise
+ * a copy of @name is returned.
+ *
+ * Returns: (nullable): the translated string or %NULL if there is no
+ *   translation.
+ */
+char *
+shell_app_cache_translate_folder (ShellAppCache *cache,
+                                  const char    *name)
+{
+  g_return_val_if_fail (SHELL_IS_APP_CACHE (cache), NULL);
+
+  if (name == NULL)
+    return NULL;
+
+  return g_strdup (g_hash_table_lookup (cache->folders, name));
+}
diff --git a/src/shell-app-system.c b/src/shell-app-system.c
index 127f29ef02..efb919bc48 100644
--- a/src/shell-app-system.c
+++ b/src/shell-app-system.c
@@ -9,6 +9,7 @@
 #include <gio/gio.h>
 #include <glib/gi18n.h>
 
+#include "shell-app-cache-private.h"
 #include "shell-app-private.h"
 #include "shell-window-tracker-private.h"
 #include "shell-app-system-private.h"
@@ -94,14 +95,14 @@ static void
 scan_startup_wm_class_to_id (ShellAppSystem *self)
 {
   ShellAppSystemPrivate *priv = self->priv;
-  GList *l;
+  const GList *l;
+  GList *all;
 
   g_hash_table_remove_all (priv->startup_wm_class_to_id);
 
-  g_list_free_full (priv->installed_apps, g_object_unref);
-  priv->installed_apps = g_app_info_get_all ();
+  all = shell_app_cache_get_all (shell_app_cache_get_default ());
 
-  for (l = priv->installed_apps; l != NULL; l = l->next)
+  for (l = all; l != NULL; l = l->next)
     {
       GAppInfo *info = l->data;
       const char *startup_wm_class, *id, *old_id;
@@ -131,7 +132,8 @@ app_is_stale (ShellApp *app)
   if (shell_app_is_window_backed (app))
     return FALSE;
 
-  info = g_desktop_app_info_new (shell_app_get_id (app));
+  info = shell_app_cache_get_info (shell_app_cache_get_default (),
+                                   shell_app_get_id (app));
   if (!info)
     return TRUE;
 
@@ -210,11 +212,9 @@ rescan_icon_theme (ShellAppSystem *self)
 }
 
 static void
-installed_changed (GAppInfoMonitor *monitor,
-                   gpointer         user_data)
+installed_changed (ShellAppCache  *cache,
+                   ShellAppSystem *self)
 {
-  ShellAppSystem *self = user_data;
-
   rescan_icon_theme (self);
   scan_startup_wm_class_to_id (self);
 
@@ -227,7 +227,7 @@ static void
 shell_app_system_init (ShellAppSystem *self)
 {
   ShellAppSystemPrivate *priv;
-  GAppInfoMonitor *monitor;
+  ShellAppCache *cache;
 
   self->priv = priv = shell_app_system_get_instance_private (self);
 
@@ -238,9 +238,9 @@ shell_app_system_init (ShellAppSystem *self)
 
   priv->startup_wm_class_to_id = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
 
-  monitor = g_app_info_monitor_get ();
-  g_signal_connect (monitor, "changed", G_CALLBACK (installed_changed), self);
-  installed_changed (monitor, self);
+  cache = shell_app_cache_get_default ();
+  g_signal_connect (cache, "changed", G_CALLBACK (installed_changed), self);
+  installed_changed (cache, self);
 }
 
 static void
@@ -293,7 +293,7 @@ shell_app_system_lookup_app (ShellAppSystem   *self,
   if (app)
     return app;
 
-  info = g_desktop_app_info_new (id);
+  info = shell_app_cache_get_info (shell_app_cache_get_default (), id);
   if (!info)
     return NULL;
 
@@ -506,7 +506,5 @@ shell_app_system_search (const char *search_string)
 GList *
 shell_app_system_get_installed (ShellAppSystem *self)
 {
-  ShellAppSystemPrivate *priv = self->priv;
-
-  return priv->installed_apps;
+  return shell_app_cache_get_all (shell_app_cache_get_default ());
 }
diff --git a/src/shell-util.c b/src/shell-util.c
index 918dcdf0b7..26ff66e4ef 100644
--- a/src/shell-util.c
+++ b/src/shell-util.c
@@ -16,6 +16,7 @@
 #include <GL/gl.h>
 #include <cogl/cogl.h>
 
+#include "shell-app-cache-private.h"
 #include "shell-util.h"
 #include <glib/gi18n-lib.h>
 #include <gtk/gtk.h>
@@ -665,3 +666,18 @@ shell_util_has_x11_display_extension (MetaDisplay *display,
   xdisplay = meta_x11_display_get_xdisplay (x11_display);
   return XQueryExtension (xdisplay, extension, &op, &event, &error);
 }
+
+/**
+ * shell_util_get_translated_folder_name:
+ * @name: the untranslated folder name
+ *
+ * Attempts to translate the folder @name using translations provided
+ * by .directory files.
+ *
+ * Returns: (nullable): a translated string or %NULL
+ */
+char *
+shell_util_get_translated_folder_name (const char *name)
+{
+  return shell_app_cache_translate_folder (shell_app_cache_get_default (), name);
+}
diff --git a/src/shell-util.h b/src/shell-util.h
index 220606de3b..99b9350f97 100644
--- a/src/shell-util.h
+++ b/src/shell-util.h
@@ -69,6 +69,8 @@ void shell_util_sd_notify (void);
 gboolean shell_util_has_x11_display_extension (MetaDisplay *display,
                                                const char  *extension);
 
+char *shell_util_get_translated_folder_name (const char *name);
+
 G_END_DECLS
 
 #endif /* __SHELL_UTIL_H__ */


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]