2
0
mirror of https://github.com/koreader/koreader synced 2024-11-16 06:12:56 +00:00
koreader/frontend/document/doccache.lua
NiLuJe fadee1f5dc
Clarify our OOP semantics across the codebase (#9586)
Basically:

* Use `extend` for class definitions
* Use `new` for object instantiations

That includes some minor code cleanups along the way:

* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
  * ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2022-10-06 02:14:48 +02:00

122 lines
4.4 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(string.format("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