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
|
|
|
|
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)
|
Document: Do not cache panel-zoom tiles to disk and fix their caching and rendering (#12303)
* Use a dedicated cache hash for partial tiles from panel-zoom
* Never dump them to disk, as it confuses DocCache's crappy heuristics that rewinds the cache to skip over the hinted page to try to dump the on-screen page to disk.
* Apply the zoom factor in the exact same way as any other page rect (i.e., floor coordinates, ceil dimensions), and make sure said rect is actually a Geom so it doesn't break the cache hash, which relies on Geom's custom tostring method for rects. Said scaling method *also* belongs to the Geom class anyway.
* Handle such pre-scaled rects properly in renderPage, so as not to apply the zoom factor to the full page, which would attempt to create a gigantic buffer.
* And now that the rect is rendered properly in an appropriately-sized buffer, use the rendered tile as-is, no need to blit it to another (potentially way too large because of the above issue) blank BB.
* The zoom factor is now computed for a scale to best-fit (honoring `imageviewer_rotate_auto_for_best_fit`), ensuring the best efficiency (ImageViewer won't have to re-scale).
* Cache: Reduce the maximum item size to 50% of the cache, instead of 75%.
* Warn about the legacy ReaderRotation module, as it turned out to be horribly broken. The whole machinery (which is spread over *a lot* of various codepaths) is left as-is, peppered with notes & fixmes hinting at the problem. Thankfully, that's not how we actually handle rotation, so it was probably hardly ever used (which possibly explains why nobody ever noticed it breaking, and that nugget possibly dates back to the inception of the kpv -> ko refactor!). (#12309)
2024-08-08 02:52:24 +00:00
|
|
|
-- We only allow a single object to fill 50% of the cache
|
|
|
|
return size*4 < self.size*2
|
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
|