Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Widgets prematurely garbage collected #74

Open
BigBoyBarney opened this issue Dec 21, 2024 · 9 comments
Open

Widgets prematurely garbage collected #74

BigBoyBarney opened this issue Dec 21, 2024 · 9 comments
Labels
bug Something isn't working

Comments

@BigBoyBarney
Copy link

BigBoyBarney commented Dec 21, 2024

Hi!
I apologise for the lengthy issue in advance.

The following minimal example of images in a ListView in a ScrolledWindow crashes after scrolling a bit.
The files are included as a .zip for easier local reproducibility.

The main idea is lazily loading the images via a custom Gdk::Paintable, that only loads when the image is actually shown. A ListView with a few hundred images is otherwise basically unusable because of the constant stutter, load time and astronomical memory usage.

window.cr
require "gtk4"

{% `blueprint-compiler batch-compile ./src/ui/compiled ./src/ui/ ./src/ui/*.blp` %}
Gio.register_resource("src/window.gresource.xml", "src")

@[Gtk::UiTemplate(resource: "/dev/ui/compiled/MainWindow.ui", children: %w(list_view))]
class MainWindow < Gtk::ApplicationWindow
  include Gtk::WidgetTemplate

  def initialize(application : Gtk::Application)
    super(application: application)

    list_view = Gtk::ListView.cast(template_child("list_view"))
    my_list = Gtk::NoSelection.new
    list_view.model = my_list

    list_store = Gio::ListStore.new(MyItem.g_type)
    my_list.model = list_store

    2000.times do |x|
      list_store.append(MyItem.new)
    end
  end
end

class MyItem < GObject::Object
  @[GObject::Property]
  property picture : MyPicture = MyPicture.new("src/images/cover.jpg")
end

class MyPicture < GObject::Object
  include Gdk::Paintable

  @[GObject::Property]
  property texture : Gdk::Texture? = nil
  property image_path : String = ""

  def initialize(image_path : String)
    super()
    @image_path = image_path
  end

  @[GObject::Virtual]
  def do_snapshot(snapshot : Gdk::Snapshot, width : Float64, height : Float64) : Nil
    if local_texture = @texture
      local_texture.snapshot(snapshot, width, height)
      return
    end

    texture = Gdk::Texture.new_from_filename(@image_path)

    GLib.idle_add do
      self.texture = texture
      self.invalidate_size
      self.invalidate_contents

      false
    end
  end

  @[GObject::Virtual]
  def do_get_intrinsic_width
    texture.try &.width || 0
  end

  @[GObject::Virtual]
  def do_get_intrinsic_height
    texture.try &.height || 0
  end

  @[GObject::Virtual]
  def do_get_flags
    Gdk::PaintableFlags::None
  end

  @[GObject::Virtual]
  def do_get_intrinsic_aspect_ratio
    if tex = texture
      tex.width / tex.height
    else
      1_f64
    end
  end

  @[GObject::Virtual]
  def do_get_current_image
    self
  end
end

class App < Gtk::Application
  def initialize
    super()
  end

  @[GObject::Virtual]
  def activate
    window = active_window.nil? ? MainWindow.new(application: self) : active_window.not_nil!
    window.present
  end
end

app = App.new
exit(app.run)

With the blueprint

Blueprint
using Gtk 4.0;

template $MainWindow: Gtk.ApplicationWindow {
  default-width: 1000;
  default-height: 800;

  ScrolledWindow scroll { 
    child: ListView list_view {
      factory: BuilderListItemFactory {
        template ListItem {
          child: Picture {
            height-request: 100;
            paintable: bind template.item as<$MyItem>.picture;
          };
        }
      };
    };
  }
}

The actual error messages are wildly inconsistent, often time I get none at all. But they're generally:

