From 21b067792d7f956306fffb0e2de300832a8f18f4 Mon Sep 17 00:00:00 2001 From: NiLuJe Date: Tue, 4 May 2021 23:13:24 +0200 Subject: [PATCH] Cache: Rewrite based on lua-lru Ought to be faster than our naive array-based approach. Especially for the glyph cache, which has a solid amount of elements, and is mostly cache hits. (There are few things worse for performance in Lua than table.remove @ !tail and table.insert @ !tail, which this was full of :/). DocCache: New module that's now an actual Cache instance instead of a weird hack. Replaces "Cache" (the instance) as used across Document & co. Only Cache instance with on-disk persistence. ImageCache: Update to new Cache. GlyphCache: Update to new Cache. Also, actually free glyph bbs on eviction. --- .../apps/reader/modules/readerzooming.lua | 6 +- frontend/apps/reader/readerui.lua | 6 +- frontend/cache.lua | 212 ++++++++---------- frontend/cacheitem.lua | 3 + frontend/document/doccache.lua | 24 ++ frontend/document/document.lua | 16 +- frontend/document/koptinterface.lua | 80 ++++--- frontend/document/pdfdocument.lua | 10 +- frontend/document/tilecacheitem.lua | 6 +- frontend/ui/data/onetime_migration.lua | 18 +- frontend/ui/rendertext.lua | 41 +++- frontend/ui/widget/imagewidget.lua | 17 +- plugins/opds.koplugin/opdsbrowser.lua | 9 +- spec/unit/cache_spec.lua | 18 +- spec/unit/koptinterface_spec.lua | 6 +- 15 files changed, 243 insertions(+), 229 deletions(-) create mode 100644 frontend/document/doccache.lua diff --git a/frontend/apps/reader/modules/readerzooming.lua b/frontend/apps/reader/modules/readerzooming.lua index ecc66f262..4b4f66c70 100644 --- a/frontend/apps/reader/modules/readerzooming.lua +++ b/frontend/apps/reader/modules/readerzooming.lua @@ -1,6 +1,6 @@ -local Cache = require("cache") local ConfirmBox = require("ui/widget/confirmbox") local Device = require("device") +local DocCache = require("document/doccache") local Event = require("ui/event") local Geom = require("ui/geometry") local GestureRange = require("ui/gesturerange") @@ -458,9 +458,9 @@ function ReaderZooming:getZoom(pageno) or self.zoom_factor zoom = zoom_w * zoom_factor end - if zoom and zoom > 10 and not Cache:willAccept(zoom * (self.dimen.w * self.dimen.h + 512)) then + if zoom and zoom > 10 and not DocCache:willAccept(zoom * (self.dimen.w * self.dimen.h + 512)) then logger.dbg("zoom too large, adjusting") - while not Cache:willAccept(zoom * (self.dimen.w * self.dimen.h + 512)) do + while not DocCache:willAccept(zoom * (self.dimen.w * self.dimen.h + 512)) do if zoom > 100 then zoom = zoom - 50 elseif zoom > 10 then diff --git a/frontend/apps/reader/readerui.lua b/frontend/apps/reader/readerui.lua index dc6521c03..ad9eb8f8a 100644 --- a/frontend/apps/reader/readerui.lua +++ b/frontend/apps/reader/readerui.lua @@ -5,10 +5,10 @@ It works using data gathered from a document interface. ]]-- local BD = require("ui/bidi") -local Cache = require("cache") local ConfirmBox = require("ui/widget/confirmbox") local Device = require("device") local DeviceListener = require("device/devicelistener") +local DocCache = require("document/doccache") local DocSettings = require("docsettings") local DocumentRegistry = require("document/documentregistry") local Event = require("ui/event") @@ -733,8 +733,8 @@ function ReaderUI:onClose(full_refresh) if self.dialog ~= self then self:saveSettings() end - -- serialize last used items for later launch - Cache:serialize() + -- Serialize the most recently displayed page for later launch + DocCache:serialize() if self.document ~= nil then logger.dbg("closing document") self:notifyCloseDocument() diff --git a/frontend/cache.lua b/frontend/cache.lua index e1f4ece73..ddbbf316f 100644 --- a/frontend/cache.lua +++ b/frontend/cache.lua @@ -1,10 +1,10 @@ --[[ -A global LRU cache +A LRU cache, based on https://github.com/starius/lua-lru ]]-- -local DataStorage = require("datastorage") local lfs = require("libs/libkoreader-lfs") local logger = require("logger") +local lru = require("ffi/lru") local md5 = require("ffi/sha2").md5 local CanvasContext = require("document/canvascontext") @@ -12,6 +12,52 @@ if CanvasContext.should_restrict_JIT then jit.off(true, true) end +local Cache = { + -- Cache configuration: + -- Max storage space, in bytes + size = 8 * 1024 * 1024, + -- Average item size is used to compute the amount of slots in the LRU + avg_itemsize = 8196, + -- 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() + -- Compute the amount of slots in the LRU based on the max size & the average item size + self.slots = math.floor(self.size / self.avg_itemsize) + self.cache = lru.new(self.slots, self.size) + + 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 +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 + -- For documentation purposes, here's a battle-tested shell version of calcFreeMem --[[ if grep -q 'MemAvailable' /proc/meminfo ; then @@ -37,7 +83,7 @@ end --]] -- And here's our simplified Lua version... -local function calcFreeMem() +function Cache:_calcFreeMem() local memtotal, memfree, memavailable, buffers, cached local meminfo = io.open("/proc/meminfo", "r") @@ -101,98 +147,23 @@ local function calcFreeMem() end end -local function calcCacheMemSize() - local min = DGLOBAL_CACHE_SIZE_MINIMUM - local max = DGLOBAL_CACHE_SIZE_MAXIMUM - local calc = calcFreeMem() * (DGLOBAL_CACHE_FREE_PROPORTION or 0) - return math.min(max, math.max(min, calc)) -end - -local cache_path = DataStorage:getDataDir() .. "/cache/" - ---[[ --- return a snapshot of disk cached items for subsequent check ---]] -local function getDiskCache() - local cached = {} - for key_md5 in lfs.dir(cache_path) do - local file = cache_path .. key_md5 - if lfs.attributes(file, "mode") == "file" then - cached[key_md5] = file - end - end - return cached -end - -local Cache = { - -- cache configuration: - max_memsize = calcCacheMemSize(), - -- cache state: - current_memsize = 0, - -- associative cache - cache = {}, - -- this will hold the LRU order of the cache - cache_order = {}, - -- disk Cache snapshot - cached = getDiskCache(), -} - -function Cache:new(o) - o = o or {} - setmetatable(o, self) - self.__index = self - return o -end - --- internal: remove reference in cache_order list -function Cache:_unref(key) - for i = #self.cache_order, 1, -1 do - if self.cache_order[i] == key then - table.remove(self.cache_order, i) - break - end - end -end - --- internal: free cache item -function Cache:_free(key) - self.current_memsize = self.current_memsize - self.cache[key].size - self.cache[key]:onFree() - self.cache[key] = nil -end - --- drop an item named via key from the cache -function Cache:drop(key) - if not self.cache[key] then return end - - self:_unref(key) - self:_free(key) -end - function Cache:insert(key, object) - -- make sure that one key only exists once: delete existing - self:drop(key) - -- If this object is single-handledly too large for the cache, we're done - if object.size > self.max_memsize then + -- If this object is single-handledly too large for the cache, don't cache it. + if not self:willAccept(object.size) then logger.warn("Too much memory would be claimed by caching", key) return end - -- If inserting this obect would blow the cache's watermark, - -- start dropping least recently used items first. - -- (they are at the end of the cache_order array) - while self.current_memsize + object.size > self.max_memsize do - local removed_key = table.remove(self.cache_order) - if removed_key then - self:_free(removed_key) - else - logger.warn("Cache accounting is broken") - break - end - end - -- Insert new object in front of the LRU order - table.insert(self.cache_order, 1, key) - self.cache[key] = object - self.current_memsize = self.current_memsize + object.size + + self.cache:set(key, object, object.size) + + -- Accounting debugging + --[[ + print(string.format("Cache %s (%d/%d) [%.2f/%.2f @ ~%db] inserted %db key: %s", + self, + self.cache:used_slots(), self.slots, self.cache:used_size() / 1024 / 1024, + self.size / 1024 / 1024, self.cache:used_size() / self.cache:used_slots(), + object.size, key)) + --]] end --[[ @@ -200,13 +171,9 @@ end -- if ItemClass is given, disk cache is also checked. --]] function Cache:check(key, ItemClass) - if self.cache[key] then - if self.cache_order[1] ~= key then - -- Move key in front of the LRU list (i.e., MRU) - self:_unref(key) - table.insert(self.cache_order, 1, key) - end - return self.cache[key] + local value = self.cache:get(key) + if value then + return value elseif ItemClass then local cached = self.cached[md5(key)] if cached then @@ -225,12 +192,21 @@ function Cache:check(key, ItemClass) end end +-- Shortcut when disk_cache is disabled +function Cache:get(key) + return self.cache:get(key) +end + function Cache:willAccept(size) - -- We only allow single objects to fill 75% of the cache - return size*4 < self.max_memsize*3 + -- We only allow a single object to fill 75% of the cache + return size*4 < self.size*3 end function Cache:serialize() + if not self.disk_cache then + return + end + -- Calculate the current disk cache size local cached_size = 0 local sorted_caches = {} @@ -243,11 +219,9 @@ function Cache:serialize() -- Only serialize the second most recently used cache item (as the MRU would be the *hinted* page). local mru_key local mru_found = 0 - for _, key in ipairs(self.cache_order) do - local cache_item = self.cache[key] - + for key, item in self.cache:pairs() do -- Only dump cache items that actually request persistence - if cache_item.persistent and cache_item.dump then + if item.persistent and item.dump then mru_key = key mru_found = mru_found + 1 if mru_found >= 2 then @@ -257,12 +231,12 @@ function Cache:serialize() end end if mru_key then - local cache_full_path = cache_path .. md5(mru_key) + 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[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 @@ -271,7 +245,7 @@ function Cache:serialize() end -- Allocate the same amount of storage to the disk cache than the memory cache - while cached_size > self.max_memsize do + while cached_size > self.size do -- discard the least recently used cache local discarded = table.remove(sorted_caches) if discarded then @@ -288,17 +262,12 @@ end -- Blank the cache function Cache:clear() - for k, _ in pairs(self.cache) do - self.cache[k]:onFree() - end - self.cache = {} - self.cache_order = {} - self.current_memsize = 0 + self.cache:clear() end -- Terribly crappy workaround: evict half the cache if we appear to be redlining on free RAM... function Cache:memoryPressureCheck() - local memfree, memtotal = calcFreeMem() + local memfree, memtotal = self:_calcFreeMem() -- Nonsensical values? (!Linux), skip this. if memtotal == 0 then @@ -308,10 +277,7 @@ function Cache:memoryPressureCheck() -- If less that 20% of the total RAM is free, drop half the Cache... if memfree / memtotal < 0.20 then logger.warn("Running low on memory, evicting half of the cache...") - for i = #self.cache_order / 2, 1, -1 do - local removed_key = table.remove(self.cache_order) - self:_free(removed_key) - end + self.cache:chop() -- And finish by forcing a GC sweep now... collectgarbage() @@ -321,11 +287,19 @@ end -- Refresh the disk snapshot (mainly used by ui/data/onetime_migration) function Cache:refreshSnapshot() - self.cached = getDiskCache() + if not self.disk_cache then + return + end + + self.cached = self:_getDiskCache() end -- Evict the disk cache (ditto) function Cache:clearDiskCache() + if not self.disk_cache then + return + end + for _, file in pairs(self.cached) do os.remove(file) end diff --git a/frontend/cacheitem.lua b/frontend/cacheitem.lua index 3b009c4d1..17bbf412f 100644 --- a/frontend/cacheitem.lua +++ b/frontend/cacheitem.lua @@ -16,6 +16,9 @@ function CacheItem:new(o) return o end +-- Called on eviction. +-- We generally use it to free C/FFI ressources *immediately* (as opposed to relying on our Userdata/FFI finalizers to do it "later" on GC). +-- c.f., TileCacheItem, GlyphCacheItem & ImageCacheItem function CacheItem:onFree() end diff --git a/frontend/document/doccache.lua b/frontend/document/doccache.lua new file mode 100644 index 000000000..4734089fd --- /dev/null +++ b/frontend/document/doccache.lua @@ -0,0 +1,24 @@ +--[[ +"Global" LRU cache used by Document & friends. +--]] + +local Cache = require("cache") +local CanvasContext = require("document/canvascontext") +local DataStorage = require("datastorage") + +local function calcCacheMemSize() + local min = DGLOBAL_CACHE_SIZE_MINIMUM + local max = DGLOBAL_CACHE_SIZE_MAXIMUM + local calc = Cache:_calcFreeMem() * (DGLOBAL_CACHE_FREE_PROPORTION or 0) + return math.min(max, math.max(min, calc)) +end + +local DocCache = Cache:new{ + size = calcCacheMemSize(), + -- 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), + disk_cache = true, + cache_path = DataStorage:getDataDir() .. "/cache/", +} + +return DocCache diff --git a/frontend/document/document.lua b/frontend/document/document.lua index 9036fa0fd..52e8b05af 100644 --- a/frontend/document/document.lua +++ b/frontend/document/document.lua @@ -1,7 +1,7 @@ local Blitbuffer = require("ffi/blitbuffer") -local Cache = require("cache") local CacheItem = require("cacheitem") local Configurable = require("configurable") +local DocCache = require("document/doccache") local DrawContext = require("ffi/drawcontext") local CanvasContext = require("document/canvascontext") local Geom = require("ui/geometry") @@ -172,14 +172,14 @@ end -- this might be overridden by a document implementation function Document:getNativePageDimensions(pageno) local hash = "pgdim|"..self.file.."|"..pageno - local cached = Cache:check(hash) + local cached = DocCache:check(hash) if cached then return cached[1] end local page = self._document:openPage(pageno) local page_size_w, page_size_h = page:getSize(self.dc_null) local page_size = Geom:new{ w = page_size_w, h = page_size_h } - Cache:insert(hash, CacheItem:new{ page_size }) + DocCache:insert(hash, CacheItem:new{ page_size }) page:close() return page_size end @@ -372,10 +372,10 @@ end function Document:renderPage(pageno, rect, zoom, rotation, gamma, render_mode) local hash_excerpt local hash = self:getFullPageHash(pageno, zoom, rotation, gamma, render_mode, self.render_color) - local tile = Cache:check(hash, TileCacheItem) + local tile = DocCache:check(hash, TileCacheItem) if not tile then hash_excerpt = hash.."|"..tostring(rect) - tile = Cache:check(hash_excerpt) + tile = DocCache:check(hash_excerpt) end if tile then return tile end @@ -385,7 +385,7 @@ function Document:renderPage(pageno, rect, zoom, rotation, gamma, render_mode) -- this will be the size we actually render local size = page_size -- we prefer to render the full page, if it fits into cache - if not Cache:willAccept(size.w * size.h * (self.render_color and 4 or 1) + 512) then + if not DocCache:willAccept(size.w * size.h * (self.render_color and 4 or 1) + 512) then -- whole page won't fit into cache logger.dbg("rendering only part of the page") --- @todo figure out how to better segment the page @@ -430,7 +430,7 @@ function Document:renderPage(pageno, rect, zoom, rotation, gamma, render_mode) local page = self._document:openPage(pageno) page:draw(dc, tile.bb, size.x, size.y, render_mode) page:close() - Cache:insert(hash, tile) + DocCache:insert(hash, tile) self:postRenderPage() return tile @@ -440,7 +440,7 @@ end --- @todo this should trigger a background operation function Document:hintPage(pageno, zoom, rotation, gamma, render_mode) --- @note: Crappy safeguard around memory issues like in #7627: if we're eating too much RAM, drop half the cache... - Cache:memoryPressureCheck() + DocCache:memoryPressureCheck() logger.dbg("hinting page", pageno) self:renderPage(pageno, nil, zoom, rotation, gamma, render_mode) diff --git a/frontend/document/koptinterface.lua b/frontend/document/koptinterface.lua index ab08ad7a2..02d0e8074 100644 --- a/frontend/document/koptinterface.lua +++ b/frontend/document/koptinterface.lua @@ -2,11 +2,11 @@ Interface to k2pdfoptlib backend. --]] -local Cache = require("cache") local CacheItem = require("cacheitem") local CanvasContext = require("document/canvascontext") local DataStorage = require("datastorage") local DEBUG = require("dbg") +local DocCache = require("document/doccache") local Document = require("document/document") local Geom = require("ui/geometry") local KOPTContext = require("ffi/koptcontext") @@ -27,11 +27,9 @@ local KoptInterface = { local ContextCacheItem = CacheItem:new{} function ContextCacheItem:onFree() - if self.kctx.free then - KoptInterface:waitForContext(self.kctx) - logger.dbg("free koptcontext", self.kctx) - self.kctx:free() - end + KoptInterface:waitForContext(self.kctx) + logger.dbg("ContextCacheItem: free KOPTContext", self.kctx) + self.kctx:free() end function ContextCacheItem:dump(filename) @@ -186,7 +184,7 @@ function KoptInterface:getAutoBBox(doc, pageno) } local context_hash = self:getContextHash(doc, pageno, bbox) local hash = "autobbox|"..context_hash - local cached = Cache:check(hash) + local cached = DocCache:check(hash) if not cached then local page = doc._document:openPage(pageno) local kc = self:createContext(doc, pageno, bbox) @@ -198,7 +196,7 @@ function KoptInterface:getAutoBBox(doc, pageno) else bbox = Document.getPageBBox(doc, pageno) end - Cache:insert(hash, CacheItem:new{ autobbox = bbox, size = 160 }) + DocCache:insert(hash, CacheItem:new{ autobbox = bbox, size = 160 }) page:close() kc:free() return bbox @@ -215,7 +213,7 @@ function KoptInterface:getSemiAutoBBox(doc, pageno) local bbox = Document.getPageBBox(doc, pageno) local context_hash = self:getContextHash(doc, pageno, bbox) local hash = "semiautobbox|"..context_hash - local cached = Cache:check(hash) + local cached = DocCache:check(hash) if not cached then local page = doc._document:openPage(pageno) local kc = self:createContext(doc, pageno, bbox) @@ -233,7 +231,7 @@ function KoptInterface:getSemiAutoBBox(doc, pageno) auto_bbox = bbox end page:close() - Cache:insert(hash, CacheItem:new{ semiautobbox = auto_bbox, size = 160 }) + DocCache:insert(hash, CacheItem:new{ semiautobbox = auto_bbox, size = 160 }) kc:free() return auto_bbox else @@ -251,7 +249,7 @@ function KoptInterface:getCachedContext(doc, pageno) local bbox = doc:getPageBBox(pageno) local context_hash = self:getContextHash(doc, pageno, bbox) local kctx_hash = "kctx|"..context_hash - local cached = Cache:check(kctx_hash, ContextCacheItem) + local cached = DocCache:check(kctx_hash, ContextCacheItem) if not cached then -- If kctx is not cached, create one and get reflowed bmp in foreground. local kc = self:createContext(doc, pageno, bbox) @@ -267,7 +265,7 @@ function KoptInterface:getCachedContext(doc, pageno) local fullwidth, fullheight = kc:getPageDim() logger.dbg("reflowed page", pageno, "fullwidth:", fullwidth, "fullheight:", fullheight) self.last_context_size = fullwidth * fullheight + 3072 -- estimation - Cache:insert(kctx_hash, ContextCacheItem:new{ + DocCache:insert(kctx_hash, ContextCacheItem:new{ persistent = true, size = self.last_context_size, kctx = kc @@ -336,12 +334,12 @@ function KoptInterface:renderReflowedPage(doc, pageno, rect, zoom, rotation, ren local context_hash = self:getContextHash(doc, pageno, bbox) local renderpg_hash = "renderpg|"..context_hash - local cached = Cache:check(renderpg_hash) + local cached = DocCache:check(renderpg_hash) if not cached then -- do the real reflowing if kctx has not been cached yet local kc = self:getCachedContext(doc, pageno) local fullwidth, fullheight = kc:getPageDim() - if not Cache:willAccept(fullwidth * fullheight) then + if not DocCache:willAccept(fullwidth * fullheight) then -- whole page won't fit into cache error("aborting, since we don't have enough cache for this page") end @@ -352,7 +350,7 @@ function KoptInterface:renderReflowedPage(doc, pageno, rect, zoom, rotation, ren } tile.bb = kc:dstToBlitBuffer() tile.size = tonumber(tile.bb.stride) * tile.bb.h + 512 -- estimation - Cache:insert(renderpg_hash, tile) + DocCache:insert(renderpg_hash, tile) return tile else return cached @@ -370,7 +368,7 @@ function KoptInterface:renderOptimizedPage(doc, pageno, rect, zoom, rotation, re local context_hash = self:getContextHash(doc, pageno, bbox) local renderpg_hash = "renderoptpg|"..context_hash..zoom - local cached = Cache:check(renderpg_hash, TileCacheItem) + local cached = DocCache:check(renderpg_hash, TileCacheItem) if not cached then local page_size = Document.getNativePageDimensions(doc, pageno) local full_page_bbox = { @@ -399,7 +397,7 @@ function KoptInterface:renderOptimizedPage(doc, pageno, rect, zoom, rotation, re tile.bb = kc:dstToBlitBuffer() tile.size = tonumber(tile.bb.stride) * tile.bb.h + 512 -- estimation kc:free() - Cache:insert(renderpg_hash, tile) + DocCache:insert(renderpg_hash, tile) return tile else return cached @@ -429,7 +427,7 @@ function KoptInterface:hintReflowedPage(doc, pageno, zoom, rotation, gamma, rend local bbox = doc:getPageBBox(pageno) local context_hash = self:getContextHash(doc, pageno, bbox) local kctx_hash = "kctx|"..context_hash - local cached = Cache:check(kctx_hash) + local cached = DocCache:check(kctx_hash) if not cached then local kc = self:createContext(doc, pageno, bbox) local page = doc._document:openPage(pageno) @@ -438,7 +436,7 @@ function KoptInterface:hintReflowedPage(doc, pageno, zoom, rotation, gamma, rend kc:setPreCache() page:reflow(kc, 0) page:close() - Cache:insert(kctx_hash, ContextCacheItem:new{ + DocCache:insert(kctx_hash, ContextCacheItem:new{ size = self.last_context_size or self.default_context_size, kctx = kc, }) @@ -496,16 +494,16 @@ function KoptInterface:getReflowedTextBoxes(doc, pageno) local bbox = doc:getPageBBox(pageno) local context_hash = self:getContextHash(doc, pageno, bbox) local hash = "rfpgboxes|"..context_hash - local cached = Cache:check(hash) + local cached = DocCache:check(hash) if not cached then local kctx_hash = "kctx|"..context_hash - cached = Cache:check(kctx_hash) + cached = DocCache:check(kctx_hash) if cached then local kc = self:waitForContext(cached.kctx) --kc:setDebug() local fullwidth, fullheight = kc:getPageDim() local boxes, nr_word = kc:getReflowedWordBoxes("dst", 0, 0, fullwidth, fullheight) - Cache:insert(hash, CacheItem:new{ rfpgboxes = boxes, size = 192 * nr_word }) -- estimation + DocCache:insert(hash, CacheItem:new{ rfpgboxes = boxes, size = 192 * nr_word }) -- estimation return boxes end else @@ -520,16 +518,16 @@ function KoptInterface:getNativeTextBoxes(doc, pageno) local bbox = doc:getPageBBox(pageno) local context_hash = self:getContextHash(doc, pageno, bbox) local hash = "nativepgboxes|"..context_hash - local cached = Cache:check(hash) + local cached = DocCache:check(hash) if not cached then local kctx_hash = "kctx|"..context_hash - cached = Cache:check(kctx_hash) + cached = DocCache:check(kctx_hash) if cached then local kc = self:waitForContext(cached.kctx) --kc:setDebug() local fullwidth, fullheight = kc:getPageDim() local boxes, nr_word = kc:getNativeWordBoxes("dst", 0, 0, fullwidth, fullheight) - Cache:insert(hash, CacheItem:new{ nativepgboxes = boxes, size = 192 * nr_word }) -- estimation + DocCache:insert(hash, CacheItem:new{ nativepgboxes = boxes, size = 192 * nr_word }) -- estimation return boxes end else @@ -546,17 +544,17 @@ function KoptInterface:getReflowedTextBoxesFromScratch(doc, pageno) local bbox = doc:getPageBBox(pageno) local context_hash = self:getContextHash(doc, pageno, bbox) local hash = "scratchrfpgboxes|"..context_hash - local cached = Cache:check(hash) + local cached = DocCache:check(hash) if not cached then local kctx_hash = "kctx|"..context_hash - cached = Cache:check(kctx_hash) + cached = DocCache:check(kctx_hash) if cached then local reflowed_kc = self:waitForContext(cached.kctx) local fullwidth, fullheight = reflowed_kc:getPageDim() local kc = self:createContext(doc, pageno) kc:copyDestBMP(reflowed_kc) local boxes, nr_word = kc:getNativeWordBoxes("dst", 0, 0, fullwidth, fullheight) - Cache:insert(hash, CacheItem:new{ scratchrfpgboxes = boxes, size = 192 * nr_word }) -- estimation + DocCache:insert(hash, CacheItem:new{ scratchrfpgboxes = boxes, size = 192 * nr_word }) -- estimation kc:free() return boxes end @@ -589,7 +587,7 @@ Done by OCR pre-processing in Tesseract and Leptonica. --]] function KoptInterface:getNativeTextBoxesFromScratch(doc, pageno) local hash = "scratchnativepgboxes|"..doc.file.."|"..pageno - local cached = Cache:check(hash) + local cached = DocCache:check(hash) if not cached then local page_size = Document.getNativePageDimensions(doc, pageno) local bbox = { @@ -602,7 +600,7 @@ function KoptInterface:getNativeTextBoxesFromScratch(doc, pageno) local page = doc._document:openPage(pageno) page:getPagePix(kc) local boxes, nr_word = kc:getNativeWordBoxes("src", 0, 0, page_size.w, page_size.h) - Cache:insert(hash, CacheItem:new{ scratchnativepgboxes = boxes, size = 192 * nr_word }) -- estimation + DocCache:insert(hash, CacheItem:new{ scratchnativepgboxes = boxes, size = 192 * nr_word }) -- estimation page:close() kc:free() return boxes @@ -619,7 +617,7 @@ function KoptInterface:getPageBlock(doc, pageno, x, y) local bbox = doc:getPageBBox(pageno) local context_hash = self:getContextHash(doc, pageno, bbox) local hash = "pageblocks|"..context_hash - local cached = Cache:check(hash) + local cached = DocCache:check(hash) if not cached then local page_size = Document.getNativePageDimensions(doc, pageno) local full_page_bbox = { @@ -633,7 +631,7 @@ function KoptInterface:getPageBlock(doc, pageno, x, y) local page = doc._document:openPage(pageno) page:getPagePix(kc) kc:findPageBlocks() - Cache:insert(hash, CacheItem:new{ kctx = kc, size = 3072 }) -- estimation + DocCache:insert(hash, CacheItem:new{ kctx = kc, size = 3072 }) -- estimation page:close() kctx = kc else @@ -646,8 +644,8 @@ end Get word from OCR providing selected word box. --]] function KoptInterface:getOCRWord(doc, pageno, wbox) - if not Cache:check(self.ocrengine) then - Cache:insert(self.ocrengine, OCREngine:new{ ocrengine = KOPTContext.new(), size = 3072 }) -- estimation + if not DocCache:check(self.ocrengine) then + DocCache:insert(self.ocrengine, OCREngine:new{ ocrengine = KOPTContext.new(), size = 3072 }) -- estimation end if doc.configurable.text_wrap == 1 then return self:getReflewOCRWord(doc, pageno, wbox.sbox) @@ -664,17 +662,17 @@ function KoptInterface:getReflewOCRWord(doc, pageno, rect) local bbox = doc:getPageBBox(pageno) local context_hash = self:getContextHash(doc, pageno, bbox) local hash = "rfocrword|"..context_hash..rect.x..rect.y..rect.w..rect.h - local cached = Cache:check(hash) + local cached = DocCache:check(hash) if not cached then local kctx_hash = "kctx|"..context_hash - cached = Cache:check(kctx_hash) + cached = DocCache:check(kctx_hash) if cached then local kc = self:waitForContext(cached.kctx) local _, word = pcall( kc.getTOCRWord, kc, "dst", rect.x, rect.y, rect.w, rect.h, self.tessocr_data, self.ocr_lang, self.ocr_type, 0, 1) - Cache:insert(hash, CacheItem:new{ rfocrword = word, size = #word + 64 }) -- estimation + DocCache:insert(hash, CacheItem:new{ rfocrword = word, size = #word + 64 }) -- estimation return word end else @@ -689,7 +687,7 @@ function KoptInterface:getNativeOCRWord(doc, pageno, rect) self.ocr_lang = doc.configurable.doc_language local hash = "ocrword|"..doc.file.."|"..pageno..rect.x..rect.y..rect.w..rect.h logger.dbg("hash", hash) - local cached = Cache:check(hash) + local cached = DocCache:check(hash) if not cached then local bbox = { x0 = rect.x - math.floor(rect.h * 0.3), @@ -707,7 +705,7 @@ function KoptInterface:getNativeOCRWord(doc, pageno, rect) kc.getTOCRWord, kc, "src", 0, 0, word_w, word_h, self.tessocr_data, self.ocr_lang, self.ocr_type, 0, 1) - Cache:insert(hash, CacheItem:new{ ocrword = word, size = #word + 64 }) -- estimation + DocCache:insert(hash, CacheItem:new{ ocrword = word, size = #word + 64 }) -- estimation logger.dbg("word", word) page:close() kc:free() @@ -721,8 +719,8 @@ end Get text from OCR providing selected text boxes. --]] function KoptInterface:getOCRText(doc, pageno, tboxes) - if not Cache:check(self.ocrengine) then - Cache:insert(self.ocrengine, OCREngine:new{ ocrengine = KOPTContext.new(), size = 3072 }) -- estimation + if not DocCache:check(self.ocrengine) then + DocCache:insert(self.ocrengine, OCREngine:new{ ocrengine = KOPTContext.new(), size = 3072 }) -- estimation end logger.info("Not implemented yet") end diff --git a/frontend/document/pdfdocument.lua b/frontend/document/pdfdocument.lua index fcb00f8a1..3cd0d4874 100644 --- a/frontend/document/pdfdocument.lua +++ b/frontend/document/pdfdocument.lua @@ -1,6 +1,6 @@ -local Cache = require("cache") local CacheItem = require("cacheitem") local CanvasContext = require("document/canvascontext") +local DocCache = require("document/doccache") local DocSettings = require("docsettings") local Document = require("document/document") local DrawContext = require("ffi/drawcontext") @@ -139,7 +139,7 @@ end function PdfDocument:getUsedBBox(pageno) local hash = "pgubbox|"..self.file.."|"..self.reflowable_font_size.."|"..pageno - local cached = Cache:check(hash) + local cached = DocCache:check(hash) if cached then return cached.ubbox end @@ -152,7 +152,7 @@ function PdfDocument:getUsedBBox(pageno) if used.x1 > pwidth then used.x1 = pwidth end if used.y0 < 0 then used.y0 = 0 end if used.y1 > pheight then used.y1 = pheight end - Cache:insert(hash, CacheItem:new{ + DocCache:insert(hash, CacheItem:new{ ubbox = used, size = 256, -- might be closer to 160 }) @@ -162,13 +162,13 @@ end function PdfDocument:getPageLinks(pageno) local hash = "pglinks|"..self.file.."|"..self.reflowable_font_size.."|"..pageno - local cached = Cache:check(hash) + local cached = DocCache:check(hash) if cached then return cached.links end local page = self._document:openPage(pageno) local links = page:getPageLinks() - Cache:insert(hash, CacheItem:new{ + DocCache:insert(hash, CacheItem:new{ links = links, size = 64 + (8 * 32 * #links), }) diff --git a/frontend/document/tilecacheitem.lua b/frontend/document/tilecacheitem.lua index fdbf81446..82c8b8c25 100644 --- a/frontend/document/tilecacheitem.lua +++ b/frontend/document/tilecacheitem.lua @@ -6,10 +6,8 @@ local logger = require("logger") local TileCacheItem = CacheItem:new{} function TileCacheItem:onFree() - if self.bb.free then - logger.dbg("free blitbuffer", self.bb) - self.bb:free() - end + logger.dbg("TileCacheItem: free blitbuffer", self.bb) + self.bb:free() end --- @note: Perhaps one day we'll be able to teach bitser or string.buffer about custom structs with pointers to buffers, diff --git a/frontend/ui/data/onetime_migration.lua b/frontend/ui/data/onetime_migration.lua index a27d5661c..52591bf58 100644 --- a/frontend/ui/data/onetime_migration.lua +++ b/frontend/ui/data/onetime_migration.lua @@ -171,9 +171,9 @@ if last_migration_date < 20210409 then logger.warn("os.rename:", err) end - -- Make sure Cache gets the memo - local Cache = require("cache") - Cache:refreshSnapshot() + -- Make sure DocCache gets the memo + local DocCache = require("document/doccache") + DocCache:refreshSnapshot() end -- Calibre, cache migration, https://github.com/koreader/koreader/pull/7528 @@ -193,9 +193,9 @@ if last_migration_date < 20210412 then logger.warn("os.rename:", err) end - -- Make sure Cache gets the memo - local Cache = require("cache") - Cache:refreshSnapshot() + -- Make sure DocCache gets the memo + local DocCache = require("document/doccache") + DocCache:refreshSnapshot() end -- Calibre, cache encoding format change, https://github.com/koreader/koreader/pull/7543 @@ -209,12 +209,12 @@ if last_migration_date < 20210414 then end end --- Cache, migration to Persist, https://github.com/koreader/koreader/pull/7624 +-- DocCache, migration to Persist, https://github.com/koreader/koreader/pull/7624 if last_migration_date < 20210503 then logger.info("Performing one-time migration for 20210503") - local Cache = require("cache") - Cache:clearDiskCache() + local DocCache = require("document/doccache") + DocCache:clearDiskCache() end -- We're done, store the current migration date diff --git a/frontend/ui/rendertext.lua b/frontend/ui/rendertext.lua index dc496ca53..b2e7ad318 100644 --- a/frontend/ui/rendertext.lua +++ b/frontend/ui/rendertext.lua @@ -24,13 +24,18 @@ end local RenderText = {} local GlyphCache = Cache:new{ - max_memsize = 512*1024, - current_memsize = 0, - cache = {}, - -- this will hold the LRU order of the cache - cache_order = {} + -- 1 MiB of glyph cache, with 1024 slots + size = 1 * 1024 * 1024, + avg_itemsize = 1024, } +local GlyphCacheItem = CacheItem:new{} + +function GlyphCacheItem:onFree() + logger.dbg("GlyphCacheItem: free blitbuffer", self.bb) + self.bb:free() +end + -- iterator over UTF8 encoded characters in a string local function utf8Chars(input_text) local function read_next_glyph(input, pos) @@ -92,7 +97,7 @@ function RenderText:getGlyph(face, charcode, bold) local glyph = GlyphCache:check(hash) if glyph then -- cache hit - return glyph[1] + return glyph end local rendered_glyph = face.ftface:renderGlyph(charcode, bold) if face.ftface:checkGlyph(charcode) == 0 then @@ -112,8 +117,15 @@ function RenderText:getGlyph(face, charcode, bold) logger.warn("error rendering glyph (charcode=", charcode, ") for face", face) return end - glyph = CacheItem:new{rendered_glyph} - glyph.size = tonumber(glyph[1].bb.stride) * glyph[1].bb.h + 320 + glyph = GlyphCacheItem:new{ + bb = rendered_glyph.bb, + l = rendered_glyph.l, + t = rendered_glyph.t, + r = rendered_glyph.r, + ax = rendered_glyph.ax, + ay = rendered_glyph.ay, + } + glyph.size = tonumber(glyph.bb.stride) * glyph.bb.h + 320 GlyphCache:insert(hash, glyph) return rendered_glyph end @@ -306,15 +318,22 @@ function RenderText:getGlyphByIndex(face, glyphindex, bold) local glyph = GlyphCache:check(hash) if glyph then -- cache hit - return glyph[1] + return glyph end local rendered_glyph = face.ftface:renderGlyphByIndex(glyphindex, bold and face.embolden_half_strength) if not rendered_glyph then logger.warn("error rendering glyph (glyphindex=", glyphindex, ") for face", face) return end - glyph = CacheItem:new{rendered_glyph} - glyph.size = tonumber(glyph[1].bb.stride) * glyph[1].bb.h + 320 + glyph = GlyphCacheItem:new{ + bb = rendered_glyph.bb, + l = rendered_glyph.l, + t = rendered_glyph.t, + r = rendered_glyph.r, + ax = rendered_glyph.ax, + ay = rendered_glyph.ay, + } + glyph.size = tonumber(glyph.bb.stride) * glyph.bb.h + 320 GlyphCache:insert(hash, glyph) return rendered_glyph end diff --git a/frontend/ui/widget/imagewidget.lua b/frontend/ui/widget/imagewidget.lua index c6f75565c..49d19735c 100644 --- a/frontend/ui/widget/imagewidget.lua +++ b/frontend/ui/widget/imagewidget.lua @@ -39,20 +39,19 @@ end local DPI_SCALE = get_dpi_scale() local ImageCache = Cache:new{ - max_memsize = 8*1024*1024, -- 8M of image cache - current_memsize = 0, - cache = {}, - -- this will hold the LRU order of the cache - cache_order = {} + -- 8 MiB of image cache, with 128 slots + -- Overwhelmingly used for our icons, which are tiny in size, and not very numerous (< 100), + -- but also by ImageViewer (on files, which we never do), and ScreenSaver (again, on image files, but not covers), + -- hence the leeway. + size = 8 * 1024 * 1024, + avg_itemsize = 64 * 1024, } local ImageCacheItem = CacheItem:new{} function ImageCacheItem:onFree() - if self.bb.free then - logger.dbg("free image blitbuffer", self.bb) - self.bb:free() - end + logger.dbg("ImageCacheItem: free blitbuffer", self.bb) + self.bb:free() end local ImageWidget = Widget:new{ diff --git a/plugins/opds.koplugin/opdsbrowser.lua b/plugins/opds.koplugin/opdsbrowser.lua index fc15f384e..6abfe3a45 100644 --- a/plugins/opds.koplugin/opdsbrowser.lua +++ b/plugins/opds.koplugin/opdsbrowser.lua @@ -25,15 +25,14 @@ local _ = require("gettext") local T = require("ffi/util").template local CatalogCacheItem = CacheItem:new{ - size = 1024, -- fixed size for catalog item + size = 1024, -- fixed size for catalog items } -- cache catalog parsed from feed xml local CatalogCache = Cache:new{ - max_memsize = 20*1024, -- keep only 20 items - current_memsize = 0, - cache = {}, - cache_order = {}, + -- Make it 20 slots + size = 20 * CatalogCacheItem.size, + avg_itemsize = CatalogCacheItem.size, } local OPDSBrowser = Menu:extend{ diff --git a/spec/unit/cache_spec.lua b/spec/unit/cache_spec.lua index 3e8dc7d08..5d34d1612 100644 --- a/spec/unit/cache_spec.lua +++ b/spec/unit/cache_spec.lua @@ -1,11 +1,11 @@ describe("Cache module", function() - local DocumentRegistry, Cache + local DocumentRegistry, DocCache local doc local max_page = 1 setup(function() require("commonrequire") DocumentRegistry = require("document/documentregistry") - Cache = require("cache") + DocCache = require("document/doccache") local sample_pdf = "spec/front/unit/data/sample.pdf" doc = DocumentRegistry:openDocument(sample_pdf) @@ -15,22 +15,22 @@ describe("Cache module", function() end) it("should clear cache", function() - Cache:clear() + DocCache:clear() end) it("should serialize blitbuffer", function() for pageno = 1, math.min(max_page, doc.info.number_of_pages) do doc:renderPage(pageno, nil, 1, 0, 1.0, 0) - Cache:serialize() + DocCache:serialize() end - Cache:clear() + DocCache:clear() end) it("should deserialize blitbuffer", function() for pageno = 1, math.min(max_page, doc.info.number_of_pages) do doc:hintPage(pageno, 1, 0, 1.0, 0) end - Cache:clear() + DocCache:clear() end) it("should serialize koptcontext", function() @@ -38,9 +38,9 @@ describe("Cache module", function() for pageno = 1, math.min(max_page, doc.info.number_of_pages) do doc:renderPage(pageno, nil, 1, 0, 1.0, 0) doc:getPageDimensions(pageno) - Cache:serialize() + DocCache:serialize() end - Cache:clear() + DocCache:clear() doc.configurable.text_wrap = 0 end) @@ -48,6 +48,6 @@ describe("Cache module", function() for pageno = 1, math.min(max_page, doc.info.number_of_pages) do doc:renderPage(pageno, nil, 1, 0, 1.0, 0) end - Cache:clear() + DocCache:clear() end) end) diff --git a/spec/unit/koptinterface_spec.lua b/spec/unit/koptinterface_spec.lua index 43d3f83c7..fbd2f42f2 100644 --- a/spec/unit/koptinterface_spec.lua +++ b/spec/unit/koptinterface_spec.lua @@ -1,10 +1,10 @@ describe("Koptinterface module", function() - local DocumentRegistry, Koptinterface, Cache + local DocCache, DocumentRegistry, Koptinterface setup(function() require("commonrequire") + DocCache = require("document/doccache") DocumentRegistry = require("document/documentregistry") Koptinterface = require("document/koptinterface") - Cache = require("cache") end) local tall_pdf = "spec/front/unit/data/tall.pdf" @@ -19,7 +19,7 @@ describe("Koptinterface module", function() doc.configurable.text_wrap = 0 complex_doc.configurable.text_wrap = 0 paper_doc.configurable.text_wrap = 0 - Cache:clear() + DocCache:clear() end) after_each(function()