Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 46 additions & 8 deletions src/invalidations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,14 @@ of trees if a package creates many methods for a single function.

For more information, see the tutorials in the online documentation.
"""
function _index_mi_nodes!(d::Dict{MethodInstance,Tuple{Int,InstanceNode}}, nodes::Vector{InstanceNode}, tree_idx::Int)
for node in nodes
isdummy(node) && continue
d[node.mi] = (tree_idx, node)
_index_mi_nodes!(d, node.children, tree_idx)
end
end

function invalidation_trees(list::InvalidationLists; consolidate::Bool=true, kwargs...)
mtrees = invalidation_trees_logmeths(list.logmeths; kwargs...)
etrees = invalidation_trees_logedges(list.logedges; kwargs...)
Expand All @@ -561,16 +569,46 @@ function invalidation_trees(list::InvalidationLists; consolidate::Bool=true, kwa
else
trees = mtrees
mindex = Dict{Union{Method,Binding},Int}(tree.method => i for (i, tree) in enumerate(mtrees)) # map method to index in mtrees
# Build MI → (tree_idx, node) index so that verify_methods callees that returned
# early (no logedges entry) can be connected back to the mtree that caused them.
mi_to_mtree_node = Dict{MethodInstance,Tuple{Int,InstanceNode}}()
for (tree_idx, tree) in enumerate(mtrees)
_index_mi_nodes!(mi_to_mtree_node, tree.backedges, tree_idx)
for (_, node) in tree.mt_backedges
_index_mi_nodes!(mi_to_mtree_node, [node], tree_idx)
end
end
for etree in etrees
if etree.reason === :unknown
push!(trees, MethodInvalidations(
nothing,
:unknown,
etree.mt_backedges,
etree.backedges,
MethodInstance[], # mt_cache
MethodInstance[] # mt_disable
))
# Each root represents a CI that verify_method returned early for (already
# invalid at C level). Its MI should appear in some mtree. If so, attach
# the logedges callers (root.children) to the matching mtree node so they
# are attributed to the correct causing method instead of "unknown nothing".
unmatched = InstanceNode[]
for root in etree.backedges
hit = get(mi_to_mtree_node, root.mi, nothing)
if hit !== nothing
tree_idx, mtree_node = hit
# root.children are at depth 1 (from the logedges tree where root
# is depth 0). Adjust to mtree_node.depth+1 before appending.
for child in root.children
adjust_depth!(child, mtree_node.depth)
end
append!(mtree_node.children, root.children)
else
push!(unmatched, root)
end
end
if !isempty(unmatched) || !isempty(etree.mt_backedges)
push!(trees, MethodInvalidations(
nothing,
:unknown,
etree.mt_backedges,
unmatched,
MethodInstance[], # mt_cache
MethodInstance[] # mt_disable
))
end
continue
end
if etree.reason === :deleting
Expand Down
45 changes: 45 additions & 0 deletions test/snoop_invalidations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,51 @@ end
Pkg.activate(cproj)
end

@testset "Unknown-tree attribution via logmeths cross-reference" begin
# When a package is loaded and its precompiled CIs are already C-level invalid
# (max_world=0), verify_method returns early without emitting an
# insert_backedges_callee logedge. The callee CI only appears as the `cause`
# argument of a verify_methods logedge. SnoopCompile must cross-reference the
# :unknown etree roots against the mtrees so that transitive callers are
# attributed to the correct inserting method rather than "unknown nothing".
cproj = Base.active_project()
olddir = pwd()
cd(joinpath(@__DIR__, "testmodules", "Invalidation"))
Pkg.activate(pwd())
Pkg.instantiate()
Pkg.precompile()
invs = @snoop_invalidations begin
using PkgE
@eval PkgE callee(x::Int) = 1
using PkgF
end
trees = invalidation_trees(invs)

# The consolidated trees should contain exactly one inserting tree (for callee(::Int))
# and no :unknown trees.
unknown_trees = filter(t -> t.reason === :unknown, trees)
inserting_trees = filter(t -> t.reason === :inserting, trees)
@test isempty(unknown_trees)
@test length(inserting_trees) == 1

callee_tree = only(inserting_trees)
@test callee_tree.method == which(PkgE.callee, (Int,))

# transitive_caller (from PkgF) must appear somewhere in the inserting tree
function any_mi_match(nodes, pred)
for node in nodes
pred(node.mi) && return true
any_mi_match(node.children, pred) && return true
end
return false
end
transitive_mi = only(SnoopCompile.specializations(which(PkgF.transitive_caller, (Int,))))
@test any_mi_match(callee_tree.backedges, mi -> mi === transitive_mi)

cd(olddir)
Pkg.activate(cproj)
end

# This needs to come after "Edge invalidations", as redefining `throw_boundserror` invalidates a lot of stuff
@testset "throw_boundserror" begin
# #268
Expand Down
3 changes: 3 additions & 0 deletions test/testmodules/Invalidation/PkgE/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name = "PkgE"
uuid = "06e9d836-39c2-490c-9c31-7a346e629b57"
version = "0.1.0"
10 changes: 10 additions & 0 deletions test/testmodules/Invalidation/PkgE/src/PkgE.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module PkgE

callee(x::Integer) = 0
# Direct caller of callee — precompiled so its CI is in memory when callee gets a more
# specific method inserted, triggering a C-level (logmeths) invalidation.
direct_caller(x::Integer) = callee(x)

precompile(direct_caller, (Int,))

end # module PkgE
6 changes: 6 additions & 0 deletions test/testmodules/Invalidation/PkgF/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name = "PkgF"
uuid = "b2b4d567-0c8b-42d7-ac82-20f7ad6e670c"
version = "0.1.0"

[deps]
PkgE = "06e9d836-39c2-490c-9c31-7a346e629b57"
12 changes: 12 additions & 0 deletions test/testmodules/Invalidation/PkgF/src/PkgF.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module PkgF

using PkgE

# Transitive caller: calls PkgE.direct_caller. When PkgE.callee gets a more specific
# method and direct_caller's CI is C-level invalidated (max_world=0), loading this
# package triggers a verify_methods logedge entry with direct_caller as the cause.
transitive_caller(x::Integer) = PkgE.direct_caller(x)

precompile(transitive_caller, (Int,))

end # module PkgF
4 changes: 4 additions & 0 deletions test/testmodules/Invalidation/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ InvalidD = "4e53dc45-bfb2-4a6a-a95b-d850d77a7d07"
InvalidE = "f916d1c3-0cdb-4773-9621-29945992b959"
PkgC = "c8e0c308-2b2c-4e17-bb0b-031502754b83"
PkgD = "3a5fa9f4-f0a4-4856-859e-2bf0c30232a7"
PkgE = "06e9d836-39c2-490c-9c31-7a346e629b57"
PkgF = "b2b4d567-0c8b-42d7-ac82-20f7ad6e670c"
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"

[sources]
Expand All @@ -16,6 +18,8 @@ InvalidD = {path = "InvalidD"}
InvalidE = {path = "InvalidE"}
PkgC = {path = "PkgC"}
PkgD = {path = "PkgD"}
PkgE = {path = "PkgE"}
PkgF = {path = "PkgF"}

[compat]
PrecompileTools = "1.3"