! Please note that this is a snapshot of our old Bugzilla server, which is read only since May 29, 2020. Please go to gitlab.xfce.org for our new server !
Async load of images
Status:
RESOLVED: MOVED
Severity:
enhancement
Product:
Ristretto
Component:
Application

Comments

Description Andre Miranda editbugs 2015-02-22 00:47:05 CET
Very large files[1] cause the UI to block while loading.
The loading of images should be handled asynchronously.

1 - http://commons.wikimedia.org/wiki/File:Lexington_original_configuration_orig.gif
Comment 1 Magnus Bergman 2019-08-24 02:10:45 CEST
I wasn't really able to recreate the problem (maybe half a second block but with the image in cache it loads instantly for me). So I can't really tell, but the patch to bug 15874 may perhaps fix this problem.
Comment 2 Andre Miranda editbugs 2019-08-24 02:32:55 CEST
For me that image blocks the UI for 2~3 seconds, same results with your patch.
Comment 3 Igor editbugs 2019-08-26 18:48:39 CEST
I agree with Andre: the UI still gets blocked with your patch.
Comment 4 Magnus Bergman 2019-08-27 04:14:50 CEST
After looking into this a bit it doesn't seem to be the image loading in itself that blocks the UI. While the image loads the loading of the thumbnail sidebar might have something to do with it (havn't looked into that yet).

