Skip to content

Commit 9ae5778

Browse files
authored
Simplify implementation for edge logs (#426)
* StaticData -> ReinferUtils * Simplify implementation for improved edge logs
1 parent f9ed663 commit 9ae5778

File tree

5 files changed

+92
-126
lines changed

5 files changed

+92
-126
lines changed

SnoopCompileCore/src/SnoopCompileCore.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ module SnoopCompileCore
22

33
using Core: MethodInstance, CodeInfo
44

5+
const ReinferUtils = isdefined(Base, :ReinferUtils) ? Base.ReinferUtils : Base.StaticData
6+
57
include("snoop_inference.jl")
68
include("snoop_invalidations.jl")
79
include("snoop_llvm.jl")

SnoopCompileCore/src/snoop_invalidations.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ macro snoop_invalidations(expr)
3636
exoff = Expr(:tryfinally,
3737
esc(expr),
3838
quote
39-
Base.StaticData.debug_method_invalidation(false)
39+
$ReinferUtils.debug_method_invalidation(false)
4040
ccall(:jl_debug_method_invalidation, Any, (Cint,), 0)
4141
end
4242
)
4343
return quote
44-
local logedges = Base.StaticData.debug_method_invalidation(true)
44+
local logedges = $ReinferUtils.debug_method_invalidation(true)
4545
local logmeths = ccall(:jl_debug_method_invalidation, Any, (Cint,), 1)
4646
$exoff
4747
$InvalidationLists(logedges, logmeths)

docs/src/explanations/devs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
Julia itself handles (in)validation when you define (or delete) methods and load packages. Julia's internal machinery provides the option of recording these invalidation decisions to a log, which is just a `Vector{Any}`. Currently (as of Julia 1.12) there are two independent logs:
88

99
- for method insertion and deletion (i.e., new methods invalidating old code), logging is handled in Julia's `src/gf.c`. You enable it with `logmeths = ccall(:jl_debug_method_invalidation, Any, (Cint,), true)` and pass a final argument of `false` to turn it off.
10-
- for validating precompiled code during package loading (i.e., "new" code being invalidated by old methods), logging is handled in Julia's `base/staticdata.jl`. You enable it with `logedges = Base.StaticData.debug_method_invalidation(true)` and pass `false` to turn it off.
10+
- for validating precompiled code during package loading (i.e., "new" code being invalidated by old methods), logging is handled in Julia's `base/staticdata.jl`. You enable it with `logedges = SnoopCompile.ReinferUtils.debug_method_invalidation(true)` and pass `false` to turn it off.
1111

1212
In both cases, the log will initially be empty, but subsequent activity (defining or deleting methods, or loading packages) may add entries.
1313

src/invalidations.jl

Lines changed: 80 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ const BackedgeMT = Pair{Union{DataType,Binding},InstanceNode} # sig=>root
183183
abstract type AbstractMethodInvalidations end
184184

185185
struct MethodInvalidations <: AbstractMethodInvalidations
186-
method::Union{Method, Binding}
186+
method::Union{Method, Binding, Nothing}
187187
reason::Symbol # :inserting, :deleting, or :rebinding
188188
mt_backedges::Vector{BackedgeMT}
189189
backedges::Vector{InstanceNode}
@@ -415,10 +415,11 @@ const EdgeNodeType = Union{DataType, Binding, MethodInstance, CodeInstance}
415415

416416
struct MultiMethodInvalidations <: AbstractMethodInvalidations
417417
methods::Union{Binding,Vector{Method}}
418+
reason::Symbol # :inserting, :deleting, or :rebinding
418419
mt_backedges::Vector{BackedgeMT}
419420
backedges::Vector{InstanceNode}
420421
end
421-
MultiMethodInvalidations(methods = Method[]) = MultiMethodInvalidations(methods, BackedgeMT[], InstanceNode[])
422+
MultiMethodInvalidations(methods, reason) = MultiMethodInvalidations(methods, reason, BackedgeMT[], InstanceNode[])
422423

423424
function Base.show(io::IO, methinvs::MultiMethodInvalidations)
424425
iscompact = get(io, :compact, false)::Bool
@@ -451,139 +452,59 @@ end
451452
Base.isempty(methinvs::MultiMethodInvalidations) = isempty(methinvs.backedges) && isempty(methinvs.mt_backedges)
452453

453454
function invalidation_trees_logedges(list; exclude_corecompiler::Bool=true)
454-
# transiently we represent the graph as a flat list of nodes, a flat list of children indexes, and a Dict to look up the node index
455-
nodes = EdgeNodeType[]
456-
calleridxss = Vector{Int}[]
457-
nodeidx = IdDict{EdgeNodeType,Int}() # get the index within `nodes` for a given key
458-
matchess = Dict{Int,Vector{Method}}() # nodeidx => Method[...]
459-
460-
function addnode(item)
461-
push!(nodes, item)
462-
k = length(nodes)
463-
nodeidx[item] = k
464-
return k
465-
end
466-
467-
function addcaller!(listlist, (calleridx, calleeidx))
468-
if length(listlist) < calleeidx
469-
resize!(listlist, calleeidx)
470-
end
471-
# calleridxs = get!(Vector{Int}, listlist, calleeidx) # why don't we have this??
472-
calleridxs = if isassigned(listlist, calleeidx)
473-
listlist[calleeidx]
474-
else
475-
listlist[calleeidx] = Int[]
476-
end
477-
push!(calleridxs, calleridx)
478-
return calleridxs
479-
end
455+
mminvs = MultiMethodInvalidations[]
456+
mmibad = MultiMethodInvalidations(Method[], :unknown)
457+
calleedict = Dict{Union{CodeInstance,MethodInstance},InstanceNode}()
480458

481-
i = 0
459+
i, mmi = 0, nothing
482460
while i + 2 < length(list)
483461
tag = list[i+2]::String
484462
if tag == "method_globalref"
485463
def, target = list[i+1]::Method, list[i+3]::CodeInstance
486464
i += 4
487465
error("implement me")
488466
elseif tag == "insert_backedges_callee"
489-
edge, target, matches = list[i+1]::EdgeNodeType, list[i+3]::CodeInstance, list[i+4]::Union{Vector{Any},Nothing}
467+
edge, target, matches = list[i+1]::EdgeNodeType, list[i+3]::CodeInstance, list[i+4]::Vector{Any}
490468
i += 4
491-
idx = get(nodeidx, edge, nothing)
492-
if idx === nothing
493-
idx = addnode(edge)
494-
if matches !== nothing
495-
matchess[idx] = matches
469+
reason = !isempty(matches) ? :inserting :
470+
isa(edge, Binding) ? :rebinding : :deleting
471+
matches = reason === :rebinding ? edge : convert(Vector{Method}, matches)
472+
mmi = MultiMethodInvalidations(matches, reason)
473+
push!(mminvs, mmi)
474+
if edge isa Type || edge isa Binding
475+
node = InstanceNode(target, 0)
476+
calleedict[target] = node
477+
push!(mmi.mt_backedges, edge=>node)
478+
else
479+
root = get(calleedict, edge, nothing)
480+
if root === nothing
481+
root = InstanceNode(edge, 0)
482+
push!(mmi.backedges, root)
483+
calleedict[edge] = root
496484
end
497-
elseif matches !== nothing
498-
@assert Set(matches) == Set(matchess[idx])
499-
end
500-
idxt = get(nodeidx, target, nothing)
501-
if idxt === nothing
502-
idxt = addnode(target)
485+
node = InstanceNode(target, root)
486+
calleedict[target] = node
503487
end
504-
addcaller!(calleridxss, idxt => idx)
505488
elseif tag == "verify_methods"
506489
caller, callee = list[i+1]::CodeInstance, list[i+3]::CodeInstance
507490
i += 3
508-
idx = get(nodeidx, callee, nothing)
509-
if idx === nothing
510-
idx = addnode(callee)
491+
caller == callee && continue
492+
node = get(calleedict, callee, nothing)
493+
if node === nothing
494+
node = InstanceNode(callee, 0)
495+
push!(mmibad.backedges, node)
496+
calleedict[callee] = node
511497
end
512-
idxt = get(nodeidx, caller, nothing)
513-
if idxt === nothing
514-
idxt = addnode(caller)
515-
end
516-
@assert idxt >= idx
517-
idxt > idx && addcaller!(calleridxss, idxt => idx)
498+
node = InstanceNode(caller, node)
499+
calleedict[caller] = node
518500
else
519501
error("tag ", tag, " unknown")
520502
end
521503
end
522-
return mmi_trees!(nodes, calleridxss, matchess)
523-
end
524-
525-
function mmi_trees!(nodes::AbstractVector{EdgeNodeType}, calleridxss::Vector{Vector{Int}}, matchess::AbstractDict{Int,Vector{Method}})
526-
iscaller = BitSet()
527-
528-
function filltree!(mminvs::MultiMethodInvalidations, i::Int)
529-
node = nodes[i]
530-
calleridxs = calleridxss[i]
531-
if isa(node, Union{DataType,Binding})
532-
while !isempty(calleridxs)
533-
j = pop!(calleridxs)
534-
push!(iscaller, j)
535-
root = InstanceNode(nodes[j], 0)
536-
push!(mminvs.mt_backedges, node => root)
537-
fillnode!(root, j)
538-
end
539-
else
540-
root = InstanceNode(node, 0)
541-
push!(mminvs.backedges, root)
542-
fillnode!(root, i)
543-
end
544-
return mminvs
504+
if !isempty(mmibad)
505+
push!(mminvs, mmibad)
545506
end
546-
547-
function fillnode!(node::InstanceNode, k)
548-
calleridxs = isassigned(calleridxss, k) ? calleridxss[k] : nothing
549-
calleridxs === nothing && return
550-
while !isempty(calleridxs)
551-
j = pop!(calleridxs)
552-
push!(iscaller, j)
553-
child = InstanceNode(nodes[j], node)
554-
fillnode!(child, j)
555-
end
556-
end
557-
558-
# If anything gets added to `mminv0`, it means the cause occurred outside observation with `@snoop_invalidations`
559-
badarg = Method[]
560-
mminv0 = MultiMethodInvalidations(badarg)
561-
562-
treeindex = Dict{Union{Vector{Method},Binding},Int}()
563-
mminvs = MultiMethodInvalidations[]
564-
for i in eachindex(nodes)
565-
if i iscaller
566-
node = nodes[i]
567-
arg = get(matchess, i, node)
568-
j = get(treeindex, arg, nothing)
569-
if j === nothing
570-
if isa(arg, Binding) || (isa(arg, Vector{Method}) && !isempty(arg))
571-
mminv = MultiMethodInvalidations(arg)
572-
push!(mminvs, mminv)
573-
j = length(mminvs)
574-
treeindex[arg] = j
575-
else
576-
mminv = mminv0
577-
end
578-
else
579-
mminv = mminvs[j]
580-
end
581-
filltree!(mminv, i)
582-
else
583-
@assert !isassigned(calleridxss, i) || isempty(calleridxss[i])
584-
end
585-
end
586-
isempty(mminv0) || push!(mminvs, mminv0)
507+
sort!(mminvs; by=countchildren)
587508
return mminvs
588509
end
589510

@@ -641,6 +562,50 @@ function invalidation_trees(list::InvalidationLists; consolidate::Bool=true, kwa
641562
trees = mtrees
642563
mindex = Dict{Union{Method,Binding},Int}(tree.method => i for (i, tree) in enumerate(mtrees)) # map method to index in mtrees
643564
for etree in etrees
565+
if etree.reason === :unknown
566+
push!(trees, MethodInvalidations(
567+
nothing,
568+
:unknown,
569+
etree.mt_backedges,
570+
etree.backedges,
571+
MethodInstance[], # mt_cache
572+
MethodInstance[] # mt_disable
573+
))
574+
continue
575+
end
576+
if etree.reason === :deleting
577+
@assert isempty(etree.backedges) # should not have any backedges
578+
# Determine whether any of the deleted methods cover this
579+
covered = false
580+
for (edge, node) in etree.mt_backedges
581+
for mtree in mtrees
582+
mtree.reason === :deleting || continue
583+
mtree.method.sig <: edge || continue
584+
# This edge is covered by the deleted method
585+
join_invalidations!(mtree.mt_backedges, edge => node)
586+
covered = true
587+
end
588+
covered && continue
589+
# Try to find a deleted method that's applicable
590+
# The challenge is we don't know any world information
591+
methodtable = @static isdefinedglobal(Core, :methodtable) ? Core.methodtable : Core.GlobalMethods
592+
methmatches = Method[]
593+
Base.visit(methodtable) do m
594+
if iszero(m.dispatch_status) && m.sig <: edge
595+
push!(methmatches, m)
596+
end
597+
end
598+
m = length(methmatches) == 1 ? first(methmatches) : nothing
599+
push!(trees, MethodInvalidations(
600+
m,
601+
:deleting,
602+
BackedgeMT[edge => node],
603+
InstanceNode[], # backedges
604+
MethodInstance[], # mt_cache
605+
MethodInstance[] # mt_disable
606+
))
607+
end
608+
end
644609
methods = etree.methods
645610
if isa(methods, Vector{Method})
646611
for method in methods

test/snoop_invalidations.jl

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -259,25 +259,24 @@ end
259259
end
260260
trees = invalidation_trees(invalidations)
261261

262-
mtrees = filter(tree -> isa(tree.method, Method), trees)
263-
tree = first(mtrees)
264-
@test tree.reason == :inserting
265-
@test tree.method.file == Symbol(@__FILE__)
262+
# Method deletion
263+
mtrees = filter(tree -> tree.reason === :deleting, trees)
264+
tree = only(mtrees)
266265
@test isempty(tree.backedges)
267-
sig, root = last(tree.mt_backedges)
266+
sig, root = only(tree.mt_backedges)
268267
@test sig.parameters[1] === typeof(PkgC.nbits)
269268
@test sig.parameters[2] === Integer
270-
@test root.mi == first(SnoopCompile.specializations(only(methods(PkgD.call_nbits))))
269+
@test root.mi == only(SnoopCompile.specializations(only(methods(PkgD.call_nbits_integer))))
271270

272271
btrees = filter(tree -> isa(tree.method, Core.Binding), trees)
273272
tree = only(filter(tree -> tree.method.globalref.name == :undefined_function, btrees))
274273
@test length(tree.mt_backedges) == 1
275274
tree = only(filter(tree -> tree.method.globalref.name == :someconst, btrees))
276-
sig, root = only(tree.mt_backedges)
275+
_, root = only(tree.mt_backedges)
277276
node = only(root.children)
278277
@test node.mi.def == only(methods(PkgD.uses_someconst))
279278
tree = only(filter(tree -> tree.method.globalref.name == :MyType, btrees))
280-
sig, root = only(tree.mt_backedges)
279+
_, root = only(tree.mt_backedges)
281280
node = only(root.children)
282281
@test node.mi.def == only(methods(PkgD.calls_mytype))
283282

0 commit comments

Comments
 (0)