mirror of
https://github.com/hkalexling/Mango.git
synced 2025-08-02 10:55:30 -04:00
219 lines
6.2 KiB
Crystal
219 lines
6.2 KiB
Crystal
require "digest"
|
|
|
|
require "./entry"
|
|
require "./title"
|
|
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
|
|
|
|
@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
|
|
self.class.to_return_t @value
|
|
end
|
|
|
|
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))
|
|
ids_to_entries value
|
|
end
|
|
|
|
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
|
|
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(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?)
|
|
entries_sig = Digest::SHA1.hexdigest (entries.map &.id).to_s
|
|
user_context = opt && opt.method == SortMethod::Progress ? username : ""
|
|
sig = Digest::SHA1.hexdigest(book_id + entries_sig + user_context +
|
|
(opt ? opt.to_tuple.to_s : "nil"))
|
|
"#{sig}:sorted_entries"
|
|
end
|
|
end
|
|
|
|
class SortedTitlesCacheEntry < CacheEntry(Array(String), Array(Title))
|
|
def self.to_save_t(value : Array(Title))
|
|
value.map &.id
|
|
end
|
|
|
|
def self.to_return_t(value : Array(String))
|
|
value.map { |title_id| Library.default.title_hash[title_id].not_nil! }
|
|
end
|
|
|
|
def instance_size
|
|
instance_sizeof(SortedTitlesCacheEntry) + # 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(username : String, titles : Array(Title), opt : SortOptions?)
|
|
titles_sig = Digest::SHA1.hexdigest (titles.map &.id).to_s
|
|
user_context = opt && opt.method == SortMethod::Progress ? username : ""
|
|
sig = Digest::SHA1.hexdigest(titles_sig + user_context +
|
|
(opt ? opt.to_tuple.to_s : "nil"))
|
|
"#{sig}:sorted_titles"
|
|
end
|
|
end
|
|
|
|
class String
|
|
def instance_size
|
|
instance_sizeof(String) + bytesize
|
|
end
|
|
end
|
|
|
|
struct Tuple(*T)
|
|
def instance_size
|
|
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
|
|
|
|
alias CacheableType = Array(Entry) | Array(Title) | String |
|
|
Tuple(String, Int32)
|
|
alias CacheEntryType = SortedEntriesCacheEntry |
|
|
SortedTitlesCacheEntry |
|
|
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? Array(Title)
|
|
SortedTitlesCacheEntry.new key, value
|
|
else
|
|
CacheEntry(typeof(value), typeof(value)).new key, value
|
|
end
|
|
end
|
|
|
|
# LRU Cache
|
|
class LRUCache
|
|
@@limit : Int128 = Int128.new 0
|
|
@@should_log = true
|
|
# key => entry
|
|
@@cache = {} of String => CacheEntryType
|
|
|
|
def self.enabled
|
|
Config.current.cache_enabled
|
|
end
|
|
|
|
def self.init
|
|
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]?
|
|
if @@should_log
|
|
Logger.debug "LRUCache #{entry.nil? ? "miss" : "hit"} #{key}"
|
|
end
|
|
return entry.value unless entry.nil?
|
|
end
|
|
|
|
def self.set(cache_entry : CacheEntryType)
|
|
return unless enabled
|
|
key = cache_entry.key
|
|
@@cache[key] = cache_entry
|
|
Logger.debug "LRUCache cached #{key}" if @@should_log
|
|
remove_least_recent_access
|
|
end
|
|
|
|
def self.invalidate(key : String)
|
|
return unless enabled
|
|
@@cache.delete key
|
|
end
|
|
|
|
def self.print
|
|
return unless @@should_log
|
|
sum = @@cache.sum { |_, entry| entry.instance_size }
|
|
Logger.debug "---- LRU Cache ----"
|
|
Logger.debug "Size: #{sum} Bytes"
|
|
Logger.debug "List:"
|
|
@@cache.each do |k, v|
|
|
Logger.debug "#{k} | #{v.atime} | #{v.instance_size}"
|
|
end
|
|
Logger.debug "-------------------"
|
|
end
|
|
|
|
private def self.is_cache_full
|
|
sum = @@cache.sum { |_, entry| entry.instance_size }
|
|
sum > @@limit
|
|
end
|
|
|
|
private def self.remove_least_recent_access
|
|
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]
|
|
min_entry = min_tuple[1]
|
|
|
|
Logger.debug " \
|
|
Target: #{min_key}, \
|
|
Last Access Time: #{min_entry.atime}" if @@should_log
|
|
invalidate min_key
|
|
end
|
|
end
|
|
end
|