Fixing invalidation - what would an expert do?

First of all, thank you for the awesome tooling around this. I appreciate there isn’t a straightforward answer to each case.

I’ve re-read and re-watched a few times:

It all makes sense when explained, but I’m struggling to convert my understanding into actions on real-life examples (eg, when to invokelatest, add concrete type annotations to caller body, open an issue with an upstream pkg or even with Julia Base)

Is there a resource that I missed / that I should look into?

Could you perhaps comment on what you would do in the following cases?

I’ve been looking at precompiling some Genie/Stipple-based apps and noticed a few cases where I didn’t know what the right next step would be.

Using the standard snippet (awesome idea to have this copy&paste-able snippet!):

using SnoopCompileCore
invalidations = @snoopr begin
    using GenieFramework
end
using SnoopCompile
length(uinvalidated(invalidations))
trees = invalidation_trees(invalidations)
methinvs = trees[end];
root = methinvs.backedges[end]
ascend(root)

1) Example with unlock(io)
On the last record (most invalidations), I get

methinvs = trees[end]
 inserting unlock(io::GarishPrint.GarishIO) @ GarishPrint ~/.julia/packages/GarishPrint/214Ip/src/io.jl:153 invalidated:
   backedges: 1: superseding unlock(::IO) @ Base io.jl:27 with MethodInstance for unlock(::IO) (385 children)
   6 mt_cache

I hit the ascend and jump to the last concrete call

julia> ascend(root)
Choose a call for analysis (q to quit):
     unlock(::IO)
       print(::IO, ::String, ::String)
         println(::IO, ::String)
 >         println(::String)
             deep_clean!(::Pkg.Resolve.Graph)
               #simplify_graph!#117(::Bool, ::typeof(Pkg.Resolve.simplify_graph!), ::Pkg.Resolve.Graph, ::Set{Int64})
                 simplify_graph!(::Pkg.Resolve.Graph, ::Set{Int64})
                   simplify_graph!(::Pkg.Resolve.Graph)
                     trigger_failure!(::Pkg.Resolve.Graph, ::Vector{Int64}, ::Tuple{Int64, Int64})
v                      _resolve(::Pkg.Resolve.Graph, ::Nothing, ::Nothing)

# that shows 
println(xs...) @ Base ~/.julia/juliaup/julia-1.9.0-rc3+0.aarch64.apple.darwin14/share/julia/base/coreio.jl:4
4 println(xs::Tuple{String}...)::Core.Const(nothing) = println(stdout::Any, xs::Tuple{String}...)

It tells me that the inference fails because Any comes from printing into stdout.
That’s confusing because:

  • it’s extremely common, so I would expect problems everywhere
  • I thought stdout is a concrete type (Base.TTY)
  • the inference correctly identifies that all the returns are nothing

Perhaps I shouldn’t be looking at the backedges, so I looked at the code that inserted the new method, but that’s defined on a concrete type.
It doesn’t feel like “stomping out the Any type” applies here.

2) Example with peek(io)
After the failure with the last “tree”, I move to the one before last

julia> ascend(trees[end-1].backedges[end])
Choose a call for analysis (q to quit):
     peek(::IO)
       read(::IO, ::Type{Char})
         iterate(::Base.ReadEachIterator{Char}, ::Nothing)
           iterate(::Base.ReadEachIterator{Char})
 >           #readuntil#433(::Bool, ::typeof(readuntil), ::IO, ::Char)
               readuntil(::IO, ::Char)
                 #readuntil#411(::Base.Pairs, ::typeof(readuntil), ::Base.AbstractPipe, ::Char)
                   readuntil(::Base.AbstractPipe, ::Char)
                     #readuntil#411(::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}, ::typeof(re
v                      readuntil(::Base.Process, ::Char)

So I descend into the last line with Base.Process, as it’s the last concrete call and I ultimately end up with:

