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|