`awful.spawn.easy_async` can consume memory faster than the garbage collector free it
Output of awesome --version:
awesome v4.3-1358-g392dbc21-dirty (Too long)
• Compiled against Lua 5.3.6 (running with Lua 5.3)
• API level: 4
• D-Bus support: yes
• xcb-errors support: no
• execinfo support: yes
• xcb-randr version: 1.6
• LGI version: 0.9.2
• Transparency enabled: yes
• Custom search paths: no
How to reproduce the issue:
I use volume-control and since the last commit Execute commands asynchronously I see a steady increase of memory usage.
diff --git i/awesomerc.lua w/awesomerc.lua
index bdbdbaa7..de74ef9c 100644
--- i/awesomerc.lua
+++ w/awesomerc.lua
@@ -128,6 +128,8 @@ mykeyboardlayout = awful.widget.keyboardlayout()
-- Create a textclock widget
mytextclock = wibox.widget.textclock()
+local volctl = require("volume-control")({})
+
-- @DOC_FOR_EACH_SCREEN@
screen.connect_signal("request::desktop_decoration", function(s)
-- Each screen has its own tag table.
@@ -204,6 +206,7 @@ screen.connect_signal("request::desktop_decoration", function(s)
layout = wibox.layout.fixed.horizontal,
mykeyboardlayout,
wibox.widget.systray(),
+ volctl.widget,
mytextclock,
s.mylayoutbox,
},
Actual result:
Opening top (or atop) to watch the process memory usage I see a steady increase of RSS by 150K every 3 seconds. After some hours, awesome takes 1GB of memory where I have to restart it to get the memory back.
I've played a bit around with the code and removing the call of awful.spawn.easy_async fixes the problem.
diff --git i/volume-control.lua w/volume-control.lua
index 50f20b9..1ffce49 100644
--- i/volume-control.lua
+++ w/volume-control.lua
@@ -10,7 +10,7 @@ local spawn = awful.spawn or awful.util.spawn
local watch = awful.spawn and awful.spawn.with_line_callback
local function exec(command, callback)
- awful.spawn.easy_async(command, callback or function() end)
+ -- awful.spawn.easy_async(command, callback or function() end)
end
But calling easy_async with an empty callback shows the leakage.
diff --git i/volume-control.lua w/volume-control.lua
index 50f20b9..c19bb95 100644
--- i/volume-control.lua
+++ w/volume-control.lua
@@ -10,7 +10,7 @@ local spawn = awful.spawn or awful.util.spawn
local watch = awful.spawn and awful.spawn.with_line_callback
local function exec(command, callback)
- awful.spawn.easy_async(command, callback or function() end)
+ awful.spawn.easy_async(command, function() end)
end
Hence, I suspect there's a leakage in easy_async. Maybe the Gio.UnixInputStream.new(stdout, true) in with_line_callback have to be freed, but I'm not really familiar with Lua.
Expected result:
Awesome should not leak memory.
i'm using bunch of widgets with easy_async for years without reproducing such issue - i suspect in this volumecontrol widget in one of callbacks there is a closure which prevents it from garbage-collecting
UPD: ah, i see you already tried to remove the callback, this is quite weird then: are you able to reproduce the memory leakage by just calling easy_async many times and garbage.collect() in clean module, without using this widget at all?
Actionless Loveless schrieb am Wed 09. Mar, 10:50 (-0800):
i'm using bunch of widgets with
easy_asyncfor years without reproducing such issue - i suspect in thisvolumecontrolwidget in one of callbacks there is a closure which prevents it from garbage-collecting
I found a minimal snippet that shows the effect. When I add this to my rc.lua the RAM usage grows bigger and bigger
local timer = gears.timer({ timeout = 0.5 })
timer:connect_signal(
"timeout",
function()
awful.spawn.easy_async({'true'}, function(output) end)
end)
timer:start()
-- Reply to this email directly or view it on GitHub: https://github.com/awesomeWM/awesome/issues/3584#issuecomment-1063248304 You are receiving this because you authored the thread.
Message ID: @.***>
and if you'll add collectgarbage("collect") ?
Actionless Loveless schrieb am Wed 16. Mar, 05:14 (-0700):
and if you'll add
collectgarbage("collect")?
I ran awesome-client 'collectgarbage("collect")' from the command line a
few times, but it doesn't change anything. The RAM usage is growing.
Can you reproduce it?
what exactly should i reproduce, ie for how long i should run it and which % grow in memory usage should i see after?
what i see on my machine - the memory is growing for the first 5 minutes from 1.1% to 1.3% of RAM and next staying the same 1.3%
what i see for the last minutes - it going from 116000 to 118000 RSS and back after garbage collector hits it and then it repeats
but not goes neither higher nor lower than those numbers
after longer time i indeed can see some small memory grow, but not sure if it's related, as it's nowhere close to 150 kb per 3 seconds
I'm using LGI 0.9.2, too. What are the versions of the other libs you use? Can you give me the output of awesome --help?
this doesn't matter much as i also can reproduce it, just with 100 or 1000 times slower leaking (i'm not sure it have anything to do with spawn specifically):
awesome v4.3-1358-g392dbc21a (Too long)
• Compiled against Lua 5.1.4 (running with LuaJIT 2.1.0-beta3)
• API level: 4
• D-Bus support: yes
• xcb-errors support: yes
• execinfo support: yes
• xcb-randr version: 1.6
• LGI version: 0.9.2
• Transparency enabled: yes
• Custom search paths: no
There is no memory leak. Everything is properly tracked by the garbage collector. You're simply "outrunning" it, by allocating memory faster than the GC can handle at your settings. Memory usage is constantly growing because the GC is never able to catch up.
To confirm:
This reports a steady growth in allocation. Sometimes, there is a dip (the GC's auto run), but the overall trend is upwards.
local timer = gears.timer({ timeout = 0.005 })
timer:connect_signal(
"timeout",
function()
awful.spawn.easy_async({'true'}, function(output)
i = i + 1
if i > 2000 then
i = 0
local k = collectgarbage("count")
print(k)
end
end)
end)
timer:start()
However, with a single line added, memory usage is constant. I could play around with the forced GC run only every n times to find the threshold, but that's besides the point.
local timer = gears.timer({ timeout = 0.005 })
timer:connect_signal(
"timeout",
function()
awful.spawn.easy_async({'true'}, function(output)
collectgarbage()
i = i + 1
if i > 2000 then
i = 0
local k = collectgarbage("count")
print(k)
end
end)
end)
timer:start()
You have several options:
- Decrease the timeout to a value where the garbage collector at its current (probably default) settings can keep up
- Adjust the settings to make the GC more aggressive, so that it can keep up with the allocations
- Use Gio/Glib sub-processess directly, as
awful.spawnprobably has a much higher memory footprint due to all the extra X11 stuff
Unfortunately, even a timeout of 5 seconds shows a steady increase with the code of volume-control.
I'll try, if the call of collectgarbage() does help.
Keep in mind that a full GC cycle (i.e. collectgarbage()) is quite costly.
I only used it in the test case to verify that memory is still tracked (i.e. it could be collected if the GC was aggressive enough) and not actually leaked.
I've edited volume-control/volume-control.lua and now the memory usage stays at 100MB after hours. This is more than 40MB without volume-control, but it's not increasing.
index 50f20b9..8e26176 100644
--- i/volume-control.lua
+++ w/volume-control.lua
@@ -56,7 +56,17 @@ function vcontrol:init(args)
self.step = args.step or '5%'
self.timer = timer({ timeout = args.timeout or 0.5 })
- self.timer:connect_signal("timeout", function() self:get() end)
+ vc_timer_run = 0
+ self.timer:connect_signal(
+ "timeout",
+ function()
+ self:get()
+ vc_timer_run = vc_timer_run + 1
+ if vc_timer_run > 10 then
+ collectgarbage()
+ end
+ end
+ )
self.timer:start()
if args.listen and watch then
That's an interesting finding @sclu1034!
I'll update the labels accordingly.
You have several options:
- Decrease the timeout to a value where the garbage collector at its current (probably default) settings can keep up
- Adjust the settings to make the GC more aggressive, so that it can keep up with the allocations
- Use Gio/Glib sub-processess directly, as awful.spawn probably has a much higher memory footprint due to all the extra X11 stuff
I'll add one more (explorational) bullet point:
- Use a daemon that exposes a GI API with update events, so you don't need to spawn a program every X time to get status update.
A shameless self-promoting example is my battery widget that is based on UPowerGlib https://github.com/Aire-One/awesome-battery_widget.
- Use a daemon that exposes a GI API with update events, so you don't need to spawn a program every X time to get status update.
That's not how GObject Introspection works. GI allows at-runtime-defined symbols for shared objects. And you cannot dlopen a process.
What you're looking for is any form of IPC, e.g. D-Bus or some custom protocol over Unix sockets. You could then write a library that implements your IPC protocol, and provide a GI typedef for that.
However, in the context of PulseAudio, the IPC and library exists already. The daemon is pulse itself, and the client library is libpulse. It doesn't have a GI typedef, though, so you need manual bindings.
Or in your case, UPower is the daemon, UPowerGLib is the client library that offers GI. And D-Bus is the IPC in use.
Experimental libpulse bindings: https://github.com/sclu1034/lua-libpulse-glib
I can tell that running the garbage collector very 10 cycles (using the patch) keeps the memory usage at 75 MB.
Duplicate of https://github.com/awesomeWM/awesome/issues/1490 ?
Yeah, more or less.
As far as I can tell, the vast majority of reports on "memory leak" aren't actual leaks. Instead they exhibit one or (more likely) both of these:
- memory allocation occurs faster than garbage collection can handle
- the userdatum is just a pointer to external heap memory, so the GC doesn't know the true size of the data (and Lua has no facility to tell the GC about that memory)