Skip to content

Commit f683000

Browse files
committed
Track invalidations during deserialization
These were printed to the console but not otherwise stored. Storing them allows one to give them more emphasis and perform analysis. With JuliaLang/julia#41913, it becomes possible to attribute them to specific method definitions or deletions. (Of course there might be multiple reasons, but we need to identify at least one in order to make progress.)
1 parent 40bba9c commit f683000

File tree

9 files changed

+190
-11
lines changed

9 files changed

+190
-11
lines changed

src/invalidations.jl

+98-11
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ mutable struct InstanceNode
7474
end
7575
end
7676

77+
Core.MethodInstance(node::InstanceNode) = node.mi
78+
Base.convert(::Type{MethodInstance}, node::InstanceNode) = node.mi
79+
AbstractTrees.children(node::InstanceNode) = node.children
80+
7781
function getroot(node::InstanceNode)
7882
while isdefined(node, :parent)
7983
node = node.parent
@@ -133,7 +137,7 @@ end
133137
struct MethodInvalidations
134138
method::Method
135139
reason::Symbol # :inserting or :deleting
136-
mt_backedges::Vector{Pair{Any,InstanceNode}} # sig=>root
140+
mt_backedges::Vector{Pair{Any,Union{InstanceNode,MethodInstance}}} # sig=>root
137141
backedges::Vector{InstanceNode}
138142
mt_cache::Vector{MethodInstance}
139143
mt_disable::Vector{MethodInstance}
@@ -145,7 +149,8 @@ end
145149

146150
Base.isempty(methinvs::MethodInvalidations) = isempty(methinvs.mt_backedges) && isempty(methinvs.backedges) # ignore mt_cache
147151

148-
countchildren(sigtree::Pair{<:Any,InstanceNode}) = countchildren(sigtree.second)
152+
countchildren(sigtree::Pair{<:Any,Union{InstanceNode,MethodInstance}}) = countchildren(sigtree.second)
153+
countchildren(::MethodInstance) = 1
149154

150155
function countchildren(methinvs::MethodInvalidations)
151156
n = 0
@@ -179,7 +184,15 @@ function Base.show(io::IO, methinvs::MethodInvalidations)
179184
sig = nothing
180185
if isa(root, Pair)
181186
print(io, "signature ")
182-
printstyled(io, root.first, color = :light_cyan)
187+
sig = root.first
188+
if isa(sig, MethodInstance)
189+
# "insert_backedges_callee"/"insert_backedges" (delayed) invalidations
190+
printstyled(io, which(sig.specTypes), color = :light_cyan)
191+
print(io, " (formerly ", sig.def, ')')
192+
else
193+
# `sig` (immediate) invalidations
194+
printstyled(io, sig, color = :light_cyan)
195+
end
183196
print(io, " triggered ")
184197
sig = root.first
185198
root = root.second
@@ -189,7 +202,7 @@ function Base.show(io::IO, methinvs::MethodInvalidations)
189202
print(io, " with ")
190203
sig = root.mi.def.sig
191204
end
192-
printstyled(io, root.mi, color = :light_yellow)
205+
printstyled(io, convert(MethodInstance, root), color = :light_yellow)
193206
print(io, " (", countchildren(root), " children)")
194207
if iscompact
195208
i < n && print(io, ", ")
@@ -275,7 +288,29 @@ function invalidation_trees(list; exclude_corecompiler::Bool=true)
275288
return reason
276289
end
277290

