mirror of
https://github.com/koreader/koreader.git
synced 2025-12-13 20:36:53 +01:00
Persist is intended for larger objects, which works best for things like cached tiles with some metadata. For many small to medium entries that you nevertheless don't necessarily want to keep around in memory, that seems a bit inefficient. SQLite then, is effectively a way of minimizing the amount of writes in this scenario. Relates to #13061. Depends on #13122.
197 lines
5.6 KiB
Lua
197 lines
5.6 KiB
Lua
--[[--
|
|
An LRU cache, based on https://github.com/starius/lua-lru
|
|
|
|
@module cache
|
|
--]]
|
|
|
|
local lfs = require("libs/libkoreader-lfs")
|
|
local logger = require("logger")
|
|
local lru = require("ffi/lru")
|
|
local md5 = require("ffi/sha2").md5
|
|
local util = require("util")
|
|
|
|
local Cache = {
|
|
-- Cache configuration:
|
|
-- 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,
|
|
-- 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()
|
|
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
|
|
self.slots = math.ceil(self.size / self.avg_itemsize)
|
|
self.cache = lru.new(self.slots, self.size, self.enable_eviction_cb)
|
|
end
|
|
|
|
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
|
|
|
|
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
|
|
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
|
|
|
|
function Cache:insert(key, object)
|
|
-- 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
|
|
|
|
self.cache:set(key, object, object.size)
|
|
|
|
-- Accounting debugging
|
|
--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)
|
|
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 or 0) / 1024 / 1024, self.cache:used_size() / self.cache:used_slots(),
|
|
size or 0, key))
|
|
end
|
|
--]]
|
|
|
|
--[[
|
|
-- check for cache item by key
|
|
-- if ItemClass is given, disk cache is also checked.
|
|
--]]
|
|
function Cache:check(key, ItemClass)
|
|
local value = self.cache:get(key)
|
|
if value then
|
|
return value
|
|
elseif ItemClass then
|
|
local cached = self.cached[md5(key)]
|
|
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
|
|
logger.warn("Failed to load on-disk cache:", msg)
|
|
--- It's apparently unusable, purge it and refresh the snapshot.
|
|
os.remove(cached)
|
|
self:refreshSnapshot()
|
|
end
|
|
end
|
|
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 a single object to fill 50% of the cache
|
|
return size*4 < self.size*2
|
|
end
|
|
|
|
-- Blank the cache
|
|
function Cache:clear()
|
|
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 = util.calcFreeMem()
|
|
|
|
-- Nonsensical values? (!Linux), skip this.
|
|
if memtotal == nil then
|
|
return
|
|
end
|
|
|
|
-- If less that 20% of the total RAM is free, drop half the Cache...
|
|
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...",
|
|
free_fraction * 100,
|
|
memfree / (1024 * 1024),
|
|
memtotal / (1024 * 1024)))
|
|
self.cache:chop()
|
|
|
|
-- And finish by forcing a GC sweep now...
|
|
collectgarbage()
|
|
collectgarbage()
|
|
end
|
|
end
|
|
|
|
-- Refresh the disk snapshot (mainly used by ui/data/onetime_migration)
|
|
function Cache:refreshSnapshot()
|
|
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
|
|
|
|
self:refreshSnapshot()
|
|
end
|
|
|
|
return Cache
|