memory_usage_spec.lua 6.1 KB

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