mirror of
https://github.com/koreader/koreader
synced 2024-10-31 21:20:20 +00:00
6b45a4ea06
the cache would behave badly when the same item was insert()ed twice: it would add the size twice to memory consumption, but would never substract it twice when purging the (actually single) object from cache. So the cache would seem to fill up while in fact it wasn't.
188 lines
5.5 KiB
Lua
188 lines
5.5 KiB
Lua
--[[
|
|
A global LRU cache
|
|
]]--
|
|
local md5 = require("MD5")
|
|
local lfs = require("libs/libkoreader-lfs")
|
|
local DEBUG = require("dbg")
|
|
|
|
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 = lfs.currentdir().."/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
|
|
DEBUG("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(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
|
|
DEBUG("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)
|
|
-- 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
|
|
DEBUG("dump cache item", key)
|
|
cache_size = cache_item:dump(cache_path..md5(key)) 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
|