Describe the bug
A timer with a lambda that references a local variable leaks memory.
To Reproduce
Detailed steps to reproduce the behavior:
vim --clean (or gvim --clean, etc.)example.vim" Show PID, for monitoring memory echom getpid() function! Function(timer) abort let l:cmd = 'let x = 2 + 2' call timer_start(1, {-> execute(l:cmd)}) endfunction let timer = timer_start(20, 'Function', {'repeat': -1})
example.vimsource example.vim
watch 'pmap <PID> | grep total'Memory continuously increases.
Expected behavior
Memory would remain constant.
Environment (please complete the following information):
$ vim --version
VIM - Vi IMproved 8.2 (2019 Dec 12, compiled Feb 15 2021 12:29:39)
Included patches: 1-2434
Modified by team...@tracker.debian.org
Compiled by team...@tracker.debian.org
Additional context
The memory leak does not occur when the executed command is not stored in a variable.
function! Function(timer) abort call timer_start(1, {-> execute('let x = 2 + 2')}) endfunction
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub, or unsubscribe.![]()
I encountered this issue using an earlier version of Vim.
After compiling the latest version from source, I still encounter the continuously increasing memory usage (monitored with watch 'pmap <PID> | grep total').
The Valgrind leak summaries differ:
==123893== LEAK SUMMARY:
==123893== definitely lost: 0 bytes in 0 blocks
==123893== indirectly lost: 0 bytes in 0 blocks
==123893== possibly lost: 342,051 bytes in 5,601 blocks
==123893== still reachable: 5,800,465 bytes in 23,704 blocks
==123893== suppressed: 0 bytes in 0 blocks
==123454== LEAK SUMMARY:
==123454== definitely lost: 520 bytes in 1 blocks
==123454== indirectly lost: 132 bytes in 6 blocks
==123454== possibly lost: 0 bytes in 0 blocks
==123454== still reachable: 77,961 bytes in 873 blocks
==123454== suppressed: 0 bytes in 0 blocks
definitely lost: 520 bytes in 1 blocks
There are some bytes lost, but that's clearly not your issue. This really does not look like a memory leak.
Can you share the complete valgrind log?
"Can you share the complete valgrind log?"
👍 Sure. Attached are both logs, for pre-installed Vim and Vim compiled from source. For the latter, I uncommented LEAK_CFLAGS = -DEXITFREE in src/Makefile, based on the instructions in :help valgrind. This was presumbably not done for the pre-installed Vim, based on there being no -DEXITFREE shown in :version output.
valgrind-compiled-vim.txt
valgrind-pre-installed-vim.txt
"Possibly it's not a real leak but inefficient use of memory."
"This really does not look like a memory leak."
I've changed the title and description accordingly.
Okay, I did not see any leaks that come from Vim directly. I skimmed through it and it looks all the leaks where caused by some libraries (libXt, libX11 etc). I do wonder, if you stop the timer, does perhaps garbage collector skip in and releases some memory later?
"I do wonder, if you stop the timer, does perhaps garbage collector skip in and releases some memory later?"
When I run :call timer_stop(timer) to stop the outer timer, the memory that grew from earlier is not released.
I also tried retaining the inner timers (those created in Function that run once) in an array and later calling timer_stop on all the IDs in the array. That also had no effect on the memory that grew from earlier.
You can see a big difference in "still reachable" between the run with and without EXITFREE. That means it's not a real leak, but Vim does keep a lot of things in memory. It might indeed be that that the garbage collector is not triggered, or that there is a dependency cycle. This may relate to register_closure() and current_funccal->fc_funcs
For the title and description, I mentioned timers, as that was the context under which I noticed the memory increase, and I thought there was a relationship.
However, the following steps also result in unreleased memory, without the use of timers.
To Reproduce
vim --clean (or gvim --clean, etc.)example.vim" Show PID, for monitoring memory echom getpid()
function! Function() abort let l:string = 'hello world!' return {-> l:string} endfunction
example.vim:source example.vim
$ watch 'pmap <PID> | grep total':for _ in range(1, 500000) | call Function() | endfor
Vim's memory usage increases to over 1GB.
What happens if you delete the function: :delfunc Function
"What happens if you delete the function:
:delfunc Function"
Memory usage remains the same (slightly above 1GB in the example above) after running :delfunc Function. I also tried :call garbagecollect(), which also did not effect the memory usage.
I've simplified the example further below. It's not necessary to use a string local variable, nor return the lambda from Function.
To Reproduce
vim --clean (or gvim --clean, etc.)example.vim" Show PID, for monitoring memory echom getpid()
function! Function() abort let x = 0 let Y = {-> x} endfunction
—
"Perhaps just mention to avoid circular dependencies and if in doubt simply use unlet! to free local variables that are no longer needed."
When using unlet!, the memory is released for the simplified example from above (without the return).
function! Function() abort let x = 0 let Y = {-> x
} unlet! Y endfunction
I also tried it with an adapted version of the function shown in :help closure (adapted to create and call Bar many times), which returns the closure from the function, which results in the memory not being released.
function Foo(arg) let i = 3 return {x -> x + i - a:arg} endfunction for _ in range(100000) let Bar = Foo(4) call Bar(6) unlet! Bar endfor
The memory only increases the first time running. That is, if I re-run the for loop, I don't see a subsequent increase in memory.
—
Had another look at this, and I see two separate problems.
First is for the original case: the timer calls the function over and over again. In this case memory will be freed as soon as you type something, so that the main loop is encountered and the garbage collector is invoked. You can see this by sourcing the script, wait until the number starts going up, then type some command repeatedly, e.g. "jk". You will see the number stops rising and may also go down, until you stop typing for a while, then it goes up again.
I would consider this normal behavior. We have had problems before calling the garbage collector when not in the main loop, since it's very difficult to track everything that might be in use somewhere halfway a command.
The second is the case where we have the circular reference: A local variable holds a reference to a closure, and the closure uses a variable in the function context. It's very difficult to detect that this cycle has no outside references. It might be possible, but quite hard to implement and test. I'll add a note in the help to avoid this. It normally would not happen anyway, the example is not useful code.
Closed #8439.
Thanks!