memory_usage_spec.lua (6490B)
1 local t = require('test.testutil') 2 local n = require('test.functional.testnvim')() 3 4 local clear = n.clear 5 local eval = n.eval 6 local eq = t.eq 7 local feed_command = n.feed_command 8 local retry = t.retry 9 local ok = t.ok 10 local source = n.source 11 local poke_eventloop = n.poke_eventloop 12 local load_adjust = n.load_adjust 13 local write_file = t.write_file 14 local is_os = t.is_os 15 local is_ci = t.is_ci 16 17 clear() 18 if t.is_asan() then 19 pending('ASAN build is difficult to estimate memory usage', function() end) 20 return 21 elseif is_os('win') then 22 if is_ci('github') then 23 pending( 24 'Windows runners in Github Actions do not have a stable environment to estimate memory usage', 25 function() end 26 ) 27 return 28 elseif eval("executable('wmic')") == 0 then 29 pending('missing "wmic" command', function() end) 30 return 31 end 32 elseif eval("executable('ps')") == 0 then 33 pending('missing "ps" command', function() end) 34 return 35 end 36 37 local monitor_memory_usage = { 38 memory_usage = function(self) 39 local handle 40 if is_os('win') then 41 handle = io.popen('wmic process where processid=' .. self.pid .. ' get WorkingSetSize') 42 else 43 handle = io.popen('ps -o rss= -p ' .. self.pid) 44 end 45 return tonumber(handle:read('*a'):match('%d+')) 46 end, 47 op = function(self) 48 retry(nil, 10000, function() 49 local val = self.memory_usage(self) 50 if self.max < val then 51 self.max = val 52 end 53 table.insert(self.hist, val) 54 ok(#self.hist > 20) 55 local result = {} 56 for key, value in ipairs(self.hist) do 57 if value ~= self.hist[key + 1] then 58 table.insert(result, value) 59 end 60 end 61 table.remove(self.hist, 1) 62 self.last = self.hist[#self.hist] 63 eq(1, #result) 64 end) 65 end, 66 dump = function(self) 67 return 'max: ' .. self.max .. ', last: ' .. self.last 68 end, 69 monitor_memory_usage = function(self, pid) 70 local obj = { 71 pid = pid, 72 max = 0, 73 last = 0, 74 hist = {}, 75 } 76 setmetatable(obj, { __index = self }) 77 obj:op() 78 return obj 79 end, 80 } 81 setmetatable(monitor_memory_usage, { 82 __call = function(self, pid) 83 return monitor_memory_usage.monitor_memory_usage(self, pid) 84 end, 85 }) 86 87 describe('memory usage', function() 88 local tmpfile = 'X_memory_usage' 89 90 after_each(function() 91 os.remove(tmpfile) 92 end) 93 94 local function check_result(tbl, status, result) 95 if not status then 96 print('') 97 for key, val in pairs(tbl) do 98 print(key, val:dump()) 99 end 100 error(result) 101 end 102 end 103 104 before_each(clear) 105 106 --[[ 107 Case: if a local variable captures a:000, funccall object will be free 108 just after it finishes. 109 ]] 110 -- 111 it('function capture vargs', function() 112 local pid = eval('getpid()') 113 local before = monitor_memory_usage(pid) 114 write_file( 115 tmpfile, 116 [[ 117 func s:f(...) 118 let x = a:000 119 endfunc 120 for _ in range(10000) 121 call s:f(0) 122 endfor 123 ]] 124 ) 125 -- TODO: check_result fails if command() is used here. Why? #16064 126 feed_command('source ' .. tmpfile) 127 poke_eventloop() 128 local after = monitor_memory_usage(pid) 129 -- Estimate the limit of max usage as 2x initial usage. 130 -- The lower limit can fluctuate a bit, use 97%. 131 check_result({ before = before, after = after }, pcall(ok, before.last * 97 / 100 < after.max)) 132 check_result({ before = before, after = after }, pcall(ok, before.last * 2 > after.max)) 133 -- In this case, garbage collecting is not needed. 134 -- The value might fluctuate a bit, allow for 3% tolerance below and 5% above. 135 -- Based on various test runs. 136 local lower = after.last * 97 / 100 137 local upper = after.last * 105 / 100 138 check_result({ before = before, after = after }, pcall(ok, lower < after.max)) 139 check_result({ before = before, after = after }, pcall(ok, after.max < upper)) 140 end) 141 142 --[[ 143 Case: if a local variable captures l: dict, funccall object will not be 144 free until garbage collector runs, but after that memory usage doesn't 145 increase so much even when rerun Xtest.vim since system memory caches. 146 ]] 147 -- 148 it('function capture lvars', function() 149 local pid = eval('getpid()') 150 local before = monitor_memory_usage(pid) 151 write_file( 152 tmpfile, 153 [[ 154 if !exists('s:defined_func') 155 func s:f() 156 let x = l: 157 endfunc 158 endif 159 let s:defined_func = 1 160 for _ in range(10000) 161 call s:f() 162 endfor 163 ]] 164 ) 165 feed_command('source ' .. tmpfile) 166 poke_eventloop() 167 local after = monitor_memory_usage(pid) 168 for _ = 1, 3 do 169 -- TODO: check_result fails if command() is used here. Why? #16064 170 feed_command('source ' .. tmpfile) 171 poke_eventloop() 172 end 173 local last = monitor_memory_usage(pid) 174 -- The usage may be a bit less than the last value, use 80%. 175 -- Allow for 20% tolerance at the upper limit. That's very permissive, but 176 -- otherwise the test fails sometimes. On FreeBSD we need to be even much 177 -- more permissive. 178 local upper_multiplier = is_os('freebsd') and 19 or 12 179 local lower = before.last * 8 / 10 180 local upper = load_adjust((after.max + (after.last - before.last)) * upper_multiplier / 10) 181 check_result({ before = before, after = after, last = last }, pcall(ok, lower < last.last)) 182 check_result({ before = before, after = after, last = last }, pcall(ok, last.last < upper)) 183 end) 184 185 it('releases memory when closing windows when folds exist', function() 186 if is_os('mac') then 187 pending('macOS memory compression causes flakiness') 188 end 189 local pid = eval('getpid()') 190 source([[ 191 new 192 " Insert lines 193 call nvim_buf_set_lines(0, 0, 0, v:false, repeat([''], 999)) 194 " Create folds 195 normal! gg 196 for _ in range(500) 197 normal! zfjj 198 endfor 199 ]]) 200 poke_eventloop() 201 local before = monitor_memory_usage(pid) 202 source([[ 203 " Split and close window multiple times 204 for _ in range(1000) 205 split 206 close 207 endfor 208 ]]) 209 poke_eventloop() 210 local after = monitor_memory_usage(pid) 211 source('bwipe!') 212 poke_eventloop() 213 -- Allow for an increase of 10% in memory usage, which accommodates minor fluctuation, 214 -- but is small enough that if memory were not released (prior to PR #14884), the test 215 -- would fail. 216 local upper = before.last * 1.10 217 check_result({ before = before, after = after }, pcall(ok, after.last <= upper)) 218 end) 219 end)