memory_usage_spec.lua 6.4 KB

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