awesome icon indicating copy to clipboard operation
awesome copied to clipboard

`awful.spawn.easy_async` can consume memory faster than the garbage collector free it

Open jo-so opened this issue 3 years ago • 17 comments

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.

jo-so avatar Mar 09 '22 07:03 jo-so

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 avatar Mar 09 '22 18:03 actionless

Actionless Loveless schrieb am Wed 09. Mar, 10:50 (-0800):

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

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: @.***>

jo-so avatar Mar 16 '22 12:03 jo-so

and if you'll add collectgarbage("collect") ?

actionless avatar Mar 16 '22 12:03 actionless

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?

jo-so avatar Mar 16 '22 14:03 jo-so

what exactly should i reproduce, ie for how long i should run it and which % grow in memory usage should i see after?

actionless avatar Mar 16 '22 15:03 actionless

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%

actionless avatar Mar 16 '22 15:03 actionless

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

actionless avatar Mar 16 '22 15:03 actionless

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

actionless avatar Mar 16 '22 15:03 actionless

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?

jo-so avatar Mar 17 '22 05:03 jo-so

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

actionless avatar Mar 17 '22 08:03 actionless

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.spawn probably has a much higher memory footprint due to all the extra X11 stuff

sclu1034 avatar Sep 12 '22 15:09 sclu1034

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.

jo-so avatar Sep 15 '22 07:09 jo-so

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.

sclu1034 avatar Sep 15 '22 07:09 sclu1034

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

jo-so avatar Sep 15 '22 17:09 jo-so

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.

Aire-One avatar Sep 17 '22 18:09 Aire-One

  • 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

sclu1034 avatar Sep 17 '22 18:09 sclu1034

I can tell that running the garbage collector very 10 cycles (using the patch) keeps the memory usage at 75 MB.

jo-so avatar Sep 18 '22 16:09 jo-so

Duplicate of https://github.com/awesomeWM/awesome/issues/1490 ?

psychon avatar Oct 01 '22 09:10 psychon

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)

sclu1034 avatar Oct 03 '22 17:10 sclu1034