291+
function handle_insert_backedges(list, i, callee)
292+
ncovered = 0
293+
while length(list) >= i+2 && list[i+2] == "insert_backedges_callee"
294+
if isa(callee, Type)
295+
newcallee = list[i+1]
296+
if isa(newcallee, MethodInstance)
297+
callee = newcallee
298+
end
299+
end
300+
i += 2
301+
end
302+
while length(list) >= i+2 && list[i+2] == "insert_backedges"
303+
caller = list[i+=1]
304+
i += 1
305+
push!(delayed, callee => caller)
306+
ncovered += 1
307+
end
308+
@assert ncovered > 0
309+
return i
310+
end
311+
278312
methodinvs = MethodInvalidations[]
313+
delayed = Pair{Any,MethodInstance}[] # from "insert_backedges" invalidations
279314
leaf = nothing
280315
mt_backedges, backedges, mt_cache, mt_disable = methinv_storage()
281316
reason = nothing
@@ -320,8 +355,13 @@ function invalidation_trees(list; exclude_corecompiler::Bool=true)
320355
end
321356
leaf = nothing
322357
end
358+
elseif loctag == "insert_backedges_callee"
359+
i = handle_insert_backedges(list, i, mi)
323360
elseif loctag == "insert_backedges"
361+
# pre Julia 1.8
324362
println("insert_backedges for ", mi)
363+
Base.VERSION < v"1.8.0-DEV" || error("unexpected failure at ", i)
364+
@assert leaf === nothing
325365
else
326366
error("unexpected loctag ", loctag, " at ", i)
327367
end
@@ -348,17 +388,51 @@ function invalidation_trees(list; exclude_corecompiler::Bool=true)
348388
leaf = nothing
349389
reason = nothing
350390
elseif isa(item, Type)
351-
root = getroot(leaf)
352-
if !exclude_corecompiler || !from_corecompiler(root.mi)
353-
push!(mt_backedges, item=>root)
391+
if length(list) > i && list[i+1] == "insert_backedges_callee"
392+
i = handle_insert_backedges(list, i+1, item)
393+
else
394+
root = getroot(leaf)
395+
if !exclude_corecompiler || !from_corecompiler(root.mi)
396+
push!(mt_backedges, item=>root)
397+
end
398+
leaf = nothing
354399
end
355-
leaf = nothing
356400
elseif isa(item, Core.TypeMapEntry) && list[i+1] == "invalidate_mt_cache"
357401
i += 1
358402
else
359403
error("unexpected item ", item, " at ", i)
360404
end
361405
end
406+
@assert all(isempty, Any[mt_backedges, backedges, #= mt_cache, =# mt_disable])
407+
# Handle the delayed invalidations
408+
callee2idx = Dict{Method,Int}()
409+
for (i, methinvs) in enumerate(methodinvs)
410+
for (sig, root) in methinvs.mt_backedges
411+
for node in PreOrderDFS(root)
412+
callee2idx[MethodInstance(node).def] = i
413+
end
414+
end
415+
for root in methinvs.backedges
416+
for node in PreOrderDFS(root)
417+
callee2idx[MethodInstance(node).def] = i
418+
end
419+
end
420+
end
421+
trouble = similar(delayed, 0)
422+
for (callee, caller) in delayed
423+
if isa(callee, MethodInstance)
424+
idx = get(callee2idx, callee.def, nothing)
425+
if idx !== nothing
426+
push!(methodinvs[idx].mt_backedges, callee => caller)
427+
continue
428+
end
429+
end
430+
push!(trouble, callee => caller)
431+
end
432+
if !isempty(trouble)
433+
@warn "Could not attribute the following delayed invalidations:"
434+
display(trouble)
435+
end
362436
return sort!(methodinvs; by=countchildren)
363437
end
364438

@@ -402,7 +476,8 @@ function filtermod(mod::Module, methinvs::MethodInvalidations; recursive::Bool=f
402476
end
403477
mt_backedges = filter(pr->hasmod(mod, pr.second), methinvs.mt_backedges)
404478
backedges = filter(root->hasmod(mod, root), methinvs.backedges)
405-
return MethodInvalidations(methinvs.method, methinvs.reason, mt_backedges, backedges, copy(methinvs.mt_cache), copy(methinvs.mt_disable))
479+
return MethodInvalidations(methinvs.method, methinvs.reason, mt_backedges, backedges,
480+
copy(methinvs.mt_cache), copy(methinvs.mt_disable))
406481
end
407482

408483
function filtermod(mod::Module, node::InstanceNode)
@@ -428,6 +503,14 @@ function filtermod(mod::Module, node::InstanceNode)
428503
return nothing
429504
end
430505

506+
function filtermod(mod::Module, mi::MethodInstance)
507+
m = mi.def
508+
if isa(m, Method)
509+
return m.module == mod ? mi : nothing
510+
end
511+
return m == mod ? mi : nothing
512+
end
513+
431514
"""
432515
methinvs = findcaller(method::Method, trees)
433516
@@ -491,12 +574,14 @@ function findcaller(meth::Method, methinvs::MethodInvalidations)
491574
for (sig, node) in methinvs.mt_backedges
492575
ret = findcaller(meth, node)
493576
ret === nothing && continue
494-
return MethodInvalidations(methinvs.method, methinvs.reason, [Pair{Any,InstanceNode}(sig, newtree(ret))], InstanceNode[], copy(methinvs.mt_cache), copy(methinvs.mt_disable))
577+
return MethodInvalidations(methinvs.method, methinvs.reason, [Pair{Any,InstanceNode}(sig, newtree(ret))], InstanceNode[],
578+
copy(methinvs.mt_cache), copy(methinvs.mt_disable))
495579
end
496580
for node in methinvs.backedges
497581
ret = findcaller(meth, node)
498582
ret === nothing && continue
499-
return MethodInvalidations(methinvs.method, methinvs.reason, Pair{Any,InstanceNode}[], [newtree(ret)], copy(methinvs.mt_cache), copy(methinvs.mt_disable))
583+
return MethodInvalidations(methinvs.method, methinvs.reason, Pair{Any,InstanceNode}[], [newtree(ret)],
584+
copy(methinvs.mt_cache), copy(methinvs.mt_disable))
500585
end
501586
return nothing
502587
end
@@ -513,6 +598,8 @@ function findcaller(meth::Method, node::InstanceNode)
513598
return nothing
514599
end
515600

601+
findcaller(meth::Method, mi::MethodInstance) = mi.def == meth ? mi : nothing
602+
516603
# Cthulhu integration
517604

518605
Cthulhu.backedges(node::InstanceNode) = sort(node.children; by=countchildren, rev=true)

test/snoopi_deep.jl

+30
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ using Random
66
using Profile
77
using MethodAnalysis
88
using Core: MethodInstance
9+
using Pkg
910
# using PyPlot: PyPlot, plt # uncomment to test visualizations
1011

1112
using SnoopCompile.FlameGraphs.AbstractTrees # For FlameGraphs tests
@@ -780,3 +781,32 @@ end
780781
@test dmp.trtd == 0
781782
# pgdsgui(axs[2], rit; bystr="Inclusive", consts=true, interactive=false)
782783
end
784+
785+
@testset "Stale" begin
786+
if Base.VERSION >= v"1.8.0-DEV.368"
787+
cproj = Base.active_project()
788+
cd(joinpath("testmodules", "Stale")) do
789+
Pkg.activate(pwd())
790+
Pkg.precompile()
791+
end
792+
invalidations = @snoopr begin
793+
using StaleA, StaleC
794+
using StaleB
795+
end
796+
smis = filter(SnoopCompile.hasstaleinstance, methodinstances(StaleA))
797+
@test length(smis) == 2
798+
stalenames = [mi.def.name for mi in smis]
799+
@test :build_stale stalenames
800+
@test :use_stale stalenames
801+
trees = invalidation_trees(invalidations)
802+
tree = only(trees)
803+
@test tree.method == which(StaleA.stale, (String,)) # defined in StaleC
804+
@test Core.MethodInstance(only(tree.backedges)).def == which(StaleA.stale, (Any,))
805+
if Base.VERSION > v"1.8.0-DEV" # FIXME
806+
@test only(tree.mt_backedges).first.def == which(StaleA.stale, (Any,))
807+
@test which(only(tree.mt_backedges).first.specTypes) == which(StaleA.stale, (String,))
808+
@test only(tree.mt_backedges).second.def == which(StaleB.useA, ())
809+
end
810+
Pkg.activate(cproj)
811+
end
812+
end

test/testmodules/Stale/Project.toml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[deps]
2+
StaleA = "daf834c3-b832-4a67-a95b-01ec1ffe9b4d"
3+
StaleB = "af730a9e-e668-4d07-a0f0-de54196c2067"
4+
StaleC = "f6b5ece7-60fa-49fc-ba7e-b783050e37f1"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
name = "StaleA"
2+
uuid = "daf834c3-b832-4a67-a95b-01ec1ffe9b4d"
3+
authors = ["Tim Holy <[email protected]>"]
4+
version = "0.1.0"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module StaleA
2+
3+
stale(x) = rand(1:8)
4+
stale(x::Int) = length(digits(x))
5+
6+
not_stale(x::String) = first(x)
7+
8+
use_stale(c) = stale(c[1]) + not_stale("hello")
9+
build_stale(x) = use_stale(Any[x])
10+
11+
# force precompilation
12+
build_stale(37)
13+
stale('c')
14+
15+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
name = "StaleB"
2+
uuid = "af730a9e-e668-4d07-a0f0-de54196c2067"
3+
authors = ["Tim Holy <[email protected]>"]
4+
version = "0.1.0"
5+
6+
[deps]
7+
StaleA = "daf834c3-b832-4a67-a95b-01ec1ffe9b4d"
8+
StaleC = "f6b5ece7-60fa-49fc-ba7e-b783050e37f1"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module StaleB
2+
3+
# StaleB does not know about StaleC when it is being built.
4+
# However, if StaleC is loaded first, we get `"insert_backedges"`
5+
# invalidations.
6+
using StaleA
7+
8+
# This will be invalidated if StaleC is loaded
9+
useA() = StaleA.stale("hello")
10+
11+
# force precompilation
12+
useA()
13+
14+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
name = "StaleC"
2+
uuid = "f6b5ece7-60fa-49fc-ba7e-b783050e37f1"
3+
authors = ["Tim Holy <[email protected]>"]
4+
version = "0.1.0"
5+
6+
[deps]
7+
StaleA = "daf834c3-b832-4a67-a95b-01ec1ffe9b4d"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module StaleC
2+
3+
using StaleA
4+
5+
StaleA.stale(x::String) = length(x)
6+
call_buildstale(x) = StaleA.build_stale(x)
7+
8+
call_buildstale("hey")
9+
10+
end # module

0 commit comments

Comments
 (0)