Errors
(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: _gtk_css_border_style_value_get: assertion 'value->class == &GTK_CSS_VALUE_BORDER_STYLE' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_widget_measure: assertion 'GTK_IS_WIDGET (widget)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_widget_measure: assertion 'GTK_IS_WIDGET (widget)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_widget_get_width: assertion 'GTK_IS_WIDGET (widget)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_widget_get_height: assertion 'GTK_IS_WIDGET (widget)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_scrollable_get_border: assertion 'GTK_IS_SCROLLABLE (scrollable)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_scrollbar_get_adjustment: assertion 'GTK_IS_SCROLLBAR (self)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_adjustment_get_value: assertion 'GTK_IS_ADJUSTMENT (adjustment)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_adjustment_get_upper: assertion 'GTK_IS_ADJUSTMENT (adjustment)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_adjustment_get_page_size: assertion 'GTK_IS_ADJUSTMENT (adjustment)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_adjustment_get_value: assertion 'GTK_IS_ADJUSTMENT (adjustment)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_adjustment_get_lower: assertion 'GTK_IS_ADJUSTMENT (adjustment)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_scrollbar_get_adjustment: assertion 'GTK_IS_SCROLLBAR (self)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_adjustment_get_value: assertion 'GTK_IS_ADJUSTMENT (adjustment)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_adjustment_get_upper: assertion 'GTK_IS_ADJUSTMENT (adjustment)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_adjustment_get_page_size: assertion 'GTK_IS_ADJUSTMENT (adjustment)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_adjustment_get_value: assertion 'GTK_IS_ADJUSTMENT (adjustment)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_adjustment_get_lower: assertion 'GTK_IS_ADJUSTMENT (adjustment)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_scrollbar_get_adjustment: assertion 'GTK_IS_SCROLLBAR (self)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_adjustment_get_lower: assertion 'GTK_IS_ADJUSTMENT (adjustment)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_adjustment_get_upper: assertion 'GTK_IS_ADJUSTMENT (adjustment)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_adjustment_get_page_size: assertion 'GTK_IS_ADJUSTMENT (adjustment)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_scrollbar_get_adjustment: assertion 'GTK_IS_SCROLLBAR (self)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_adjustment_get_lower: assertion 'GTK_IS_ADJUSTMENT (adjustment)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_adjustment_get_upper: assertion 'GTK_IS_ADJUSTMENT (adjustment)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_adjustment_get_page_size: assertion 'GTK_IS_ADJUSTMENT (adjustment)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_widget_measure: assertion 'GTK_IS_WIDGET (widget)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_widget_measure: assertion 'GTK_IS_WIDGET (widget)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_widget_get_width: assertion 'GTK_IS_WIDGET (widget)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_widget_get_height: assertion 'GTK_IS_WIDGET (widget)' failed

(crystal-run-window.tmp:187127): Gtk-CRITICAL **: 00:54:41.870: gtk_scrollable_get_border: assertion 'GTK_IS_SCROLLABLE (scrollable)' failed
Invalid memory access (signal 11) at address 0x0
[0x435576] *Exception::CallStack::print_backtrace:Nil +118 in /home/barney/.cache/crystal/crystal-run-window.tmp
[0x41d8a6] ~procProc(Int32, Pointer(LibC::SiginfoT), Pointer(Void), Nil) +310 in /home/barney/.cache/crystal/crystal-run-window.tmp
[0x7f13911cddd0] ?? +139722015694288 in /lib64/libc.so.6
[0x7f1391854243] ?? +139722022535747 in /lib64/libgtk-4.so.1
[0x7f1391755719] ?? +139722021492505 in /lib64/libgtk-4.so.1
[0x7f139180095b] ?? +139722022193499 in /lib64/libgtk-4.so.1
[0x7f1391801988] ?? +139722022197640 in /lib64/libgtk-4.so.1
[0x7f13918023e9] gtk_widget_snapshot_child +121 in /lib64/libgtk-4.so.1
[0x7f13917e611e] ?? +139722022084894 in /lib64/libgtk-4.so.1
[0x7f1391800ed1] ?? +139722022194897 in /lib64/libgtk-4.so.1
[0x7f1391801988] ?? +139722022197640 in /lib64/libgtk-4.so.1
[0x7f13918021b3] ?? +139722022199731 in /lib64/libgtk-4.so.1
[0x7f1391803359] ?? +139722022204249 in /lib64/libgtk-4.so.1
[0x7f139196e849] ?? +139722023692361 in /lib64/libgtk-4.so.1
[0x7f139212b55c] ?? +139722031805788 in /lib64/libgobject-2.0.so.0
[0x7f139212b671] g_signal_emit_valist +65 in /lib64/libgobject-2.0.so.0
[0x7f139212b733] g_signal_emit +147 in /lib64/libgobject-2.0.so.0
[0x7f13919ff5a9] ?? +139722024285609 in /lib64/libgtk-4.so.1
[0x7f139212b55c] ?? +139722031805788 in /lib64/libgobject-2.0.so.0
[0x7f139212b671] g_signal_emit_valist +65 in /lib64/libgobject-2.0.so.0
[0x7f139212b733] g_signal_emit +147 in /lib64/libgobject-2.0.so.0
[0x7f13919e3ccd] ?? +139722024172749 in /lib64/libgtk-4.so.1
[0x7f13919e40fe] ?? +139722024173822 in /lib64/libgtk-4.so.1
[0x7f1391ffa519] ?? +139722030556441 in /lib64/libglib-2.0.so.0
[0x7f1391ff428c] ?? +139722030531212 in /lib64/libglib-2.0.so.0
[0x7f13920547b8] ?? +139722030925752 in /lib64/libglib-2.0.so.0
[0x7f1391ff5783] g_main_context_iteration +51 in /lib64/libglib-2.0.so.0
[0x7f13914e0dcd] g_application_run +493 in /lib64/libgio-2.0.so.0
[0x53b61d] *App +29 in /home/barney/.cache/crystal/crystal-run-window.tmp
[0x4023f0] __crystal_main +1344 in /home/barney/.cache/crystal/crystal-run-window.tmp
[0x486d26] *Crystal::main_user_code<Int32, Pointer(Pointer(UInt8))>:Nil +6 in /home/barney/.cache/crystal/crystal-run-window.tmp
[0x486c9a] *Crystal::main<Int32, Pointer(Pointer(UInt8))>:Int32 +58 in /home/barney/.cache/crystal/crystal-run-window.tmp
[0x40fa26] main +6 in /home/barney/.cache/crystal/crystal-run-window.tmp
[0x7f13911b7248] ?? +139722015601224 in /lib64/libc.so.6
[0x7f13911b730b] __libc_start_main +139 in /lib64/libc.so.6
[0x401de5] _start +37 in /home/barney/.cache/crystal/crystal-run-window.tmp
[0x0] ???
Video demonstration
Screencast.From.2024-12-21.00-59-19.mp4

Disabling the GC at the beginning with GC.disable completely solves the issue. Interestingly, this issue doesn't occur with a fewer number of rows, for example 500. My assumption is that in that case, the GC isn't inclined to trigger yet.

I have attached a valgrind report as well.
valgrind.log

Minimal Example.zip


Big thanks to @BlobCodes. I had no idea what was going on.
Thanks in advance!

@BigBoyBarney
Copy link
Author

BigBoyBarney commented Dec 21, 2024

I just discovered that this is unrelated to the lazy loading of the texture. Completely removing it and using an empty snapshot results in the same error. FWIW, omitting the virtual methods (other than snapshot) causes no crash, but I'm including them here for completeness' sake.

class MyPicture < GObject::Object
  include Gdk::Paintable

  def initialize
    super()
  end

  @[GObject::Virtual]
  def do_snapshot(snapshot : Gdk::Snapshot, width : Float64, height : Float64) : Nil
  end

  @[GObject::Virtual]
  def do_get_intrinsic_width
    0
  end

  @[GObject::Virtual]
  def do_get_intrinsic_height
    0
  end

  @[GObject::Virtual]
  def do_get_flags
    Gdk::PaintableFlags::None
  end

  @[GObject::Virtual]
  def do_get_intrinsic_aspect_ratio
    1_f64
  end

  @[GObject::Virtual]
  def do_get_current_image
    self
  end
end

Using a Gdk::Texture instead of this makeshift MyPicture class solves the issue as well.

Something about the way I'm using the class must conflict with the GC 🤔

@BigBoyBarney
Copy link
Author

BigBoyBarney commented Dec 21, 2024

Another discovery.
Using @ysbaddaden's Immix implementation instead of Boehm solves the issue as well, so it is 100% Boehm clashing with something.

Disabling GC but manually calling GC.collect every few seconds does not result in a crash.

@BlobCodes
Copy link
Contributor

Immix doesn't even implement the additional allocating functions used by this shard, so that doesn't work.

https://github.com/hugopl/gi-crystal/blob/main/src/gi-crystal/toggle_ref_manager.cr

@BigBoyBarney
Copy link
Author

so is it the equivalent of GC.disable? That's a bummer :/

@hugopl hugopl added the bug Something isn't working label Dec 23, 2024
@BigBoyBarney
Copy link
Author

BigBoyBarney commented Dec 26, 2024

I've reduced the example even further

require "libadwaita"

{% `blueprint-compiler batch-compile ./src/ui/compiled ./src/ui/ ./src/ui/*.blp` %}
Gio.register_resource("src/ui/window.gresource.xml", "src/ui")

@[Gtk::UiTemplate(resource: "/moe/shisho/compiled/MinimalExample.ui", children: %w(grid_view scroll))]
class MainWindow < Adw::ApplicationWindow
  include Gtk::WidgetTemplate

  def initialize(application : Adw::Application)
    super(application: application)

   # Main window widget setup
    scroll = Gtk::ScrolledWindow.cast(template_child("scroll"))
    grid_view = Gtk::GridView.cast(template_child("grid_view"))

    my_list = Gtk::SingleSelection.new
    list_store = Gio::ListStore.new(MyItem.g_type)
    my_list.model = list_store
    grid_view.model = my_list
    scroll.child = grid_view


   # Assigning a few `MyItem`s to the ListStore.
    array = [] of MyItem
    10.times do |x|
      array << MyItem.new
    end
    list_store.splice(0, 0, array)
  end
end

# In a normal use-case, this would have other properties that hold text for example
# to create a proper `ListView` row or something.
class MyItem < GObject::Object
  @[GObject::Property]
  property picture : MyPicture # This is a paintable

  def initialize
    super()
    @picture = MyPicture.new
  end
end

# Paintable that does literally nothing.
class MyPicture < GObject::Object
  include Gdk::Paintable

  def initialize
    super()
  end

  @[GObject::Virtual]
  def do_snapshot(snapshot : Gdk::Snapshot, width : Float64, height : Float64) : Nil
  end
end

# General application stuff
class App < Adw::Application
  def initialize
    super(application_id: "minimal.example", flags: Gio::ApplicationFlags::DefaultFlags)
  end

  @[GObject::Virtual]
  def activate
    window = active_window.nil? ? MainWindow.new(application: self) : active_window.not_nil!
    window.present
  end
end

app = App.new
exit(app.run)

with the blueprint

using Gtk 4.0;
using Adw 1;

template $MainWindow: Adw.ApplicationWindow {
  default-width: 1000;
  default-height: 800;

  ScrolledWindow scroll {}
}

GridView grid_view {
  factory: grid_view_factory;
}

BuilderListItemFactory grid_view_factory {
  template ListItem {
    child: Picture grid_pic {
      paintable: bind template.item as <$MyItem>.picture;
    };
  }
}

As you can see, I'm not doing anything fancy. Assigning a few MyItem : GObject::Object instances to a Gio::ListStore. A property of MyItem is a custom Gdk::Paintable that does literally nothing.

This example crashes after resizing the window a bunch. With GC disabled it does not crash, but will obvious leak memory so it's unusable.

Custom Gdk::Paintables are necessary for things like rounded corners around the image, blurring etc. so it's not a an obscure use-case either 🤣.

Here's a video demonstrating the issue:

Screencast.From.2024-12-26.21-14-55.mp4

I would LOVE to help, but I know little to no C. If there is anything I can do to assist you please let me know! ❤️

@BigBoyBarney
Copy link
Author

BigBoyBarney commented Dec 26, 2024

Another finding, the issue is entirely caused by the following:

class MyItem < GObject::Object
  @[GObject::Property]
  property picture : MyPicture

  def initialize
    super()
    @picture = MyPicture.new
  end
end

# Paintable that does literally nothing.
class MyPicture < GObject::Object
  include Gdk::Paintable

  def initialize
    super()
  end

  @[GObject::Virtual]
  def do_snapshot(snapshot : Gdk::Snapshot, width : Float64, height : Float64) : Nil
  end
end

Replacing MyPicture with an empty Gtk::Picture does not crash.

class MyItem < GObject::Object
  @[GObject::Property]
  property picture : Gtk::Picture.new

  def initialize
    super()
  end
end

Further minimising the issue and skipping the MyItem intermediary step and using MyPicture directly as source for the paintable crashes too.

  template ListItem {
    child: Picture {
      paintable: bind template.item as <$MyPicture>;
    };
  }
}
list_store = Gio::ListStore.new(MyPicture.g_type)
array = [] of MyPicture
10.times do |x|
  array << MyPicture.new
