Makie: animation becomes slow after redrawing surface about 25 times

Context

I am working on an example of a 3-D visualization of a 2-D glacier flow. Until now the visualization is done for a very small problem (256x256 grid points). Later it should also work for much larger problems.

Problem

The animation becomes slow after redrawing surface about 25 times. Here is an outline of the visualization strategy chosen (B_v is the topography, “the mountain”, and H_v the glacier height for visualization. #(…) denotes omitted code):

First I set up a scene and draw an empty glacier on it (all NaNs)

#(...) 
B_v .= B
H_v  = fill(NaN, nx, ny)
scene = Makie.surface(X, Y, transpose(B_v), colormap = cgrad(:terrain, scale=:exp10))
glacier = Makie.surface!(scene, X, Y, H_v)

Then, I use record around the time steps of the simulation and do a redraw of the glacier surface at each time step:

record(scene, "glacierflow.mp4"; framerate = 25) do io
    for it = 1:nt
        H_v .= H
        H_v[H_v.<=0.01] .= NaN
        HB_v .= transpose(B_v.+H_v)
        Makie.surface!(glacier, X, Y, HB_v, colormap=ice_color, shininess=Float32(2^10), transparency=true, shading=true)
        recordframe!(io)
        #(...) 
    end
end

The glacier starts beautifully flowing down the mountain, but after about 25 time steps the glacier flow computations become slow - as if it would do constantly some GC invocations or something. Without the visualisation this slow down is of course not observed. All arrays for the computations are preallocated: there is no allocation happening during the simulation.
When I monitored the memory usage, I saw that it started at about 930 MB before the visulalization and then it increased slowly and at about 25 times steps it had reached about 1 GB. There the memory usage started to oscillate between about 980 MB and 1050 MB.
Note that during the simulation there was still about 5 GBs free memory.

Questions

Do you have any advice on how to mitigate this problem? Should I maybe use other Makie functions for this?

Thanks!!

1 Like

This is pretty much exactly the same problem as in: Makie volume animation froze!

1 Like

Thanks @sdanisch! I am glad to hear that it is an already solved issue :slight_smile:
However, I am completely new to Makie; so could you be a little more specific on what should be changed?

Moreover, could you please explain me the following: to my understanding Makie.surface! would do a update of the surface in place without reallocation. If this is not the case, what is the meaning of the ! in the function name?

It adds a new plot, just like push!

@sdanisch so how would you recommend to update the glacier surface within the time loop?

I think your confusion comes from this line

glacier = Makie.surface!(scene, X, Y, H_v)

The mutating plotting functions like plot!(scene, ... return the scene, not the plot object created (bit weird, but that’s how it is).

You are currently adding a new surface to the scene each frame. That’s super slow.

Instead, you need to update an Observable or Node (same thing) that contains the plotted data. This will cause an update of the plot automatically. There are two ways to do that:

Either you create the Node first, plot it, then update it:

# create the node
data = Node(rand(10, 10))
# plot it
surface(1:10, 1:10, data)
# update it
data[] = rand(10, 10)
# plot updates automatically

Or you plot normal data, it gets wrapped in a Node automatically internally, then you update that internal node by accessing it via index notation. In a surface, the height matrix is the third argument, so this should work:

data = rand(10, 10)
# plot the data
scene = surface(1:10, 1:10, data)
# extract the plot object from the scene (last added plot)
actual_surface_plot = scene[end]
# get the internal node for the height matrix
internal_data_node = actual_surface_plot[3]
# update it
internal_data_node[] = rand(10, 10)
# plot updates automatically

So in your record loop, the only thing you need to do is the node update, the rest is automatic.

6 Likes

Thanks @jules! You got it 100%. I assumed

glacier = Makie.surface!(scene, X, Y, H_v)

would give back a handle for the surface, not for the scene. So, I applied your first suggestion with Observables and got

#(...) 
HB_v = fill(NaN, nx, ny)
HB_vn = Node(HB_v)
scene = Makie.surface(0:dx:lx, 0:dy:ly, transpose(B_v), colormap = cgrad(:terrain, scale=:exp10))
Makie.surface!(scene, X, Y, HB_vn, colormap=ice_color, shininess=Float32(2^10), transparency=true, shading=true)

record(scene, "glacierflow.gif"; framerate = 25) do io
    for it = 1:nt
        #(...) 
        HB_v .= transpose(B_v.+H_v)
        HB_vn[] = HB_v
        recordframe!(io)
        #(...) 
    end
end

This works, but now, I have still one little doubt: I am not familiar with the syntax data[], i.e. what does that exactly do?

Then, here you see my first little thing done with Makie:

It’s very cartoon-like :slight_smile: What is not bad as it is a purely synthetic setup, but I would very much appreciate any ideas to improve it! The colors are certainly something to change…

Thanks!!

5 Likes

Cool plot! Sad that the glacier just melts away like that :wink:

I am not familiar with the syntax data[]

It’s quite simple really, it just looks weird. Square brackets without assignment are special syntax for the function Base.getindex, and assignment with square brackets is special syntax for Base.setindex!. You can overload those however you please for your own types to give square brackets special meaning.

As it happens, Observables (or Nodes) have those two overloaded, so getindex or observable[] just returns the wrapped value inside the Observable, and setindex! or observable[] = xyz sets a new value and additionally calls all listeners of the Observable to “inform” them of the new value.

You can look at the implementation here, it’s very simple https://github.com/JuliaGizmos/Observables.jl/blob/master/src/Observables.jl#L125-L136

This is needed because if you have an Observable observable = Observable(2) then observable = 3 would just assign the Integer 3 to that variable name, while you want to change the content of the Observable observable. It’s the same mechanism as with Base.Ref.

1 Like

Thanks @jules for the additional explanations!

Cool plot! Sad that the glacier just melts away like that 

BTW: what you see here is a simple glacier flow simulation using shallow ice equations: it is a simple nonlinear model of glacier flow over topography under the effect of gravity. To reproduce more what you observe in nature, one would need to add of course other things like precipitation, melting etc.

So thanks again, and if you have any suggestions how to make it look nicer I would be very grateful! BTW: at 22:06 in my JuliaCon 2020 talk, there is a simulation using the same model on a real topography; but the visualization was not done with Julia. I would like to make it at least as nice as that with Makie :slight_smile:

PS: for some reason, I cannot mark @jules first reply as solution. I will retry later…