But after the image is loaded a lot of time is spent in gdk_pixbuf_animation_iter_get_pixbuf() called from cb_rstto_image_viewer_update_pixbuf(). One might assume this function just gives you the pixbuf that the image was loaded into, but apparently not, the GIF loader seems to do lazy loading then using the animation API. This might be a GIF specific problem, which probably can be solved by preloading the frame if there is only one. (Since GIF images can be animated they must always be treated as animations, that's just how gdk-pixbuf works).

Then some time is spent in gdk_cairo_set_source_pixbuf() and cairo_paint() called from paint_image(). This is probably nothing more than the general slowness that comes from scaling down large images. The only solution I know of is to prescale smaller versions to make the rendering swifter.
Comment 5 Igor editbugs 2019-08-29 15:53:38 CEST
I've tried to put the whole thing into a g_idle_add() call but that didn't help.

To be fair, other image viewers I've tried (gthumb, eog) also have the UI frozen UI while loading a large gif. Do you know a (gtk) viewer that doesn't suffer from that?
Comment 6 Magnus Bergman 2019-08-29 20:20:22 CEST
Using g_idle_add() wont help as that always runs in the same thread (as the main loop) and will always block the GUI. Running the whole thing, including the call to gdk_pixbuf_animation_iter_get_pixbuf(), in a thread might help to some degree (remove up to 2/3 of the time it blocks according to my measurements). But the actual drawing probably needs to be done in the GUI thread, that is the calls to gdk_cairo_set_source_pixbuf() and cairo_paint(). Drawing simultaneously from two threads might work in some circumstances, but to my experience it's unreliable (at best).

All image viewers based on gdk-pixbuf will likely exhibit the same behavior, as the GIF backend of gdk-pixbuf is part of the problem (which could be solved in gdk-pixbuf but will likely never be). The slow scaling and drawing of large images is a bit harder to solve, but at least GEGL (used by GIMP and Gnome Photo) provides a solution. So Gnome Photo maybe?
Comment 7 Magnus Bergman 2019-09-07 21:39:50 CEST
Created attachment 9011 
Testcase to show what takes time

This testcase shows which function calls that takes time, and prints how long it takes. There is one function for loading, one for getting a frame (of an animation) and one for rendering the image (to a memory buffer which can optionally be saved an a PNG file).

Running it through callgrind shows the loading takes 6 649 023 986 cycles (40%), getting the frame 5 889 084 490 cycles (35%) and drawing the image 4 163 199 811 cycles (25%).

The loading can easily be done asynchronously (preferable in a different thread), so that taking cycles is obviously not the problem. Getting the frame could theoretically be done asynchronously as well (which isn't happening now). But I think it's a better idea to move as much as possible to the loading phase instead, that is always fetching the first frame before the image is considered fully loaded (ideally I think the image loader itself should do this but gdk-pixbuf apparently isn't). And also create scaled down versions of the image for faster drawing.

Doing this only for the first frame of an animation of an animation will of course not do anything for speeding up the drawing of the following frames. But gracefully handling GIF images that are either animated OR large is a fair trade-off, I think. Anything else would be to try to solve the problem of playing scaled down 4K+ video from a quite sub-optimal video format, which probably is beyond the scope of Ristretto.

(Please note that the testcase is not intended the teach good practice of memory management or error handling.)
Comment 8 Magnus Bergman 2019-09-13 22:34:11 CEST
Created attachment 9023 
Testcase to demonstrate two steps towards a solution

As mentioned above a lot could be improved by preloading the first frame of a (potentially animated) GIF image. After investigating the matter I found out that gdk-pixbuf already does this, but you don't automatically get it, you have to use a special call to avoid loading the frame again (see testcase_frame()). This cuts the cycles used down to 380 for getting the frame. That effectively makes it more than fifteen million times faster!

The second thing demonstrated in the testcase is splitting the drawing into two steps, one for scaling the image and one for actually drawing it. The idea is that the first step can be done in a thread and only the second step needs to block the GUI (but without making the image appear on the screen any faster). Scaling the image takes 4 162 677 242 cycles (almost the same as drawing it in the first testcase) and drawing it takes 2 431 365 cycles. This, however, is a worst case scenario then drawing to a memory buffer (and it obviously depends on the target size which is arbitrary set to 2000x2000 in the testcase). Then drawing to the screen cairo has the potential of utilizing accelerated drawing, by putting the scaled image (surface) directly in video memory. (I'm not sure if that happens, and in which situations exactly, just that cairo has a special surface creation function to allow it.)

Together these two measure cuts the time the GUI needs to be blocked with 99.976%. Which almost solves the problem. The only issue left is that scaling the image still takes time (even if it doesn't block the GUI), and needs to be done each time the image is resized or or the window is enlarged. As described above this could be solved by creating a smaller version of the image to use then zoomed out. It could also be used to quickly draw the image with a (slightly) reduced quality to display while the image is being properly scaled in a thread. I'll continue to look into that.
Comment 9 Magnus Bergman 2019-09-13 23:07:21 CEST
Created attachment 9024 
Avoid the expensive call to gdk_pixbuf_animation_iter_get_pixbuf() for static GIF images

This simple patch will not completely solve the problem but should cut the time the GUI is blocked with almost 60%.
Comment 10 Magnus Bergman 2019-12-29 05:15:05 CET
Created attachment 9335 
Diagram describing the problem of rendering large images scaled down fast

Here is a general description of the problem (including a nifty diagram), which I think could be helpful in discussing the way forward.

The main factor that determines the time it takes to render an image is the number of pixels used from the source image. So the worst case scenario is rendering all the pixels (zoom to fit). There is an arbitrary limit there the number of pixels is too high and the time it takes to render is too long to feel instant. It can be assumed that this limit is somewhere above the maximum pixels on screen (otherwise the situation is homeless). So rendering any image at 1:1 zoom or higher is never a problem. If the number of pixels in the image is below the limit, it can always be rendered instantly in any zoom level.

The problem is zooming out an image with a pixel count above the limit, as the leftmost blue line indicates. To speed things up a smaller version could be generated beforehand, scaled down so the total number of pixels is below the limit, as the rightmost blue line shows. This mostly solves the problem.

But if the image is large enough there will be a gap at certain zoom leves where the original image is too large too render and using the smaller one will result in reduced quality, as indicated be the yellow area. Ideally where would be images in sizes in between, as indicated by the dotted blue lines. Then where will always be an image that is fast enought and have enough pixels at any zoom level, indicated by a blue line in the green area. Another solution (since viewing images partyly zoomed out is a relatively uncommon case) is too first render the smaller image, which results in reduced quality, and start rendering it in the background to display in full quality then done.
Comment 11 Magnus Bergman 2020-03-07 08:09:46 CET
Created attachment 9542 
Testcase to demonstrate my final ideas for a solution

This is my final testcase which implements the ideas described above. It's based on the assumption that any computer is capable of quickly drawing at least four times the amount of pixels of the screen (which is the worst case scenario). I think this is generally  true with a large margin since quickly just means not long enough for the gui to feel unresponsive (it doesn't even need to happen within a frame, but perhaps in five frames or so). The constant MAX_SIZE (which is set very low to demonstrate the function of the code) should be set to double the screen width in order for the algorithm to adjust to the (assumed) capability of the computer it's runing on.

Unfortunately my previous testcases, as well as Ristretto I presume, no longer work because of a bug in gdk-pixbuf 2.40. It causes the GIF loader to grind to a halt then using the progressive API (at least with largeish files). Feeding it the whole file at once doesn't trigger the problem however, which is why it's done that way in this testcase.

Now some of the code is moved from testcase_load() to testcase_frame(). But since both are excpected to run in a thread it doesn't matter much, together they take 4 813 157 465 cycles. Then there are two functions for drawing: testcase_draw_quick() and testcase_draw(). The first one will always run quickly (72 729 677 cycles), but perhaps not render the picture in full quality. However it always will for the two common cases, displaying the image in original size, and scaling down the image to fit the screen. In other cases the function testcase_prepare() needs to be called (in a thread), which takes 1 415 834 026 cycles, and then the picture can rendered in full quality, which takes 126 729 674 cycles.

It would be very easy to change the code so only one draw function would be needed, but testcase_frame() would take a lot longer instead.

It's no trivial task to turn this code into a patch for solving the issue. I think the first logical step is to move to a threaded design in general (as described in bug 15874). I'm not afraid to get my hands dirty and write the code. But I would really like to discuss the design and the path forward with the mainteiner first. Feel free to contact me.
Comment 12 Git Bot editbugs 2020-05-25 00:30:52 CEST
-- GitLab Migration Automatic Message --

This bug has been migrated to xfce.org's GitLab instance and has been closed from further activity.

You can subscribe and participate further through the new bug through this link to our GitLab instance: https://gitlab.xfce.org/apps/ristretto/-/issues/16.

Please create an account or use an existing account on one of our supported OAuth providers. 

If you want to fork to submit patches and merge requests please continue reading here: https://docs.xfce.org/contribute/dev/git/start#gitlab_forks_and_merge_requests

Also feel free to reach out to us on the mailing list https://mail.xfce.org/mailman/listinfo/xfce4-dev

Bug #11577

Reported by:
Andre Miranda
Reported on: 2015-02-22
Last modified on: 2020-05-25

People

CC List:
2 users

Version

Version:
master

Attachments

Additional information