2021-05-04 21:13:24 +00:00
|
|
|
--[[
|
|
|
|
"Global" LRU cache used by Document & friends.
|
|
|
|
--]]
|
|
|
|
|
|
|
|
local Cache = require("cache")
|
|
|
|
local CanvasContext = require("document/canvascontext")
|
|
|
|
local DataStorage = require("datastorage")
|
2022-09-14 01:49:50 +00:00
|
|
|
local lfs = require("libs/libkoreader-lfs")
|
2021-06-17 15:08:21 +00:00
|
|
|
local logger = require("logger")
|
2022-09-14 01:49:50 +00:00
|
|
|
local md5 = require("ffi/sha2").md5
|
2022-09-19 21:25:18 +00:00
|
|
|
local util = require("util")
|
2021-05-04 21:13:24 +00:00
|
|
|
|
2022-09-27 23:10:50 +00:00
|
|
|
local DHINTCOUNT = G_defaults:readSetting("DHINTCOUNT")
|
|
|
|
|
2021-05-04 21:13:24 +00:00
|
|
|
local function calcCacheMemSize()
|
2022-09-27 23:10:50 +00:00
|
|
|
local min = G_defaults:readSetting("DGLOBAL_CACHE_SIZE_MINIMUM")
|
|
|
|
local max = G_defaults:readSetting("DGLOBAL_CACHE_SIZE_MAXIMUM")
|
2022-09-27 23:19:00 +00:00
|
|
|
local memfree, _ = util.calcFreeMem() or 0, 0
|
|
|
|
local calc = memfree * G_defaults:readSetting("DGLOBAL_CACHE_FREE_PROPORTION")
|
2021-09-09 23:07:04 +00:00
|
|
|
return math.min(max, math.max(min, calc))
|
|
|
|
end
|
2022-09-14 01:49:50 +00:00
|
|
|
local doccache_size = calcCacheMemSize()
|
2021-09-09 23:07:04 +00:00
|
|
|
|
|
|
|
local function computeCacheSize()
|
2022-09-14 01:49:50 +00:00
|
|
|
local mb_size = doccache_size / 1024 / 1024
|
2021-09-09 23:07:04 +00:00
|
|
|
|
|
|
|
-- 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))
|
2022-09-14 01:49:50 +00:00
|
|
|
return doccache_size
|
2021-09-09 23:07:04 +00:00
|
|
|
else
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function computeCacheSlots()
|
2022-09-14 01:49:50 +00:00
|
|
|
local mb_size = doccache_size / 1024 / 1024
|
2021-06-17 15:08:21 +00:00
|
|
|
|
2021-09-09 23:07:04 +00:00
|
|
|
--- ...otherwise, effectively disable the cache by making it single slot...
|
|
|
|
if mb_size < 8 then
|
2023-10-12 12:52:11 +00:00
|
|
|
logger.dbg("Setting up a minimal single slot global document cache")
|
2021-09-09 23:07:04 +00:00
|
|
|
return 1
|
|
|
|
else
|
|
|
|
return nil
|
|
|
|
end
|
2021-05-04 21:13:24 +00:00
|
|
|
end
|
|
|
|
|
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 00:14:48 +00:00
|
|
|
-- NOTE: This is a singleton!
|
2021-05-04 21:13:24 +00:00
|
|
|
local DocCache = Cache:new{
|
2021-09-09 23:07:04 +00:00
|
|
|
slots = computeCacheSlots(),
|
|
|
|
size = computeCacheSize(),
|
2021-05-04 21:13:24 +00:00
|
|
|
-- 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),
|
2021-05-07 01:59:27 +00:00
|
|
|
-- Rely on CacheItem's eviction callback to free resources *immediately* on eviction.
|
|
|
|
enable_eviction_cb = true,
|
2021-05-04 21:13:24 +00:00
|
|
|
disk_cache = true,
|
|
|
|
cache_path = DataStorage:getDataDir() .. "/cache/",
|
|
|
|
}
|
|
|
|
|
2022-09-14 01:49:50 +00:00
|
|
|
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
|
|
|
|
|
2021-05-04 21:13:24 +00:00
|
|
|
return DocCache
|