Skip to content

Commit 77b90b9

Browse files
authored
bpart: Properly track methods with invalidated source after require_world (#58830)
There are three categories of methods we need to worry about during staticdata validation: 1. New methods added to existing generic functions 2. New methods added to new generic functions 3. Existing methods that now have new CodeInstances In each of these cases, we need to check whether any of the implicit binding edges from the method's source was invalidated. Currently, we handle this for 1 and 2 by explicitly scanning the method on load. However, we were not tracking it for case 3. Fix that by using an extra bit in did_scan_method that gets set when we see an existing method getting invalidated, so we know that we need to drop the corresponding CodeInstances during load. Fixes #58346
1 parent d48a675 commit 77b90b9

File tree

7 files changed

+33
-27
lines changed

7 files changed

+33
-27
lines changed

base/invalidation.jl

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ function invalidate_method_for_globalref!(gr::GlobalRef, method::Method, invalid
6969
src = _uncompressed_ir(method)
7070
invalidate_all = should_invalidate_code_for_globalref(gr, src)
7171
end
72+
if invalidate_all && !Base.generating_output()
73+
@atomic method.did_scan_source |= 0x4
74+
end
7275
invalidated_any = false
7376
for mi in specializations(method)
7477
isdefined(mi, :cache) || continue
@@ -182,7 +185,7 @@ function binding_was_invalidated(b::Core.Binding)
182185
b.partitions.min_world > unsafe_load(cglobal(:jl_require_world, UInt))
183186
end
184187

185-
function scan_new_method!(methods_with_invalidated_source::IdSet{Method}, method::Method, image_backedges_only::Bool)
188+
function scan_new_method!(method::Method, image_backedges_only::Bool)
186189
isdefined(method, :source) || return
187190
if image_backedges_only && !has_image_globalref(method)
188191
return
@@ -195,21 +198,20 @@ function scan_new_method!(methods_with_invalidated_source::IdSet{Method}, method
195198
# TODO: We could turn this into an addition if condition. For now, use it as a reasonably cheap
196199
# additional consistency check
197200
@assert !image_backedges_only
198-
push!(methods_with_invalidated_source, method)
201+
@atomic method.did_scan_source |= 0x4
199202
end
200203
maybe_add_binding_backedge!(b, method)
201204
end
205+
@atomic method.did_scan_source |= 0x1
202206
end
203207

204-
function scan_new_methods(extext_methods::Vector{Any}, internal_methods::Vector{Any}, image_backedges_only::Bool)
205-
methods_with_invalidated_source = IdSet{Method}()
208+
function scan_new_methods!(extext_methods::Vector{Any}, internal_methods::Vector{Any}, image_backedges_only::Bool)
206209
for method in internal_methods
207210
if isa(method, Method)
208-
scan_new_method!(methods_with_invalidated_source, method, image_backedges_only)
211+
scan_new_method!(method, image_backedges_only)
209212
end
210213
end
211214
for tme::Core.TypeMapEntry in extext_methods
212-
scan_new_method!(methods_with_invalidated_source, tme.func::Method, image_backedges_only)
215+
scan_new_method!(tme.func::Method, image_backedges_only)
213216
end
214-
return methods_with_invalidated_source
215217
end

base/runtime_internals.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,6 +1414,8 @@ Returns the world the [current_task()](@ref) is executing within.
14141414
"""
14151415
tls_world_age() = ccall(:jl_get_tls_world_age, UInt, ())
14161416

1417+
get_require_world() = unsafe_load(cglobal(:jl_require_world, UInt))
1418+
14171419
"""
14181420
propertynames(x, private=false)
14191421

base/staticdata.jl

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,20 @@ function insert_backedges(edges::Vector{Any}, ext_ci_list::Union{Nothing,Vector{
1717
# determine which CodeInstance objects are still valid in our image
1818
# to enable any applicable new codes
1919
backedges_only = unsafe_load(cglobal(:jl_first_image_replacement_world, UInt)) == typemax(UInt)
20-
methods_with_invalidated_source = Base.scan_new_methods(extext_methods, internal_methods, backedges_only)
20+
Base.scan_new_methods!(extext_methods, internal_methods, backedges_only)
2121
stack = CodeInstance[]
2222
visiting = IdDict{CodeInstance,Int}()
23-
_insert_backedges(edges, stack, visiting, methods_with_invalidated_source)
23+
_insert_backedges(edges, stack, visiting)
2424
if ext_ci_list !== nothing
25-
_insert_backedges(ext_ci_list, stack, visiting, methods_with_invalidated_source, #=external=#true)
25+
_insert_backedges(ext_ci_list, stack, visiting, #=external=#true)
2626
end
2727
end
2828

29-
function _insert_backedges(edges::Vector{Any}, stack::Vector{CodeInstance}, visiting::IdDict{CodeInstance,Int}, mwis::IdSet{Method}, external::Bool=false)
29+
function _insert_backedges(edges::Vector{Any}, stack::Vector{CodeInstance}, visiting::IdDict{CodeInstance,Int}, external::Bool=false)
3030
for i = 1:length(edges)
3131
codeinst = edges[i]::CodeInstance
3232
validation_world = get_world_counter()
33-
verify_method_graph(codeinst, stack, visiting, mwis, validation_world)
33+
verify_method_graph(codeinst, stack, visiting, validation_world)
3434
# After validation, under the world_counter_lock, set max_world to typemax(UInt) for all dependencies
3535
# (recursively). From that point onward the ordinary backedge mechanism is responsible for maintaining
3636
# validity.
@@ -54,16 +54,14 @@ function _insert_backedges(edges::Vector{Any}, stack::Vector{CodeInstance}, visi
5454
end
5555
end
5656

57-
function verify_method_graph(codeinst::CodeInstance, stack::Vector{CodeInstance}, visiting::IdDict{CodeInstance,Int}, mwis::IdSet{Method}, validation_world::UInt)
57+
function verify_method_graph(codeinst::CodeInstance, stack::Vector{CodeInstance}, visiting::IdDict{CodeInstance,Int}, validation_world::UInt)
5858
@assert isempty(stack); @assert isempty(visiting);
59-
child_cycle, minworld, maxworld = verify_method(codeinst, stack, visiting, mwis, validation_world)
59+
child_cycle, minworld, maxworld = verify_method(codeinst, stack, visiting, validation_world)
6060
@assert child_cycle == 0
6161
@assert isempty(stack); @assert isempty(visiting);
6262
nothing
6363
end
6464

65-
get_require_world() = unsafe_load(cglobal(:jl_require_world, UInt))
66-
6765
function gen_staged_sig(def::Method, mi::MethodInstance)
6866
isdefined(def, :generator) || return nothing
6967
isdispatchtuple(mi.specTypes) || return nothing
@@ -113,7 +111,7 @@ end
113111
# - Visit the entire call graph, starting from edges[idx] to determine if that method is valid
114112
# - Implements Tarjan's SCC (strongly connected components) algorithm, simplified to remove the count variable
115113
# and slightly modified with an early termination option once the computation reaches its minimum
116-
function verify_method(codeinst::CodeInstance, stack::Vector{CodeInstance}, visiting::IdDict{CodeInstance,Int}, mwis::IdSet{Method}, validation_world::UInt)
114+
function verify_method(codeinst::CodeInstance, stack::Vector{CodeInstance}, visiting::IdDict{CodeInstance,Int}, validation_world::UInt)
117115
world = codeinst.min_world
118116
let max_valid2 = codeinst.max_world
119117
if max_valid2 WORLD_AGE_REVALIDATION_SENTINEL
@@ -127,13 +125,13 @@ function verify_method(codeinst::CodeInstance, stack::Vector{CodeInstance}, visi
127125
end
128126

129127
# Implicitly referenced bindings in the current module do not get explicit edges.
130-
# If they were invalidated, they'll be in `mwis`. If they weren't, they imply a minworld
128+
# If they were invalidated, they'll have the flag set in did_scan_source. If they weren't, they imply a minworld
131129
# of `get_require_world`. In principle, this is only required for methods that do reference
132130
# an implicit globalref. However, we already don't perform this validation for methods that
133131
# don't have any (implicit or explicit) edges at all. The remaining corner case (some explicit,
134132
# but no implicit edges) is rare and there would be little benefit to lower the minworld for it
135133
# in any case, so we just always use `get_require_world` here.
136-
local minworld::UInt, maxworld::UInt = get_require_world(), validation_world
134+
local minworld::UInt, maxworld::UInt = Base.get_require_world(), validation_world
137135
if haskey(visiting, codeinst)
138136
return visiting[codeinst], minworld, maxworld
139137
end
@@ -143,7 +141,11 @@ function verify_method(codeinst::CodeInstance, stack::Vector{CodeInstance}, visi
143141
# TODO JL_TIMING(VERIFY_IMAGE, VERIFY_Methods)
144142
callees = codeinst.edges
145143
# Check for invalidation of the implicit edges from GlobalRef in the Method source
146-
if def in mwis
144+
if (def.did_scan_source & 0x1) == 0x0
145+
backedges_only = unsafe_load(cglobal(:jl_first_image_replacement_world, UInt)) == typemax(UInt)
146+
Base.scan_new_method!(def, backedges_only)
147+
end
148+
if (def.did_scan_source & 0x4) != 0x0
147149
maxworld = 0
148150
invalidations = _jl_debug_method_invalidation[]
149151
if invalidations !== nothing
@@ -153,7 +155,7 @@ function verify_method(codeinst::CodeInstance, stack::Vector{CodeInstance}, visi
153155
# verify current edges
154156
if isempty(callees)
155157
# quick return: no edges to verify (though we probably shouldn't have gotten here from WORLD_AGE_REVALIDATION_SENTINEL)
156-
elseif maxworld == get_require_world()
158+
elseif maxworld == Base.get_require_world()
157159
# if no new worlds were allocated since serializing the base module, then no new validation is worth doing right now either
158160
else
159161
j = 1
@@ -231,7 +233,7 @@ function verify_method(codeinst::CodeInstance, stack::Vector{CodeInstance}, visi
231233
end
232234
callee = edge
233235
local min_valid2::UInt, max_valid2::UInt
234-
child_cycle, min_valid2, max_valid2 = verify_method(callee, stack, visiting, mwis, validation_world)
236+
child_cycle, min_valid2, max_valid2 = verify_method(callee, stack, visiting, validation_world)
235237
if minworld < min_valid2
236238
minworld = min_valid2
237239
end

src/jltypes.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3607,7 +3607,7 @@ void jl_init_types(void) JL_GC_DISABLED
36073607
0, 1, 10);
36083608
//const static uint32_t method_constfields[1] = { 0b0 }; // (1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<6)|(1<<9)|(1<<10)|(1<<17)|(1<<21)|(1<<22)|(1<<23)|(1<<24)|(1<<25)|(1<<26)|(1<<27)|(1<<28)|(1<<29)|(1<<30);
36093609
//jl_method_type->name->constfields = method_constfields;
3610-
const static uint32_t method_atomicfields[1] = { 0x00000030 }; // (1<<4)|(1<<5)
3610+
const static uint32_t method_atomicfields[1] = { 0x10000030 }; // (1<<4)|(1<<5)||(1<<28)
36113611
jl_method_type->name->atomicfields = method_atomicfields;
36123612

36133613
jl_method_instance_type =

src/julia.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ typedef struct _jl_method_t {
368368
uint8_t nospecializeinfer;
369369
// bit flags, 0x01 = scanned
370370
// 0x02 = added to module scanned list (either from scanning or inference edge)
371+
// 0x04 = Source was invalidated since jl_require_world
371372
_Atomic(uint8_t) did_scan_source;
372373

373374
// uint8 settings

test/core.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ end
3636
for (T, c) in (
3737
(Core.CodeInfo, []),
3838
(Core.CodeInstance, [:next, :min_world, :max_world, :inferred, :edges, :debuginfo, :ipo_purity_bits, :invoke, :specptr, :specsigflags, :precompile, :time_compile]),
39-
(Core.Method, [:primary_world, :dispatch_status]),
39+
(Core.Method, [:primary_world, :did_scan_source, :dispatch_status]),
4040
(Core.MethodInstance, [:cache, :flags]),
4141
(Core.MethodTable, [:defs]),
4242
(Core.MethodCache, [:leafcache, :cache, :var""]),

test/precompile.jl

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,9 +1083,8 @@ precompile_test_harness("code caching") do dir
10831083
@test hasvalid(mi, world) # was compiled with the new method
10841084
m = only(methods(MA.fib))
10851085
mi = m.specializations::Core.MethodInstance
1086-
@test isdefined(mi, :cache) # it was precompiled by StaleB
1087-
@test_broken !hasvalid(mi, world) # invalidated by redefining `gib` before loading StaleB
1088-
@test_broken MA.fib() === 2.0
1086+
@test !hasvalid(mi, world) # invalidated by redefining `gib` before loading StaleB
1087+
@test MA.fib() === 2.0
10891088

10901089
# Reporting test (ensure SnoopCompile works)
10911090
@test all(i -> isassigned(invalidations, i), eachindex(invalidations))

0 commit comments

Comments
 (0)