end
list_store.splice(0, 0, array)

class MyPicture < GObject::Object
  include Gdk::Paintable

  def initialize
    super()
  end

  @[GObject::Virtual]
  def do_snapshot(snapshot : Gdk::Snapshot, width : Float64, height : Float64) : Nil
  end
end

@BigBoyBarney
Copy link
Author

BigBoyBarney commented Dec 26, 2024

Final comment:
The error is completed unrelated to me frantically dragging the window around like an absolute lemon 🤣
The issue happens whenever the GC tries to collect.
The following example crashes after the 5 second delay is over.

class MainWindow < Adw::ApplicationWindow
  def initialize(application : Adw::Application)
    super(application: application)

    pic = Gtk::Picture.new(paintable: MyPicture.new)
    self.content = pic

    spawn do
      sleep 5.seconds
      GLib.idle_add do
        GC.collect
        false
      end
    end
  end
end

class MyPicture < GObject::Object
  include Gdk::Paintable

  def initialize
    super()
  end

  @[GObject::Virtual]
  def do_snapshot(snapshot : Gdk::Snapshot, width : Float64, height : Float64) : Nil
  end
end

As you can see, it's also independent of ScrolledWindow, GridView etc. Including Gdk::Paintable, and making it the paintable property of a Gtk::Image is fundamentally incompatible with the GC currently.

