Fast Time to First Plot with a Custom Sysimage

A relatively long time to first plot is a well-known issue in Julia. Fortunately, there is a workaround this problem using custom a sysimage.

by Martin D. Maas, Ph.D

@MartinDMaas

Last updated: 2021-09-20

Get a fast time to first plot by customizing your Julia sysimage.

When using Julia interactively, we regularly want to plot some data.

Now, the first time we inted to do that within a Julia session, it turns out that we have to wait several seconds before we can see our first plot

For example, when I run the following commands in my Intel i7 laptop:

@time using Plots
@time (p = plot(rand(5), rand(5)); display(p))
3.780245 seconds (6.90 M allocations: 500.497 MiB, 7.18% gc time, 1.11% compilation time)
9.233954 seconds (17.33 M allocations: 990.041 MiB, 3.45% gc time, 16.27% compilation time)

it turns out I have to wait a grand total of 13 seconds for the JIT compiler to finish its job, before I get to see my first plot. Subsequent plotting is, of course, very fast.

Of the possible alternatives to be able to get a fast time to first plot in Julia is the creation of a custom sysimage. This will also allow us to get a first glimpse at a very interesting library, PackageCompiler.jl.

Create a Custom Sysimage

In a few words, a sysimage is a Julia session dumped into a file. It contains precompiled packages, functions, and methods, which can be loaded into memory and used very easily, just as any fast compiled function.

Now, Julia ships with a default sysimage, which only contains the standard library. That is the reason why packages in the standard library can be loaded really fast. For example:

@time using Pkg
0.111626 seconds (1.31 k allocations: 102.734 KiB, 5.12% compilation time)

Naturally, we would like to get the same speed when using other packages, which are not part of the standard library? The answer is to create a custom sysimage with PackageCompiler.jl.

Here’s how to do it:

Step 1: Create a new project

Create a folder to store your project’s files and start Julia. For example, in a bash shell, type:

mkdir MyProject
cd Myproject
julia

Now we need to activate the new project and install the necessary packages. In our case, it will be just Plots. Within a Julia REPL, do the following.

] 
pkg > activate .
(MyProject) pkg > add Plots
(MyProject) pkg > Ctrl+C

We should now have a Project.toml file in the new directory, with some content similar to:

[deps]
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"

and also a Manifest.toml file automatically generated by Julia, describing all of the package dependencies and their corresponding version numbers.

Step 2: Create a file with a representative workflow

It is not only packages that need to be precompiled, but actual functions within those packages. As Julia functions can be called with multiple data types, we need to give the compiler more information on what are the data types that we intend to use in our typicall workflow.

For our example, we want to run the following workflow of loading Plots and displaying some data:

# precompile_plots.jl
using Plots
p = plot(rand(5), rand(5))
display(p)

Step 3: Compile your Custom Sysimage with PackageCompiler.jl

Start a Julia session within your project’s folder.

For convenience, I prefer to use a single-threaded kernel, which can lead to longer compilation times, but allows me to work simultaneously on other things in my PC.

cd MyProject
julia --threads 1

In order to compiler our image, we need to run the following commands:

using PackageCompiler
create_sysimage(:Plots, sysimage_path="JuliaSysimage.so", precompile_execution_file="precompile_plots.jl")
[ Info: ===== Start precompile execution =====
[ Info: ===== End precompile execution =====
[ Info: PackageCompiler: creating system image object file, this might take a while...

This should take a little while (around four minutes in my old Laptop) and we get a file called JuliaSysimage.so of around 200 MB, which is our custom sysimage.

Step 4: Initialize Julia with the Custom Sysimage

We now need a way to tell Julia to load our image instead of the default one.

For example, in order to use our newly created sysimage when starting a Julia REPL from the terminal, we should pass the following option:

julia --sysimage JuliaSysimage.so

Now let’s test our time to first plot again:

@time using Plots
@time (p = plot(LinRange(0,1,5), rand(5),label="Fast Plot"); display(p))
0.000062 seconds (81 allocations: 5.984 KiB)
0.311673 seconds (167.38 k allocations: 3.263 MiB)

Yup! Now the time to first plot is now a fraction of a second!

Compiling and Using a Custom Sysimage in VSCode

Now, what happens if you are working inside an IDE like VSCode?

Interestingly, it looks like the Julia-VSCode team is seeking to automate the whole process detailed in this post. The Julia-VSCode extensions provides this documentation about sysimages.

We can produce a custom sysimage of the current environment by simply running the following commands in the command pallette (Ctrl+Shift+P):

Tasks: Run Build Task
Julia: Build custom sysimage for current environment

However, it should be noted that, at least at the time of writing this, this feature is still experimental. In particular, VSCode will sucessfully call PackageCompiler automatically, but there seems to be no easy way to include a “precompile_execution_file” directly from the VSCode interface. Check out this issue for news on this matter.

So, for the moment, we still have to create a custom sysimage from a terminal, using the above-mentioned four-step procedure.

Now, in order to get VSCode to automatically select our custom image when we start a REPL, we should make sure of the following:

That’s it! Now you will automatically have a fast time to first from within VSCode!