2
0
mirror of https://github.com/koreader/koreader synced 2024-11-18 03:25:46 +00:00
koreader/frontend/document/doccache.lua
2023-10-12 14:52:11 +02:00

122 lines
4.3 KiB
Lua

--[[
"Global" LRU cache used by Document & friends.
--]]
local Cache = require("cache")
local CanvasContext = require("document/canvascontext")
local DataStorage = require("datastorage")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local md5 = require("ffi/sha2").md5
local util = require("util")
local DHINTCOUNT = G_defaults:readSetting("DHINTCOUNT")
local function calcCacheMemSize()
local min = G_defaults:readSetting("DGLOBAL_CACHE_SIZE_MINIMUM")
local max = G_defaults:readSetting("DGLOBAL_CACHE_SIZE_MAXIMUM")
local memfree, _ = util.calcFreeMem() or 0, 0
local calc = memfree * G_defaults:readSetting("DGLOBAL_CACHE_FREE_PROPORTION")
return math.min(max, math.max(min, calc))
end
local doccache_size = calcCacheMemSize()
local function computeCacheSize()
local mb_size = doccache_size / 1024 / 1024
-- If we end up with a not entirely ridiculous cache size, use that...
if mb_size >= 8 then
logger.dbg(string.format("Allocating a %dMB budget for the global document cache", mb_size))
return doccache_size
else
return nil
end
end
local function computeCacheSlots()
local mb_size = doccache_size / 1024 / 1024
--- ...otherwise, effectively disable the cache by making it single slot...
if mb_size < 8 then
logger.dbg("Setting up a minimal single slot global document cache")
return 1
else
return nil
end
end
-- NOTE: This is a singleton!
local DocCache = Cache:new{
slots = computeCacheSlots(),
size = computeCacheSize(),
-- Average item size is a screen's worth of bitmap, mixed with a few much smaller tables (pgdim, pglinks, etc.), hence the / 3
avg_itemsize = math.floor(CanvasContext:getWidth() * CanvasContext:getHeight() * (CanvasContext.is_color_rendering_enabled and 4 or 1) / 3),
-- Rely on CacheItem's eviction callback to free resources *immediately* on eviction.
enable_eviction_cb = true,
disk_cache = true,
cache_path = DataStorage:getDataDir() .. "/cache/",
}
function DocCache:serialize(doc_path)
if not self.disk_cache then
return
end
-- Calculate the current disk cache size
local cached_size = 0
local sorted_caches = {}
for _, file in pairs(self.cached) do
table.insert(sorted_caches, {file=file, time=lfs.attributes(file, "access")})
cached_size = cached_size + (lfs.attributes(file, "size") or 0)
end
table.sort(sorted_caches, function(v1, v2) return v1.time > v2.time end)
-- Rewind a bit in order to serialize the currently *displayed* page for the current document,
-- as the actual MRU item would be the most recently *hinted* page, which wouldn't be helpful ;).
if doc_path then
local mru_key
local mru_found = 0
for key, item in self.cache:pairs() do
-- Only dump items that actually request persistence and match the current document.
if item.persistent and item.dump and item.doc_path == doc_path then
mru_key = key
mru_found = mru_found + 1
if mru_found >= (1 + DHINTCOUNT) then
-- We found the right item, i.e., the *displayed* page
break
end
end
end
if mru_key then
local cache_full_path = self.cache_path .. md5(mru_key)
local cache_file_exists = lfs.attributes(cache_full_path)
if not cache_file_exists then
logger.dbg("Dumping cache item", mru_key)
local cache_item = self.cache:get(mru_key)
local cache_size = cache_item:dump(cache_full_path)
if cache_size then
cached_size = cached_size + cache_size
end
end
end
end
-- Allocate the same amount of storage to the disk cache than the memory cache
while cached_size > self.size do
-- discard the least recently used cache
local discarded = table.remove(sorted_caches)
if discarded then
cached_size = cached_size - lfs.attributes(discarded.file, "size")
os.remove(discarded.file)
else
logger.warn("Cache accounting is broken")
break
end
end
-- We may have updated the disk cache's content, so refresh its state
self:refreshSnapshot()
end
return DocCache