@hugopl
Copy link
Owner

hugopl commented Dec 26, 2024

I won't have time to investigate this soon... 😔

@BigBoyBarney
Copy link
Author

BigBoyBarney commented Dec 27, 2024

No worries, take your time! I'll keep adding comments as I narrow the issue down more and more. It's the most I can do, without knowing any C 😔 . Also, I'm not sure whether I'm supposed to open issues here or under GI-Crystal.

So there appear to be 2 separate issues here. The first one is probably related to MainWindow being collected for some reason.

The following example crashes when GC.collect is ran (both with and without -Dpreview_mt).

class MainWindow < Adw::ApplicationWindow
  def initialize(application : Adw::Application)
    super(application: application)

    button = Gtk::Button.new(label: "GC collect")

    button.clicked_signal.connect do
      GLib.idle_add do
        GC.collect
        false
      end
    end

    self.content = button
  end
end

The solution here is very simple: Create a helper constant that keeps the reference to the window alive.

KEEP_ALIVE = [] of GObject::Object
class MainWindow < Adw::ApplicationWindow
  def initialize(application : Adw::Application)
    super(application: application)

    KEEP_ALIVE << self

    # rest of the code
  end
end

Huge thanks to BlobCodes for discovering this.

For the other GC problem, I will open another issue, as that's a bit more esoteric.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants