2
0
mirror of https://github.com/koreader/koreader synced 2024-11-13 19:11:25 +00:00
koreader/frontend/cache.lua
Frans de Jonge 0e61eb7a7b
[fix] Cache: don't overwrite same CacheItem (#3847)
If the most recently used global cache item refers to the same
page, it would be overwritten. This will happen when closing a
paged (PDF/DjVu) document on the same page you opened it, as well
as when opening and closing EPUBs with crengine because it uses
its own cache.

Fixes #3846.
2018-04-08 16:02:48 +02:00

200 lines
5.8 KiB
Lua

--[[
A global LRU cache
]]--
local DataStorage = require("datastorage")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local md5 = require("ffi/MD5")
if require("device"):isAndroid() then
require("jit").off(true, true)
end
local function calcFreeMem()
local meminfo = io.open("/proc/meminfo", "r")
local freemem = 0
if meminfo then
for line in meminfo:lines() do
local free, buffer, cached, n
free, n = line:gsub("^MemFree:%s-(%d+) kB", "%1")
if n ~= 0 then freemem = freemem + tonumber(free)*1024 end
buffer, n = line:gsub("^Buffers:%s-(%d+) kB", "%1")
if n ~= 0 then freemem = freemem + tonumber(buffer)*1024 end
cached, n = line:gsub("^Cached:%s-(%d+) kB", "%1")
if n ~= 0 then freemem = freemem + tonumber(cached)*1024 end
end
meminfo:close()
end
return freemem
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)
end
end
end
-- internal: free cache item
function Cache:_free(key)
if not self.cache[key] then return end
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)
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)
-- guarantee that we have enough memory in cache
if (object.size > self.max_memsize) then
logger.warn("too much memory claimed for", key)
return
end
-- delete objects that least recently used
-- (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)
self:_free(removed_key)
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
end
--[[
-- check for cache item for key
-- 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
-- put key in front of the LRU list
self:_unref(key)
table.insert(self.cache_order, 1, key)
end
return self.cache[key]
elseif ItemClass then
local cached = self.cached[md5.sum(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("discard cache", msg)
end
end
end
end
function Cache:willAccept(size)
-- we only allow single objects to fill 75% of the cache
if size*4 < self.max_memsize*3 then
return true
end
end
function Cache:serialize()
-- calculate 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)
-- only serialize the most recently used cache
local cache_size = 0
for _, key in ipairs(self.cache_order) do
local cache_item = self.cache[key]
-- only dump cache item that requests serialization explicitly
if cache_item.persistent and cache_item.dump then
local cache_full_path = cache_path..md5.sum(key)
local cache_file_exists = lfs.attributes(cache_full_path)
if cache_file_exists then break end
logger.dbg("dump cache item", key)
cache_size = cache_item:dump(cache_full_path) or 0
if cache_size > 0 then break end
end
end
-- set disk cache the same limit as memory cache
while cached_size + cache_size - self.max_memsize > 0 do
-- discard the least recently used cache
local discarded = table.remove(sorted_caches)
cached_size = cached_size - lfs.attributes(discarded.file, "size")
os.remove(discarded.file)
end
-- disk cache may have changes so need to refresh disk cache snapshot
self.cached = getDiskCache()
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
end
return Cache