How to efficiently empty Makie figure / delete widgets?

I’m very new to Makie, so there is probably something I did not understand.

I’m trying to build some sort of UI with Makie, in which the number of widgets will change over time. In order to handle this I tried an approach where I empty the Makie figure and recreate new widgets. However, this quickly becomes very inefficient (it gives me the impression that zombie widgets still exist from earlier versions of the scene).

A MWE illustrating this is the following “countdown timer”: even though there are less and less widgets, it takes more and more time to display them

julia> using GLMakie

julia> f = Figure()

julia> for i in 15:-1:1
           @time begin
               empty!(f)
               for j in 1:i
                   Label(f[j,1], "$j")
               end
           end
       end
  2.370084 seconds (4.86 M allocations: 306.997 MiB, 3.96% gc time, 96.17% compilation time: 25% of which was recompilation)
  0.102843 seconds (242.75 k allocations: 14.234 MiB, 48.58% compilation time: 33% of which was recompilation)
  0.060081 seconds (174.01 k allocations: 9.734 MiB)
  0.091768 seconds (158.91 k allocations: 8.907 MiB, 15.85% gc time)
  0.116466 seconds (145.34 k allocations: 8.150 MiB)
  0.178382 seconds (134.00 k allocations: 7.480 MiB)
  0.291626 seconds (126.24 k allocations: 6.964 MiB)
  0.520312 seconds (124.74 k allocations: 6.718 MiB)
  0.903502 seconds (132.60 k allocations: 6.730 MiB, 1.59% gc time)
  1.468623 seconds (155.38 k allocations: 7.494 MiB)
  2.448408 seconds (200.61 k allocations: 8.872 MiB)
  3.882972 seconds (276.48 k allocations: 11.876 MiB)
  5.793398 seconds (385.54 k allocations: 15.622 MiB, 0.12% gc time)
  7.856370 seconds (505.28 k allocations: 21.093 MiB)
  7.769431 seconds (535.33 k allocations: 21.545 MiB, 0.07% gc time)

What am I doing wrong?

Hm we do test a couple things about deletion to hopefully remove all ties. But it’s possible something’s missing. Maybe one could do a heap snapshot to see what’s building up

On which version are you?
For newest tagged Makie I get:

  0.041127 seconds (112.59 k allocations: 5.182 MiB, 68.33% gc time)
  0.013915 seconds (114.00 k allocations: 5.144 MiB)
  0.008990 seconds (103.88 k allocations: 4.709 MiB)
  0.007763 seconds (94.08 k allocations: 4.284 MiB)
  0.007153 seconds (84.58 k allocations: 3.870 MiB)
  0.006207 seconds (75.39 k allocations: 3.466 MiB)
  0.005614 seconds (66.51 k allocations: 3.072 MiB)
  0.004626 seconds (57.94 k allocations: 2.691 MiB)
  0.003989 seconds (49.68 k allocations: 2.321 MiB)
  0.003480 seconds (41.73 k allocations: 1.961 MiB)
  0.003151 seconds (34.09 k allocations: 1.612 MiB)
  0.002326 seconds (26.76 k allocations: 1.273 MiB)
  0.001638 seconds (19.74 k allocations: 967.023 KiB)
  0.001061 seconds (13.02 k allocations: 641.359 KiB)
  0.000571 seconds (6.62 k allocations: 326.461 KiB)

Sorry, I should probably have mentioned it upfront:

pkg> status
Status `~/projets/DataView.jl/Project.toml`
  [e9467ef8] GLMakie v0.8.8
  [033835bb] JLD2 v0.4.33

EDIT: and it does not change anything if I pkg> add GLMakie#master

I would probably try this for fun https://julialang.org/blog/2023/04/julia-1.9-highlights/#heap_snapshot

Thanks!
So this is what it gives me:

But I’m not sure how to interpret this. I tried filtering using Makie as a “Class filter”, which gives

But again I’m not sure how to interpret the results. For example:

  • does the Makie.Label line indicate that there are 15 instances of Makie.Label in memory? If so, this is neither what I would have expected (only one label remaining in the end) nor what I feared (120 labels accumulated over the 15 iterations)
  • also, I’m have no idea whether it is normal for there to be 16 instances of a Makie.Scene

What does the second entry in the list expand to? It has the highest “shallow percentage” whatever that means

So I’m not able to get much from this second entry:

However, maybe the following is more interesting: I took 2 different heap snapshots and tried to compare them. The first snapshot is taken after the 3rd iteration (when everything seems to be compiled, but things are still fast)

And the second snapshot is taken in the end:

In both, I filtered everything related to Makie. It looks like the Makie.Screen object (which overall always takes the most space, apparently because it contains almost everything else) has seen its size increase by a factor of 8.

