[gnome-games/wip/exalm/db: 8/9] database: Cache games



commit cc98ff6ec4e854f8c59fbe0e24d641121646e0b7
Author: Alexander Mikhaylenko <alexm gnome org>
Date:   Sat Feb 8 16:36:51 2020 +0500

    database: Cache games
    
    Currently, when starting the app we load everything anew. Instead, cache
    the current state in the database and load it immediately when starting
    the app. Then, run the current discovery process (since it's threaded,
    there are no UI stalls) and amend the game collection and the database.
    
    This should significantly speed up game loading.
    
    Remove pausing, as loading should be fast enough now. If it becomes a
    problem, it can be restored separately.

 src/core/game-collection.vala   |  88 +++++++++++++++--
 src/core/game-model.vala        |  33 ++++++-
 src/core/game.vala              |   2 +
 src/core/platform-model.vala    |  14 +++
 src/core/platform-register.vala |   4 +
 src/database/database-game.vala |  47 +++++++++
 src/database/database.vala      | 214 +++++++++++++++++++++++++++++++++++++++-
 src/meson.build                 |   1 +
 src/ui/application-window.vala  |   5 -
 src/ui/application.vala         |  16 +--
 src/ui/game-icon-view.vala      |  26 ++++-
 src/ui/game-thumbnail.vala      |   2 +
 12 files changed, 415 insertions(+), 37 deletions(-)
