|
| 1 | +# World age issue |
| 2 | + |
| 3 | +## Motivation: lazy loading |
| 4 | + |
| 5 | +The goal of FileIO is to provide a unified IO frontend so that users can easily deal with file IO |
| 6 | +with the simple `load`/`save` functions. The actual IO work will be dispatched to various IO |
| 7 | +backends. For instance, [PNGFiles.jl](https://github.com/JuliaIO/PNGFiles.jl) is used to load PNG |
| 8 | +format images. If `using FileIO` were to load all registered IO backends, then it would be very slow |
| 9 | +to load, hurting all users of FileIO. For any given user, most of those backends would also be |
| 10 | +unnecessary -- for example, people who don't do image processing probably don't want to load any |
| 11 | +thing related to image IO. |
| 12 | + |
| 13 | +To avoid such unnecessary loading latency, FileIO defers package loading until it's actually used. |
| 14 | +For instance, when you use FileIO, you'll probably observe something like this: |
| 15 | + |
| 16 | +```julia |
| 17 | +julia> using TestImages, FileIO |
| 18 | + |
| 19 | +julia> path = testimage("cameraman"; download_only=true) |
| 20 | +"/home/jc/.julia/artifacts/27a4c26bcdd47eb717bee089ec231a899cb8ef69/cameraman.tif" |
| 21 | + |
| 22 | +julia> load(path) # actual backend loading happens here |
| 23 | +[ Info: Precompiling ImageIO [82e4d734-157c-48bb-816b-45c225c6df19] |
| 24 | +[ Info: Precompiling TiffImages [731e570b-9d59-4bfa-96dc-6df516fadf69] |
| 25 | +... |
| 26 | +``` |
| 27 | +
|
| 28 | +ImageIO and TiffImages were loaded because the file in `path` was detected to be a TIFF image, well |
| 29 | +after FileIO was loaded into the session. |
| 30 | +
|
| 31 | +## The hidden issue |
| 32 | +
|
| 33 | +Although this lazy-loading trick reduces the time needed for `using FileIO`, it isn't normal |
| 34 | +practice in Julia because it introduces a so-called _world age issue_ or _world age problem_. The |
| 35 | +world age issue happens when you call methods that get compiled in a newer "world" (get compiled |
| 36 | +after initial compilation finishes) than the one you called them from. |
| 37 | +
|
| 38 | +Let's demonstrate the problem concretely. In case you don't have a suitable file to play with, let's |
| 39 | +first create one: |
| 40 | +
|
| 41 | +```julia |
| 42 | +julia> using IndirectArrays, ImageCore |
| 43 | + |
| 44 | +julia> img = IndirectArray(rand(1:5, 4, 4), rand(RGB, 5)) |
| 45 | +4×4 IndirectArray{RGB{Float64}, 2, Int64, Matrix{Int64}, Vector{RGB{Float64}}}: |
| 46 | +... |
| 47 | + |
| 48 | +julia> save("indexed_image.png", img) |
| 49 | +``` |
| 50 | +
|
| 51 | +Now, **reopen a new julia REPL** (this is crucial for demonstrating the problem) and call `load` |
| 52 | +from **within a function** (this is also crucial): |
| 53 | +
|
| 54 | +```julia |
| 55 | +julia> using FileIO |
| 56 | + |
| 57 | +julia> f() = size(load("indexed_image.png")) |
| 58 | +f (generic function with 1 method) |
| 59 | + |
| 60 | +julia> f() |
| 61 | +ERROR: MethodError: no method matching size(::IndirectArrays.IndirectArray{ColorTypes.RGB{FixedPointNumbers.N0f8}, 2, UInt8, Matrix{UInt8}, OffsetArrays.OffsetVector{ColorTypes.RGB{FixedPointNumbers.N0f8}, Vector{ColorTypes.RGB{FixedPointNumbers.N0f8}}}}) |
| 62 | +The applicable method may be too new: running in world age 32382, while current world is 32416. |
| 63 | +Closest candidates are: |
| 64 | + size(::IndirectArrays.IndirectArray) at ~/.julia/packages/IndirectArrays/BUQO3/src/IndirectArrays.jl:52 (method too new to be called from this world context.) |
| 65 | + size(::AbstractArray{T, N}, ::Any) where {T, N} at abstractarray.jl:42 |
| 66 | + size(::Union{LinearAlgebra.Adjoint{T, var"#s880"}, LinearAlgebra.Transpose{T, var"#s880"}} where {T, var"#s880"<:(AbstractVector)}) at /Applications/Julia-1.8.app/Contents/Resources/julia/share/julia/stdlib/v1.8/LinearAlgebra/src/adjtrans.jl:173 |
| 67 | + ... |
| 68 | +Stacktrace: |
| 69 | + [1] f() |
| 70 | + @ Main ./REPL[2]:1 |
| 71 | + [2] top-level scope |
| 72 | + @ REPL[3]:1 |
| 73 | +``` |
| 74 | +
|
| 75 | +To understand why this happened, you have to understand the order of events: |
| 76 | +
|
| 77 | +1. When calling `f()` from the REPL, Julia first compiled `f`. Importantly, when compiling, Julia |
| 78 | + didn't know what type of object was going to be returned by `load`, so in the compiled code it |
| 79 | + waits to see what object actually gets returned before figuring out which method of `size` to |
| 80 | + call. (This is called _runtime dispatch_.) |
| 81 | +2. It queried the file, recognized a PNG file, and _loaded the ImageIO and PNGFiles packages_. (It's |
| 82 | + for the loading of these packages that you needed to start a fresh Julia session.) |
| 83 | +3. FileIO calls the appropriate PNG-specific `load` function in PNGFiles. (We'll have more to say |
| 84 | + about this step further below.) This causes an image to be returned, which is an array of a type |
| 85 | + defined by the IndirectArrays package (a dependency of PNGFiles). |
| 86 | +4. `f` calls `size` on the returned image. However, this fails, because at the time you called `f`, |
| 87 | + the IndirectArrays package wasn't loaded. |
| 88 | +
|
| 89 | +In other words, `size` method for `IndirectArray` lives in a world that's newer than the one from |
| 90 | +which you called `f()`. This leads to the observed error. |
| 91 | +
|
| 92 | +!!! note |
| 93 | + World age is crucial to Julia's ability to allow you to _redefine_ methods interactively, but |
| 94 | + the error we're illustrating is an unfortunate side-effect. |
| 95 | +
|
| 96 | +The good news is it's easy to fix, just try calling `f()` again: |
| 97 | +
|
| 98 | +```julia |
| 99 | +julia> f() |
| 100 | +(4, 4) |
| 101 | +``` |
| 102 | +
|
| 103 | +The second `f()` works because this time you're calling `f()` in the latest world age with the |
| 104 | +necessary `size(::IndirectArray)` already defined. In essence, you fast-forward to the latest world |
| 105 | +with each statement you type at the REPL. |
| 106 | +
|
| 107 | +## Solutions |
| 108 | +
|
| 109 | +### `Base.invokelatest` |
| 110 | +
|
| 111 | +One solution is to make the call to `size` via `Base.invokelatest`, which exists explicitly to work |
| 112 | +around this world-age dispatch problem. Literally, `invokelatest` dispatches the supplied call using |
| 113 | +the latest world age (which may be newer than when you typed `f()` at the REPL). **In a fresh Julia |
| 114 | +session**, |
| 115 | +
|
| 116 | +```julia |
| 117 | +julia> using FileIO |
| 118 | + |
| 119 | +julia> f() = Base.invokelatest(size, load("indexed_image.png")) |
| 120 | +f (generic function with 1 method) |
| 121 | + |
| 122 | +julia> f() |
| 123 | +(4, 4) |
| 124 | +``` |
| 125 | +
|
| 126 | +!!! note |
| 127 | + In step 3 above ("FileIO calls the appropriate PNG-specific `load` function in PNGFiles"), |
| 128 | + the call to the `load` function defined in PNGFiles is made via `invokelatest`. |
| 129 | + Otherwise, even ordinary interactive usage of FileIO (without burying `load` inside a function) |
| 130 | + would cause world-age errors. |
| 131 | +
|
| 132 | +!!! warning |
| 133 | + Using `invokelatest` slows your code considerably. Use it only when absolutely necessary. |
| 134 | +
|
| 135 | +### Eagerly load the required packages first |
| 136 | +
|
| 137 | +Another solution to the world age issue is simple and doesn't have long-term downsides: **eagerly |
| 138 | +load the needed packages**. For instance, if you're seeing world age issue complaining methods |
| 139 | +related to `IndirectArray`, then load IndirectArrays eagerly: |
| 140 | +
|
| 141 | +```julia |
| 142 | +julia> using FileIO, IndirectArrays # try this on a new Julia REPL |
| 143 | + |
| 144 | +julia> f() = size(load("indexed_image.png")) |
| 145 | +f (generic function with 1 method) |
| 146 | + |
| 147 | +julia> f() |
| 148 | +(4, 4) |
| 149 | +``` |
| 150 | +
|
| 151 | +Thus if you want to build a package, it could be something like this: |
| 152 | +
|
| 153 | +```julia |
| 154 | +module MyFancyPackage |
| 155 | + |
| 156 | +# This ensures that whoever loads `MyFancyPackage`, he has IndirectArrays loaded and |
| 157 | +# thus avoid the world age issue. |
| 158 | +using IndirectArrays, FileIO |
| 159 | + |
| 160 | +f(file) = length(load(file)) |
| 161 | +end |
| 162 | +``` |
| 163 | +
|
| 164 | +Enjoy the FileIO and its lazy loading, but be aware that its speedy loading comes with some caveats. |
0 commit comments