diff --git a/Project.toml b/Project.toml index f7eb817db..a108580f7 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "SnoopCompile" uuid = "aa65fe97-06da-5843-b5b1-d5d13cad87d2" author = ["Tim Holy "] -version = "1.2.4" +version = "1.3.0" [deps] OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" @@ -16,11 +16,13 @@ julia = "1" ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" +InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" JLD = "4138dd39-2aa7-5051-a626-17a0bb65d9c8" MatLang = "05b439c0-bb3c-11e9-1d8d-1f0a9ebca87a" +MethodAnalysis = "85b6ec6f-f7df-4429-9514-a64bcd9ee824" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Pkg", "ColorTypes", "Documenter", "Test", "FixedPointNumbers", "JLD", "SparseArrays", "MatLang"] +test = ["ColorTypes", "Documenter", "FixedPointNumbers", "InteractiveUtils", "JLD", "MatLang", "MethodAnalysis", "Pkg", "SparseArrays", "Test"] diff --git a/docs/make.jl b/docs/make.jl index e11c70012..6d0252a9a 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -7,7 +7,7 @@ makedocs( prettyurls = get(ENV, "CI", nothing) == "true" ), modules = [SnoopCompile], - pages = ["index.md", "snoopi.md", "snoopc.md", "userimg.md", "bot.md", "reference.md"] + pages = ["index.md", "snoopi.md", "snoopc.md", "userimg.md", "bot.md", "snoopr.md", "reference.md"] ) deploydocs( diff --git a/docs/src/index.md b/docs/src/index.md index a7c82d035..04518f555 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -5,6 +5,13 @@ functions and argument types it's compiling. From these lists of methods, you can generate lists of `precompile` directives that may reduce the latency between loading packages and using them to do "real work." +SnoopCompile can also detect and analyze *method cache invalidations*, +which occur when new method definitions alter dispatch in a way that forces Julia to discard previously-compiled code. +Any later usage of invalidated methods requires recompilation. +Invalidation can trigger a domino effect, in which all users of invalidated code also become invalidated, propagating all the way back to the top-level call. +When a source of invalidation can be identified and either eliminated or mitigated, +you can reduce the amount of work that the compiler needs to repeat and take better advantage of precompilation. + ## Background Julia uses diff --git a/docs/src/snoopr.md b/docs/src/snoopr.md new file mode 100644 index 000000000..b908e335f --- /dev/null +++ b/docs/src/snoopr.md @@ -0,0 +1,217 @@ +# Snooping on invalidations: `@snoopr` + +## Recording invalidations + +```@meta +DocTestFilters = r"(REPL\[\d+\]|none):\d+" +DocTestSetup = quote + using SnoopCompile +end +``` + +Invalidations occur when there is a danger that new methods would supersede older methods in previously-compiled code. +We can illustrate this process with the following example: + +```jldoctest invalidations +julia> f(::Real) = 1; + +julia> callf(container) = f(container[1]); + +julia> call2f(container) = callf(container); +``` + +Let's run this with different container types: +```jldoctest invalidations +julia> c64 = [1.0]; c32 = [1.0f0]; cabs = AbstractFloat[1.0]; + +julia> call2f(c64) +1 + +julia> call2f(c32) +1 + +julia> call2f(cabs) +1 +``` + +It's important that you actually execute these methods: code doesn't get compiled until it gets run, and invalidations only affect compiled code. + +Now we'll define a new `f` method, one specialized for `Float64`. +So we can see the consequences for the compiled code, we'll make this definition while snooping on the compiler with `@snoopr`: + +```jldoctest invalidations +julia> trees = invalidation_trees(@snoopr f(::Float64) = 2) +1-element Array{SnoopCompile.MethodInvalidations,1}: + insert f(::Float64) in Main at REPL[9]:1 invalidated: + backedges: 1: superseding f(::Real) in Main at REPL[2]:1 with MethodInstance for f(::Float64) (2 children) more specific + 2: superseding f(::Real) in Main at REPL[2]:1 with MethodInstance for f(::AbstractFloat) (2 children) more specific +``` + +The list of `MethodInvalidations` indicates that some previously-compiled code got invalidated. +In this case, "`insert f(::Float64)`" means that a new method, for `f(::Float64)`, was added. +There were two proximal triggers for the invalidation, both of which superseded the method `f(::Real)`. +One of these had been compiled specifically for `Float64`, due to our `call2f(c64)`. +The other had been compiled specifically for `AbstractFloat`, due to our `call2f(cabs)`. + +You can look at these invalidation trees in greater detail: + +```jldoctest invalidations +julia> tree = trees[1]; + +julia> root = tree.backedges[1] +MethodInstance for f(::Float64) at depth 0 with 2 children + +julia> show(root) +MethodInstance for f(::Float64) (2 children) + MethodInstance for callf(::Array{Float64,1}) (1 children) + ⋮ + +julia> show(root; minchildren=0) +MethodInstance for f(::Float64) (2 children) + MethodInstance for callf(::Array{Float64,1}) (1 children) + MethodInstance for call2f(::Array{Float64,1}) (0 children) +``` + +You can see that the sequence of invalidations proceeded all the way up to `call2f`. +Examining `root2 = tree.backedges[2]` yields similar results, but for `Array{AbstractFloat,1}`. + +The structure of these trees can be considerably more complicated. For example, if `callf` +also got called by some other method, and that method had also been executed (forcing it to be compiled), +then `callf` would have multiple children. +This is often seen with more complex, real-world tests: + +```julia +julia> trees = invalidation_trees(@snoopr using SIMD) +4-element Array{SnoopCompile.MethodInvalidations,1}: + insert convert(::Type{Tuple{Vararg{R,N}}}, v::Vec{N,T}) where {N, R, T} in SIMD at /home/tim/.julia/packages/SIMD/Am38N/src/SIMD.jl:182 invalidated: + mt_backedges: 1: signature Tuple{typeof(convert),Type{Tuple{DataType,DataType,DataType}},Any} triggered MethodInstance for Pair{DataType,Tuple{DataType,DataType,DataType}}(::Any, ::Any) (0 children) ambiguous + 2: signature Tuple{typeof(convert),Type{NTuple{8,DataType}},Any} triggered MethodInstance for Pair{DataType,NTuple{8,DataType}}(::Any, ::Any) (0 children) ambiguous + 3: signature Tuple{typeof(convert),Type{NTuple{7,DataType}},Any} triggered MethodInstance for Pair{DataType,NTuple{7,DataType}}(::Any, ::Any) (0 children) ambiguous + + insert convert(::Type{Tuple}, v::Vec{N,T}) where {N, T} in SIMD at /home/tim/.julia/packages/SIMD/Am38N/src/SIMD.jl:188 invalidated: + mt_backedges: 1: signature Tuple{typeof(convert),Type{Tuple},Any} triggered MethodInstance for Distributed.RemoteDoMsg(::Any, ::Any, ::Any) (1 children) more specific + 2: signature Tuple{typeof(convert),Type{Tuple},Any} triggered MethodInstance for Distributed.CallMsg{:call}(::Any, ::Any, ::Any) (1 children) more specific + 3: signature Tuple{typeof(convert),Type{Tuple},Any} triggered MethodInstance for Distributed.CallMsg{:call_fetch}(::Any, ::Any, ::Any) (1 children) more specific + 4: signature Tuple{typeof(convert),Type{Tuple},Any} triggered MethodInstance for Distributed.CallWaitMsg(::Any, ::Any, ::Any) (4 children) more specific + 12 mt_cache + + insert <<(x1::T, v2::Vec{N,T}) where {N, T<:Union{Bool, Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8}} in SIMD at /home/tim/.julia/packages/SIMD/Am38N/src/SIMD.jl:1061 invalidated: + mt_backedges: 1: signature Tuple{typeof(<<),UInt64,Any} triggered MethodInstance for <<(::UInt64, ::Integer) (0 children) ambiguous + 2: signature Tuple{typeof(<<),UInt64,Any} triggered MethodInstance for copy_chunks_rtol!(::Array{UInt64,1}, ::Integer, ::Integer, ::Integer) (0 children) ambiguous + 3: signature Tuple{typeof(<<),UInt64,Any} triggered MethodInstance for copy_chunks_rtol!(::Array{UInt64,1}, ::Int64, ::Int64, ::Integer) (0 children) ambiguous + 4: signature Tuple{typeof(<<),UInt64,Any} triggered MethodInstance for copy_chunks_rtol!(::Array{UInt64,1}, ::Integer, ::Int64, ::Integer) (0 children) ambiguous + 5: signature Tuple{typeof(<<),UInt64,Any} triggered MethodInstance for <<(::UInt64, ::Unsigned) (16 children) ambiguous + 20 mt_cache + + insert +(s1::Union{Bool, Float16, Float32, Float64, Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8, Ptr}, v2::Vec{N,T}) where {N, T<:Union{Float16, Float32, Float64}} in SIMD at /home/tim/.julia/packages/SIMD/Am38N/src/SIMD.jl:1165 invalidated: + mt_backedges: 1: signature Tuple{typeof(+),Ptr{UInt8},Any} triggered MethodInstance for handle_err(::JuliaInterpreter.Compiled, ::JuliaInterpreter.Frame, ::Any) (0 children) ambiguous + 2: signature Tuple{typeof(+),Ptr{UInt8},Any} triggered MethodInstance for #methoddef!#5(::Bool, ::typeof(LoweredCodeUtils.methoddef!), ::Any, ::Set{Any}, ::JuliaInterpreter.Frame) (0 children) ambiguous + 3: signature Tuple{typeof(+),Ptr{UInt8},Any} triggered MethodInstance for #get_def#94(::Set{Tuple{Revise.PkgData,String}}, ::typeof(Revise.get_def), ::Method) (0 children) ambiguous + 4: signature Tuple{typeof(+),Ptr{Nothing},Any} triggered MethodInstance for filter_valid_cachefiles(::String, ::Array{String,1}) (0 children) ambiguous + 5: signature Tuple{typeof(+),Ptr{Union{Int64, Symbol}},Any} triggered MethodInstance for pointer(::Array{Union{Int64, Symbol},N} where N, ::Int64) (1 children) ambiguous + 6: signature Tuple{typeof(+),Ptr{Char},Any} triggered MethodInstance for pointer(::Array{Char,N} where N, ::Int64) (2 children) ambiguous + 7: signature Tuple{typeof(+),Ptr{_A} where _A,Any} triggered MethodInstance for pointer(::Array{T,N} where N where T, ::Int64) (4 children) ambiguous + 8: signature Tuple{typeof(+),Ptr{Nothing},Any} triggered MethodInstance for _show_default(::IOContext{Base.GenericIOBuffer{Array{UInt8,1}}}, ::Any) (49 children) ambiguous + 9: signature Tuple{typeof(+),Ptr{Nothing},Any} triggered MethodInstance for _show_default(::Base.GenericIOBuffer{Array{UInt8,1}}, ::Any) (336 children) ambiguous + 10: signature Tuple{typeof(+),Ptr{UInt8},Any} triggered MethodInstance for pointer(::String, ::Integer) (1027 children) ambiguous + 2 mt_cache +``` + +Your specific output will surely be different from this, depending on which packages you have loaded, +which versions of those packages are installed, and which version of Julia you are using. +In this example, there were four different methods that triggered invalidations, and the invalidated methods were in `Base`, +`Distributed`, `JuliaInterpeter`, and `LoweredCodeUtils`. (The latter two were a consequence of loading `Revise`.) +You can see that collectively more than a thousand independent compiled methods needed to be invalidated; indeed, the last +entry alone invalidates 1027 method instances: + +``` +julia> sig, node = trees[end].mt_backedges[10] +Pair{Any,SnoopCompile.InstanceTree}(Tuple{typeof(+),Ptr{UInt8},Any}, MethodInstance for pointer(::String, ::Integer) at depth 0 with 1027 children) + +julia> node +MethodInstance for pointer(::String, ::Integer) at depth 0 with 1027 children + +julia> show(node) +MethodInstance for pointer(::String, ::Integer) (1027 children) + MethodInstance for repeat(::String, ::Integer) (1023 children) + MethodInstance for ^(::String, ::Integer) (1019 children) + MethodInstance for #handle_message#2(::Nothing, ::Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}, ::typeof(Base.CoreLogging.handle_message), ::Logging.ConsoleLogger, ::Base.CoreLogging.LogLevel, ::String, ::Module, ::Symbol, ::Symbol, ::String, ::Int64) (906 children) + MethodInstance for handle_message(::Logging.ConsoleLogger, ::Base.CoreLogging.LogLevel, ::String, ::Module, ::Symbol, ::Symbol, ::String, ::Int64) (902 children) + MethodInstance for log_event_global!(::Pkg.Resolve.Graph, ::String) (35 children) + ⋮ + MethodInstance for #artifact_meta#20(::Pkg.BinaryPlatforms.Platform, ::typeof(Pkg.Artifacts.artifact_meta), ::String, ::Dict{String,Any}, ::String) (43 children) + ⋮ + MethodInstance for Dict{Base.UUID,Pkg.Types.PackageEntry}(::Dict) (79 children) + ⋮ + MethodInstance for read!(::Base.Process, ::LibGit2.GitCredential) (80 children) + ⋮ + MethodInstance for handle_err(::JuliaInterpreter.Compiled, ::JuliaInterpreter.Frame, ::Any) (454 children) + ⋮ + ⋮ + ⋮ + ⋮ + ⋮ +⋮ +``` + +Many nodes in this tree have multiple "child" branches. + +## Avoiding or fixing invalidations + +Invalidations occur in situations like our `call2f(c64)` example, where we changed our mind about what value `f` should return for `Float64`. +Julia could not have returned the newly-correct answer without recompiling the call chain. + +Aside from cases like these, most invalidations occur whenever new types are introduced, +and some methods were previously compiled for abstract types. +In some cases, this is inevitable, and the resulting invalidations simply need to be accepted as a consequence of a dynamic, updateable language. +(You can often minimize invalidations by loading all your code at the beginning of your session, before triggering the compilation of more methods.) +However, in many circumstances an invalidation indicates an opportunity to improve code. +In our first example, note that the call `call2f(c32)` did not get invalidated: this is because the compiler +knew all the specific types, and new methods did not affect any of those types. +The main tips for writing invalidation-resistant code are: + +- use [concrete types](https://docs.julialang.org/en/latest/manual/performance-tips/#man-performance-abstract-container-1) wherever possible +- write inferrable code +- don't engage in [type-piracy](https://docs.julialang.org/en/latest/manual/style-guide/#Avoid-type-piracy-1) (our `c64` example is essentially like type-piracy, where we redefined behavior for a pre-existing type) + +Since these tips also improve performance and allow programs to behave more predictably, +these guidelines are not intrusive. +Indeed, searching for and eliminating invalidations can help you improve the quality of your code. +In cases where invalidations occur, but you can't use concrete types (there are many valid uses of `Vector{Any}`), +you can often prevent the invalidation using some additional knowledge. +For example, suppose you're writing code that parses Julia's `Expr` type: + +```julia +julia> ex = :(Array{Float32,3}) +:(Array{Float32, 3}) + +julia> dump(ex) +Expr + head: Symbol curly + args: Array{Any}((3,)) + 1: Symbol Array + 2: Symbol Float32 + 3: Int64 3 +``` + +`ex.args` is a `Vector{Any}`. +However, for a `:curly` expression only certain types will be found among the arguments; you could write key portions of your code as + +``` +a = ex.args[2] +if a isa Symbol + # inside this block, Julia knows `a` is a Symbol, and so methods called on `a` will be resistant to invalidation + foo(a) +elseif a isa Expr && length((a::Expr).args) > 2 + a = a::Expr # sometimes you have to help inference by adding a type-assert + x = bar(a) # `bar` is now resistant to invalidation +elsef a isa Integer + # even though you've not made this fully-inferrable, you've at least reduced the scope for invalidations + # by limiting the subset of `foobar` methods that might be called + y = foobar(a) +end +``` + +Adding type-assertions and fixing inference problems are the most common approaches for fixing invalidations. +You can discover these manually, but the [Cthulhu](https://github.com/JuliaDebug/Cthulhu.jl) package is highly recommended. +Cthulu's `ascend`, in particular, allows you to navigate an invalidation tree and focus on those branches with the most severe consequences (frequently, the most children). diff --git a/examples/invalidations_blog.jl b/examples/invalidations_blog.jl new file mode 100644 index 000000000..8166dbfe7 --- /dev/null +++ b/examples/invalidations_blog.jl @@ -0,0 +1,56 @@ +using SnoopCompile +using SnoopCompile: countchildren + +function hastv(typ) + isa(typ, UnionAll) && return true + if isa(typ, DataType) + for p in typ.parameters + hastv(p) && return true + end + end + return false +end + +trees = invalidation_trees(@snoopr using Revise) + +function summary(trees) + npartial = ngreater = nlesser = nambig = nequal = 0 + for methodtree in trees + method = methodtree.method + invs = methodtree.invalidations + for fn in (:mt_backedges, :backedges) + list = getfield(invs, fn) + for item in list + sig = nothing + if isa(item, Pair) + sig = item.first + item = item.second + else + sig = item.mi.def.sig + end + # if hastv(sig) + # npartial += countchildren(invtree) + # else + ms1, ms2 = method.sig <: sig, sig <: method.sig + if ms1 && !ms2 + ngreater += countchildren(item) + elseif ms2 && !ms1 + nlesser += countchildren(item) + elseif ms1 && ms2 + nequal += countchildren(item) + else + # if hastv(sig) + # npartial += countchildren(item) + # else + nambig += countchildren(item) + # end + end + # end + end + end + end + @assert nequal == 0 + println("$ngreater | $nlesser | $nambig |") # $npartial |") +end + +summary(trees) diff --git a/src/SnoopCompile.jl b/src/SnoopCompile.jl index bfcfc58d1..3d8e459b1 100644 --- a/src/SnoopCompile.jl +++ b/src/SnoopCompile.jl @@ -1,6 +1,7 @@ module SnoopCompile using Serialization, OrderedCollections +using Core: MethodInstance, CodeInfo export timesum # @snoopi and @snoopc are exported from their files of definition @@ -20,4 +21,8 @@ include("parcel_snoopc.jl") include("write.jl") include("bot.jl") +if VERSION >= v"1.6.0-DEV.154" + include("invalidations.jl") +end + end # module diff --git a/src/bot/snoopi_bench.jl b/src/bot/snoopi_bench.jl index efd58ac0f..138a6c1bd 100644 --- a/src/bot/snoopi_bench.jl +++ b/src/bot/snoopi_bench.jl @@ -44,7 +44,7 @@ end timesum(runSnoop) ``` """ -function timesum(snoop::Vector{Tuple{Float64, Core.MethodInstance}}) +function timesum(snoop::Vector{Tuple{Float64, MethodInstance}}) if isempty(snoop) return 0.0 else diff --git a/src/invalidations.jl b/src/invalidations.jl new file mode 100644 index 000000000..539fb3fa4 --- /dev/null +++ b/src/invalidations.jl @@ -0,0 +1,294 @@ +export @snoopr, invalidation_trees, filtermod + +dummy() = nothing +dummy() +const dummyinstance = which(dummy, ()).specializations[1] + +mutable struct InstanceTree + mi::MethodInstance + depth::Int32 + children::Vector{InstanceTree} + parent::InstanceTree + + # Create tree root, but return a leaf + function InstanceTree(mi::MethodInstance, depth) + tree = new(mi, depth, InstanceTree[]) + child = tree + while depth > 0 + depth -= 1 + parent = new(dummyinstance, depth, InstanceTree[]) + push!(parent.children, child) + child.parent = parent + child = parent + end + return tree + end + # Create child + function InstanceTree(mi::MethodInstance, parent::InstanceTree, depth) + @assert parent.depth + Int32(1) == depth + new(mi, depth, InstanceTree[], parent) + end +end + +function getroot(node::InstanceTree) + while isdefined(node, :parent) + node = node.parent + end + return node +end + +function Base.any(f, node::InstanceTree) + f(node) && return true + return any(f, node.children) +end + +function Base.show(io::IO, node::InstanceTree; methods=false, maxdepth::Int=5, minchildren::Int=round(Int, sqrt(countchildren(node)))) + if get(io, :limit, false) + print(io, node.mi, " at depth ", node.depth, " with ", countchildren(node), " children") + else + nc = map(countchildren, node.children) + s = sum(nc) + length(node.children) + indent = " "^Int(node.depth) + print(io, indent, methods ? node.mi.def : node.mi) + println(io, " (", s, " children)") + p = sortperm(nc) + skipped = false + for i in p + child = node.children[i] + if child.depth <= maxdepth && nc[i] >= minchildren + show(io, child; methods=methods, maxdepth=maxdepth, minchildren=minchildren) + else + skipped = true + end + end + if skipped + println(io, indent, "⋮") + return nothing + end + end +end +Base.show(node::InstanceTree; kwargs...) = show(stdout, node; kwargs...) + +struct MethodInvalidations + method::Method + reason::Symbol # :insert or :delete + mt_backedges::Vector{Pair{Any,InstanceTree}} # sig=>tree + backedges::Vector{InstanceTree} + mt_cache::Vector{MethodInstance} +end +methinv_storage() = Pair{Any,InstanceTree}[], InstanceTree[], MethodInstance[] +function MethodInvalidations(method::Method, reason::Symbol) + MethodInvalidations(method, reason, methinv_storage()...) +end + +Base.isempty(inv::MethodInvalidations) = isempty(inv.mt_backedges) && isempty(inv.backedges) # ignore mt_cache + +function countchildren(tree::InstanceTree) + n = length(tree.children) + for child in tree.children + n += countchildren(child) + end + return n +end +countchildren(sigtree::Pair{<:Any,InstanceTree}) = countchildren(sigtree.second) + +function countchildren(invalidations::MethodInvalidations) + n = 0 + for list in (invalidations.mt_backedges, invalidations.backedges) + for tree in list + n += countchildren(tree) + end + end + return n +end + +function Base.sort!(invalidations::MethodInvalidations) + sort!(invalidations.mt_backedges; by=countchildren) + sort!(invalidations.backedges; by=countchildren) + return invalidations +end + +# We could use AbstractTrees here, but typically one is not interested in the full tree, +# just the top method and the number of children it has +function Base.show(io::IO, invalidations::MethodInvalidations) + iscompact = get(io, :compact, false)::Bool + method = invalidations.method + + function showlist(io, treelist, indent=0) + nc = map(countchildren, treelist) + n = length(treelist) + nd = ndigits(n) + for i = 1:n + print(io, lpad(i, nd), ": ") + tree = treelist[i] + sig = nothing + if isa(tree, Pair) + print(io, "signature ", tree.first, " triggered ") + sig = tree.first + tree = tree.second + else + print(io, "superseding ", tree.mi.def , " with ") + sig = tree.mi.def.sig + end + print(io, tree.mi, " (", countchildren(tree), " children)") + if sig !== nothing + ms1, ms2 = method.sig <: sig, sig <: method.sig + diagnosis = if ms1 && !ms2 + "more specific" + elseif ms2 && !ms1 + "less specific" + elseif ms1 && ms1 + "equal specificity" + else + "ambiguous" + end + printstyled(io, ' ', diagnosis, color=:cyan) + end + if iscompact + i < n && print(io, ", ") + else + print(io, '\n') + i < n && print(io, " "^indent) + end + end + end + + println(io, invalidations.reason, " ", invalidations.method, " invalidated:") + indent = iscompact ? "" : " " + for fn in (:mt_backedges, :backedges) + val = getfield(invalidations, fn) + if !isempty(val) + print(io, indent, fn, ": ") + showlist(io, val, length(indent)+length(String(fn))+2) + end + iscompact && print(io, "; ") + end + if !isempty(invalidations.mt_cache) + println(io, indent, length(invalidations.mt_cache), " mt_cache") + end + iscompact && print(io, ';') +end + +# `list` is in RPN format, with the "reason" coming after the items +# Here is a brief summary of the cause and resulting entries +# delete_method: +# [zero or more (mi, "invalidate_mt_cache") pairs..., zero or more (depth1 tree, loctag) pairs..., method, loctag] with loctag = "jl_method_table_disable" +# method insertion: +# [zero or more (depth0 tree, sig) pairs..., same info as with delete_method except loctag = "jl_method_table_insert"] + +function invalidation_trees(list) + function checkreason(reason, loctag) + if loctag == "jl_method_table_disable" + @assert reason === nothing || reason === :delete + reason = :delete + elseif loctag == "jl_method_table_insert" + @assert reason === nothing || reason === :insert + reason = :insert + else + error("unexpected reason ", loctag) + end + return reason + end + + methodinvs = MethodInvalidations[] + tree = nothing + mt_backedges, backedges, mt_cache = methinv_storage() + reason = nothing + i = 0 + while i < length(list) + item = list[i+=1] + if isa(item, MethodInstance) + mi = item + item = list[i+=1] + if isa(item, Int32) + depth = item + if tree === nothing + tree = InstanceTree(mi, depth) + else + # Recurse back up the tree until we find the right parent + while tree.depth >= depth + tree = tree.parent + end + newtree = InstanceTree(mi, tree, depth) + push!(tree.children, newtree) + tree = newtree + end + elseif isa(item, String) + loctag = item + if loctag == "invalidate_mt_cache" + push!(mt_cache, mi) + tree = nothing + elseif loctag == "jl_method_table_insert" + tree = getroot(tree) + tree.mi = mi + push!(backedges, tree) + tree = nothing + elseif loctag == "insert_backedges" + println("insert_backedges for ", mi) + else + error("unexpected loctag ", loctag, " at ", i) + end + else + error("unexpected item ", item, " at ", i) + end + elseif isa(item, Method) + method = item + isassigned(list, i+1) || @show i + item = list[i+=1] + if isa(item, String) + reason = checkreason(reason, item) + push!(methodinvs, sort!(MethodInvalidations(method, reason, mt_backedges, backedges, mt_cache))) + mt_backedges, backedges, mt_cache = methinv_storage() + tree = nothing + else + error("unexpected item ", item, " at ", i) + end + elseif isa(item, String) + # This shouldn't happen + reason = checkreason(reason, item) + push!(backedges, getroot(tree)) + tree = nothing + elseif isa(item, Type) + push!(mt_backedges, item=>getroot(tree)) + tree = nothing + else + error("unexpected item ", item, " at ", i) + end + end + return sort!(methodinvs; by=countchildren) +end + +""" + thinned = filtermod(module, trees::AbstractVector{MethodInvalidations}) + +Select just the cases of invalidating a method defined in `module`. +""" +function filtermod(mod::Module, trees::AbstractVector{MethodInvalidations}) + # We don't just broadcast because we want to filter at all levels + thinned = MethodInvalidations[] + for invs in trees + _invs = filtermod(mod, invs) + isempty(_invs) || push!(thinned, _invs) + end + return sort!(thinned; by=countchildren) +end + +function filtermod(mod::Module, invs::MethodInvalidations) + hasmod(mod, node::InstanceTree) = node.mi.def.module === mod + + mt_backedges = filter(pr->hasmod(mod, pr.second), invs.mt_backedges) + backedges = filter(tree->hasmod(mod, tree), invs.backedges) + return MethodInvalidations(invs.method, invs.reason, mt_backedges, backedges, copy(invs.mt_cache)) +end + + +macro snoopr(expr) + quote + local invalidations = ccall(:jl_debug_method_invalidation, Any, (Cint,), 1) + Expr(:tryfinally, + $(esc(expr)), + ccall(:jl_debug_method_invalidation, Any, (Cint,), 0) + ) + invalidations + end +end diff --git a/src/parcel_snoopi.jl b/src/parcel_snoopi.jl index 665d230f0..347d2a95b 100644 --- a/src/parcel_snoopi.jl +++ b/src/parcel_snoopi.jl @@ -193,7 +193,7 @@ function handle_kwbody(topmod::Module, m::Method, paramrepr, tt, fstr="fbody") return nothing end -function parcel(tinf::AbstractVector{Tuple{Float64,Core.MethodInstance}}; subst=Vector{Pair{String, String}}(), blacklist=String[]) +function parcel(tinf::AbstractVector{Tuple{Float64,MethodInstance}}; subst=Vector{Pair{String, String}}(), blacklist=String[]) pc = Dict{Symbol, Set{String}}() # output modgens = Dict{Module, Vector{Method}}() # methods with generators in a module mods = OrderedSet{Module}() # module of each parameter for a given method diff --git a/src/snoopi.jl b/src/snoopi.jl index 8b64589c4..9b8a49b58 100644 --- a/src/snoopi.jl +++ b/src/snoopi.jl @@ -1,6 +1,6 @@ export @snoopi -const __inf_timing__ = Tuple{Float64,Core.MethodInstance}[] +const __inf_timing__ = Tuple{Float64,MethodInstance}[] if isdefined(Core.Compiler, :Params) function typeinf_ext_timed(linfo::Core.MethodInstance, params::Core.Compiler.Params) diff --git a/test/runtests.jl b/test/runtests.jl index f74fdd87f..e1356f9c8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -87,3 +87,7 @@ end include("colortypes.jl") include("bot/bot.jl") + +if isdefined(SnoopCompile, :invalidation_trees) + include("snoopr.jl") +end diff --git a/test/snoopr.jl b/test/snoopr.jl new file mode 100644 index 000000000..661936f12 --- /dev/null +++ b/test/snoopr.jl @@ -0,0 +1,107 @@ +using SnoopCompile, InteractiveUtils, MethodAnalysis, Test + +module SnooprTests +f(x::Int) = 1 +f(x::Bool) = 2 +applyf(container) = f(container[1]) +callapplyf(container) = applyf(container) +end + +@testset "@snoopr" begin + c = Any[1] + @test SnooprTests.callapplyf(c) == 1 + mi1 = instance(SnooprTests.applyf, (Vector{Any},)) + mi2 = instance(SnooprTests.callapplyf, (Vector{Any},)) + + invs = @snoopr SnooprTests.f(::AbstractFloat) = 3 + @test !isempty(invs) + trees = invalidation_trees(invs) + + tree = only(trees) + m = which(SnooprTests.f, (AbstractFloat,)) + @test tree.method == m + @test tree.reason === :insert + sig, node = only(tree.mt_backedges) + @test sig === Tuple{typeof(SnooprTests.f), Any} + @test node.mi == mi1 + @test SnoopCompile.getroot(node) === node + @test node.depth == 0 + child = only(node.children) + @test child.mi == mi2 + @test SnoopCompile.getroot(child) === node + @test child.depth == 1 + @test isempty(child.children) + @test isempty(tree.backedges) + + io = IOBuffer() + print(io, tree) + str = String(take!(io)) + @test startswith(str, "insert f(::AbstractFloat)") + @test occursin("mt_backedges: 1: signature", str) + @test occursin("triggered MethodInstance for applyf(::Array{Any,1}) (1 children) more specific", str) + + cf = Any[1.0f0] + @test SnooprTests.callapplyf(cf) == 3 + mi3 = instance(SnooprTests.f, (AbstractFloat,)) + invs = @snoopr SnooprTests.f(::Float32) = 4 + @test !isempty(invs) + trees = invalidation_trees(invs) + + tree = only(trees) + m = which(SnooprTests.f, (Float32,)) + # These next are identical to the above + @test tree.method == m + @test tree.reason === :insert + sig, node = only(tree.mt_backedges) + @test sig === Tuple{typeof(SnooprTests.f), Any} + @test node.mi == mi1 + @test SnoopCompile.getroot(node) === node + @test node.depth == 0 + child = only(node.children) + @test child.mi == mi2 + @test SnoopCompile.getroot(child) === node + @test child.depth == 1 + @test isempty(child.children) + # But we add backedges + node = only(tree.backedges) + @test node.mi == mi3 + @test SnoopCompile.getroot(node) === node + @test node.depth == 0 + child = only(node.children) + @test child.mi == mi1 + @test SnoopCompile.getroot(child) === node + @test child.depth == 1 + + @test any(nd->nd.mi == mi1, node) + @test !any(nd->nd.mi == mi3, child) + + print(io, tree) + str = String(take!(io)) + @test startswith(str, "insert f(::Float32)") + @test occursin("mt_backedges: 1: signature", str) + @test occursin("triggered MethodInstance for applyf(::Array{Any,1}) (1 children) more specific", str) + @test occursin("backedges: 1: superseding f(::AbstractFloat)", str) + @test occursin("with MethodInstance for f(::AbstractFloat) (1 children) more specific", str) + + show(io, node; minchildren=0) + str = String(take!(io)) + lines = split(chomp(str), '\n') + @test length(lines) == 2 + @test lines[1] == "MethodInstance for f(::AbstractFloat) (1 children)" + @test lines[2] == " MethodInstance for applyf(::Array{Any,1}) (0 children)" + show(io, node; minchildren=1) + str = String(take!(io)) + lines = split(chomp(str), '\n') + @test length(lines) == 2 + @test lines[1] == "MethodInstance for f(::AbstractFloat) (1 children)" + @test lines[2] == "⋮" + + ftrees = filtermod(SnooprTests, trees) + ftree = only(ftrees) + @test ftree.mt_backedges == tree.mt_backedges + @test isempty(ftree.backedges) + ftrees = filtermod(@__MODULE__, trees) + ftree = only(ftrees) + @test ftree.backedges == tree.backedges + @test isempty(ftree.mt_backedges) +end