Using a custom Julia sysimage from Quarto

Speeding up time-to-first-plot️ in the Quarto literate programming environment using a PackageCompiler.jl custom sysimage.
Published

15 August 2022

RGBCube, from Lazlo Alonso’s BeautifulMakie

I’ve been looking for a literate programming environment for Julia, and having just come across J.J. Allaire’s JuliaCon 2022 talk, I’m stunned by how much Quarto is getting right.1

But coming to Quarto from a world of REPL-based development (and having never once voluntarily used a Jupyter notebook…) there was some familiarity with Quarto’s Jupyter-based backend that I needed to develop.

In my case, this need to go digging into how Quarto was working came up in the form of Julia’s notorious time-to-first-plot issue. My documents were taking well over a minute to render, and I’m not even nearly that patient.

Elsewhere in the Juliaverse, this can be improved by the PackageCompiler.jl package, which can compile custom sysimages, including projects’ pre-compiled dependencies, and then Julia can be started with the --sysimage <path> flag.

But how do you pass arguments to the Julia runtime when you’re starting it via Quarto?

Configuring Quarto’s Julia kernel

Quarto (it turns out) renders using a Jupyter notebook, and Julia support is implemented via a kernel using the IJulia.jl package. Therefore, the trick to customising the Julia runtime in Quarto was to somehow configure that kernel.

Some rummaging around in Jupyter’s config files revealed a kernels directory, which contained config files for all of the different kernels that had been installed. Each of these had a kernel.json file, which contained the configuration for that kernel — including an argv array containing the flags which were passed to the kernel’s respective binary. 🎉

Later searching (once I’d already identified that kernel.json was a thing) uncovered a command jupyter kernelspec list which lists all kernels available to Jupyter, which would probably be a smarter way for the next person to go looking for these files:

~ ❱ jupyter kernelspec list
Available kernels:
  julia-1.8    /path/to/jupyter/kernels/julia-1.8
  etc.

These contain the kernel.json files, which in Julia’s case looked something like this:

{
  "display_name": "Julia 1.8.0",
  "argv": [
    "/path/to/julia"
    "-i",
    "--color=yes",
    "--project=@.",
    "/path/to/IJulia/kernel.jl",
    "{connection_file}"
  ],
  "env": {},
  "interrupt_mode": "signal",
  "language": "julia"
}

So all that needed to be done was for the sysimage path to be passed in as an argument:

  "argv": [
    # ... args ...
    "--sysimage=/path/to/sysimage.dylib",
    # ... args ...
  ],

What about having a dynamic path to the sysimage? Yeah, I don’t know! I’ll maybe update this post if I ever need it, but the --project=@. argument might give a hint. Otherwise, setting up different kernels for different projects might be the go. Maybe there are Python people screaming at me right now, I don’t know.

Some half-arsed benchmarking

So I’ve successfully customised the Julia kernel config. I think.

Has it actually worked?

Well, here is the plot from the top of this article, rendered directly from this document using CairoMakie (did I not mention this page was being rendered with Quarto? 🙃):

using CairoMakie
using Colors
using GeometryBasics: Rect3f

positions = vec(
    [Point3f(i/5, j/5, k/5) for i=1:7, j=1:7, k=1:7]
)

fig, ax, obj = meshscatter(positions;
    marker = Rect3f(Vec3f(-0.5), Vec3f(1.8)),
    color = [RGBA(positions[i]..., 0.5) for i in 1:length(positions)],
    figure = (; resolution = (1200, 800)),
    transparency = true,
)

display(fig)

Here are some pretty characteristic results from running this a couple of times with v.s. without a compiled sysimage including Makie (and other packages):

So without the sysimage…

 time quarto render
[1/2] 2022/quarto-sysimage/index.qmd

Starting julia-1.8 kernel...Done

Executing 'index.ipynb'
  Cell 1/1...Done

[2/2] index.qmd

Output created: _site/index.html

       51.97 real        48.89 user         3.48 sys

And with the sysimage…

 time quarto render
[1/2] 2022/quarto-sysimage/index.qmd

Starting julia-1.8 kernel...Done

Executing 'index.ipynb'
  Cell 1/1...Done

[2/2] index.qmd

Output created: _site/index.html

       22.19 real        18.61 user         3.13 sys

Over a 2x speedup. Nice! 👏

Footnotes

  1. This is by comparison to other attempts like VS Code notebooks (no shared language server state across cells) and Pluto.jl, which I genuinely adore as a project except for its exclusive focus on being beginner-friendly at the expense of “advanced” features like — check’s notes — using your own text editor. 🤷↩︎