---
diff --git a/src/core/game-collection.vala b/src/core/game-collection.vala
index b09b433a..57ad191c 100644
--- a/src/core/game-collection.vala
+++ b/src/core/game-collection.vala
@@ -2,24 +2,32 @@
 
 private class Games.GameCollection : Object {
        public signal void game_added (Game game);
+       public signal void game_replaced (Game game, Game prev_game);
+       public signal void game_removed (Game game);
 
+       private HashTable<string, Game> cached_games;
        private GenericSet<Game> games;
        private UriSource[] sources;
        private UriGameFactory[] factories;
        private RunnerFactory[] runner_factories;
+       private Database database;
 
        private HashTable<string, Array<UriGameFactory>> factories_for_mime_type;
        private HashTable<string, Array<UriGameFactory>> factories_for_scheme;
        private HashTable<Platform, Array<RunnerFactory>> runner_factories_for_platforms;
 
-       public bool paused { get; set; }
        private SourceFunc search_games_cb;
 
-       construct {
+       public GameCollection (Database database) {
+               this.database = database;
+
+               cached_games = new HashTable<string, Game> (str_hash, str_equal);
                games = new GenericSet<Game> (Game.hash, Game.equal);
                factories_for_mime_type = new HashTable<string, Array<UriGameFactory>> (str_hash, str_equal);
                factories_for_scheme = new HashTable<string, Array<UriGameFactory>> (str_hash, str_equal);
                runner_factories_for_platforms = new HashTable<Platform, Array<RunnerFactory>> 
(Platform.hash, Platform.equal);
+
+               add_source (database.get_uri_source ());
        }
 
        public void add_source (UriSource source) {
@@ -84,15 +92,45 @@ private class Games.GameCollection : Object {
                search_games_cb = search_games.callback;
 
                ThreadFunc<void*> run = () => {
-                       foreach (var source in sources)
-                               foreach (var uri in source) {
-                                       if (paused) {
-                                               Idle.add ((owned) search_games_cb);
-                                               return null;
-                                       }
+                       try {
+                               database.list_cached_games ((game) => {
+                                       cached_games[game.get_uri ().to_string ()] = game;
+                                       if (games.contains (game))
+                                               return;
+
+                                       games.add (game);
+                                       Idle.add (() => {
+                                               game_added (game);
+                                               return Source.REMOVE;
+                                       });
+                               });
+                       }
+                       catch (Error e) {
+                               critical ("Couldn't load cached games: %s", e.message);
+                       }
 
+                       foreach (var source in sources)
+                               foreach (var uri in source)
                                        add_uri (uri);
+
+                       cached_games.foreach_steal ((uri, game) => {
+                               var removed = false;
+                               try {
+                                       removed = database.remove_game (uri, game);
                                }
+                               catch (Error e) {
+                                       warning ("Couldn't remove game: %s", e.message);
+                               }
+
+                               games.remove (game);
+                               if (removed)
+                                       Idle.add (() => {
+                                               game_removed (game);
+                                               return Source.REMOVE;
+                                       });
+
+                               return true;
+                       });
 
                        Idle.add ((owned) search_games_cb);
                        return null;
@@ -173,12 +211,42 @@ private class Games.GameCollection : Object {
        }
 
        private void store_game (Game game) {
-               if (games.contains (game))
+               var uri = game.get_uri ().to_string ();
+               if (cached_games.contains (uri)) {
+                       var cached_game = cached_games.take (uri);
+
+                       try {
+                               database.update_game (game, cached_game);
+                       }
+                       catch (Error e) {
+                               warning ("Couldn't update game: %s", e.message);
+                       }
+
+                       Idle.add (() => {
+                               game_replaced (game, cached_game);
+                               return Source.REMOVE;
+                       });
+
+                       return;
+               }
+
+               Game? prev_game = null;
+               try {
+                       prev_game = database.store_game (game);
+               }
+               catch (Error e) {
+                       warning ("Couldn't cache game: %s", e.message);
+               }
+
+               if (games.contains (game) && prev_game == null)
                        return;
 
                games.add (game);
                Idle.add (() => {
-                       game_added (game);
+                       if (prev_game != null)
+                               game_replaced (game, prev_game);
+                       else
+                               game_added (game);
                        return Source.REMOVE;
                });
        }
diff --git a/src/core/game-model.vala b/src/core/game-model.vala
index f98795c8..c19d9e12 100644
--- a/src/core/game-model.vala
+++ b/src/core/game-model.vala
@@ -2,6 +2,7 @@
 
 private class Games.GameModel : Object, ListModel {
        public signal void game_added (Game game);
+       public signal void game_removed (Game game);
 
        private Sequence<Game> sequence;
        private int n_games;
@@ -33,18 +34,44 @@ private class Games.GameModel : Object, ListModel {
                game_added (game);
        }
 
+       public void replace_game (Game game, Game prev_game) {
+               // Title changed, just hope it doesn't happen too often
+               if (prev_game.name != game.name) {
+                       remove_game (prev_game);
+                       add_game (game);
+
+                       return;
+               }
+
+               // Title didn't change, try to make it seamless
+               prev_game.replaced (game);
+       }
+
+       public void remove_game (Game game) {
+               var iter = sequence.lookup (game, compare_func);
+
+               var pos = iter.get_position ();
+               iter.remove ();
+
+               items_changed (pos, 1, 0);
+               game_removed (game);
+       }
+
        private int compare_func (Game a, Game b) {
                var ret = a.name.collate (b.name);
-
                if (ret != 0)
                        return ret;
 
-               ret = a.get_platform ().get_name ().collate (b.get_platform ().get_name ());
+               ret = a.get_platform ().get_name ().collate (
+                     b.get_platform ().get_name ());
                if (ret != 0)
                        return ret;
 
                try {
-                       return a.get_uid ().get_uid ().collate (b.get_uid ().get_uid ());
+                       var uid1 = a.get_uid ().get_uid ();
+                       var uid2 = b.get_uid ().get_uid ();
+
+                       return uid1.collate (uid2);
                }
                catch (Error e) {
                        assert_not_reached ();
diff --git a/src/core/game.vala b/src/core/game.vala
index 94a3a34b..479c5d95 100644
--- a/src/core/game.vala
+++ b/src/core/game.vala
@@ -1,6 +1,8 @@
 // This file is part of GNOME Games. License: GPL-3.0+.
 
 public interface Games.Game : Object {
+       public signal void replaced (Game new_game);
+
        public abstract string name { get; }
 
        public abstract Uid get_uid ();
diff --git a/src/core/platform-model.vala b/src/core/platform-model.vala
index 372608bb..c07a088c 100644
--- a/src/core/platform-model.vala
+++ b/src/core/platform-model.vala
@@ -13,6 +13,7 @@ private class Games.PlatformModel : Object, ListModel {
                n_games = new HashTable<Platform, uint> (Platform.hash, Platform.equal);
 
                game_model.game_added.connect (game_added);
+               game_model.game_removed.connect (game_removed);
        }
 
        public Object? get_item (uint position) {
@@ -40,6 +41,19 @@ private class Games.PlatformModel : Object, ListModel {
                n_games[platform] = n_games[platform] + 1;
        }
 
+       private void game_removed (Game game) {
+               var platform = game.get_platform ();
+
+               n_games[platform] = n_games[platform] - 1;
+
+               if (n_games[platform] == 0) {
+                       var iter = sequence.lookup (platform, compare_func);
+                       var pos = iter.get_position ();
+                       iter.remove ();
+                       items_changed (pos, 1, 0);
+               }
+       }
+
        private int compare_func (Platform a, Platform b) {
                return a.get_name ().collate (b.get_name ());
        }
diff --git a/src/core/platform-register.vala b/src/core/platform-register.vala
index 0e77b6c9..b03290dc 100644
--- a/src/core/platform-register.vala
+++ b/src/core/platform-register.vala
@@ -34,4 +34,8 @@ private class Games.PlatformRegister : Object {
 
                return result;
        }
+
+       public Platform get_platform (string id) {
+               return platforms[id];
+       }
 }
diff --git a/src/database/database-game.vala b/src/database/database-game.vala
new file mode 100644
index 00000000..9454a39a
--- /dev/null
+++ b/src/database/database-game.vala
@@ -0,0 +1,47 @@
+// This file is part of GNOME Games. License: GPL-3.0+.
+
+public class Games.DatabaseGame : Object, Game {
+       private string game_title;
+       public string name {
+               get { return game_title; }
+       }
+
+       private Uid game_uid;
+       private Uri game_uri;
+       private Platform game_platform;
+       private MediaSet game_media_set;
+
+       public DatabaseGame (string uid, string uri, string title, string platform, string? media_set) {
+               game_uid = new GenericUid (uid);
+               game_uri = new Uri (uri);
+               game_title = title;
+               game_platform = PlatformRegister.get_register ().get_platform (platform);
+
+               if (media_set != null)
+                       game_media_set = new MediaSet.parse (new Variant.parsed (media_set));
+       }
+
+       public Uid get_uid () {
+               return game_uid;
+       }
+
+       public Uri get_uri () {
+               return game_uri;
+       }
+
+       public MediaSet? get_media_set () {
+               return game_media_set;
+       }
+
+       public Icon get_icon () {
+               return new DummyIcon ();
+       }
+
+       public Cover get_cover () {
+               return new DummyCover ();
+       }
+
+       public Platform get_platform () {
+               return game_platform;
+       }
+}
diff --git a/src/database/database.vala b/src/database/database.vala
index 09638579..7166decc 100644
--- a/src/database/database.vala
+++ b/src/database/database.vala
@@ -10,6 +10,57 @@ private class Games.Database : Object {
                );
        """;
 
+       private const string CREATE_GAMES_TABLE_QUERY = """
+               CREATE TABLE IF NOT EXISTS games (
+                       id INTEGER PRIMARY KEY NOT NULL,
+                       uid TEXT NOT NULL UNIQUE,
+                       title TEXT NOT NULL,
+                       platform TEXT NOT NULL,
+                       media_set TEXT NULL
+               );
+       """;
+
+       private const string CREATE_URIS_TABLE_QUERY = """
+               CREATE TABLE IF NOT EXISTS uris (
+                       id INTEGER PRIMARY KEY NOT NULL,
+                       uid TEXT NOT NULL,
+                       uri TEXT NOT NULL UNIQUE,
+                       FOREIGN KEY(uid) REFERENCES games(uid)
+               );
+       """;
+
+       private const string ADD_GAME_QUERY = """
+               INSERT INTO games (uid, title, platform, media_set) VALUES ($UID, $TITLE, $PLATFORM, 
$MEDIA_SET);
+       """;
+
+       private const string ADD_GAME_URI_QUERY = """
+               INSERT INTO uris (uid, uri) VALUES ($UID, $URI);
+       """;
+
+       private const string UPDATE_GAME_QUERY = """
+               UPDATE games SET title = $TITLE, media_set = $MEDIA_SET WHERE uid = $UID;
+       """;
+
+       private const string DELETE_GAME_QUERY = """
+               DELETE FROM games WHERE uid = $UID;
+       """;
+
+       private const string DELETE_URI_QUERY = """
+               DELETE FROM uris WHERE uri = $URI;
+       """;
+
+       private const string FIND_GAME_URIS_QUERY = """
+               SELECT uri FROM uris WHERE uid = $UID;
+       """;
+
+       private const string GET_CACHED_GAME_QUERY = """
+               SELECT uri, title, platform, media_set FROM games JOIN uris ON games.uid == uris.uid WHERE 
games.uid == $UID;
+       """;
+
+       private const string LIST_CACHED_GAMES_QUERY = """
+               SELECT games.uid, uri, title, platform, media_set FROM games JOIN uris ON games.uid == 
uris.uid ORDER BY title;
+       """;
+
        private const string ADD_GAME_RESOURCE_QUERY = """
                INSERT INTO game_resources (id, uri) VALUES (NULL, $URI);
        """;
@@ -58,6 +109,8 @@ private class Games.Database : Object {
 
        private void create_tables () throws Error {
                exec (CREATE_RESOURCES_TABLE_QUERY, null);
+               exec (CREATE_GAMES_TABLE_QUERY, null);
+               exec (CREATE_URIS_TABLE_QUERY, null);
        }
 
        private void exec (string query, Sqlite.Callback? callback) throws Error {
@@ -75,11 +128,168 @@ private class Games.Database : Object {
                return statement;
        }
 
-       internal static void bind_text (Sqlite.Statement statement, string parameter, string text) throws 
Error {
+       internal static void bind_text (Sqlite.Statement statement, string parameter, string? text) throws 
Error {
                var position = statement.bind_parameter_index (parameter);
                if (position <= 0)
                        throw new DatabaseError.BINDING_FAILED ("Couldn't bind text to the parameter ā€œ%sā€, 
unexpected position: %d.", parameter, position);
 
-               statement.bind_text (position, text);
+               if (text != null)
+                       statement.bind_text (position, text);
+               else
+                       statement.bind_null (position);
+       }
+
+       private string? serialize_media_set (Game game) {
+               var media_set = game.get_media_set ();
+
+               if (media_set == null)
+                       return null;
+
+               return media_set.serialize ().print (true);
+       }
+
+       private string[] get_media_uris (Game game) {
+               var media_set = game.get_media_set ();
+
+               if (media_set == null)
+                       return {};
+
+               string[] uris = {};
+               media_set.foreach_media (media => {
+                       foreach (var uri in media.get_uris ())
+                               uris += uri.to_string ();
+               });
+
+               return uris;
+       }
+
+       private void store_game_uri (string uid, string uri) throws Error {
+               var statement = prepare (database, ADD_GAME_URI_QUERY);
+               bind_text (statement, "$UID", uid);
+               bind_text (statement, "$URI", uri);
+
+               var result = statement.step ();
+               if (result != Sqlite.DONE && result != Sqlite.CONSTRAINT)
+                       throw new DatabaseError.EXECUTION_FAILED ("Couldn't add uri (%s, %s)", uid, uri);
+       }
+
+       public Game? store_game (Game game) throws Error {
+               var uid = game.get_uid ().get_uid ();
+               var uri = game.get_uri ().to_string ();
+               var title = game.name;
+               var platform = game.get_platform ().get_id ();
+               var media_set = serialize_media_set (game);
+
+               // TODO transaction
+
+               if (game.get_media_set () != null)
+                       foreach (var media_uri in get_media_uris (game))
+                               store_game_uri (uid, media_uri);
+               else
+                       store_game_uri (uid, uri);
+
+               var statement = prepare (database, ADD_GAME_QUERY);
+               bind_text (statement, "$UID", uid);
+               bind_text (statement, "$TITLE", title);
+               bind_text (statement, "$PLATFORM", platform);
+               bind_text (statement, "$MEDIA_SET", media_set);
+
+               var result = statement.step ();
+               if (result == Sqlite.CONSTRAINT) {
+                       var prev_game = get_cached_game (uid);
+                       update_game (game, prev_game);
+                       return prev_game;
+               }
+
+               if (result != Sqlite.DONE)
+                       throw new DatabaseError.EXECUTION_FAILED ("Couldn't add game (%s, %s, %s, %s)", uid, 
title, platform, media_set);
+
+               return null;
+       }
+
+       public void update_game (Game game, Game? prev_game = null) throws Error {
+               var uid = game.get_uid ().get_uid ();
+               var uri = game.get_uri ().to_string ();
+               var title = game.name;
+               var media_set = serialize_media_set (game);
+               var old_title = prev_game != null ? prev_game.name : null;
+               var old_media_set = prev_game != null ? serialize_media_set (prev_game) : null;
+
+               if (old_title == title && old_media_set == media_set)
+                       return;
+
+               var statement = prepare (database, UPDATE_GAME_QUERY);
+               bind_text (statement, "$UID", uid);
+               bind_text (statement, "$TITLE", title);
+               bind_text (statement, "$MEDIA_SET", media_set);
+
+               if (statement.step () != Sqlite.DONE)
+                       throw new DatabaseError.EXECUTION_FAILED ("Couldn't update game (%s, %s, %s)", uid, 
title, media_set);
+
+               if (game.get_media_set () != null)
+                       foreach (var media_uri in get_media_uris (game))
+                               store_game_uri (uid, media_uri);
+               else
+                       store_game_uri (uid, uri);
+       }
+
+       public bool remove_game (string uri, Game game) throws Error {
+               var uid = game.get_uid ().get_uid ();
+
+               var statement = prepare (database, DELETE_URI_QUERY);
+               bind_text (statement, "$URI", uri);
+
+               if (statement.step () != Sqlite.DONE)
+                       throw new DatabaseError.EXECUTION_FAILED ("Couldn't delete uri (%s)", uri);
+
+               statement = prepare (database, FIND_GAME_URIS_QUERY);
+               bind_text (statement, "$UID", uid);
+
+               var result = statement.step ();
+               if (result == Sqlite.ROW)
+                       return false;
+
+               if (result != Sqlite.DONE)
+                       throw new DatabaseError.EXECUTION_FAILED ("Couldn't find uris (%s)", uid);
+
+               statement = prepare (database, DELETE_GAME_QUERY);
+               bind_text (statement, "$UID", uid);
+
+               if (statement.step () != Sqlite.DONE)
+                       throw new DatabaseError.EXECUTION_FAILED ("Couldn't delete game (%s)", uid);
+
+               return true;
+       }
+
+       private Game get_cached_game (string game_uid) throws Error {
+               var statement = prepare (database, GET_CACHED_GAME_QUERY);
+               bind_text (statement, "$UID", game_uid);
+
+               if (statement.step () == Sqlite.ROW) {
+                       var uid = statement.column_text (0);
+                       var uri = statement.column_text (1);
+                       var title = statement.column_text (2);
+                       var platform = statement.column_text (3);
+                       var media_set = statement.column_text (4);
+
+                       return new DatabaseGame (uid, uri, title, platform, media_set);
+               }
+
+               throw new DatabaseError.EXECUTION_FAILED ("Couldn't get game for uid (%s)", game_uid);
+       }
+
+       public void list_cached_games (GameCallback game_callback) throws Error {
+               var statement = prepare (database, LIST_CACHED_GAMES_QUERY);
+
+               while (statement.step () == Sqlite.ROW) {
+                       var uid = statement.column_text (0);
+                       var uri = statement.column_text (1);
+                       var title = statement.column_text (2);
+                       var platform = statement.column_text (3);
+                       var media_set = statement.column_text (4);
+
+                       var game = new DatabaseGame (uid, uri, title, platform, media_set);
+                       game_callback (game);
+               }
        }
 }
diff --git a/src/meson.build b/src/meson.build
index 01113131..d338ad19 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -46,6 +46,7 @@ vala_sources = [
 
   'database/database.vala',
   'database/database-error.vala',
+  'database/database-game.vala',
   'database/database-uri-iterator.vala',
   'database/database-uri-source.vala',
 
diff --git a/src/ui/application-window.vala b/src/ui/application-window.vala
index 63115f60..d7689441 100644
--- a/src/ui/application-window.vala
+++ b/src/ui/application-window.vala
@@ -21,11 +21,6 @@ private class Games.ApplicationWindow : Gtk.ApplicationWindow {
 
                        if (current_view != null)
                                current_view.is_view_active = true;
-
-                       var app = application as Application;
-                       assert (app != null);
-
-                       app.set_pause_loading (current_view != collection_view);
                }
        }
 
diff --git a/src/ui/application.vala b/src/ui/application.vala
index 02a35307..d3f5bb3f 100644
--- a/src/ui/application.vala
+++ b/src/ui/application.vala
@@ -205,6 +205,8 @@ public class Games.Application : Gtk.Application {
 
                game_model = new GameModel ();
                game_collection.game_added.connect (game_model.add_game);
+               game_collection.game_replaced.connect (game_model.replace_game);
+               game_collection.game_removed.connect (game_model.remove_game);
 
                cover_loader = new CoverLoader ();
        }
@@ -244,9 +246,7 @@ public class Games.Application : Gtk.Application {
                        debug (e.message);
                }
 
-               game_collection = new GameCollection ();
-               if (database != null)
-                       game_collection.add_source (database.get_uri_source ());
+               game_collection = new GameCollection (database);
 
                if (tracker_uri_source != null)
                        game_collection.add_source (tracker_uri_source);
@@ -347,16 +347,6 @@ public class Games.Application : Gtk.Application {
                        window.loading_notification = false;
        }
 
-       public void set_pause_loading (bool paused) {
-               if (game_collection.paused == paused)
-                       return;
-
-               game_collection.paused = paused;
-
-               if (!paused)
-                       load_game_list.begin ();
-       }
-
        private void preferences () {
                if (preferences_window == null) {
                        preferences_window = new PreferencesWindow ();
diff --git a/src/ui/game-icon-view.vala b/src/ui/game-icon-view.vala
index 26c1f362..40f720ca 100644
--- a/src/ui/game-icon-view.vala
+++ b/src/ui/game-icon-view.vala
@@ -7,14 +7,32 @@ private class Games.GameIconView : Gtk.Box {
        [GtkChild]
        private Gtk.Label title;
 
-       public Game game { get; construct; }
+       private ulong game_replaced_id;
 
-       construct {
-               thumbnail.game = game;
-               title.label = game.name;
+       private Game _game;
+       public Game game {
+               get { return _game; }
+               construct set {
+                       if (game == value)
+                               return;
+
+                       if (game_replaced_id > 0)
+                               game.disconnect (game_replaced_id);
+
+                       _game = value;
+
+                       thumbnail.game = game;
+                       title.label = game.name;
+
+                       game_replaced_id = game.replaced.connect (game_replaced);
+               }
        }
 
        public GameIconView (Game game) {
                Object (game: game);
        }
+
+       private void game_replaced (Game new_game) {
+               game = new_game;
+       }
 }
diff --git a/src/ui/game-thumbnail.vala b/src/ui/game-thumbnail.vala
index edc03970..8b5ef689 100644
--- a/src/ui/game-thumbnail.vala
+++ b/src/ui/game-thumbnail.vala
@@ -24,6 +24,8 @@ private class Games.GameThumbnail : Gtk.DrawingArea {
                        icon = game.get_icon ();
                        cover = game.get_cover ();
 
+                       try_load_cover = true;
+
                        if (cover != null)
                                cover_changed_id = cover.changed.connect (() => {
                                        try_load_cover = true;


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