gotk4 icon indicating copy to clipboard operation
gotk4 copied to clipboard

Widget memory is not freed when removed

Open jgillich opened this issue 1 year ago • 17 comments

I've noticed that memory usage increases over time in my application, and it seems to be directly related to the replacement of widgets. A GtkColumnView does not leak memory when re-rendering, but when I remove and recreate widgets myself, it appears that the old ones are not freed. Here's an example program:

package main

import (
	"fmt"
	"os"
	"runtime"

	"github.com/diamondburned/gotk4/pkg/gio/v2"
	"github.com/diamondburned/gotk4/pkg/gtk/v4"
	"github.com/pkg/profile"
)

func main() {
	gtk.Init()

	app := gtk.NewApplication("foo.bar.baz", gio.ApplicationFlagsNone)

	app.ConnectActivate(func() {

		w := gtk.NewApplicationWindow(app)
		w.SetApplication(app)
		w.SetSizeRequest(400, 400)
		defer w.Show()

		s := profile.Start(profile.MemProfile)
		w.ConnectCloseRequest(func() (ok bool) {
			s.Stop()
			return false
		})

		root := gtk.NewBox(gtk.OrientationVertical, 4)
		root.SetMarginTop(4)
		root.SetMarginStart(4)
		root.SetMarginEnd(4)
		w.SetChild(root)

		btn := gtk.NewButton()
		btn.SetLabel("Leak memory")
		root.Append(btn)

		sw := gtk.NewScrolledWindow()
		root.Append(sw)
		items := gtk.NewBox(gtk.OrientationVertical, 4)
		items.SetVExpand(true)
		sw.SetChild(items)

		btn.ConnectClicked(func() {
			for {
				c := items.FirstChild()
				if c != nil {
					items.Remove(c)
				} else {
					break
				}
			}
			for i := 0; i < 1000; i++ {
				items.Append(gtk.NewLabel(fmt.Sprintf("Label %v", i)))
			}
			runtime.GC()
		})
	})

	app.Run(os.Args)
}

With enough clicks, you can reach one gigabyte and more. The widgets aren't referenced after being removed so I'd expect GC to trigger, but it doesn't.

jgillich avatar Jan 30 '24 17:01 jgillich

Could you try running the program with this?

GOTK4_DEBUG=trace-objects,toggle-refs go run -v .

This will cause gotk4 to give you a log file that you can tail -f with to see the live output. You can then click the button (probably once) and call GC() to see if they're being freed at all.

diamondburned avatar Feb 02 '24 06:02 diamondburned

It might also be worth it to run runtime/pprof on the heap to see if the memory bloat is coming from the Go heap or the C heap.

diamondburned avatar Feb 02 '24 06:02 diamondburned

Also as a side note, it can be really hard for Go's GC to know when to actually run, because the bulk of the allocations are on the C heap. For this reason, gotkit/gtkutil/aggressivegc exists.

diamondburned avatar Feb 02 '24 06:02 diamondburned

I can manually trigger GC on every button click and it makes little difference, memory still goes up each time. Tried looking at the log, but I don't see anything useful in there. Edit: is it supposed to show when an object is freed? All I see is these two lines being repeated:

2024/02/02 23:05:35 0x1effb50 (GtkLabel): Get: will introduce new box, current ref = 1
2024/02/02 23:05:35 0x1effb50 (GtkLabel): Get: introduced new box, current ref = 2

Here's a pprof graph with total memory being a few hundred megs:

pprof001

And here's the same with memory >1GB:

pprof002

So while most of the memory is C heap, I think it's Go that's keeping the references alive. But this stuff's a little out of my league :smile:

Also updated the code above with pprof and GC calls if you want to try for yourself.

jgillich avatar Feb 02 '24 22:02 jgillich

Edit: is it supposed to show when an object is freed? All I see is these two lines being repeated:

2024/02/02 23:05:35 0x1effb50 (GtkLabel): Get: will introduce new box, current ref = 1
2024/02/02 23:05:35 0x1effb50 (GtkLabel): Get: introduced new box, current ref = 2

Yeah, this isn't good. The debug is supposed to say when objects are being freed. I'm busy this weekends but I will look into this as soon as I can. I think the same bug is appearing on gtkcord4 as well.

diamondburned avatar Feb 03 '24 02:02 diamondburned

I figured it out! The ref-counting for floating references was all out of wack.

diamondburned avatar Feb 06 '24 00:02 diamondburned

--- a/pkg/core/intern/intern.go
+++ b/pkg/core/intern/intern.go
        // We should already have a strong reference. Sink the object in case. This
        // will force the reference to be truly strong.
        if C.g_object_is_floating(C.gpointer(gobject)) != C.FALSE {
+               // First, we need to ref_sink the object to convert the floating
+               // reference to a strong reference.
                C.g_object_ref_sink(C.gpointer(gobject))
+               // Then, we need to unref it to balance the ref_sink.
+               C.g_object_unref(C.gpointer(gobject))
+
+               if toggleRefs != nil {
+                       toggleRefs.Println(objInfo(gobject),
+                               "Get: ref_sink'd the object, current ref =", objRefCount(gobject))
+               }
        }

diamondburned avatar Feb 06 '24 00:02 diamondburned

As a side note, using toggle references for all objects likely kills performance a lot. It's much more ideal if they're only used if we're working with closures attached to objects.

diamondburned avatar Feb 06 '24 00:02 diamondburned

Awesome, that fixes it! Thank you

jgillich avatar Feb 06 '24 01:02 jgillich

Reopening due to a regression.

diamondburned avatar Jul 18 '24 17:07 diamondburned

would this cause common.items.Splice(0, int(common.items.NItems()), handler.entries...) to leak as well? Or is that a different issue eventually?

abenz1267 avatar Aug 31 '24 07:08 abenz1267

The splice call likely isn't the cause, it's probably handler.entries. Unfortunately, I don't currently have time to experiment with this :(

diamondburned avatar Sep 01 '24 15:09 diamondburned

Hm... because if i remove the splice call, memory is fine.

Edit: I've switched handler.entries to a newly created inline struct slice, the more slice items, the more memory leakage. I do suspect indeed splice to be the issue... or widget memory as this issue suggests. Maybe I'm missing something.

abenz1267 avatar Sep 04 '24 06:09 abenz1267

Ah, it looks like gioutil.ListModel leaks after all, but thankfully not related to this issue! This issue is a much more tedious bug that involves GC cyclic references, while ListModel just leaks because it's missing some gbox calls to free up items.

Could you open a separate issue for this?

diamondburned avatar Sep 04 '24 22:09 diamondburned

would somekinda bug bounty be an option? is anyone else able to fix this?

i'm afraid money doesn't change the fact @diamondburned has no time to fix this.... someone else?

abenz1267 avatar Nov 17 '24 08:11 abenz1267

Actually, once https://github.com/golang/go/issues/67535 lands in Go 1.24, I'll definitely be fixing this problem, now that it's... actually fixable.

diamondburned avatar Nov 17 '24 23:11 diamondburned

Words alone cannot express how excited I am for this feature to land so I can finally fix this years-old bug in gotk4 that I spent years thinking about @@

diamondburned avatar Nov 17 '24 23:11 diamondburned

We're downstream from this issue in Omarchy with Walker seeing memory leaks that's leading to 1GB+ process sizes. Are there still upstream releases that need to happen in go or gtk4 before this is solvable here?

dhh avatar Aug 12 '25 09:08 dhh