You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
koreader/frontend/cache.lua

195 lines
5.6 KiB
Lua

--[[
A LRU cache, based on https://github.com/starius/lua-lru
]]--
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 75% of the cache
return size*4 < self.size*3
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