neovim/test/functional/legacy/memory_usage_spec.lua
2024-04-10 15:53:50 +01:00

219 lines
6.3 KiB
Lua

local t = require('test.functional.testutil')()
local clear = t.clear
local eval = t.eval
local eq = t.eq
local feed_command = t.feed_command
local retry = t.retry
local ok = t.ok
local source = t.source
local poke_eventloop = t.poke_eventloop
local load_adjust = t.load_adjust
local write_file = t.write_file
local is_os = t.is_os
local is_ci = t.is_ci
local is_asan = t.is_asan
clear()
if is_asan() then
pending('ASAN build is difficult to estimate memory usage', function() end)
return
elseif is_os('win') then
if is_ci('github') then
pending(
'Windows runners in Github Actions do not have a stable environment to estimate memory usage',
function() end
)
return
elseif eval("executable('wmic')") == 0 then
pending('missing "wmic" command', function() end)
return
end
elseif eval("executable('ps')") == 0 then
pending('missing "ps" command', function() end)
return
end
local monitor_memory_usage = {
memory_usage = function(self)
local handle
if is_os('win') then
handle = io.popen('wmic process where processid=' .. self.pid .. ' get WorkingSetSize')
else
handle = io.popen('ps -o rss= -p ' .. self.pid)
end
return tonumber(handle:read('*a'):match('%d+'))
end,
op = function(self)
retry(nil, 10000, function()
local val = self.memory_usage(self)
if self.max < val then
self.max = val
end
table.insert(self.hist, val)
ok(#self.hist > 20)
local result = {}
for key, value in ipairs(self.hist) do
if value ~= self.hist[key + 1] then
table.insert(result, value)
end
end
table.remove(self.hist, 1)
self.last = self.hist[#self.hist]
eq(1, #result)
end)
end,
dump = function(self)
return 'max: ' .. self.max .. ', last: ' .. self.last
end,
monitor_memory_usage = function(self, pid)
local obj = {
pid = pid,
max = 0,
last = 0,
hist = {},
}
setmetatable(obj, { __index = self })
obj:op()
return obj
end,
}
setmetatable(monitor_memory_usage, {
__call = function(self, pid)
return monitor_memory_usage.monitor_memory_usage(self, pid)
end,
})
describe('memory usage', function()
local tmpfile = 'X_memory_usage'
after_each(function()
os.remove(tmpfile)
end)
local function check_result(tbl, status, result)
if not status then
print('')
for key, val in pairs(tbl) do
print(key, val:dump())
end
error(result)
end
end
before_each(clear)
--[[
Case: if a local variable captures a:000, funccall object will be free
just after it finishes.
]]
--
it('function capture vargs', function()
local pid = eval('getpid()')
local before = monitor_memory_usage(pid)
write_file(
tmpfile,
[[
func s:f(...)
let x = a:000
endfunc
for _ in range(10000)
call s:f(0)
endfor
]]
)
-- TODO: check_result fails if command() is used here. Why? #16064
feed_command('source ' .. tmpfile)
poke_eventloop()
local after = monitor_memory_usage(pid)
-- Estimate the limit of max usage as 2x initial usage.
-- The lower limit can fluctuate a bit, use 97%.
check_result({ before = before, after = after }, pcall(ok, before.last * 97 / 100 < after.max))
check_result({ before = before, after = after }, pcall(ok, before.last * 2 > after.max))
-- In this case, garbage collecting is not needed.
-- The value might fluctuate a bit, allow for 3% tolerance below and 5% above.
-- Based on various test runs.
local lower = after.last * 97 / 100
local upper = after.last * 105 / 100
check_result({ before = before, after = after }, pcall(ok, lower < after.max))
check_result({ before = before, after = after }, pcall(ok, after.max < upper))
end)
--[[
Case: if a local variable captures l: dict, funccall object will not be
free until garbage collector runs, but after that memory usage doesn't
increase so much even when rerun Xtest.vim since system memory caches.
]]
--
it('function capture lvars', function()
local pid = eval('getpid()')
local before = monitor_memory_usage(pid)
write_file(
tmpfile,
[[
if !exists('s:defined_func')
func s:f()
let x = l:
endfunc
endif
let s:defined_func = 1
for _ in range(10000)
call s:f()
endfor
]]
)
feed_command('source ' .. tmpfile)
poke_eventloop()
local after = monitor_memory_usage(pid)
for _ = 1, 3 do
-- TODO: check_result fails if command() is used here. Why? #16064
feed_command('source ' .. tmpfile)
poke_eventloop()
end
local last = monitor_memory_usage(pid)
-- The usage may be a bit less than the last value, use 80%.
-- Allow for 20% tolerance at the upper limit. That's very permissive, but
-- otherwise the test fails sometimes. On FreeBSD we need to be even much
-- more permissive.
local upper_multiplier = is_os('freebsd') and 19 or 12
local lower = before.last * 8 / 10
local upper = load_adjust((after.max + (after.last - before.last)) * upper_multiplier / 10)
check_result({ before = before, after = after, last = last }, pcall(ok, lower < last.last))
check_result({ before = before, after = after, last = last }, pcall(ok, last.last < upper))
end)
it('releases memory when closing windows when folds exist', function()
if is_os('mac') then
pending('macOS memory compression causes flakiness')
end
local pid = eval('getpid()')
source([[
new
" Insert lines
call nvim_buf_set_lines(0, 0, 0, v:false, repeat([''], 999))
" Create folds
normal! gg
for _ in range(500)
normal! zfjj
endfor
]])
poke_eventloop()
local before = monitor_memory_usage(pid)
source([[
" Split and close window multiple times
for _ in range(1000)
split
close
endfor
]])
poke_eventloop()
local after = monitor_memory_usage(pid)
source('bwipe!')
poke_eventloop()
-- Allow for an increase of 10% in memory usage, which accommodates minor fluctuation,
-- but is small enough that if memory were not released (prior to PR #14884), the test
-- would fail.
local upper = before.last * 1.10
check_result({ before = before, after = after }, pcall(ok, after.last <= upper))
end)
end)