Inside it, the screens objects take up 5 times more space in the end than at the beginning. But perhaps even more interesting is the renderlist field, which goes from almost nothing to nearly half the size of the Makie.Screen object (its size has been multiplied by 265).

In your opinion, could that be the origin of my issue?

Maybe? Simon would have to say but if the renderlist is not cleared correctly that would at least not allow those plot primitives to be GC’ed. Although it’s weird that it works for Simon and not you. What system are you on?

I’m on Ubuntu

julia> versioninfo()
Julia Version 1.9.3
Commit bed2cd540a1 (2023-08-24 14:43 UTC)
Build Info:
  Official https://julialang.org/ release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 16 × 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-14.0.6 (ORCJIT, tigerlake)
  Threads: 1 on 16 virtual cores
Environment:
  LD_LIBRARY_PATH = /home/francois/.local/lib
  JULIA_PROJECT = @.

@jules (or others) could you please try to see whether the snippet mentioned in the OP works (i.e. is fast) for you?

using GLMakie

f = Figure()

for i in 15:-1:1
    @time begin
        empty!(f)
        for j in 1:i
            Label(f[j,1], "$j")
        end
    end
end

I can reproduce


  1.272646 seconds (3.43 M allocations: 220.149 MiB, 5.45% gc time, 91.31% compilation time)
  0.070894 seconds (223.28 k allocations: 12.950 MiB, 31.78% compilation time)
  0.042328 seconds (175.42 k allocations: 9.932 MiB)
  0.042804 seconds (160.22 k allocations: 9.090 MiB)
  0.041718 seconds (146.54 k allocations: 8.317 MiB)
  0.044816 seconds (135.09 k allocations: 7.633 MiB)
  0.068257 seconds (127.22 k allocations: 7.101 MiB, 19.62% gc time)
  0.075375 seconds (125.61 k allocations: 6.840 MiB)
  0.104191 seconds (133.37 k allocations: 6.837 MiB)
  0.159355 seconds (156.04 k allocations: 7.585 MiB)
  0.250372 seconds (201.15 k allocations: 8.948 MiB)
  0.389533 seconds (276.92 k allocations: 11.937 MiB)
  0.570912 seconds (385.87 k allocations: 15.668 MiB, 2.39% gc time)
  0.733303 seconds (505.50 k allocations: 21.124 MiB)
julia> versioninfo()
Julia Version 1.9.3
Commit bed2cd540a1 (2023-08-24 14:43 UTC)
Build Info:
  Official https://julialang.org/ release
Platform Info:
  OS: macOS (arm64-apple-darwin22.4.0)
  CPU: 10 × Apple M1 Max
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-14.0.6 (ORCJIT, apple-m1)
  Threads: 1 on 8 virtual cores
Environment:
  JULIA_IMAGE_THREADS = 1
julia> Pkg.status()
Status `~/temp/sandbox/Project.toml`
  [e9467ef8] GLMakie v0.8.8
1 Like

Looks better inside a function thtough:

julia> using GLMakie
g      
julia> function go()
       f = Figure()

       for i in 15:-1:1
           @time begin
               empty!(f)
               for j in 1:i
                   Label(f[j,1], "$j")
               end
           end
       end
       end
go (generic function with 1 method)

julia> go()
  0.903762 seconds (3.03 M allocations: 185.278 MiB, 4.81% gc time, 98.02% compilation time)
  0.024625 seconds (137.36 k allocations: 6.724 MiB, 60.83% compilation time)
  0.017196 seconds (105.29 k allocations: 4.781 MiB, 46.93% gc time)
  0.007553 seconds (95.24 k allocations: 4.344 MiB)
  0.006975 seconds (85.62 k allocations: 3.923 MiB)
  0.006105 seconds (76.32 k allocations: 3.513 MiB)
  0.005620 seconds (67.33 k allocations: 3.114 MiB)
  0.004727 seconds (58.65 k allocations: 2.727 MiB)
  0.004196 seconds (50.29 k allocations: 2.352 MiB)
  0.003704 seconds (42.24 k allocations: 1.987 MiB)
  0.002903 seconds (34.50 k allocations: 1.633 MiB)
  0.002200 seconds (27.08 k allocations: 1.289 MiB)
  0.001664 seconds (19.97 k allocations: 979.258 KiB)
  0.001081 seconds (13.18 k allocations: 649.297 KiB)
  0.000568 seconds (6.69 k allocations: 330.320 KiB)
1 Like

But it doesn’t display anything, right?

If I insert display calls to actually see the varying number of widgets, then it’s slowing down again:

using GLMakie