readuntil((pipe_reader(io::Base.AbstractPipe)::Any::IO::Type{IO})::IO, arg::Char; kw::Base.Pairs{Symbol,…

I’m bit lost because I have no idea how to concretize what will come out of that pipe.
That’s another failure on my part.

3) Example with convert(T,…)
So I clearly have no idea when it comes to IO, so I figured to jump to convert function next, because I have noticed that it features in every single attempt I’ve made at analyzing invalidations (across many different packages).

So I jump into:

 inserting convert(::Type{T}, x::EzXML.ReaderType) where T<:Integer @ EzXML ~/.julia/packages/EzXML/ZNwhK/src/streamreader.jl:54 invalidated:
   backedges: 1: superseding convert(::Type{T}, x::Number) where T<:Number @ Base number.jl:7 with MethodInstance for convert(::Type{Int64}, ::Integer) (22 children)
              2: superseding convert(::Type{T}, x::Number) where T<:Number @ Base number.jl:7 with MethodInstance for convert(::Type{UInt64}, ::Integer) (28 children)
              3: superseding convert(::Type{T}, x::Number) where T<:Number @ Base number.jl:7 with MethodInstance for convert(::Type{UInt64}, ::Integer) (1 children)
              4: superseding convert(::Type{T}, x::Number) where T<:Number @ Base number.jl:7 with MethodInstance for convert(::Type{UInt64}, ::Integer) (7 children)
              5: superseding convert(::Type{T}, x::Number) where T<:Number @ Base number.jl:7 with MethodInstance for convert(::Type{Int8}, ::Integer) (40 children)
              6: superseding convert(::Type{T}, x::Number) where T<:Number @ Base number.jl:7 with MethodInstance for convert(::Type{UInt64}, ::Integer) (10 children)
              7: superseding convert(::Type{T}, x::Number) where T<:Number @ Base number.jl:7 with MethodInstance for convert(::Type{Int64}, ::Integer) (68 children)

And descript scrolling to the bottom of the list, I haven’t found any line where the “reds” (abstract types) start:

julia> ascend(trees[end-4].backedges[end])
Choose a call for analysis (q to quit):
^      to_index(::Integer)
         to_index(::Vector{Int64}, ::Integer)
           _to_indices1(::Vector{Int64}, ::Tuple{}, ::Integer)
             to_indices(::Vector{Int64}, ::Tuple{}, ::Tuple{Integer})
               to_indices(::Vector{Int64}, ::Tuple{Integer})
                 setindex!(::Vector{Int64}, ::Int64, ::Union{Integer, CartesianIndex})
                   sortperm_int_range(::Vector{T} where T<:Integer, ::Any, ::Any)
 >                   #_sortperm#33(::Base.Sort.MissingOptimization{Base.Sort.BoolOptimization{Base.Sort.Small{10
                       kwcall(::NamedTuple{(:alg, :order, :scratch), Tuple{Base.Sort.MissingOptimization{Base.So
v                        #sortperm#32(::Base.Sort.MissingOptimization{Base.Sort.BoolOptimization{Base.Sort.Small

...

 >       to_index(::Vector{Symbol}, ::Number)
           _to_indices1(::Vector{Symbol}, ::Tuple{Base.OneTo{Int64}}, ::Number)
             to_indices(::Vector{Symbol}, ::Tuple{Base.OneTo{Int64}}, ::Tuple{Number})
               to_indices(::Vector{Symbol}, ::Tuple{Number})
                 getindex(::Vector{Symbol}, ::Number)
                   maybeview(::Vector{Symbol}, ::Union{Number, Base.AbstractCartesianIndex})

Apologies for the long post, but would you have any tips / advice how to:

  • proceed in the above cases?
  • identify which cases are “easier” to fix for people learning about invals. (eg, “avoid all convert()”, “avoid io”, etc.)

Thank you!

1 Like

stdout is a global variable that is just guaranteed to be an IO - which is an abstract type. You can change stdout, e.g.,

julia> redirect_stdout(devnull) do
           @info "Printing" typeof(stdout) typeof(stderr)
       end
┌ Info: Printing
│   typeof(stdout) = Base.DevNull
└   typeof(stderr) = Base.TTY

It’s not only about the inference of return types but also about the methods that need to be called. Since unlock(::IO) appears to be used and a new method of unlock is added, invalidations occur.

2 Likes

Oh, right! I can see why invalidations happen.

What could be the solution? (If there is even one)
It seems like something that will affect any package that implements its own IO.

That’s not a problem if the result isn’t used. Inference failures only matter when Julia can’t predict which MethodInstance to call, and an unused output is never an argument in a call.

One other thing to note: anything under deep_clean! is in Pkg. While it’s great to make Julia and its stdlibs more resistant to invalidation (I’ve spent a lot of time doing that myself), do keep in mind that it may be irrelevant for specific tasks you want to perform. That is to say, this invalidation may affect you the next time you, say, update your packages in the same session, but it may not affect GenieFramework code unless GenieFramework interacts with Pkg.

So I descend into the last line with Base.Process, as it’s the last concrete call

IO objects are tough this way:

julia> fieldnames(Base.Process)
(:cmd, :handle, :in, :out, :err, :exitcode, :termsignal, :exitnotify)

julia> fieldtypes(Base.Process)
(Cmd, Ptr{Nothing}, IO, IO, IO, Int64, Int32, Base.GenericCondition{Base.Threads.SpinLock})

The specific IO subtype is not typically specialized on, and that’s what sets up the invalidation you’re seeing. This is a very difficult invalidation to fix. You might be better off with PrecompileTools.@recompile_invalidations.

sortperm_int_range(::Vector{T} where T<:Integer, ::Any, ::Any)

There seems to possibly be some missing info about the caller here? (The ...) Whatever is creating the Vector{<:Integer} is likely the problem.

1 Like