From 244f97a68e9d0f1aab34206caa43254c210ebb1b Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Mon, 30 Aug 2021 08:13:50 +0900 Subject: [PATCH 01/26] Cache entries' cover_url --- src/library/entry.cr | 12 ++++++++++-- src/library/title.cr | 5 ++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/library/entry.cr b/src/library/entry.cr index 92f4def..b5e582f 100644 --- a/src/library/entry.cr +++ b/src/library/entry.cr @@ -81,9 +81,17 @@ class Entry def cover_url return "#{Config.current.base_url}img/icon.png" if @err_msg + + unless @book.entry_cover_url_cache + TitleInfo.new @book.dir do |info| + @book.entry_cover_url_cache = info.entry_cover_url + end + end + entry_cover_url = @book.entry_cover_url_cache + url = "#{Config.current.base_url}api/cover/#{@book.id}/#{@id}" - TitleInfo.new @book.dir do |info| - info_url = info.entry_cover_url[@title]? + if entry_cover_url + info_url = entry_cover_url[@title]? unless info_url.nil? || info_url.empty? url = File.join Config.current.base_url, info_url end diff --git a/src/library/title.cr b/src/library/title.cr index 61c9813..6f2cf8c 100644 --- a/src/library/title.cr +++ b/src/library/title.cr @@ -3,9 +3,12 @@ require "../archive" class Title getter dir : String, parent_id : String, title_ids : Array(String), entries : Array(Entry), title : String, id : String, - encoded_title : String, mtime : Time, signature : UInt64 + encoded_title : String, mtime : Time, signature : UInt64, + entry_cover_url_cache : Hash(String, String)? + setter entry_cover_url_cache : Hash(String, String)? @entry_display_name_cache : Hash(String, String)? + @entry_cover_url_cache : Hash(String, String)? def initialize(@dir : String, @parent_id) storage = Storage.default From 51a47b5dddec78840dd496ec03fe956a264ae1c5 Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Mon, 30 Aug 2021 08:17:52 +0900 Subject: [PATCH 02/26] Cache display_name --- src/library/title.cr | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/library/title.cr b/src/library/title.cr index 6f2cf8c..2983a69 100644 --- a/src/library/title.cr +++ b/src/library/title.cr @@ -9,6 +9,7 @@ class Title @entry_display_name_cache : Hash(String, String)? @entry_cover_url_cache : Hash(String, String)? + @cached_display_name : String? def initialize(@dir : String, @parent_id) storage = Storage.default @@ -180,11 +181,15 @@ class Title end def display_name + cached_display_name = @cached_display_name + return cached_display_name unless cached_display_name.nil? + dn = @title TitleInfo.new @dir do |info| info_dn = info.display_name dn = info_dn unless info_dn.empty? end + @cached_display_name = dn dn end @@ -208,6 +213,7 @@ class Title end def set_display_name(dn) + @cached_display_name = nil TitleInfo.new @dir do |info| info.display_name = dn info.save @@ -217,6 +223,7 @@ class Title def set_display_name(entry_name : String, dn) TitleInfo.new @dir do |info| info.entry_display_name[entry_name] = dn + @entry_display_name_cache = info.entry_display_name info.save end end From 00c9cc1fcdffa1b41dddfbb21bf348252c450706 Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Mon, 30 Aug 2021 08:19:15 +0900 Subject: [PATCH 03/26] Prevent saving a sort opt unnecessarily --- src/library/library.cr | 5 ----- src/library/title.cr | 6 +----- src/routes/main.cr | 6 +++--- src/util/web.cr | 20 ++++++++++++++++++++ 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/library/library.cr b/src/library/library.cr index a5a4a80..30e93b2 100644 --- a/src/library/library.cr +++ b/src/library/library.cr @@ -51,11 +51,6 @@ class Library def sorted_titles(username, opt : SortOptions? = nil) if opt.nil? opt = SortOptions.from_info_json @dir, username - else - TitleInfo.new @dir do |info| - info.sort_by[username] = opt.to_tuple - info.save - end end # Helper function from src/util/util.cr diff --git a/src/library/title.cr b/src/library/title.cr index 2983a69..aa4a479 100644 --- a/src/library/title.cr +++ b/src/library/title.cr @@ -1,3 +1,4 @@ +require "digest" require "../archive" class Title @@ -329,11 +330,6 @@ class Title def sorted_entries(username, opt : SortOptions? = nil) if opt.nil? opt = SortOptions.from_info_json @dir, username - else - TitleInfo.new @dir do |info| - info.sort_by[username] = opt.to_tuple - info.save - end end case opt.not_nil!.method diff --git a/src/routes/main.cr b/src/routes/main.cr index 57917bb..4aa7da6 100644 --- a/src/routes/main.cr +++ b/src/routes/main.cr @@ -41,7 +41,7 @@ struct MainRouter username = get_username env sort_opt = SortOptions.from_info_json Library.default.dir, username - get_sort_opt + get_and_save_sort_opt Library.default.dir titles = Library.default.sorted_titles username, sort_opt percentage = titles.map &.load_percentage username @@ -59,12 +59,12 @@ struct MainRouter username = get_username env sort_opt = SortOptions.from_info_json title.dir, username - get_sort_opt + get_and_save_sort_opt title.dir entries = title.sorted_entries username, sort_opt - percentage = title.load_percentage_for_all_entries username, sort_opt title_percentage = title.titles.map &.load_percentage username + layout "title" rescue e Logger.error e diff --git a/src/util/web.cr b/src/util/web.cr index 12459e5..5704ea8 100644 --- a/src/util/web.cr +++ b/src/util/web.cr @@ -107,6 +107,26 @@ macro get_sort_opt end end +macro get_and_save_sort_opt(dir) + sort_method = env.params.query["sort"]? + + if sort_method + is_ascending = true + + ascend = env.params.query["ascend"]? + if ascend && ascend.to_i? == 0 + is_ascending = false + end + + sort_opt = SortOptions.new sort_method, is_ascending + + TitleInfo.new {{dir}} do |info| + info.sort_by[username] = sort_opt.to_tuple + info.save + end + end +end + module HTTP class Client private def self.exec(uri : URI, tls : TLSContext = nil) From 4a09aee1771b8aca11b666a83c7ad906355c43e2 Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Sun, 22 Aug 2021 23:36:28 +0900 Subject: [PATCH 04/26] Implement library caching TitleInfo * Cache sum of entry progress * Cache cover_url * Cache display_name * Cache sort_opt --- src/library/cache.cr | 159 +++++++++++++++++++++++++++++++++++++++++ src/library/entry.cr | 10 +++ src/library/library.cr | 4 ++ src/library/title.cr | 25 ++++++- src/library/types.cr | 3 + src/util/web.cr | 1 + 6 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 src/library/cache.cr diff --git a/src/library/cache.cr b/src/library/cache.cr new file mode 100644 index 0000000..00c65ba --- /dev/null +++ b/src/library/cache.cr @@ -0,0 +1,159 @@ +require "digest" + +class InfoCache + alias ProgressCache = Tuple(String, Int32) + + def self.clear + clear_cover_url + clear_progress_cache + clear_sort_opt + end + + def self.clean + clean_cover_url + clean_progress_cache + clean_sort_opt + end + + # item id => cover_url + @@cached_cover_url = {} of String => String + @@cached_cover_url_previous = {} of String => String # item id => cover_url + + def self.set_cover_url(id : String, cover_url : String) + @@cached_cover_url[id] = cover_url + end + + def self.get_cover_url(id : String) + @@cached_cover_url[id]? + end + + def self.invalidate_cover_url(id : String) + @@cached_cover_url.delete id + end + + def self.move_cover_url(id : String) + if @@cached_cover_url_previous[id]? + @@cached_cover_url[id] = @@cached_cover_url_previous[id] + end + end + + private def self.clear_cover_url + @@cached_cover_url_previous = @@cached_cover_url + @@cached_cover_url = {} of String => String + end + + private def self.clean_cover_url + @@cached_cover_url_previous = {} of String => String + end + + # book.id:username => {signature, sum} + @@progress_cache = {} of String => ProgressCache + # book.id => username => {signature, sum} + @@progress_cache_previous = {} of String => Hash(String, ProgressCache) + + def self.set_progress_cache(book_id : String, username : String, + entries : Array(Entry), sum : Int32) + progress_cache_id = "#{book_id}:#{username}" + progress_cache_sig = Digest::SHA1.hexdigest (entries.map &.id).to_s + @@progress_cache[progress_cache_id] = {progress_cache_sig, sum} + Logger.debug "Progress Cached #{progress_cache_id}" + end + + def self.get_progress_cache(book_id : String, username : String, + entries : Array(Entry)) + progress_cache_id = "#{book_id}:#{username}" + progress_cache_sig = Digest::SHA1.hexdigest (entries.map &.id).to_s + cached = @@progress_cache[progress_cache_id]? + if cached && cached[0] == progress_cache_sig + Logger.debug "Progress Cache Hit! #{progress_cache_id}" + return cached[1] + end + end + + def self.invalidate_progress_cache(book_id : String, username : String) + progress_cache_id = "#{book_id}:#{username}" + if @@progress_cache[progress_cache_id]? + @@progress_cache.delete progress_cache_id + Logger.debug "Progress Invalidate Cache #{progress_cache_id}" + end + end + + def self.move_progress_cache(book_id : String) + if @@progress_cache_previous[book_id]? + @@progress_cache_previous[book_id].each do |username, cached| + id = "#{book_id}:#{username}" + unless @@progress_cache[id]? + # It would be invalidated when entries changed + @@progress_cache[id] = cached + end + end + end + end + + private def self.clear_progress_cache + @@progress_cache_previous = {} of String => Hash(String, ProgressCache) + @@progress_cache.each do |id, cached| + splitted = id.split(':', 2) + book_id = splitted[0] + username = splitted[1] + unless @@progress_cache_previous[book_id]? + @@progress_cache_previous[book_id] = {} of String => ProgressCache + end + + @@progress_cache_previous[book_id][username] = cached + end + @@progress_cache = {} of String => ProgressCache + end + + private def self.clean_progress_cache + @@progress_cache_previous = {} of String => Hash(String, ProgressCache) + end + + # book.dir:username => SortOptions + @@cached_sort_opt = {} of String => SortOptions + @@cached_sort_opt_previous = {} of String => Hash(String, SortOptions) + + def self.set_sort_opt(dir : String, username : String, sort_opt : SortOptions) + id = "#{dir}:#{username}" + @@cached_sort_opt[id] = sort_opt + end + + def self.get_sort_opt(dir : String, username : String) + id = "#{dir}:#{username}" + @@cached_sort_opt[id]? + end + + def self.invalidate_sort_opt(dir : String, username : String) + id = "#{dir}:#{username}" + @@cached_sort_opt.delete id + end + + def self.move_sort_opt(dir : String) + if @@cached_sort_opt_previous[dir]? + @@cached_sort_opt_previous[dir].each do |username, cached| + id = "#{dir}:#{username}" + unless @@cached_sort_opt[id]? + @@cached_sort_opt[id] = cached + end + end + end + end + + private def self.clear_sort_opt + @@cached_sort_opt_previous = {} of String => Hash(String, SortOptions) + @@cached_sort_opt.each do |id, cached| + splitted = id.split(':', 2) + book_dir = splitted[0] + username = splitted[1] + unless @@cached_sort_opt_previous[book_dir]? + @@cached_sort_opt_previous[book_dir] = {} of String => SortOptions + end + @@cached_sort_opt_previous[book_dir][username] = cached + end + @@cached_sort_opt = {} of String => SortOptions + end + + private def self.clean_sort_opt + @@cached_sort_opt_previous = {} of String => Hash(String, SortOptions) + end +end diff --git a/src/library/entry.cr b/src/library/entry.cr index b5e582f..cbebf7f 100644 --- a/src/library/entry.cr +++ b/src/library/entry.cr @@ -44,6 +44,8 @@ class Entry MIME.from_filename? e.filename end file.close + + InfoCache.move_cover_url @id end def to_slim_json : String @@ -81,6 +83,8 @@ class Entry def cover_url return "#{Config.current.base_url}img/icon.png" if @err_msg + cached_cover_url = InfoCache.get_cover_url @id + return cached_cover_url if cached_cover_url unless @book.entry_cover_url_cache TitleInfo.new @book.dir do |info| @@ -96,6 +100,7 @@ class Entry url = File.join Config.current.base_url, info_url end end + InfoCache.set_cover_url @id, url url end @@ -178,6 +183,11 @@ class Entry # For backward backward compatibility with v0.1.0, we save entry titles # instead of IDs in info.json def save_progress(username, page) + InfoCache.invalidate_progress_cache @book.id, username + @book.parents.each do |parent| + InfoCache.invalidate_progress_cache parent.id, username + end + TitleInfo.new @book.dir do |info| if info.progress[username]?.nil? info.progress[username] = {@title => page} diff --git a/src/library/library.cr b/src/library/library.cr index 30e93b2..2638ffd 100644 --- a/src/library/library.cr +++ b/src/library/library.cr @@ -102,6 +102,8 @@ class Library storage = Storage.new auto_close: false + InfoCache.clear + (Dir.entries @dir) .select { |fn| !fn.starts_with? "." } .map { |fn| File.join @dir, fn } @@ -115,6 +117,8 @@ class Library @title_ids << title.id end + InfoCache.clean + storage.bulk_insert_ids storage.close diff --git a/src/library/title.cr b/src/library/title.cr index aa4a479..14b5754 100644 --- a/src/library/title.cr +++ b/src/library/title.cr @@ -60,6 +60,10 @@ class Title @entries.sort! do |a, b| sorter.compare a.title, b.title end + + InfoCache.move_cover_url @id + InfoCache.move_progress_cache @id + InfoCache.move_sort_opt @dir end def to_slim_json : String @@ -230,6 +234,9 @@ class Title end def cover_url + cached_cover_url = InfoCache.get_cover_url @id + return cached_cover_url if cached_cover_url + url = "#{Config.current.base_url}img/icon.png" readable_entries = @entries.select &.err_msg.nil? if readable_entries.size > 0 @@ -241,10 +248,12 @@ class Title url = File.join Config.current.base_url, info_url end end + InfoCache.set_cover_url @id, url url end def set_cover_url(url : String) + InfoCache.invalidate_cover_url @id TitleInfo.new @dir do |info| info.cover_url = url info.save @@ -252,6 +261,8 @@ class Title end def set_cover_url(entry_name : String, url : String) + selected_entry = @entries.find { |entry| entry.display_name == entry_name } + InfoCache.invalidate_cover_url selected_entry.id if selected_entry TitleInfo.new @dir do |info| info.entry_cover_url[entry_name] = url info.save @@ -273,8 +284,13 @@ class Title end def deep_read_page_count(username) : Int32 - load_progress_for_all_entries(username).sum + - titles.flat_map(&.deep_read_page_count username).sum + # CACHE HERE + cached_sum = InfoCache.get_progress_cache @id, username, @entries + return cached_sum unless cached_sum.nil? + sum = load_progress_for_all_entries(username).sum + + titles.flat_map(&.deep_read_page_count username).sum + InfoCache.set_progress_cache @id, username, @entries, sum + sum end def deep_total_page_count : Int32 @@ -422,6 +438,11 @@ class Title end def bulk_progress(action, ids : Array(String), username) + InfoCache.invalidate_progress_cache @id, username + parents.each do |parent| + InfoCache.invalidate_progress_cache parent.id, username + end + selected_entries = ids .map { |id| @entries.find &.id.==(id) diff --git a/src/library/types.cr b/src/library/types.cr index 4e83135..094cb64 100644 --- a/src/library/types.cr +++ b/src/library/types.cr @@ -35,12 +35,15 @@ class SortOptions end def self.from_info_json(dir, username) + cached_opt = InfoCache.get_sort_opt dir, username + return cached_opt if cached_opt opt = SortOptions.new TitleInfo.new dir do |info| if info.sort_by.has_key? username opt = SortOptions.from_tuple info.sort_by[username] end end + InfoCache.set_sort_opt dir, username, opt opt end diff --git a/src/util/web.cr b/src/util/web.cr index 5704ea8..5e873ca 100644 --- a/src/util/web.cr +++ b/src/util/web.cr @@ -120,6 +120,7 @@ macro get_and_save_sort_opt(dir) sort_opt = SortOptions.new sort_method, is_ascending + InfoCache.set_sort_opt {{dir}}, username, sort_opt TitleInfo.new {{dir}} do |info| info.sort_by[username] = sort_opt.to_tuple info.save From bf81a4e48b5eca75a6fc43ba011593f681dfac3a Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Sun, 22 Aug 2021 23:51:23 +0900 Subject: [PATCH 05/26] Implement sorted entries cache sorted_entries cached --- src/library/cache.cr | 87 ++++++++++++++++++++++++++++++++++++++++++ src/library/entry.cr | 5 +++ src/library/library.cr | 4 ++ src/library/title.cr | 11 ++++++ 4 files changed, 107 insertions(+) diff --git a/src/library/cache.cr b/src/library/cache.cr index 00c65ba..1d2dd53 100644 --- a/src/library/cache.cr +++ b/src/library/cache.cr @@ -157,3 +157,90 @@ class InfoCache @@cached_sort_opt_previous = {} of String => Hash(String, SortOptions) end end + +private class SortedEntriesCacheEntry + getter key : String, atime : Time + + def initialize(@ctime : Time, @key : String, @value : Array(String)) + @atime = @ctime + end + + def value + @atime = Time.utc + @value + end + + def instance_size + @value.size * (instance_sizeof(String) + sizeof(String)) + + @value.sum(&.size) + instance_sizeof(SortedEntriesCacheEntry) + end +end + +# LRU Cache +class SortedEntriesCache + @@limit : Int128 = Int128.new 1024 * 1024 * 50 # 50MB + # key => entry + @@cache = {} of String => SortedEntriesCacheEntry + + def self.gen_key(book_id : String, username : String, + entries : Array(Entry), opt : SortOptions?) + sig = Digest::SHA1.hexdigest (entries.map &.id).to_s + user_context = opt && opt.method == SortMethod::Progress ? username : "" + Digest::SHA1.hexdigest (book_id + sig + user_context + + (opt ? opt.to_tuple.to_s : "nil")) + end + + def self.get(key : String) + entry = @@cache[key]? + Logger.debug "SortedEntries Cache Hit! #{key}" unless entry.nil? + Logger.debug "SortedEntries Cache Miss #{key}" if entry.nil? + return ids2entries entry.value unless entry.nil? + end + + def self.set(key : String, value : Array(Entry)) + @@cache[key] = SortedEntriesCacheEntry.new Time.utc, key, value.map &.id + Logger.debug "SortedEntries Cached #{key}" + remove_victim_cache + end + + def self.invalidate(key : String) + @@cache.delete key + end + + def self.print + sum = @@cache.sum { |_, entry| entry.instance_size } + Logger.debug "---- Sorted Entries Cache ----" + Logger.debug "Size: #{sum} Bytes" + Logger.debug "List:" + @@cache.each { |k, v| Logger.debug "#{k} | #{v.atime}" } + Logger.debug "------------------------------" + end + + private def self.ids2entries(ids : Array(String)) + e_map = Library.default.deep_entries.to_h { |entry| {entry.id, entry} } + entries = [] of Entry + begin + ids.each do |id| + entries << e_map[id] + end + return entries if ids.size == entries.size + rescue + end + end + + private def self.is_cache_full + sum = @@cache.sum { |_, entry| entry.instance_size } + sum > @@limit + end + + private def self.remove_victim_cache + while is_cache_full && @@cache.size > 0 + Logger.debug "SortedEntries Cache Full! Remove LRU" + min = @@cache.min_by? { |_, entry| entry.atime } + Logger.debug "Target: #{min[0]}, Last Access Time: #{min[1].atime}" if min + invalidate min[0] if min + + print if Logger.get_severity == Log::Severity::Debug + end + end +end diff --git a/src/library/entry.cr b/src/library/entry.cr index cbebf7f..5b1f3ce 100644 --- a/src/library/entry.cr +++ b/src/library/entry.cr @@ -187,6 +187,11 @@ class Entry @book.parents.each do |parent| InfoCache.invalidate_progress_cache parent.id, username end + [false, true].each do |ascend| + sorted_entries_cache_key = SortedEntriesCache.gen_key @book.id, username, + @book.entries, SortOptions.new(SortMethod::Progress, ascend) + SortedEntriesCache.invalidate sorted_entries_cache_key + end TitleInfo.new @book.dir do |info| if info.progress[username]?.nil? diff --git a/src/library/library.cr b/src/library/library.cr index 2638ffd..21e5c8b 100644 --- a/src/library/library.cr +++ b/src/library/library.cr @@ -61,6 +61,10 @@ class Library titles + titles.flat_map &.deep_titles end + def deep_entries + titles.flat_map &.deep_entries + end + def to_slim_json : String JSON.build do |json| json.object do diff --git a/src/library/title.cr b/src/library/title.cr index 14b5754..6a78ea5 100644 --- a/src/library/title.cr +++ b/src/library/title.cr @@ -344,6 +344,10 @@ class Title # use the default (auto, ascending) # When `opt` is not nil, it saves the options to info.json def sorted_entries(username, opt : SortOptions? = nil) + cache_key = SortedEntriesCache.gen_key @id, username, @entries, opt + cached_entries = SortedEntriesCache.get cache_key + return cached_entries if cached_entries + if opt.nil? opt = SortOptions.from_info_json @dir, username end @@ -377,6 +381,7 @@ class Title ary.reverse! unless opt.not_nil!.ascend + SortedEntriesCache.set cache_key, ary ary end @@ -442,6 +447,12 @@ class Title parents.each do |parent| InfoCache.invalidate_progress_cache parent.id, username end + [false, true].each do |ascend| + sorted_entries_cache_key = + SortedEntriesCache.gen_key @id, username, @entries, + SortOptions.new(SortMethod::Progress, ascend) + SortedEntriesCache.invalidate sorted_entries_cache_key + end selected_entries = ids .map { |id| From e988a8c121c630bea3d48d4fd749fcfdd7f56043 Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Mon, 30 Aug 2021 22:53:30 +0900 Subject: [PATCH 06/26] Add config for sorted entries cache optional --- src/config.cr | 2 ++ src/library/cache.cr | 8 +++++++- src/mango.cr | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/config.cr b/src/config.cr index 332a159..0647aee 100644 --- a/src/config.cr +++ b/src/config.cr @@ -20,6 +20,8 @@ class Config property plugin_path : String = File.expand_path "~/mango/plugins", home: true property download_timeout_seconds : Int32 = 30 + property sorted_entries_cache_enable = false + property sorted_entries_cache_capacity_kbs = 51200 property disable_login = false property default_username = "" property auth_proxy_header_name = "" diff --git a/src/library/cache.cr b/src/library/cache.cr index 1d2dd53..959e0eb 100644 --- a/src/library/cache.cr +++ b/src/library/cache.cr @@ -178,10 +178,16 @@ end # LRU Cache class SortedEntriesCache - @@limit : Int128 = Int128.new 1024 * 1024 * 50 # 50MB + @@limit : Int128 = Int128.new 0 # key => entry @@cache = {} of String => SortedEntriesCacheEntry + def self.init + enabled = Config.current.sorted_entries_cache_enable + cache_size = Config.current.sorted_entries_cache_capacity_kbs + @@limit = Int128.new cache_size * 1024 if enabled + end + def self.gen_key(book_id : String, username : String, entries : Array(Entry), opt : SortOptions?) sig = Digest::SHA1.hexdigest (entries.map &.id).to_s diff --git a/src/mango.cr b/src/mango.cr index e8d32a3..9b58d50 100644 --- a/src/mango.cr +++ b/src/mango.cr @@ -55,6 +55,7 @@ class CLI < Clim Config.load(opts.config).set_current # Initialize main components + SortedEntriesCache.init Storage.default Queue.default Library.default From 601346b209fbc79c3a72ca6fbf7f51f79d184241 Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Mon, 30 Aug 2021 23:07:59 +0900 Subject: [PATCH 07/26] Set cache if enabled --- src/library/title.cr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/library/title.cr b/src/library/title.cr index 6a78ea5..54779c2 100644 --- a/src/library/title.cr +++ b/src/library/title.cr @@ -381,7 +381,9 @@ class Title ary.reverse! unless opt.not_nil!.ascend - SortedEntriesCache.set cache_key, ary + if Config.current.sorted_entries_cache_enable + SortedEntriesCache.set cache_key, ary + end ary end From 365f71cd1d2f2c760f90f2296e80c1cd0278c3b4 Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Mon, 30 Aug 2021 23:09:36 +0900 Subject: [PATCH 08/26] Change kbs to mbs --- src/config.cr | 2 +- src/library/cache.cr | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config.cr b/src/config.cr index 0647aee..69feccd 100644 --- a/src/config.cr +++ b/src/config.cr @@ -21,7 +21,7 @@ class Config home: true property download_timeout_seconds : Int32 = 30 property sorted_entries_cache_enable = false - property sorted_entries_cache_capacity_kbs = 51200 + property sorted_entries_cache_size_mbs = 50 property disable_login = false property default_username = "" property auth_proxy_header_name = "" diff --git a/src/library/cache.cr b/src/library/cache.cr index 959e0eb..0165496 100644 --- a/src/library/cache.cr +++ b/src/library/cache.cr @@ -184,8 +184,8 @@ class SortedEntriesCache def self.init enabled = Config.current.sorted_entries_cache_enable - cache_size = Config.current.sorted_entries_cache_capacity_kbs - @@limit = Int128.new cache_size * 1024 if enabled + cache_size = Config.current.sorted_entries_cache_size_mbs + @@limit = Int128.new cache_size * 1024 * 1024 if enabled end def self.gen_key(book_id : String, username : String, From 0a8fd993e524fb36a7249d753992594811e0f3f9 Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Fri, 3 Sep 2021 11:11:28 +0900 Subject: [PATCH 09/26] Use bytesize and add comments --- src/library/cache.cr | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/library/cache.cr b/src/library/cache.cr index 0165496..7870df5 100644 --- a/src/library/cache.cr +++ b/src/library/cache.cr @@ -171,8 +171,10 @@ private class SortedEntriesCacheEntry end def instance_size - @value.size * (instance_sizeof(String) + sizeof(String)) + - @value.sum(&.size) + instance_sizeof(SortedEntriesCacheEntry) + instance_sizeof(SortedEntriesCacheEntry) + # sizeof itself + instance_sizeof(String) + @key.bytesize + # allocated memory for @key + @value.size * (instance_sizeof(String) + sizeof(String)) + + @value.sum(&.bytesize) # elements in Array(String) end end From 9e90aa17b934baa923ce979af3571bcb4b3650bd Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Sat, 4 Sep 2021 14:13:33 +0900 Subject: [PATCH 10/26] Move entry specific method --- src/library/cache.cr | 72 +++++++++++++++++++++++--------------------- src/library/entry.cr | 4 +-- src/library/title.cr | 4 +-- 3 files changed, 42 insertions(+), 38 deletions(-) diff --git a/src/library/cache.cr b/src/library/cache.cr index 7870df5..c6e9374 100644 --- a/src/library/cache.cr +++ b/src/library/cache.cr @@ -159,23 +159,47 @@ class InfoCache end private class SortedEntriesCacheEntry + @value : Array(String) + getter key : String, atime : Time - def initialize(@ctime : Time, @key : String, @value : Array(String)) + def initialize(@ctime : Time, @key : String, value : Array(Entry)) @atime = @ctime + @value = value.map &.id end def value @atime = Time.utc - @value + SortedEntriesCacheEntry.ids2entries @value + end + + # private? + def self.ids2entries(ids : Array(String)) + e_map = Library.default.deep_entries.to_h { |entry| {entry.id, entry} } + entries = [] of Entry + begin + ids.each do |id| + entries << e_map[id] + end + return entries if ids.size == entries.size + rescue + end end def instance_size - instance_sizeof(SortedEntriesCacheEntry) + # sizeof itself + instance_sizeof(SortedEntriesCacheEntry) + # sizeof itself instance_sizeof(String) + @key.bytesize + # allocated memory for @key @value.size * (instance_sizeof(String) + sizeof(String)) + @value.sum(&.bytesize) # elements in Array(String) end + + def self.gen_key(book_id : String, username : String, + entries : Array(Entry), opt : SortOptions?) + sig = Digest::SHA1.hexdigest (entries.map &.id).to_s + user_context = opt && opt.method == SortMethod::Progress ? username : "" + Digest::SHA1.hexdigest (book_id + sig + user_context + + (opt ? opt.to_tuple.to_s : "nil")) + end end # LRU Cache @@ -190,24 +214,16 @@ class SortedEntriesCache @@limit = Int128.new cache_size * 1024 * 1024 if enabled end - def self.gen_key(book_id : String, username : String, - entries : Array(Entry), opt : SortOptions?) - sig = Digest::SHA1.hexdigest (entries.map &.id).to_s - user_context = opt && opt.method == SortMethod::Progress ? username : "" - Digest::SHA1.hexdigest (book_id + sig + user_context + - (opt ? opt.to_tuple.to_s : "nil")) - end - def self.get(key : String) entry = @@cache[key]? - Logger.debug "SortedEntries Cache Hit! #{key}" unless entry.nil? - Logger.debug "SortedEntries Cache Miss #{key}" if entry.nil? - return ids2entries entry.value unless entry.nil? + Logger.debug "LRUCache Cache Hit! #{key}" unless entry.nil? + Logger.debug "LRUCache Cache Miss #{key}" if entry.nil? + return entry.value unless entry.nil? end def self.set(key : String, value : Array(Entry)) - @@cache[key] = SortedEntriesCacheEntry.new Time.utc, key, value.map &.id - Logger.debug "SortedEntries Cached #{key}" + @@cache[key] = SortedEntriesCacheEntry.new Time.utc, key, value + Logger.debug "LRUCache Cached #{key}" remove_victim_cache end @@ -217,23 +233,11 @@ class SortedEntriesCache def self.print sum = @@cache.sum { |_, entry| entry.instance_size } - Logger.debug "---- Sorted Entries Cache ----" + Logger.debug "---- LRU Cache ----" Logger.debug "Size: #{sum} Bytes" Logger.debug "List:" @@cache.each { |k, v| Logger.debug "#{k} | #{v.atime}" } - Logger.debug "------------------------------" - end - - private def self.ids2entries(ids : Array(String)) - e_map = Library.default.deep_entries.to_h { |entry| {entry.id, entry} } - entries = [] of Entry - begin - ids.each do |id| - entries << e_map[id] - end - return entries if ids.size == entries.size - rescue - end + Logger.debug "-------------------" end private def self.is_cache_full @@ -243,12 +247,12 @@ class SortedEntriesCache private def self.remove_victim_cache while is_cache_full && @@cache.size > 0 - Logger.debug "SortedEntries Cache Full! Remove LRU" + Logger.debug "LRUCache Cache Full! Remove LRU" min = @@cache.min_by? { |_, entry| entry.atime } - Logger.debug "Target: #{min[0]}, Last Access Time: #{min[1].atime}" if min + Logger.debug " \ + Target: #{min[0]}, \ + Last Access Time: #{min[1].atime}" if min invalidate min[0] if min - - print if Logger.get_severity == Log::Severity::Debug end end end diff --git a/src/library/entry.cr b/src/library/entry.cr index 5b1f3ce..c7599ff 100644 --- a/src/library/entry.cr +++ b/src/library/entry.cr @@ -188,8 +188,8 @@ class Entry InfoCache.invalidate_progress_cache parent.id, username end [false, true].each do |ascend| - sorted_entries_cache_key = SortedEntriesCache.gen_key @book.id, username, - @book.entries, SortOptions.new(SortMethod::Progress, ascend) + sorted_entries_cache_key = SortedEntriesCacheEntry.gen_key @book.id, + username, @book.entries, SortOptions.new(SortMethod::Progress, ascend) SortedEntriesCache.invalidate sorted_entries_cache_key end diff --git a/src/library/title.cr b/src/library/title.cr index 54779c2..ed546ca 100644 --- a/src/library/title.cr +++ b/src/library/title.cr @@ -344,7 +344,7 @@ class Title # use the default (auto, ascending) # When `opt` is not nil, it saves the options to info.json def sorted_entries(username, opt : SortOptions? = nil) - cache_key = SortedEntriesCache.gen_key @id, username, @entries, opt + cache_key = SortedEntriesCacheEntry.gen_key @id, username, @entries, opt cached_entries = SortedEntriesCache.get cache_key return cached_entries if cached_entries @@ -451,7 +451,7 @@ class Title end [false, true].each do |ascend| sorted_entries_cache_key = - SortedEntriesCache.gen_key @id, username, @entries, + SortedEntriesCacheEntry.gen_key @id, username, @entries, SortOptions.new(SortMethod::Progress, ascend) SortedEntriesCache.invalidate sorted_entries_cache_key end From 5e919d3e19c802084ce4076341c41c21e19d8f4d Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Sat, 4 Sep 2021 14:37:29 +0900 Subject: [PATCH 11/26] Make entry generic --- src/library/cache.cr | 60 +++++++++++++++++++++++++++++++++++--------- src/library/title.cr | 4 +-- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/library/cache.cr b/src/library/cache.cr index c6e9374..6136dd3 100644 --- a/src/library/cache.cr +++ b/src/library/cache.cr @@ -1,5 +1,7 @@ require "digest" +require "./entry" + class InfoCache alias ProgressCache = Tuple(String, Int32) @@ -158,23 +160,46 @@ class InfoCache end end -private class SortedEntriesCacheEntry - @value : Array(String) - +private class CacheEntry(SaveT, ReturnT) getter key : String, atime : Time - def initialize(@ctime : Time, @key : String, value : Array(Entry)) - @atime = @ctime - @value = value.map &.id + @value : SaveT + + def initialize(@key : String, value : ReturnT) + @atime = @ctime = Time.utc + @value = self.class.to_save_t value end def value @atime = Time.utc - SortedEntriesCacheEntry.ids2entries @value + self.class.to_return_t @value end - # private? - def self.ids2entries(ids : Array(String)) + def self.to_save_t(value : ReturnT) + value + end + + def self.to_return_t(value : SaveT) + value + end + + def instance_size + instance_sizeof(CacheEntry(SaveT, ReturnT)) + # sizeof itself + instance_sizeof(String) + @key.bytesize + # allocated memory for @key + @value.instance_size + end +end + +class SortedEntriesCacheEntry < CacheEntry(Array(String), Array(Entry)) + def self.to_save_t(value : Array(Entry)) + value.map &.id + end + + def self.to_return_t(value : Array(String)) + ids2entries value + end + + private def self.ids2entries(ids : Array(String)) e_map = Library.default.deep_entries.to_h { |entry| {entry.id, entry} } entries = [] of Entry begin @@ -202,11 +227,21 @@ private class SortedEntriesCacheEntry end end +alias CacheEntryType = SortedEntriesCacheEntry + +def generate_cache_entry(key : String, value : Array(Entry) | Int32 | String) + if value.is_a? Array(Entry) + SortedEntriesCacheEntry.new key, value + else + CacheEntry(typeof(value), typeof(value)).new key, value + end +end + # LRU Cache class SortedEntriesCache @@limit : Int128 = Int128.new 0 # key => entry - @@cache = {} of String => SortedEntriesCacheEntry + @@cache = {} of String => CacheEntryType def self.init enabled = Config.current.sorted_entries_cache_enable @@ -221,8 +256,9 @@ class SortedEntriesCache return entry.value unless entry.nil? end - def self.set(key : String, value : Array(Entry)) - @@cache[key] = SortedEntriesCacheEntry.new Time.utc, key, value + def self.set(cache_entry : CacheEntryType) + key = cache_entry.key + @@cache[key] = cache_entry Logger.debug "LRUCache Cached #{key}" remove_victim_cache end diff --git a/src/library/title.cr b/src/library/title.cr index ed546ca..c377738 100644 --- a/src/library/title.cr +++ b/src/library/title.cr @@ -346,7 +346,7 @@ class Title def sorted_entries(username, opt : SortOptions? = nil) cache_key = SortedEntriesCacheEntry.gen_key @id, username, @entries, opt cached_entries = SortedEntriesCache.get cache_key - return cached_entries if cached_entries + return cached_entries if cached_entries.is_a? Array(Entry) if opt.nil? opt = SortOptions.from_info_json @dir, username @@ -382,7 +382,7 @@ class Title ary.reverse! unless opt.not_nil!.ascend if Config.current.sorted_entries_cache_enable - SortedEntriesCache.set cache_key, ary + SortedEntriesCache.set generate_cache_entry cache_key, ary end ary end From 0fd7caef4be10d480b58a3c0c217f0b65e8a1728 Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Sun, 5 Sep 2021 00:02:05 +0900 Subject: [PATCH 12/26] Rename --- src/library/cache.cr | 2 +- src/library/entry.cr | 2 +- src/library/title.cr | 6 +++--- src/mango.cr | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/library/cache.cr b/src/library/cache.cr index 6136dd3..b399632 100644 --- a/src/library/cache.cr +++ b/src/library/cache.cr @@ -238,7 +238,7 @@ def generate_cache_entry(key : String, value : Array(Entry) | Int32 | String) end # LRU Cache -class SortedEntriesCache +class LRUCache @@limit : Int128 = Int128.new 0 # key => entry @@cache = {} of String => CacheEntryType diff --git a/src/library/entry.cr b/src/library/entry.cr index c7599ff..176efe1 100644 --- a/src/library/entry.cr +++ b/src/library/entry.cr @@ -190,7 +190,7 @@ class Entry [false, true].each do |ascend| sorted_entries_cache_key = SortedEntriesCacheEntry.gen_key @book.id, username, @book.entries, SortOptions.new(SortMethod::Progress, ascend) - SortedEntriesCache.invalidate sorted_entries_cache_key + LRUCache.invalidate sorted_entries_cache_key end TitleInfo.new @book.dir do |info| diff --git a/src/library/title.cr b/src/library/title.cr index c377738..a5a6850 100644 --- a/src/library/title.cr +++ b/src/library/title.cr @@ -345,7 +345,7 @@ class Title # When `opt` is not nil, it saves the options to info.json def sorted_entries(username, opt : SortOptions? = nil) cache_key = SortedEntriesCacheEntry.gen_key @id, username, @entries, opt - cached_entries = SortedEntriesCache.get cache_key + cached_entries = LRUCache.get cache_key return cached_entries if cached_entries.is_a? Array(Entry) if opt.nil? @@ -382,7 +382,7 @@ class Title ary.reverse! unless opt.not_nil!.ascend if Config.current.sorted_entries_cache_enable - SortedEntriesCache.set generate_cache_entry cache_key, ary + LRUCache.set generate_cache_entry cache_key, ary end ary end @@ -453,7 +453,7 @@ class Title sorted_entries_cache_key = SortedEntriesCacheEntry.gen_key @id, username, @entries, SortOptions.new(SortMethod::Progress, ascend) - SortedEntriesCache.invalidate sorted_entries_cache_key + LRUCache.invalidate sorted_entries_cache_key end selected_entries = ids diff --git a/src/mango.cr b/src/mango.cr index 9b58d50..f27165e 100644 --- a/src/mango.cr +++ b/src/mango.cr @@ -55,7 +55,7 @@ class CLI < Clim Config.load(opts.config).set_current # Initialize main components - SortedEntriesCache.init + LRUCache.init Storage.default Queue.default Library.default From de410f42b8cd67769aa1778d89837cdbde3966a9 Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Sun, 5 Sep 2021 01:54:19 +0900 Subject: [PATCH 13/26] Replace InfoCache to LRUCache --- src/library/cache.cr | 197 ++++++++--------------------------------- src/library/entry.cr | 12 ++- src/library/library.cr | 4 - src/library/title.cr | 28 +++--- src/library/types.cr | 7 +- src/util/web.cr | 3 +- 6 files changed, 61 insertions(+), 190 deletions(-) diff --git a/src/library/cache.cr b/src/library/cache.cr index b399632..f772c66 100644 --- a/src/library/cache.cr +++ b/src/library/cache.cr @@ -1,164 +1,7 @@ require "digest" require "./entry" - -class InfoCache - alias ProgressCache = Tuple(String, Int32) - - def self.clear - clear_cover_url - clear_progress_cache - clear_sort_opt - end - - def self.clean - clean_cover_url - clean_progress_cache - clean_sort_opt - end - - # item id => cover_url - @@cached_cover_url = {} of String => String - @@cached_cover_url_previous = {} of String => String # item id => cover_url - - def self.set_cover_url(id : String, cover_url : String) - @@cached_cover_url[id] = cover_url - end - - def self.get_cover_url(id : String) - @@cached_cover_url[id]? - end - - def self.invalidate_cover_url(id : String) - @@cached_cover_url.delete id - end - - def self.move_cover_url(id : String) - if @@cached_cover_url_previous[id]? - @@cached_cover_url[id] = @@cached_cover_url_previous[id] - end - end - - private def self.clear_cover_url - @@cached_cover_url_previous = @@cached_cover_url - @@cached_cover_url = {} of String => String - end - - private def self.clean_cover_url - @@cached_cover_url_previous = {} of String => String - end - - # book.id:username => {signature, sum} - @@progress_cache = {} of String => ProgressCache - # book.id => username => {signature, sum} - @@progress_cache_previous = {} of String => Hash(String, ProgressCache) - - def self.set_progress_cache(book_id : String, username : String, - entries : Array(Entry), sum : Int32) - progress_cache_id = "#{book_id}:#{username}" - progress_cache_sig = Digest::SHA1.hexdigest (entries.map &.id).to_s - @@progress_cache[progress_cache_id] = {progress_cache_sig, sum} - Logger.debug "Progress Cached #{progress_cache_id}" - end - - def self.get_progress_cache(book_id : String, username : String, - entries : Array(Entry)) - progress_cache_id = "#{book_id}:#{username}" - progress_cache_sig = Digest::SHA1.hexdigest (entries.map &.id).to_s - cached = @@progress_cache[progress_cache_id]? - if cached && cached[0] == progress_cache_sig - Logger.debug "Progress Cache Hit! #{progress_cache_id}" - return cached[1] - end - end - - def self.invalidate_progress_cache(book_id : String, username : String) - progress_cache_id = "#{book_id}:#{username}" - if @@progress_cache[progress_cache_id]? - @@progress_cache.delete progress_cache_id - Logger.debug "Progress Invalidate Cache #{progress_cache_id}" - end - end - - def self.move_progress_cache(book_id : String) - if @@progress_cache_previous[book_id]? - @@progress_cache_previous[book_id].each do |username, cached| - id = "#{book_id}:#{username}" - unless @@progress_cache[id]? - # It would be invalidated when entries changed - @@progress_cache[id] = cached - end - end - end - end - - private def self.clear_progress_cache - @@progress_cache_previous = {} of String => Hash(String, ProgressCache) - @@progress_cache.each do |id, cached| - splitted = id.split(':', 2) - book_id = splitted[0] - username = splitted[1] - unless @@progress_cache_previous[book_id]? - @@progress_cache_previous[book_id] = {} of String => ProgressCache - end - - @@progress_cache_previous[book_id][username] = cached - end - @@progress_cache = {} of String => ProgressCache - end - - private def self.clean_progress_cache - @@progress_cache_previous = {} of String => Hash(String, ProgressCache) - end - - # book.dir:username => SortOptions - @@cached_sort_opt = {} of String => SortOptions - @@cached_sort_opt_previous = {} of String => Hash(String, SortOptions) - - def self.set_sort_opt(dir : String, username : String, sort_opt : SortOptions) - id = "#{dir}:#{username}" - @@cached_sort_opt[id] = sort_opt - end - - def self.get_sort_opt(dir : String, username : String) - id = "#{dir}:#{username}" - @@cached_sort_opt[id]? - end - - def self.invalidate_sort_opt(dir : String, username : String) - id = "#{dir}:#{username}" - @@cached_sort_opt.delete id - end - - def self.move_sort_opt(dir : String) - if @@cached_sort_opt_previous[dir]? - @@cached_sort_opt_previous[dir].each do |username, cached| - id = "#{dir}:#{username}" - unless @@cached_sort_opt[id]? - @@cached_sort_opt[id] = cached - end - end - end - end - - private def self.clear_sort_opt - @@cached_sort_opt_previous = {} of String => Hash(String, SortOptions) - @@cached_sort_opt.each do |id, cached| - splitted = id.split(':', 2) - book_dir = splitted[0] - username = splitted[1] - unless @@cached_sort_opt_previous[book_dir]? - @@cached_sort_opt_previous[book_dir] = {} of String => SortOptions - end - @@cached_sort_opt_previous[book_dir][username] = cached - end - @@cached_sort_opt = {} of String => SortOptions - end - - private def self.clean_sort_opt - @@cached_sort_opt_previous = {} of String => Hash(String, SortOptions) - end -end +require "./types" private class CacheEntry(SaveT, ReturnT) getter key : String, atime : Time @@ -227,11 +70,45 @@ class SortedEntriesCacheEntry < CacheEntry(Array(String), Array(Entry)) end end -alias CacheEntryType = SortedEntriesCacheEntry +class SortOptionsCacheEntry < CacheEntry(Tuple(String, Bool), SortOptions) + def self.to_save_t(value : SortOptions) + value.to_tuple + end -def generate_cache_entry(key : String, value : Array(Entry) | Int32 | String) + def self.to_return_t(value : Tuple(String, Bool)) + SortOptions.from_tuple value + end + + def instance_size + instance_sizeof(SortOptionsCacheEntry) + + @value[0].instance_size + end +end + +class String + def instance_size + instance_sizeof(String) + bytesize + end +end + +struct Tuple(*T) + def instance_size + sizeof(T) # iterate T and add instance_size of that + end +end + +alias CacheableType = Array(Entry) | String | Tuple(String, Int32) | + SortOptions +alias CacheEntryType = SortedEntriesCacheEntry | + SortOptionsCacheEntry | + CacheEntry(String, String) | + CacheEntry(Tuple(String, Int32), Tuple(String, Int32)) + +def generate_cache_entry(key : String, value : CacheableType) if value.is_a? Array(Entry) SortedEntriesCacheEntry.new key, value + elsif value.is_a? SortOptions + SortOptionsCacheEntry.new key, value else CacheEntry(typeof(value), typeof(value)).new key, value end diff --git a/src/library/entry.cr b/src/library/entry.cr index 176efe1..2c1ae34 100644 --- a/src/library/entry.cr +++ b/src/library/entry.cr @@ -44,8 +44,6 @@ class Entry MIME.from_filename? e.filename end file.close - - InfoCache.move_cover_url @id end def to_slim_json : String @@ -83,8 +81,8 @@ class Entry def cover_url return "#{Config.current.base_url}img/icon.png" if @err_msg - cached_cover_url = InfoCache.get_cover_url @id - return cached_cover_url if cached_cover_url + cached_cover_url = LRUCache.get "#{@id}:cover_url" + return cached_cover_url if cached_cover_url.is_a? String unless @book.entry_cover_url_cache TitleInfo.new @book.dir do |info| @@ -100,7 +98,7 @@ class Entry url = File.join Config.current.base_url, info_url end end - InfoCache.set_cover_url @id, url + LRUCache.set generate_cache_entry "#{@id}:cover_url", url url end @@ -183,9 +181,9 @@ class Entry # For backward backward compatibility with v0.1.0, we save entry titles # instead of IDs in info.json def save_progress(username, page) - InfoCache.invalidate_progress_cache @book.id, username + LRUCache.invalidate "#{@book.id}:#{username}:progress_sum" @book.parents.each do |parent| - InfoCache.invalidate_progress_cache parent.id, username + LRUCache.invalidate "#{parent.id}:#{username}:progress_sum" end [false, true].each do |ascend| sorted_entries_cache_key = SortedEntriesCacheEntry.gen_key @book.id, diff --git a/src/library/library.cr b/src/library/library.cr index 21e5c8b..9351e60 100644 --- a/src/library/library.cr +++ b/src/library/library.cr @@ -106,8 +106,6 @@ class Library storage = Storage.new auto_close: false - InfoCache.clear - (Dir.entries @dir) .select { |fn| !fn.starts_with? "." } .map { |fn| File.join @dir, fn } @@ -121,8 +119,6 @@ class Library @title_ids << title.id end - InfoCache.clean - storage.bulk_insert_ids storage.close diff --git a/src/library/title.cr b/src/library/title.cr index a5a6850..cdf78fd 100644 --- a/src/library/title.cr +++ b/src/library/title.cr @@ -60,10 +60,6 @@ class Title @entries.sort! do |a, b| sorter.compare a.title, b.title end - - InfoCache.move_cover_url @id - InfoCache.move_progress_cache @id - InfoCache.move_sort_opt @dir end def to_slim_json : String @@ -234,8 +230,8 @@ class Title end def cover_url - cached_cover_url = InfoCache.get_cover_url @id - return cached_cover_url if cached_cover_url + cached_cover_url = LRUCache.get "#{@id}:cover_url" + return cached_cover_url if cached_cover_url.is_a? String url = "#{Config.current.base_url}img/icon.png" readable_entries = @entries.select &.err_msg.nil? @@ -248,12 +244,12 @@ class Title url = File.join Config.current.base_url, info_url end end - InfoCache.set_cover_url @id, url + LRUCache.set generate_cache_entry "#{@id}:cover_url", url url end def set_cover_url(url : String) - InfoCache.invalidate_cover_url @id + LRUCache.invalidate "#{@id}:cover_url" TitleInfo.new @dir do |info| info.cover_url = url info.save @@ -262,7 +258,7 @@ class Title def set_cover_url(entry_name : String, url : String) selected_entry = @entries.find { |entry| entry.display_name == entry_name } - InfoCache.invalidate_cover_url selected_entry.id if selected_entry + LRUCache.invalidate "#{selected_entry.id}:cover_url" if selected_entry TitleInfo.new @dir do |info| info.entry_cover_url[entry_name] = url info.save @@ -284,12 +280,14 @@ class Title end def deep_read_page_count(username) : Int32 - # CACHE HERE - cached_sum = InfoCache.get_progress_cache @id, username, @entries - return cached_sum unless cached_sum.nil? + key = "#{@id}:#{username}:progress_sum" + sig = Digest::SHA1.hexdigest (entries.map &.id).to_s + cached_sum = LRUCache.get key + return cached_sum[1] if cached_sum.is_a? Tuple(String, Int32) && + cached_sum[0] == sig sum = load_progress_for_all_entries(username).sum + titles.flat_map(&.deep_read_page_count username).sum - InfoCache.set_progress_cache @id, username, @entries, sum + LRUCache.set generate_cache_entry key, {sig, sum} sum end @@ -445,9 +443,9 @@ class Title end def bulk_progress(action, ids : Array(String), username) - InfoCache.invalidate_progress_cache @id, username + LRUCache.invalidate "#{@id}:#{username}:progress_sum" parents.each do |parent| - InfoCache.invalidate_progress_cache parent.id, username + LRUCache.invalidate "#{parent.id}:#{username}:progress_sum" end [false, true].each do |ascend| sorted_entries_cache_key = diff --git a/src/library/types.cr b/src/library/types.cr index 094cb64..891ee08 100644 --- a/src/library/types.cr +++ b/src/library/types.cr @@ -35,15 +35,16 @@ class SortOptions end def self.from_info_json(dir, username) - cached_opt = InfoCache.get_sort_opt dir, username - return cached_opt if cached_opt + key = "#{dir}:#{username}:sort_opt" + cached_opt = LRUCache.get key + return cached_opt if cached_opt.is_a? SortOptions opt = SortOptions.new TitleInfo.new dir do |info| if info.sort_by.has_key? username opt = SortOptions.from_tuple info.sort_by[username] end end - InfoCache.set_sort_opt dir, username, opt + LRUCache.set generate_cache_entry key, opt opt end diff --git a/src/util/web.cr b/src/util/web.cr index 5e873ca..9b967da 100644 --- a/src/util/web.cr +++ b/src/util/web.cr @@ -120,7 +120,8 @@ macro get_and_save_sort_opt(dir) sort_opt = SortOptions.new sort_method, is_ascending - InfoCache.set_sort_opt {{dir}}, username, sort_opt + key = "#{{{dir}}}:#{username}:sort_opt" + LRUCache.set generate_cache_entry key, sort_opt TitleInfo.new {{dir}} do |info| info.sort_by[username] = sort_opt.to_tuple info.save From 847f516a65c9dac283b11e4f31de8d9ee449df54 Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Sun, 5 Sep 2021 02:35:44 +0900 Subject: [PATCH 14/26] Cache TitleInfo using LRUCache --- src/library/types.cr | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/library/types.cr b/src/library/types.cr index 891ee08..46d2a7c 100644 --- a/src/library/types.cr +++ b/src/library/types.cr @@ -92,6 +92,18 @@ class TitleInfo @@mutex_hash = {} of String => Mutex def self.new(dir, &) + key = "#{dir}:info.json" + info = LRUCache.get key + if info.is_a? String + begin + instance = TitleInfo.from_json info + instance.dir = dir + yield instance + return + rescue + end + end + if @@mutex_hash[dir]? mutex = @@mutex_hash[dir] else @@ -105,6 +117,7 @@ class TitleInfo instance = TitleInfo.from_json File.read json_path end instance.dir = dir + LRUCache.set generate_cache_entry key, instance.to_json yield instance end end @@ -112,5 +125,7 @@ class TitleInfo def save json_path = File.join @dir, "info.json" File.write json_path, self.to_pretty_json + key = "#{@dir}:info.json" + LRUCache.set generate_cache_entry key, self.to_json end end From 11976b15f9f66b92df78bf00fbecdf45a8850905 Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Sun, 5 Sep 2021 03:02:20 +0900 Subject: [PATCH 15/26] Make LRUCache togglable --- src/library/cache.cr | 10 ++++++++-- src/library/title.cr | 4 +--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/library/cache.cr b/src/library/cache.cr index f772c66..5b3d2bd 100644 --- a/src/library/cache.cr +++ b/src/library/cache.cr @@ -120,13 +120,17 @@ class LRUCache # key => entry @@cache = {} of String => CacheEntryType + def self.enabled + Config.current.sorted_entries_cache_enable + end + def self.init - enabled = Config.current.sorted_entries_cache_enable cache_size = Config.current.sorted_entries_cache_size_mbs @@limit = Int128.new cache_size * 1024 * 1024 if enabled end def self.get(key : String) + return unless enabled entry = @@cache[key]? Logger.debug "LRUCache Cache Hit! #{key}" unless entry.nil? Logger.debug "LRUCache Cache Miss #{key}" if entry.nil? @@ -134,6 +138,7 @@ class LRUCache end def self.set(cache_entry : CacheEntryType) + return unless enabled key = cache_entry.key @@cache[key] = cache_entry Logger.debug "LRUCache Cached #{key}" @@ -141,6 +146,7 @@ class LRUCache end def self.invalidate(key : String) + return unless enabled @@cache.delete key end @@ -149,7 +155,7 @@ class LRUCache Logger.debug "---- LRU Cache ----" Logger.debug "Size: #{sum} Bytes" Logger.debug "List:" - @@cache.each { |k, v| Logger.debug "#{k} | #{v.atime}" } + @@cache.each { |k, v| Logger.debug "#{k} | #{v.atime} | #{v.instance_size}" } Logger.debug "-------------------" end diff --git a/src/library/title.cr b/src/library/title.cr index cdf78fd..8333642 100644 --- a/src/library/title.cr +++ b/src/library/title.cr @@ -379,9 +379,7 @@ class Title ary.reverse! unless opt.not_nil!.ascend - if Config.current.sorted_entries_cache_enable - LRUCache.set generate_cache_entry cache_key, ary - end + LRUCache.set generate_cache_entry cache_key, ary ary end From c75c71709ff64bfb97a10a4f0d577f0f2f6ff764 Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Sun, 5 Sep 2021 11:21:53 +0900 Subject: [PATCH 16/26] make check --- src/library/cache.cr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/library/cache.cr b/src/library/cache.cr index 5b3d2bd..ca6af53 100644 --- a/src/library/cache.cr +++ b/src/library/cache.cr @@ -155,7 +155,9 @@ class LRUCache Logger.debug "---- LRU Cache ----" Logger.debug "Size: #{sum} Bytes" Logger.debug "List:" - @@cache.each { |k, v| Logger.debug "#{k} | #{v.atime} | #{v.instance_size}" } + @@cache.each do |k, v| + Logger.debug "#{k} | #{v.atime} | #{v.instance_size}" + end Logger.debug "-------------------" end From c5b6a8b5b950d685f2490633c8914e53a6640c05 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Sun, 5 Sep 2021 13:57:20 +0000 Subject: [PATCH 17/26] Improve instance_size for Tuple --- src/library/cache.cr | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/library/cache.cr b/src/library/cache.cr index ca6af53..612345c 100644 --- a/src/library/cache.cr +++ b/src/library/cache.cr @@ -93,7 +93,15 @@ end struct Tuple(*T) def instance_size - sizeof(T) # iterate T and add instance_size of that + sizeof(T) + # total size of non-reference types + self.sum do |e| + next 0 unless e.is_a? Reference + if e.responds_to? :instance_size + e.instance_size + else + instance_sizeof(typeof(e)) + end + end end end From 565a535d22f16d230d2048aa7e6d9ed1cd2b8fe4 Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Mon, 6 Sep 2021 02:23:02 +0900 Subject: [PATCH 18/26] Remove caching verbosely, add cached_cover_url --- src/library/cache.cr | 21 +-------------------- src/library/entry.cr | 3 --- src/library/title.cr | 10 ++++------ src/library/types.cr | 4 ---- src/util/web.cr | 1 - 5 files changed, 5 insertions(+), 34 deletions(-) diff --git a/src/library/cache.cr b/src/library/cache.cr index 612345c..5d3797e 100644 --- a/src/library/cache.cr +++ b/src/library/cache.cr @@ -70,21 +70,6 @@ class SortedEntriesCacheEntry < CacheEntry(Array(String), Array(Entry)) end end -class SortOptionsCacheEntry < CacheEntry(Tuple(String, Bool), SortOptions) - def self.to_save_t(value : SortOptions) - value.to_tuple - end - - def self.to_return_t(value : Tuple(String, Bool)) - SortOptions.from_tuple value - end - - def instance_size - instance_sizeof(SortOptionsCacheEntry) + - @value[0].instance_size - end -end - class String def instance_size instance_sizeof(String) + bytesize @@ -105,18 +90,14 @@ struct Tuple(*T) end end -alias CacheableType = Array(Entry) | String | Tuple(String, Int32) | - SortOptions +alias CacheableType = Array(Entry) | String | Tuple(String, Int32) alias CacheEntryType = SortedEntriesCacheEntry | - SortOptionsCacheEntry | CacheEntry(String, String) | CacheEntry(Tuple(String, Int32), Tuple(String, Int32)) def generate_cache_entry(key : String, value : CacheableType) if value.is_a? Array(Entry) SortedEntriesCacheEntry.new key, value - elsif value.is_a? SortOptions - SortOptionsCacheEntry.new key, value else CacheEntry(typeof(value), typeof(value)).new key, value end diff --git a/src/library/entry.cr b/src/library/entry.cr index 2c1ae34..28b7122 100644 --- a/src/library/entry.cr +++ b/src/library/entry.cr @@ -81,8 +81,6 @@ class Entry def cover_url return "#{Config.current.base_url}img/icon.png" if @err_msg - cached_cover_url = LRUCache.get "#{@id}:cover_url" - return cached_cover_url if cached_cover_url.is_a? String unless @book.entry_cover_url_cache TitleInfo.new @book.dir do |info| @@ -98,7 +96,6 @@ class Entry url = File.join Config.current.base_url, info_url end end - LRUCache.set generate_cache_entry "#{@id}:cover_url", url url end diff --git a/src/library/title.cr b/src/library/title.cr index 8333642..8d97387 100644 --- a/src/library/title.cr +++ b/src/library/title.cr @@ -11,6 +11,7 @@ class Title @entry_display_name_cache : Hash(String, String)? @entry_cover_url_cache : Hash(String, String)? @cached_display_name : String? + @cached_cover_url : String? def initialize(@dir : String, @parent_id) storage = Storage.default @@ -230,8 +231,8 @@ class Title end def cover_url - cached_cover_url = LRUCache.get "#{@id}:cover_url" - return cached_cover_url if cached_cover_url.is_a? String + cached_cover_url = @cached_cover_url + return cached_cover_url unless cached_cover_url.nil? url = "#{Config.current.base_url}img/icon.png" readable_entries = @entries.select &.err_msg.nil? @@ -244,12 +245,11 @@ class Title url = File.join Config.current.base_url, info_url end end - LRUCache.set generate_cache_entry "#{@id}:cover_url", url + @cached_cover_url = url url end def set_cover_url(url : String) - LRUCache.invalidate "#{@id}:cover_url" TitleInfo.new @dir do |info| info.cover_url = url info.save @@ -257,8 +257,6 @@ class Title end def set_cover_url(entry_name : String, url : String) - selected_entry = @entries.find { |entry| entry.display_name == entry_name } - LRUCache.invalidate "#{selected_entry.id}:cover_url" if selected_entry TitleInfo.new @dir do |info| info.entry_cover_url[entry_name] = url info.save diff --git a/src/library/types.cr b/src/library/types.cr index 46d2a7c..a4de007 100644 --- a/src/library/types.cr +++ b/src/library/types.cr @@ -35,16 +35,12 @@ class SortOptions end def self.from_info_json(dir, username) - key = "#{dir}:#{username}:sort_opt" - cached_opt = LRUCache.get key - return cached_opt if cached_opt.is_a? SortOptions opt = SortOptions.new TitleInfo.new dir do |info| if info.sort_by.has_key? username opt = SortOptions.from_tuple info.sort_by[username] end end - LRUCache.set generate_cache_entry key, opt opt end diff --git a/src/util/web.cr b/src/util/web.cr index 9b967da..1b3a42c 100644 --- a/src/util/web.cr +++ b/src/util/web.cr @@ -121,7 +121,6 @@ macro get_and_save_sort_opt(dir) sort_opt = SortOptions.new sort_method, is_ascending key = "#{{{dir}}}:#{username}:sort_opt" - LRUCache.set generate_cache_entry key, sort_opt TitleInfo.new {{dir}} do |info| info.sort_by[username] = sort_opt.to_tuple info.save From 9807db6ac02682d20dd9ce1021725c5257b749d2 Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Mon, 6 Sep 2021 02:29:31 +0900 Subject: [PATCH 19/26] Fix bug on entry_cover_url_cache --- src/library/title.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/library/title.cr b/src/library/title.cr index 8d97387..f93bf3c 100644 --- a/src/library/title.cr +++ b/src/library/title.cr @@ -259,6 +259,7 @@ class Title def set_cover_url(entry_name : String, url : String) TitleInfo.new @dir do |info| info.entry_cover_url[entry_name] = url + @entry_cover_url_cache = info.entry_cover_url info.save end end From 5cb85ea8577c606e6aeceeab49294a8c10e3e9bb Mon Sep 17 00:00:00 2001 From: Leeingnyo Date: Mon, 6 Sep 2021 09:40:53 +0900 Subject: [PATCH 20/26] Set cached data when changed --- src/library/title.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/library/title.cr b/src/library/title.cr index f93bf3c..f1915d4 100644 --- a/src/library/title.cr +++ b/src/library/title.cr @@ -215,7 +215,7 @@ class Title end def set_display_name(dn) - @cached_display_name = nil + @cached_display_name = dn TitleInfo.new @dir do |info| info.display_name = dn info.save @@ -250,6 +250,7 @@ class Title end def set_cover_url(url : String) + @cached_cover_url = url TitleInfo.new @dir do |info| info.cover_url = url info.save From 79ef7bcd1cc592b0fced32dc992295e8f90ee990 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Mon, 6 Sep 2021 03:01:21 +0000 Subject: [PATCH 21/26] Remove unused variable --- src/util/web.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/util/web.cr b/src/util/web.cr index 1b3a42c..5704ea8 100644 --- a/src/util/web.cr +++ b/src/util/web.cr @@ -120,7 +120,6 @@ macro get_and_save_sort_opt(dir) sort_opt = SortOptions.new sort_method, is_ascending - key = "#{{{dir}}}:#{username}:sort_opt" TitleInfo.new {{dir}} do |info| info.sort_by[username] = sort_opt.to_tuple info.save From 51806f18db114b3878616d293f9930edb7b0906d Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Mon, 6 Sep 2021 03:35:46 +0000 Subject: [PATCH 22/26] Rename config fields and improve logging --- src/config.cr | 5 +++-- src/library/cache.cr | 31 +++++++++++++++++++------------ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/config.cr b/src/config.cr index 69feccd..99fb5b9 100644 --- a/src/config.cr +++ b/src/config.cr @@ -20,8 +20,9 @@ class Config property plugin_path : String = File.expand_path "~/mango/plugins", home: true property download_timeout_seconds : Int32 = 30 - property sorted_entries_cache_enable = false - property sorted_entries_cache_size_mbs = 50 + property cache_enabled = false + property cache_size_mbs = 50 + property cache_log_enabled = true property disable_login = false property default_username = "" property auth_proxy_header_name = "" diff --git a/src/library/cache.cr b/src/library/cache.cr index 5d3797e..496442f 100644 --- a/src/library/cache.cr +++ b/src/library/cache.cr @@ -106,23 +106,26 @@ end # LRU Cache class LRUCache @@limit : Int128 = Int128.new 0 + @@should_log = true # key => entry @@cache = {} of String => CacheEntryType def self.enabled - Config.current.sorted_entries_cache_enable + Config.current.cache_enabled end def self.init - cache_size = Config.current.sorted_entries_cache_size_mbs + cache_size = Config.current.cache_size_mbs @@limit = Int128.new cache_size * 1024 * 1024 if enabled + @@should_log = Config.current.cache_log_enabled end def self.get(key : String) return unless enabled entry = @@cache[key]? - Logger.debug "LRUCache Cache Hit! #{key}" unless entry.nil? - Logger.debug "LRUCache Cache Miss #{key}" if entry.nil? + if @@should_log + Logger.debug "LRUCache #{entry.nil? ? "miss" : "hit"} #{key}" + end return entry.value unless entry.nil? end @@ -130,8 +133,8 @@ class LRUCache return unless enabled key = cache_entry.key @@cache[key] = cache_entry - Logger.debug "LRUCache Cached #{key}" - remove_victim_cache + Logger.debug "LRUCache cached #{key}" if @@should_log + remove_least_recent_access end def self.invalidate(key : String) @@ -140,6 +143,7 @@ class LRUCache end def self.print + return unless @@should_log sum = @@cache.sum { |_, entry| entry.instance_size } Logger.debug "---- LRU Cache ----" Logger.debug "Size: #{sum} Bytes" @@ -155,14 +159,17 @@ class LRUCache sum > @@limit end - private def self.remove_victim_cache + private def self.remove_least_recent_access + Logger.debug "Removing entries from LRUCache" if @@should_log while is_cache_full && @@cache.size > 0 - Logger.debug "LRUCache Cache Full! Remove LRU" - min = @@cache.min_by? { |_, entry| entry.atime } + min_tuple = @@cache.min_by { |_, entry| entry.atime } + min_key = min_tuple[0] + min_entry = min_tuple[1] + Logger.debug " \ - Target: #{min[0]}, \ - Last Access Time: #{min[1].atime}" if min - invalidate min[0] if min + Target: #{min_key}, \ + Last Access Time: #{min_entry.atime}" if @@should_log + invalidate min_key end end end From 15a54f4f23038ce16cf58b607a7042d1ec6efe41 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Mon, 6 Sep 2021 08:10:13 +0000 Subject: [PATCH 23/26] Add `:sorted_entries` suffix to `gen_key` --- src/library/cache.cr | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/library/cache.cr b/src/library/cache.cr index 496442f..37359f2 100644 --- a/src/library/cache.cr +++ b/src/library/cache.cr @@ -63,10 +63,11 @@ class SortedEntriesCacheEntry < CacheEntry(Array(String), Array(Entry)) def self.gen_key(book_id : String, username : String, entries : Array(Entry), opt : SortOptions?) - sig = Digest::SHA1.hexdigest (entries.map &.id).to_s + entries_sig = Digest::SHA1.hexdigest (entries.map &.id).to_s user_context = opt && opt.method == SortMethod::Progress ? username : "" - Digest::SHA1.hexdigest (book_id + sig + user_context + - (opt ? opt.to_tuple.to_s : "nil")) + sig = Digest::SHA1.hexdigest (book_id + entries_sig + user_context + + (opt ? opt.to_tuple.to_s : "nil")) + "#{sig}:sorted_entries" end end From 44d9c51ff9e86da4b45e552797d4e379556c5dc4 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Mon, 6 Sep 2021 08:10:42 +0000 Subject: [PATCH 24/26] Fix logging --- src/library/cache.cr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/library/cache.cr b/src/library/cache.cr index 37359f2..0784363 100644 --- a/src/library/cache.cr +++ b/src/library/cache.cr @@ -161,7 +161,9 @@ class LRUCache end private def self.remove_least_recent_access - Logger.debug "Removing entries from LRUCache" if @@should_log + if @@should_log && is_cache_full + Logger.debug "Removing entries from LRUCache" + end while is_cache_full && @@cache.size > 0 min_tuple = @@cache.min_by { |_, entry| entry.atime } min_key = min_tuple[0] From ca1e221b10459775569d93cd1816efe5e462e880 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Mon, 6 Sep 2021 08:23:31 +0000 Subject: [PATCH 25/26] Rename `ids2entries` -> `ids_to_entries` --- src/library/cache.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/library/cache.cr b/src/library/cache.cr index 0784363..05529fb 100644 --- a/src/library/cache.cr +++ b/src/library/cache.cr @@ -39,10 +39,10 @@ class SortedEntriesCacheEntry < CacheEntry(Array(String), Array(Entry)) end def self.to_return_t(value : Array(String)) - ids2entries value + ids_to_entries value end - private def self.ids2entries(ids : Array(String)) + private def self.ids_to_entries(ids : Array(String)) e_map = Library.default.deep_entries.to_h { |entry| {entry.id, entry} } entries = [] of Entry begin From d809c21ee13429872950c7f5f299da236feb7ba7 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Mon, 6 Sep 2021 08:23:54 +0000 Subject: [PATCH 26/26] Document `CacheEntry` --- src/library/cache.cr | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/library/cache.cr b/src/library/cache.cr index 05529fb..d0d3f01 100644 --- a/src/library/cache.cr +++ b/src/library/cache.cr @@ -3,6 +3,16 @@ require "digest" require "./entry" require "./types" +# Base class for an entry in the LRU cache. +# There are two ways to use it: +# 1. Use it as it is by instantiating with the appropriate `SaveT` and +# `ReturnT`. Note that in this case, `SaveT` and `ReturnT` must be the +# same type. That is, the input value will be stored as it is without +# any transformation. +# 2. You can also subclass it and provide custom implementations for +# `to_save_t` and `to_return_t`. This allows you to transform and store +# the input value to a different type. See `SortedEntriesCacheEntry` as +# an example. private class CacheEntry(SaveT, ReturnT) getter key : String, atime : Time