function go()
    f = Figure()
    for i in 15:-1:1
        @time begin
            empty!(f)
            for j in 1:i
                Label(f[j,1], "$j")
            end
        end
        display(f)
    end
end
julia> go()
  1.438462 seconds (3.27 M allocations: 201.081 MiB, 4.15% gc time, 98.18% compilation time: 8% of which was recompilation)
  0.246430 seconds (327.38 k allocations: 18.899 MiB, 82.06% compilation time: 8% of which was recompilation)
  0.055805 seconds (173.28 k allocations: 9.702 MiB, 18.98% gc time)
  0.051158 seconds (157.58 k allocations: 8.860 MiB)
  0.068819 seconds (142.92 k allocations: 8.063 MiB)
  0.101130 seconds (129.59 k allocations: 7.308 MiB)
  0.156710 seconds (118.29 k allocations: 6.647 MiB)
  0.260445 seconds (110.58 k allocations: 6.137 MiB)
  0.443615 seconds (107.77 k allocations: 5.775 MiB)
  0.741725 seconds (112.62 k allocations: 5.745 MiB)
  1.226510 seconds (128.92 k allocations: 6.280 MiB)
  1.956521 seconds (160.76 k allocations: 7.194 MiB)
  2.889633 seconds (209.41 k allocations: 8.792 MiB)
  1.485407 seconds (267.54 k allocations: 11.238 MiB, 1.40% compilation time)
  1.216857 seconds (273.18 k allocations: 11.717 MiB)

But again, maybe this is simply me not using Makie in its intended way?

1 Like

Indeed, I can reproduce the slow down with the display(f):

julia> go()
  0.013044 seconds (113.66 k allocations: 5.236 MiB)
  0.077153 seconds (205.50 k allocations: 11.579 MiB, 9.72% gc time, 34.07% compilation time)
  0.041326 seconds (174.70 k allocations: 9.900 MiB)
  0.036855 seconds (158.89 k allocations: 9.042 MiB)
  0.036637 seconds (144.12 k allocations: 8.230 MiB)
  0.035747 seconds (130.68 k allocations: 7.460 MiB)
  0.047572 seconds (119.28 k allocations: 6.784 MiB, 17.33% gc time)
  0.044979 seconds (111.46 k allocations: 6.258 MiB)
  0.057971 seconds (108.53 k allocations: 5.882 MiB)
  0.082069 seconds (113.28 k allocations: 5.836 MiB)
  0.121589 seconds (129.47 k allocations: 6.356 MiB)
  0.184119 seconds (161.20 k allocations: 7.255 MiB)
  0.269222 seconds (209.73 k allocations: 8.838 MiB)
  0.357629 seconds (263.83 k allocations: 11.015 MiB)
  0.359248 seconds (273.29 k allocations: 11.732 MiB, 1.42% gc time)

Rather strange that @sdanisch can’t reproduce because my system (macos) is quite different from yours. Windows ?

1 Like

It makes sense that display makes the difference because of the involvement of the renderlist which is only populated for display

2 Likes

I’m not sure if it is that simple, but you could try screen = display(fig) and then look for renderlist in the attributes of screen and maybe empty! it? That might free the plot objects

1 Like

Interesting experiment, thanks!

using GLMakie

function foo()
    f = Figure()
    for i in 15:-1:1
        @time begin
            empty!(f)
            for j in 1:i
                Label(f[j,1], "$j")
            end
        end
        screen = display(f)
        empty!(screen.renderlist)
    end
end

foo()

Behaviorwise, this does not change anything significantly: it causes no error, and the slow down is still there. The heap profile (taken at the end) does change significantly, though: now the renderlist takes almost no space, as expected

Does this mean renderlist wasn’t the culprit?

At least not the only one… The tests for this deletion behavior in Makie actually test without display so I’m not surprised they didn’t catch this. We should investigate and add tests for each backend.

2 Likes

I actually also didn’t look very closely at the example and just pasted it into a begin ... end block, not realizing it relied on being run line by line to display…
So I can actually reproduce it as well.

I don’t think the renderlist is the problem, it’s just where all the render objects live, and as far as I can tell, it gets emptied completely (but, since you run the empty! before the next loop, there will be lots of renderobjects in there at the end of the loop).
I do see lots of Observables{Any}(true) in the profile, after empty! & gc, which I think is the problem…
Gotta figure out where those are coming from.
That’s how I’d profile it:

using GLMakie
Profile.take_heapsnapshot("before.heapsnapshot")
begin
    f = Figure()
    display(f)
    for i in 15:-1:1
        for j in 1:i
            Label(f[j, 1], "$j")
        end
        empty!(f)
    end
end
Profile.take_heapsnapshot("after.heapsnapshot")
2 Likes

Is there a diff tool for these snapshots in Julia?