2012-05-18 22:35:09 +00:00
|
|
|
--[[
|
2021-05-04 21:13:24 +00:00
|
|
|
A LRU cache, based on https://github.com/starius/lua-lru
|
2012-05-18 22:35:09 +00:00
|
|
|
]]--
|
2017-04-29 14:30:16 +00:00
|
|
|
|
|
|
|
local lfs = require("libs/libkoreader-lfs")
|
2016-12-29 08:10:38 +00:00
|
|
|
local logger = require("logger")
|
2021-05-04 21:13:24 +00:00
|
|
|
local lru = require("ffi/lru")
|
2020-07-21 21:25:46 +00:00
|
|
|
local md5 = require("ffi/sha2").md5
|
2022-09-19 21:25:18 +00:00
|
|
|
local util = require("util")
|
2014-04-30 15:24:44 +00:00
|
|
|
|
2019-02-18 16:01:00 +00:00
|
|
|
local CanvasContext = require("document/canvascontext")
|
|
|
|
if CanvasContext.should_restrict_JIT then
|
2020-12-26 19:23:51 +00:00
|
|
|
jit.off(true, true)
|
2016-04-07 15:28:52 +00:00
|
|
|
end
|
|
|
|
|
2021-05-04 21:13:24 +00:00
|
|
|
local Cache = {
|
|
|
|
-- Cache configuration:
|
2021-05-07 01:59:27 +00:00
|
|
|
-- Max storage space, in bytes...
|
|
|
|
size = nil,
|
|
|
|
-- ...Average item size, used to compute the amount of slots in the LRU.
|
|
|
|
avg_itemsize = nil,
|
|
|
|
-- Or, simply set the number of slots, with no storage space limitation.
|
|
|
|
-- c.f., GlyphCache, CatalogCache
|
|
|
|
slots = nil,
|
|
|
|
-- Should LRU call the object's onFree method on eviction? Implies using CacheItem instead of plain tables/objects.
|
|
|
|
-- c.f., DocCache
|
|
|
|
enable_eviction_cb = false,
|
2021-05-04 21:13:24 +00:00
|
|
|
-- Generally, only DocCache uses this
|
|
|
|
disk_cache = false,
|
|
|
|
cache_path = nil,
|
|
|
|
}
|
|
|
|
|
|
|
|
function Cache:new(o)
|
|
|
|
o = o or {}
|
|
|
|
setmetatable(o, self)
|
|
|
|
self.__index = self
|
|
|
|
if o.init then o:init() end
|
|
|
|
return o
|
|
|
|
end
|
|
|
|
|
|
|
|
function Cache:init()
|
2021-05-07 01:59:27 +00:00
|
|
|
if self.slots then
|
|
|
|
-- Caller doesn't care about storage space, just slot count
|
|
|
|
self.cache = lru.new(self.slots, nil, self.enable_eviction_cb)
|
|
|
|
else
|
|
|
|
-- Compute the amount of slots in the LRU based on the max size & the average item size
|
2021-09-09 23:07:04 +00:00
|
|
|
self.slots = math.ceil(self.size / self.avg_itemsize)
|
2021-05-07 01:59:27 +00:00
|
|
|
self.cache = lru.new(self.slots, self.size, self.enable_eviction_cb)
|
|
|
|
end
|
2021-05-04 21:13:24 +00:00
|
|
|
|
|
|
|
if self.disk_cache then
|
|
|
|
self.cached = self:_getDiskCache()
|
|
|
|
else
|
|
|
|
-- No need to go through our own check or even get methods if there's no disk cache, hit lru directly
|
|
|
|
self.check = self.cache.get
|
|
|
|
end
|
2021-05-07 01:59:27 +00:00
|
|
|
|
|
|
|
if not self.enable_eviction_cb or not self.size then
|
|
|
|
-- We won't be using CacheItem here, so we can pass the size manually if necessary.
|
|
|
|
-- e.g., insert's signature is now (key, value, [size]), instead of relying on CacheItem's size field.
|
|
|
|
self.insert = self.cache.set
|
|
|
|
|
|
|
|
-- With debug info (c.f., below)
|
|
|
|
--self.insert = self.set
|
|
|
|
end
|
2021-05-04 21:13:24 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
-- return a snapshot of disk cached items for subsequent check
|
|
|
|
--]]
|
|
|
|
function Cache:_getDiskCache()
|
|
|
|
local cached = {}
|
|
|
|
for key_md5 in lfs.dir(self.cache_path) do
|
|
|
|
local file = self.cache_path .. key_md5
|
|
|
|
if lfs.attributes(file, "mode") == "file" then
|
|
|
|
cached[key_md5] = file
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return cached
|
|
|
|
end
|
|
|
|
|
2012-05-18 22:35:09 +00:00
|
|
|
function Cache:insert(key, object)
|
2021-05-04 21:13:24 +00:00
|
|
|
-- If this object is single-handledly too large for the cache, don't cache it.
|
|
|
|
if not self:willAccept(object.size) then
|
2021-05-03 03:20:14 +00:00
|
|
|
logger.warn("Too much memory would be claimed by caching", key)
|
2014-10-23 05:48:45 +00:00
|
|
|
return
|
2014-03-13 13:52:43 +00:00
|
|
|
end
|
2021-05-04 21:13:24 +00:00
|
|
|
|
|
|
|
self.cache:set(key, object, object.size)
|
|
|
|
|
|
|
|
-- Accounting debugging
|
2021-05-07 01:59:27 +00:00
|
|
|
--self:_insertion_stats(key, object.size)
|
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
function Cache:set(key, object, size)
|
|
|
|
self.cache:set(key, object, size)
|
|
|
|
|
|
|
|
-- Accounting debugging
|
|
|
|
self:_insertion_stats(key, size)
|
|
|
|
end
|
|
|
|
|
|
|
|
function Cache:_insertion_stats(key, size)
|
2021-05-04 21:13:24 +00:00
|
|
|
print(string.format("Cache %s (%d/%d) [%.2f/%.2f @ ~%db] inserted %db key: %s",
|
|
|
|
self,
|
2021-05-07 01:59:27 +00:00
|
|
|
self.cache:used_slots(), self.slots,
|
|
|
|
self.cache:used_size() / 1024 / 1024, (self.size or 0) / 1024 / 1024, self.cache:used_size() / self.cache:used_slots(),
|
|
|
|
size or 0, key))
|
2012-05-18 22:35:09 +00:00
|
|
|
end
|
2021-05-07 01:59:27 +00:00
|
|
|
--]]
|
2012-05-18 22:35:09 +00:00
|
|
|
|
2014-04-30 15:24:44 +00:00
|
|
|
--[[
|
2021-05-03 03:20:14 +00:00
|
|
|
-- check for cache item by key
|
2014-04-30 15:24:44 +00:00
|
|
|
-- if ItemClass is given, disk cache is also checked.
|
|
|
|
--]]
|
|
|
|
function Cache:check(key, ItemClass)
|
2021-05-04 21:13:24 +00:00
|
|
|
local value = self.cache:get(key)
|
|
|
|
if value then
|
|
|
|
return value
|
2014-04-30 15:24:44 +00:00
|
|
|
elseif ItemClass then
|
2020-07-21 21:25:46 +00:00
|
|
|
local cached = self.cached[md5(key)]
|
2014-04-30 15:24:44 +00:00
|
|
|
if cached then
|
|
|
|
local item = ItemClass:new{}
|
|
|
|
local ok, msg = pcall(item.load, item, cached)
|
|
|
|
if ok then
|
|
|
|
self:insert(key, item)
|
|
|
|
return item
|
|
|
|
else
|
2021-05-03 03:20:14 +00:00
|
|
|
logger.warn("Failed to load on-disk cache:", msg)
|
|
|
|
--- It's apparently unusable, purge it and refresh the snapshot.
|
|
|
|
os.remove(cached)
|
|
|
|
self:refreshSnapshot()
|
2014-04-30 15:24:44 +00:00
|
|
|
end
|
|
|
|
end
|
2014-03-13 13:52:43 +00:00
|
|
|
end
|
2012-05-18 22:35:09 +00:00
|
|
|
end
|
|
|
|
|
2021-05-04 21:13:24 +00:00
|
|
|
-- Shortcut when disk_cache is disabled
|
|
|
|
function Cache:get(key)
|
|
|
|
return self.cache:get(key)
|
|
|
|
end
|
|
|
|
|
2012-05-18 22:35:09 +00:00
|
|
|
function Cache:willAccept(size)
|
2021-05-04 21:13:24 +00:00
|
|
|
-- We only allow a single object to fill 75% of the cache
|
|
|
|
return size*4 < self.size*3
|
2012-05-18 22:35:09 +00:00
|
|
|
end
|
|
|
|
|
2021-05-03 03:20:14 +00:00
|
|
|
-- Blank the cache
|
2012-05-18 22:35:09 +00:00
|
|
|
function Cache:clear()
|
2021-05-04 21:13:24 +00:00
|
|
|
self.cache:clear()
|
2012-05-18 22:35:09 +00:00
|
|
|
end
|
2013-10-18 20:38:07 +00:00
|
|
|
|
2021-05-03 03:20:14 +00:00
|
|
|
-- Terribly crappy workaround: evict half the cache if we appear to be redlining on free RAM...
|
|
|
|
function Cache:memoryPressureCheck()
|
2022-09-19 21:25:18 +00:00
|
|
|
local memfree, memtotal = util.calcFreeMem()
|
2021-05-03 03:20:14 +00:00
|
|
|
|
|
|
|
-- Nonsensical values? (!Linux), skip this.
|
2022-09-19 21:25:18 +00:00
|
|
|
if memtotal == nil then
|
2021-05-03 03:20:14 +00:00
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
-- If less that 20% of the total RAM is free, drop half the Cache...
|
2021-05-07 01:59:27 +00:00
|
|
|
local free_fraction = memfree / memtotal
|
|
|
|
if free_fraction < 0.20 then
|
|
|
|
logger.warn(string.format("Running low on memory (~%d%%, ~%.2f/%d MiB), evicting half of the cache...",
|
2022-09-19 21:25:18 +00:00
|
|
|
free_fraction * 100,
|
|
|
|
memfree / (1024 * 1024),
|
|
|
|
memtotal / (1024 * 1024)))
|
2021-05-04 21:13:24 +00:00
|
|
|
self.cache:chop()
|
2021-05-03 03:20:14 +00:00
|
|
|
|
|
|
|
-- And finish by forcing a GC sweep now...
|
|
|
|
collectgarbage()
|
|
|
|
collectgarbage()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-04-13 15:54:11 +00:00
|
|
|
-- Refresh the disk snapshot (mainly used by ui/data/onetime_migration)
|
|
|
|
function Cache:refreshSnapshot()
|
2021-05-04 21:13:24 +00:00
|
|
|
if not self.disk_cache then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
self.cached = self:_getDiskCache()
|
2021-04-13 15:54:11 +00:00
|
|
|
end
|
|
|
|
|
2021-05-03 03:20:14 +00:00
|
|
|
-- Evict the disk cache (ditto)
|
|
|
|
function Cache:clearDiskCache()
|
2021-05-04 21:13:24 +00:00
|
|
|
if not self.disk_cache then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2021-05-03 03:20:14 +00:00
|
|
|
for _, file in pairs(self.cached) do
|
|
|
|
os.remove(file)
|
|
|
|
end
|
|
|
|
|
|
|
|
self:refreshSnapshot()
|
|
|
|
end
|
|
|
|
|
2013-10-18 20:38:07 +00:00
|
|
|
return Cache
|