Skip to content

Commit 5659aef

Browse files
KenoKristofferC
authored andcommitted
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 (cherry picked from commit 77b90b9)
1 parent 5d106c5 commit 5659aef

File tree

7 files changed

+38
-27
lines changed

7 files changed

+38
-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 chekc
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
@@ -1266,6 +1266,8 @@ Returns the world the [current_task()](@ref) is executing within.
12661266
"""
12671267
tls_world_age() = ccall(:jl_get_tls_world_age, UInt, ())
12681268

1269+
get_require_world() = unsafe_load(cglobal(:jl_require_world, UInt))
1270+
12691271
"""
12701272
propertynames(x, private=false)
12711273

base/staticdata.jl

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

38-
function _insert_backedges(edges::Vector{Any}, stack::Vector{CodeInstance}, visiting::IdDict{CodeInstance,Int}, mwis::IdSet{Method}, external::Bool=false)
38+
function _insert_backedges(edges::Vector{Any}, stack::Vector{CodeInstance}, visiting::IdDict{CodeInstance,Int}, external::Bool=false)
3939
for i = 1:length(edges)
4040
codeinst = edges[i]::CodeInstance
4141
validation_world = get_world_counter()
42-
verify_method_graph(codeinst, stack, visiting, mwis, validation_world)
42+
verify_method_graph(codeinst, stack, visiting, validation_world)
4343
# After validation, under the world_counter_lock, set max_world to typemax(UInt) for all dependencies
4444
# (recursively). From that point onward the ordinary backedge mechanism is responsible for maintaining
4545
# validity.
@@ -63,16 +63,14 @@ function _insert_backedges(edges::Vector{Any}, stack::Vector{CodeInstance}, visi
6363
end
6464
end
6565

66-
function verify_method_graph(codeinst::CodeInstance, stack::Vector{CodeInstance}, visiting::IdDict{CodeInstance,Int}, mwis::IdSet{Method}, validation_world::UInt)
66+
function verify_method_graph(codeinst::CodeInstance, stack::Vector{CodeInstance}, visiting::IdDict{CodeInstance,Int}, validation_world::UInt)
6767
@assert isempty(stack); @assert isempty(visiting);
68-
child_cycle, minworld, maxworld = verify_method(codeinst, stack, visiting, mwis, validation_world)
68+
child_cycle, minworld, maxworld = verify_method(codeinst, stack, visiting, validation_world)
6969
@assert child_cycle == 0
7070
@assert isempty(stack); @assert isempty(visiting);
7171
nothing
7272
end
7373

74-
get_require_world() = unsafe_load(cglobal(:jl_require_world, UInt))
75-
7674
function gen_staged_sig(def::Method, mi::MethodInstance)
7775
isdefined(def, :generator) || return nothing
7876
isdispatchtuple(mi.specTypes) || return nothing
@@ -122,7 +120,7 @@ end
122120
# - Visit the entire call graph, starting from edges[idx] to determine if that method is valid
123121
# - Implements Tarjan's SCC (strongly connected components) algorithm, simplified to remove the count variable
124122
# and slightly modified with an early termination option once the computation reaches its minimum
125-
function verify_method(codeinst::CodeInstance, stack::Vector{CodeInstance}, visiting::IdDict{CodeInstance,Int}, mwis::IdSet{Method}, validation_world::UInt)
123+
function verify_method(codeinst::CodeInstance, stack::Vector{CodeInstance}, visiting::IdDict{CodeInstance,Int}, validation_world::UInt)
126124
world = codeinst.min_world
127125
let max_valid2 = codeinst.max_world
128126
if max_valid2 WORLD_AGE_REVALIDATION_SENTINEL
@@ -136,13 +134,13 @@ function verify_method(codeinst::CodeInstance, stack::Vector{CodeInstance}, visi
136134
end
137135

138136
# Implicitly referenced bindings in the current module do not get explicit edges.
139-
# If they were invalidated, they'll be in `mwis`. If they weren't, they imply a minworld
137+
# If they were invalidated, they'll have the flag set in did_scan_source. If they weren't, they imply a minworld
140138
# of `get_require_world`. In principle, this is only required for methods that do reference
141139
# an implicit globalref. However, we already don't perform this validation for methods that
142140
# don't have any (implicit or explicit) edges at all. The remaining corner case (some explicit,
143141
# but no implicit edges) is rare and there would be little benefit to lower the minworld for it
144142
# in any case, so we just always use `get_require_world` here.
145-
local minworld::UInt, maxworld::UInt = get_require_world(), validation_world
143+
local minworld::UInt, maxworld::UInt = Base.get_require_world(), validation_world
146144
if haskey(visiting, codeinst)
147145
return visiting[codeinst], minworld, maxworld
148146
end
@@ -152,7 +150,11 @@ function verify_method(codeinst::CodeInstance, stack::Vector{CodeInstance}, visi
152150
# TODO JL_TIMING(VERIFY_IMAGE, VERIFY_Methods)
153151
callees = codeinst.edges
154152
# Check for invalidation of the implicit edges from GlobalRef in the Method source
155-
if def in mwis
153+
if (def.did_scan_source & 0x1) == 0x0
154+
backedges_only = unsafe_load(cglobal(:jl_first_image_replacement_world, UInt)) == typemax(UInt)
155+
Base.scan_new_method!(def, backedges_only)
156+
end
157+
if (def.did_scan_source & 0x4) != 0x0
156158
maxworld = 0
157159
invalidations = _jl_debug_method_invalidation[]
158160
if invalidations !== nothing
@@ -162,7 +164,7 @@ function verify_method(codeinst::CodeInstance, stack::Vector{CodeInstance}, visi
162164
# verify current edges
163165
if isempty(callees)
164166
# quick return: no edges to verify (though we probably shouldn't have gotten here from WORLD_AGE_REVALIDATION_SENTINEL)
165-
elseif maxworld == get_require_world()
167+
elseif maxworld == Base.get_require_world()
166168
# if no new worlds were allocated since serializing the base module, then no new validation is worth doing right now either
167169
else
168170
j = 1
@@ -240,7 +242,7 @@ function verify_method(codeinst::CodeInstance, stack::Vector{CodeInstance}, visi
240242
end
241243
callee = edge
242244
local min_valid2::UInt, max_valid2::UInt
243-
child_cycle, min_valid2, max_valid2 = verify_method(callee, stack, visiting, mwis, validation_world)
245+
child_cycle, min_valid2, max_valid2 = verify_method(callee, stack, visiting, validation_world)
244246
if minworld < min_valid2
245247
minworld = min_valid2
246248
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
@@ -377,6 +377,7 @@ typedef struct _jl_method_t {
377377
uint8_t nospecializeinfer;
378378
// bit flags, 0x01 = scanned
379379
// 0x02 = added to module scanned list (either from scanning or inference edge)
380+
// 0x04 = Source was invalidated since jl_require_world
380381
_Atomic(uint8_t) did_scan_source;
381382

382383
// 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: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,8 +1034,6 @@ precompile_test_harness("code caching") do dir
10341034
@eval using $StaleA
10351035
MA = invokelatest(getfield, @__MODULE__, StaleA)
10361036
Base.eval(MA, :(nbits(::UInt8) = 8))
1037-
<<<<<<< HEAD
1038-
=======
10391037
Base.eval(MA, quote
10401038
struct InvalidatedBinding
10411039
x::Float64
@@ -1052,7 +1050,6 @@ precompile_test_harness("code caching") do dir
10521050
end
10531051
const glbi = LogBindingInvalidation(2.0)
10541052
end)
1055-
>>>>>>> 75946ce31c (Add binding invalidations to log (#58226))
10561053
@eval using $StaleC
10571054
invalidations = Base.StaticData.debug_method_invalidation(true)
10581055
@eval using $StaleB
@@ -1083,6 +1080,13 @@ precompile_test_harness("code caching") do dir
10831080
m = only(methods(MC.call_buildstale))
10841081
mi = m.specializations::Core.MethodInstance
10851082
@test hasvalid(mi, world) # was compiled with the new method
1083+
<<<<<<< HEAD
1084+
=======
1085+
m = only(methods(MA.fib))
1086+
mi = m.specializations::Core.MethodInstance
1087+
@test !hasvalid(mi, world) # invalidated by redefining `gib` before loading StaleB
1088+
@test MA.fib() === 2.0
1089+
>>>>>>> 77b90b9e62 (bpart: Properly track methods with invalidated source after require_world (#58830))
10861090

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

0 commit comments

Comments
 (0)