2012-05-18 22:35:09 +00:00
|
|
|
--[[
|
|
|
|
A global LRU cache
|
|
|
|
]]--
|
2017-04-29 14:30:16 +00:00
|
|
|
|
2015-10-03 06:18:47 +00:00
|
|
|
local DataStorage = require("datastorage")
|
2017-04-29 14:30:16 +00:00
|
|
|
local lfs = require("libs/libkoreader-lfs")
|
2016-12-29 08:10:38 +00:00
|
|
|
local logger = require("logger")
|
2020-07-21 21:25:46 +00:00
|
|
|
local md5 = require("ffi/sha2").md5
|
2014-04-30 15:24:44 +00:00
|
|
|
|
2019-02-18 16:01:00 +00:00
|
|
|
local CanvasContext = require("document/canvascontext")
|
|
|
|
if CanvasContext.should_restrict_JIT then
|
2020-12-26 19:23:51 +00:00
|
|
|
jit.off(true, true)
|
2016-04-07 15:28:52 +00:00
|
|
|
end
|
|
|
|
|
2021-05-03 03:20:14 +00:00
|
|
|
-- For documentation purposes, here's a battle-tested shell version of calcFreeMem
|
|
|
|
--[[
|
|
|
|
if grep -q 'MemAvailable' /proc/meminfo ; then
|
|
|
|
# We'll settle for 85% of available memory to leave a bit of breathing room
|
|
|
|
tmpfs_size="$(awk '/MemAvailable/ {printf "%d", $2 * 0.85}' /proc/meminfo)"
|
|
|
|
elif grep -q 'Inactive(file)' /proc/meminfo ; then
|
|
|
|
# Basically try to emulate the kernel's computation, c.f., https://unix.stackexchange.com/q/261247
|
|
|
|
# Again, 85% of available memory
|
|
|
|
tmpfs_size="$(awk -v low=$(grep low /proc/zoneinfo | awk '{k+=$2}END{printf "%d", k}') \
|
|
|
|
'{a[$1]=$2}
|
|
|
|
END{
|
|
|
|
printf "%d", (a["MemFree:"]+a["Active(file):"]+a["Inactive(file):"]+a["SReclaimable:"]-(12*low))*0.85;
|
|
|
|
}' /proc/meminfo)"
|
|
|
|
else
|
|
|
|
# Ye olde crap workaround of Free + Buffers + Cache...
|
|
|
|
# Take it with a grain of salt, and settle for 80% of that...
|
|
|
|
tmpfs_size="$(awk \
|
|
|
|
'{a[$1]=$2}
|
|
|
|
END{
|
|
|
|
printf "%d", (a["MemFree:"]+a["Buffers:"]+a["Cached:"])*0.80;
|
|
|
|
}' /proc/meminfo)"
|
|
|
|
fi
|
|
|
|
--]]
|
|
|
|
|
|
|
|
-- And here's our simplified Lua version...
|
2014-01-19 03:40:34 +00:00
|
|
|
local function calcFreeMem()
|
2021-05-03 03:20:14 +00:00
|
|
|
local memtotal, memfree, memavailable, buffers, cached
|
|
|
|
|
2014-03-13 13:52:43 +00:00
|
|
|
local meminfo = io.open("/proc/meminfo", "r")
|
|
|
|
if meminfo then
|
|
|
|
for line in meminfo:lines() do
|
2021-05-03 03:20:14 +00:00
|
|
|
if not memtotal then
|
|
|
|
memtotal = line:match("^MemTotal:%s-(%d+) kB")
|
|
|
|
if memtotal then
|
|
|
|
-- Next!
|
|
|
|
goto continue
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if not memfree then
|
|
|
|
memfree = line:match("^MemFree:%s-(%d+) kB")
|
|
|
|
if memfree then
|
|
|
|
-- Next!
|
|
|
|
goto continue
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if not memavailable then
|
|
|
|
memavailable = line:match("^MemAvailable:%s-(%d+) kB")
|
|
|
|
if memavailable then
|
|
|
|
-- Best case scenario, we're done :)
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if not buffers then
|
|
|
|
buffers = line:match("^Buffers:%s-(%d+) kB")
|
|
|
|
if buffers then
|
|
|
|
-- Next!
|
|
|
|
goto continue
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if not cached then
|
|
|
|
cached = line:match("^Cached:%s-(%d+) kB")
|
|
|
|
if cached then
|
|
|
|
-- Ought to be the last entry we care about, we're done
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
::continue::
|
2014-03-13 13:52:43 +00:00
|
|
|
end
|
|
|
|
meminfo:close()
|
2021-05-03 03:20:14 +00:00
|
|
|
else
|
|
|
|
-- Not on Linux?
|
|
|
|
return 0, 0
|
|
|
|
end
|
|
|
|
|
|
|
|
if memavailable then
|
|
|
|
-- Leave a bit of margin, and report 85% of that...
|
|
|
|
return math.floor(memavailable * 0.85) * 1024, memtotal * 1024
|
|
|
|
else
|
|
|
|
-- Crappy Free + Buffers + Cache version, because the zoneinfo approach is a tad hairy...
|
|
|
|
-- So, leave an even larger margin, and only report 75% of that...
|
|
|
|
return math.floor((memfree + buffers + cached) * 0.75) * 1024, memtotal * 1024
|
2014-03-13 13:52:43 +00:00
|
|
|
end
|
2014-01-19 03:40:34 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
local function calcCacheMemSize()
|
2014-03-13 13:52:43 +00:00
|
|
|
local min = DGLOBAL_CACHE_SIZE_MINIMUM
|
|
|
|
local max = DGLOBAL_CACHE_SIZE_MAXIMUM
|
2021-05-03 03:20:14 +00:00
|
|
|
local calc = calcFreeMem() * (DGLOBAL_CACHE_FREE_PROPORTION or 0)
|
2014-03-13 13:52:43 +00:00
|
|
|
return math.min(max, math.max(min, calc))
|
2014-01-19 03:40:34 +00:00
|
|
|
end
|
|
|
|
|
2015-10-03 06:18:47 +00:00
|
|
|
local cache_path = DataStorage:getDataDir() .. "/cache/"
|
2014-04-30 15:24:44 +00:00
|
|
|
|
|
|
|
--[[
|
|
|
|
-- return a snapshot of disk cached items for subsequent check
|
|
|
|
--]]
|
2014-11-11 14:33:41 +00:00
|
|
|
local function getDiskCache()
|
2014-04-30 15:24:44 +00:00
|
|
|
local cached = {}
|
|
|
|
for key_md5 in lfs.dir(cache_path) do
|
2021-05-03 03:20:14 +00:00
|
|
|
local file = cache_path .. key_md5
|
2014-04-30 15:24:44 +00:00
|
|
|
if lfs.attributes(file, "mode") == "file" then
|
|
|
|
cached[key_md5] = file
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return cached
|
|
|
|
end
|
|
|
|
|
2013-10-18 20:38:07 +00:00
|
|
|
local Cache = {
|
2014-03-13 13:52:43 +00:00
|
|
|
-- cache configuration:
|
|
|
|
max_memsize = calcCacheMemSize(),
|
|
|
|
-- cache state:
|
|
|
|
current_memsize = 0,
|
|
|
|
-- associative cache
|
|
|
|
cache = {},
|
|
|
|
-- this will hold the LRU order of the cache
|
2014-04-30 15:24:44 +00:00
|
|
|
cache_order = {},
|
|
|
|
-- disk Cache snapshot
|
|
|
|
cached = getDiskCache(),
|
2012-05-18 22:35:09 +00:00
|
|
|
}
|
|
|
|
|
2013-03-24 10:37:33 +00:00
|
|
|
function Cache:new(o)
|
2014-03-13 13:52:43 +00:00
|
|
|
o = o or {}
|
|
|
|
setmetatable(o, self)
|
|
|
|
self.__index = self
|
|
|
|
return o
|
2013-03-24 10:37:33 +00:00
|
|
|
end
|
|
|
|
|
2014-11-12 14:15:55 +00:00
|
|
|
-- 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)
|
2021-05-03 03:20:14 +00:00
|
|
|
break
|
2014-11-12 14:15:55 +00:00
|
|
|
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)
|
2021-05-03 03:20:14 +00:00
|
|
|
if not self.cache[key] then return end
|
|
|
|
|
2014-11-12 14:15:55 +00:00
|
|
|
self:_unref(key)
|
|
|
|
self:_free(key)
|
|
|
|
end
|
|
|
|
|
2012-05-18 22:35:09 +00:00
|
|
|
function Cache:insert(key, object)
|
2014-11-12 14:15:55 +00:00
|
|
|
-- make sure that one key only exists once: delete existing
|
|
|
|
self:drop(key)
|
2021-05-03 03:20:14 +00:00
|
|
|
-- If this object is single-handledly too large for the cache, we're done
|
|
|
|
if object.size > self.max_memsize then
|
|
|
|
logger.warn("Too much memory would be claimed by caching", key)
|
2014-10-23 05:48:45 +00:00
|
|
|
return
|
2014-03-13 13:52:43 +00:00
|
|
|
end
|
2021-05-03 03:20:14 +00:00
|
|
|
-- If inserting this obect would blow the cache's watermark,
|
|
|
|
-- start dropping least recently used items first.
|
2014-03-13 13:52:43 +00:00
|
|
|
-- (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)
|
2021-05-03 03:20:14 +00:00
|
|
|
if removed_key then
|
|
|
|
self:_free(removed_key)
|
|
|
|
else
|
|
|
|
logger.warn("Cache accounting is broken")
|
|
|
|
break
|
|
|
|
end
|
2014-03-13 13:52:43 +00:00
|
|
|
end
|
2021-05-03 03:20:14 +00:00
|
|
|
-- Insert new object in front of the LRU order
|
2014-03-13 13:52:43 +00:00
|
|
|
table.insert(self.cache_order, 1, key)
|
|
|
|
self.cache[key] = object
|
|
|
|
self.current_memsize = self.current_memsize + object.size
|
2012-05-18 22:35:09 +00:00
|
|
|
end
|
|
|
|
|
2014-04-30 15:24:44 +00:00
|
|
|
--[[
|
2021-05-03 03:20:14 +00:00
|
|
|
-- check for cache item by key
|
2014-04-30 15:24:44 +00:00
|
|
|
-- if ItemClass is given, disk cache is also checked.
|
|
|
|
--]]
|
|
|
|
function Cache:check(key, ItemClass)
|
2014-03-13 13:52:43 +00:00
|
|
|
if self.cache[key] then
|
|
|
|
if self.cache_order[1] ~= key then
|
2021-05-03 03:20:14 +00:00
|
|
|
-- Move key in front of the LRU list (i.e., MRU)
|
2014-11-12 14:15:55 +00:00
|
|
|
self:_unref(key)
|
2014-03-13 13:52:43 +00:00
|
|
|
table.insert(self.cache_order, 1, key)
|
|
|
|
end
|
|
|
|
return self.cache[key]
|
2014-04-30 15:24:44 +00:00
|
|
|
elseif ItemClass then
|
2020-07-21 21:25:46 +00:00
|
|
|
local cached = self.cached[md5(key)]
|
2014-04-30 15:24:44 +00:00
|
|
|
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
|
2021-05-03 03:20:14 +00:00
|
|
|
logger.warn("Failed to load on-disk cache:", msg)
|
|
|
|
--- It's apparently unusable, purge it and refresh the snapshot.
|
|
|
|
os.remove(cached)
|
|
|
|
self:refreshSnapshot()
|
2014-04-30 15:24:44 +00:00
|
|
|
end
|
|
|
|
end
|
2014-03-13 13:52:43 +00:00
|
|
|
end
|
2012-05-18 22:35:09 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function Cache:willAccept(size)
|
2021-05-03 03:20:14 +00:00
|
|
|
-- We only allow single objects to fill 75% of the cache
|
|
|
|
return size*4 < self.max_memsize*3
|
2012-05-18 22:35:09 +00:00
|
|
|
end
|
|
|
|
|
2014-04-30 15:24:44 +00:00
|
|
|
function Cache:serialize()
|
2021-05-03 03:20:14 +00:00
|
|
|
-- Calculate the current disk cache size
|
2014-04-30 15:24:44 +00:00
|
|
|
local cached_size = 0
|
|
|
|
local sorted_caches = {}
|
2021-04-13 15:54:11 +00:00
|
|
|
for _, file in pairs(self.cached) do
|
2014-04-30 15:24:44 +00:00
|
|
|
table.insert(sorted_caches, {file=file, time=lfs.attributes(file, "access")})
|
|
|
|
cached_size = cached_size + (lfs.attributes(file, "size") or 0)
|
|
|
|
end
|
2021-05-03 03:20:14 +00:00
|
|
|
table.sort(sorted_caches, function(v1, v2) return v1.time > v2.time end)
|
|
|
|
|
|
|
|
-- Only serialize the second most recently used cache item (as the MRU would be the *hinted* page).
|
|
|
|
local mru_key
|
|
|
|
local mru_found = 0
|
2014-04-30 15:24:44 +00:00
|
|
|
for _, key in ipairs(self.cache_order) do
|
2014-10-30 03:16:37 +00:00
|
|
|
local cache_item = self.cache[key]
|
2018-04-08 14:02:48 +00:00
|
|
|
|
2021-05-03 03:20:14 +00:00
|
|
|
-- Only dump cache items that actually request persistence
|
2014-10-30 03:16:37 +00:00
|
|
|
if cache_item.persistent and cache_item.dump then
|
2021-05-03 03:20:14 +00:00
|
|
|
mru_key = key
|
|
|
|
mru_found = mru_found + 1
|
|
|
|
if mru_found >= 2 then
|
|
|
|
-- We found the second MRU item, i.e., the *displayed* page
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if mru_key then
|
|
|
|
local cache_full_path = cache_path .. md5(mru_key)
|
|
|
|
local cache_file_exists = lfs.attributes(cache_full_path)
|
2018-04-08 14:02:48 +00:00
|
|
|
|
2021-05-03 03:20:14 +00:00
|
|
|
if not cache_file_exists then
|
|
|
|
logger.dbg("Dumping cache item", mru_key)
|
|
|
|
local cache_item = self.cache[mru_key]
|
|
|
|
local cache_size = cache_item:dump(cache_full_path)
|
|
|
|
if cache_size then
|
|
|
|
cached_size = cached_size + cache_size
|
|
|
|
end
|
2014-04-30 15:24:44 +00:00
|
|
|
end
|
|
|
|
end
|
2021-05-03 03:20:14 +00:00
|
|
|
|
|
|
|
-- Allocate the same amount of storage to the disk cache than the memory cache
|
|
|
|
while cached_size > self.max_memsize do
|
2014-04-30 15:24:44 +00:00
|
|
|
-- discard the least recently used cache
|
|
|
|
local discarded = table.remove(sorted_caches)
|
2021-05-03 03:20:14 +00:00
|
|
|
if discarded then
|
|
|
|
cached_size = cached_size - lfs.attributes(discarded.file, "size")
|
|
|
|
os.remove(discarded.file)
|
|
|
|
else
|
|
|
|
logger.warn("Cache accounting is broken")
|
|
|
|
break
|
|
|
|
end
|
2014-04-30 15:24:44 +00:00
|
|
|
end
|
2021-05-03 03:20:14 +00:00
|
|
|
-- We may have updated the disk cache's content, so refresh its state
|
|
|
|
self:refreshSnapshot()
|
2014-04-30 15:24:44 +00:00
|
|
|
end
|
|
|
|
|
2021-05-03 03:20:14 +00:00
|
|
|
-- Blank the cache
|
2012-05-18 22:35:09 +00:00
|
|
|
function Cache:clear()
|
2014-03-13 13:52:43 +00:00
|
|
|
for k, _ in pairs(self.cache) do
|
|
|
|
self.cache[k]:onFree()
|
|
|
|
end
|
|
|
|
self.cache = {}
|
|
|
|
self.cache_order = {}
|
|
|
|
self.current_memsize = 0
|
2012-05-18 22:35:09 +00:00
|
|
|
end
|
2013-10-18 20:38:07 +00:00
|
|
|
|
2021-05-03 03:20:14 +00:00
|
|
|
-- Terribly crappy workaround: evict half the cache if we appear to be redlining on free RAM...
|
|
|
|
function Cache:memoryPressureCheck()
|
|
|
|
local memfree, memtotal = calcFreeMem()
|
|
|
|
|
|
|
|
-- Nonsensical values? (!Linux), skip this.
|
|
|
|
if memtotal == 0 then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
-- 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
|
|
|
|
|
|
|
|
-- And finish by forcing a GC sweep now...
|
|
|
|
collectgarbage()
|
|
|
|
collectgarbage()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-04-13 15:54:11 +00:00
|
|
|
-- Refresh the disk snapshot (mainly used by ui/data/onetime_migration)
|
|
|
|
function Cache:refreshSnapshot()
|
|
|
|
self.cached = getDiskCache()
|
|
|
|
end
|
|
|
|
|
2021-05-03 03:20:14 +00:00
|
|
|
-- Evict the disk cache (ditto)
|
|
|
|
function Cache:clearDiskCache()
|
|
|
|
for _, file in pairs(self.cached) do
|
|
|
|
os.remove(file)
|
|
|
|
end
|
|
|
|
|
|
|
|
self:refreshSnapshot()
|
|
|
|
end
|
|
|
|
|
2013-10-18 20:38:07 +00:00
|
|
|
return Cache
|