Performance issues in my `GtkColumnView`

I am building a GTK application that uses a GtkColumnView to display images in the first row and information on that image in the rows below. I tested the application with 5 images, each with about 110 rows below and the loading time for the table is about 4 seconds. The GUI is blocking during this time.

I profiled the application with cProfile and found out that one culprit is _gtktemplate.py with 0.8 seconds. I assume that this is .ui template loading. (Another culprit is the file loading, but I think I should use thumbnails instead.)

I defined various cell types based on content and instantiate them in my bind_column method on demand. Each cell type is a widget class with its own .ui template. Is there any way to pre-compile them, so they don’t have to be parsed during runtime?

And how to build such a table efficiently?

By logging the calls to setup_column and bind_column, I also noticed that these two methods are called every time I update the table content with Gio.ListModel.splice. My understanding is that setup_column only creates elements and those could be re-used later. So, it shouldn’t always call setup_column whenever the table contents change, right?

1 Like

Hi,

You may also want to use Sysprof to profile what happens on gtk side. Make sure to have debug symbols installed (refer to your distro for how to install them).

Images loading can be quite slow, yes.
An option is to just display a placeholder at widget creation, then spawn a thread to load the real image asynchronously in background.

It’s a known gtk bug: GtkColumnView excessively creating column cells (#6557) · Issues · GNOME / gtk · GitLab
When the list model changes, all widgets are re-created instead of being recycled.

1 Like

Thanks for your answer, I’ll try Sysprof and post an update when succeeded. I am using Flatpak to test the app because my system’s libraries are a little bit too old.

An option is to just display a placeholder at widget creation, then spawn a thread to load the real image asynchronously in background.

Good idea, thank you very much!

It’s a known gtk bug: GtkColumnView excessively creating column cells (#6557) · Issues · GNOME / gtk · GitLab

I see. Is there any known mitigation? It looks like this bug was postponed multiple times.

Besides, I was thinking about instead of creating the cell widgets on-demand in the bind method, I could also implement them as a GtkStack and just select the displayed page based on the content. But this could be slower, right?

Not AFAIK. List widgets like GtkColumnView use a quite complex machinery, and fixing it is not trivial.

I don’t think it makes sense…
You anyway have to create the widgets in setup, and bind is supposed to only update the properties. Otherwise yo may get weird issues like jumping scrollbars.

Sounds a bit convoluted…

On a side note, if I understand correctly what you’re trying to do, it looks like you may want to use a Gtk.ColumnView:header-factory to create the images, then for the information rows below use the normal Gtk.ColumnView:row-factory.
This way you can setup different widgets types for each factory, which should be more optimized than mixing images and informations in the same factory.
Or is that already what you implemented?

Thank you very much for your suggestions. In fact,

On a side note, if I understand correctly what you’re trying to do, it looks like you may want to use a Gtk.ColumnView:header-factory to create the images, then for the information rows below use the normal Gtk.ColumnView:row-factory.

sounds very much like how I should do it. However, I currently have 4 types of cells:

  • Preview cell (an image or picture, only the first row)
  • Path cell (label with a button, usually the second row)
  • Text cell (just a label, most rows)
  • Keyword list cell (each keyword is wrapped into its own box, usually one or two rows, no fixed location)

In all, I have about 100 rows and one columns per file. Realistically, there are usually 2-3 files. If I instantiate the preview cell in the header, I still have 3 options for the other cells. Maybe, I could merge the text and path cell, but the keyword cell is quite different from them. What would you suggest in this situation?

I’ll try Sysprof and post an update when succeeded.

BTW, regarding sysprof, it’s apparently not that trivial to install it within the flatpak and I’m currently unable to run my app natively because it requires Libadwaita 1.7 which currently isn’t natively available on Ubuntu LTS 24.04. So, I probably need some more time for that…

I haven’t done anything that complex, so I can only suggest experimenting.

But in general, only create widgets in setup (maybe a GtkBox with all 3 options, that you hide by default), and in bind set the relevant one visible.

I think if you use the gnome-builder IDE, it has an embedded sysprof support that automatically runs on the app within the sandbox when building as flatpak. But I never used like that so can’t help more here.

1 Like

Thanks for your suggestions. This has become a blocking issue so far, so I decided to prioritize functional requirements of my app for now and tackle the performance problem later. I’ll post a solution here as soon as I find one. Or I’ll link the code here when published, if I cannot find the solution myself.

maybe a GtkBox with all 3 options, that you hide by default

That was actually my first approach. I created a GtkStack with the 3 options as pages and set visibility in bind. It was already slow, so I changed to the option described above, where I create elements on-demand in bind.

I think, a good next step would be to examine the number of setup invocations relative to bind. As far as I understand, it should trigger bind more often when scrolling through the app.

I think if you use the gnome-builder IDE, it has an embedded sysprof support that automatically runs on the app within the sandbox when building as flatpak. But I never used like that so can’t help more here.

Exactly, I’m using gnome-builder and there’s a profiling option, and it suddenly works! When I first tried it, it didn’t worked, but I guess, it required me to install sysprof on my system to work.

1 Like

Small update

Profiling

I now got a profiling with sysprof via GNOME Builder, but it’s of limited use for me. It mainly shows library functions and apparently is missing references to my Python code. For example, it’s not clear where the set_property call was triggered from my code because there are naturally multiple of it.

Instead, I now profile with Python’s cProfile module:

python -m cProfile -o my-app.prof /app/bin/my-app

I can visualize the result with

snakeviz my-app.prof

The biggest chunks in my profiling:

  • Gio.py:127(run): ~7700 ms (probably the main loop?)
  • _gtktemplate.py:162(init_template): ~1400 ms
  • Load 5 images for preview: ~400 ms
  • Read information from 5 files: ~200 ms

I also measured the wall time along the critical path:

  1. Read information: ~200 ms
  2. Load preview row: ~300 ms
  3. Load path row: ~20 ms
  4. Load other rows (about 100 * 5): ~2400 ms

Optimization 1

I am now using asynchronous programming in my application to load images. Since GtkPicture.set_file apparently already supports async, it was almost as easy as setting up the application as described in that article and changing the load function to async.

This reduces the wall time of the preview row loading from 300 ms to about 20 ms. However, in the profiling, it still shows up as a significant portion, but at least it doesn’t block the critical path anymore.

1 Like

Small Update

Optimization 2

By successively deconstructing my application and measuring cumulative wall time of code parts, I noticed that about 600 ms is spent in total on creating GtkVideo objects as part of my preview. The file preview is a stack containing a GtkImage, GtkPicture and a GtkVideo, each with file = None. The former two take about 100 ms each to construct. These three elements are constructed for all 728 elements on the grid, hence GtkVideo’s normally unnoticeable initialization time of about 1 ms becomes a heavy weight in my program.

@gwillems earlier proposal to use header fields for the previews is a good mitigation. Thank you again!

As a quick fix to test the time, I initialize the GtkVideo object now on demand inside the asynchronous preview loading function. This reduces the time for building the other rows from about 2400 ms to about 1100 ms. If I instantiate all three children on-demand, the time goes down slightly to 1040 ms.

1 Like

Small Update

Optimization 3

My table has a right-click context menu. Because the content and actions of this context menu depends on the cell value, I defined this menu as part of this cell widget. This leads to one separate menu instance for each cell, i.e., a lot of redundant initialization.

The mitigation for this is easy: define the menu once within the column view. However, since the content and behavior of this menu depends on the clicked cell value, this implementation isn’t very straight-forward, to the best of my knowledge, GTK4 don’t provide any straight-forward methods for this and I don’t know any open-source GTK4 application that I could use as a reference. The only resource I’ve found is this forum post that misses a conclusion.

So, I came up with my own solution. It’s a bit hacky for my taste and I’m not proud of it, so I welcome anyone with a better solution. I subclass Gtk.PopoverMenu and add a mutable GObject.Property named selected_content. The column view now contains an instance of this popover menu as a child and submits this instance as constructor argument to each of the cell widgets within the setup_column callback method. The cell widget stores the reference internally and when registering a right click or long press, it sets the selected_content property of the popover menu, then sets its coordinates as follows:

column_view = self._popover_menu.get_parent()
position = Gdk.Rectangle()
position.x, position.y = self.translate_coordinates(column_view, x, y)
self._popover_menu.set_pointing_to(position)

The translate_coordinates is important because x and y are relative to the internal coordinate system of the cell widget and must be translated to the coordinate system of the column view. Otherwise the popover menu will always appear at the top left of the column view.

It finally calls popover_menu.popup() within the event handler.

This reduces the initialization time by a few hundred milliseconds from 1100 ms to about 730 ms.

Optimization 4

Since my cells can contain different kinds of data with different representations, I’m currently using a Gtk.Stack and set the stack page according to the data type. However, the application has to create 4 times the number of elements it actually needs, so it takes a toll on the performance. However, most cells only contain raw text, while only a few cells contain a special kind of data, so it makes sense to always instantiate the text label in setup and only instantiate the other three cell widgets on demand in bind.

I implemented this with properties that, when accessed, lazily create and add the widget before returning it.

As a consequence, I can reduce the row initialization time from about 730 ms to about 350 ms.

1 Like