Skip to content

Commit a1a2ac6

Browse files
authored
finish implementation of upgradable stdlibs (#54739)
This now allows the user to load any number of copies of a module, and uses the combination of the environment, explicitly loaded modules, and the requirements of the precompile caches to determine the meaning of a name and which files need to be loaded. Note however that package extensions continue to primarily only apply to the explicitly loaded modules, although they may get loaded incidentally as the dependency of another package, they won't get defined for every pair of combinations of triggering modules. Fixes #53983
1 parent 3849c9d commit a1a2ac6

File tree

3 files changed

+96
-66
lines changed

3 files changed

+96
-66
lines changed

base/Base.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,10 @@ function __init__()
606606
init_active_project()
607607
append!(empty!(_sysimage_modules), keys(loaded_modules))
608608
empty!(explicit_loaded_modules)
609+
@assert isempty(loaded_precompiles)
610+
for (mod, key) in module_keys
611+
loaded_precompiles[key => module_build_id(mod)] = mod
612+
end
609613
if haskey(ENV, "JULIA_MAX_NUM_PRECOMPILE_FILES")
610614
MAX_NUM_PRECOMPILE_FILES[] = parse(Int, ENV["JULIA_MAX_NUM_PRECOMPILE_FILES"])
611615
end

base/loading.jl

Lines changed: 85 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,8 +1218,8 @@ function _include_from_serialized(pkg::PkgId, path::String, ocachepath::Union{No
12181218
dep = depmods[i]
12191219
dep isa Module && continue
12201220
_, depkey, depbuild_id = dep::Tuple{String, PkgId, UInt128}
1221-
@assert root_module_exists(depkey)
1222-
dep = root_module(depkey)
1221+
dep = loaded_precompiles[depkey => depbuild_id]
1222+
@assert PkgId(dep) == depkey && module_build_id(dep) === depbuild_id
12231223
depmods[i] = dep
12241224
end
12251225

@@ -1276,7 +1276,8 @@ function register_restored_modules(sv::SimpleVector, pkg::PkgId, path::String)
12761276
push!(Base.Docs.modules, M)
12771277
end
12781278
if parentmodule(M) === M
1279-
register_root_module(M)
1279+
push!(loaded_modules_order, M)
1280+
loaded_precompiles[pkg => module_build_id(M)] = M
12801281
end
12811282
end
12821283

@@ -1703,7 +1704,7 @@ function compilecache_path(pkg::PkgId;
17031704
if staledeps === true
17041705
continue
17051706
end
1706-
staledeps, _ = staledeps::Tuple{Vector{Any}, Union{Nothing, String}}
1707+
staledeps, _, _ = staledeps::Tuple{Vector{Any}, Union{Nothing, String}, UInt128}
17071708
# finish checking staledeps module graph
17081709
for i in 1:length(staledeps)
17091710
dep = staledeps[i]
@@ -1791,23 +1792,23 @@ end
17911792
# search for a precompile cache file to load, after some various checks
17921793
function _tryrequire_from_serialized(modkey::PkgId, build_id::UInt128)
17931794
assert_havelock(require_lock)
1794-
if root_module_exists(modkey)
1795-
loaded = root_module(modkey)
1796-
else
1795+
loaded = maybe_root_module(modkey)
1796+
if loaded === nothing
17971797
loaded = start_loading(modkey)
1798-
if loaded === nothing
1799-
try
1800-
modpath = locate_package(modkey)
1801-
modpath === nothing && return nothing
1802-
set_pkgorigin_version_path(modkey, String(modpath))
1803-
loaded = _require_search_from_serialized(modkey, String(modpath), build_id, true)
1804-
finally
1805-
end_loading(modkey, loaded)
1806-
end
1807-
if loaded isa Module
1808-
insert_extension_triggers(modkey)
1809-
run_package_callbacks(modkey)
1810-
end
1798+
end
1799+
if loaded === nothing
1800+
try
1801+
modpath = locate_package(modkey)
1802+
isnothing(modpath) && error("Cannot locate source for $(repr("text/plain", modkey))")
1803+
modpath = String(modpath)::String
1804+
set_pkgorigin_version_path(modkey, modpath)
1805+
loaded = _require_search_from_serialized(modkey, modpath, build_id, true)
1806+
finally
1807+
end_loading(modkey, loaded)
1808+
end
1809+
if loaded isa Module
1810+
insert_extension_triggers(modkey)
1811+
run_package_callbacks(modkey)
18111812
end
18121813
end
18131814
if loaded isa Module && PkgId(loaded) == modkey && module_build_id(loaded) === build_id
@@ -1880,10 +1881,12 @@ function _tryrequire_from_serialized(pkg::PkgId, path::String, ocachepath::Union
18801881
depmods[i] = dep
18811882
end
18821883
# then load the file
1883-
return _include_from_serialized(pkg, path, ocachepath, depmods, ignore_native)
1884+
loaded = _include_from_serialized(pkg, path, ocachepath, depmods, ignore_native)
1885+
loaded isa Module && register_root_module(loaded)
1886+
return loaded
18841887
end
18851888

1886-
# returns `nothing` if require found a precompile cache for this sourcepath, but couldn't load it
1889+
# returns `nothing` if require found a precompile cache for this sourcepath, but couldn't load it or it was stale
18871890
# returns the set of modules restored if the cache load succeeded
18881891
@constprop :none function _require_search_from_serialized(pkg::PkgId, sourcepath::String, build_id::UInt128, stalecheck::Bool; reasons=nothing, DEPOT_PATH::typeof(DEPOT_PATH)=DEPOT_PATH)
18891892
assert_havelock(require_lock)
@@ -1895,7 +1898,7 @@ end
18951898
continue
18961899
end
18971900
try
1898-
staledeps, ocachefile = staledeps::Tuple{Vector{Any}, Union{Nothing, String}}
1901+
staledeps, ocachefile, build_id = staledeps::Tuple{Vector{Any}, Union{Nothing, String}, UInt128}
18991902
# finish checking staledeps module graph
19001903
for i in 1:length(staledeps)
19011904
dep = staledeps[i]
@@ -1907,14 +1910,19 @@ end
19071910
if modstaledeps === true
19081911
continue
19091912
end
1910-
modstaledeps, modocachepath = modstaledeps::Tuple{Vector{Any}, Union{Nothing, String}}
1913+
modstaledeps, modocachepath, _ = modstaledeps::Tuple{Vector{Any}, Union{Nothing, String}, UInt128}
19111914
staledeps[i] = (modpath, modkey, modbuild_id, modpath_to_try, modstaledeps, modocachepath)
19121915
@goto check_next_dep
19131916
end
19141917
@debug "Rejecting cache file $path_to_try because required dependency $modkey with build ID $(UUID(modbuild_id)) is missing from the cache."
19151918
@goto check_next_path
19161919
@label check_next_dep
19171920
end
1921+
M = get(loaded_precompiles, pkg => build_id, nothing)
1922+
if isa(M, Module)
1923+
stalecheck && register_root_module(M)
1924+
return M
1925+
end
19181926
if stalecheck
19191927
try
19201928
touch(path_to_try) # update timestamp of precompilation file
@@ -1927,26 +1935,25 @@ end
19271935
dep = staledeps[i]
19281936
dep isa Module && continue
19291937
modpath, modkey, modbuild_id, modcachepath, modstaledeps, modocachepath = dep::Tuple{String, PkgId, UInt128, String, Vector{Any}, Union{Nothing, String}}
1930-
dep = nothing
1931-
if root_module_exists(modkey)
1932-
dep = root_module(modkey)
1938+
dep = get(loaded_precompiles, modkey => modbuild_id, nothing)
1939+
if dep === nothing
1940+
dep = maybe_root_module(modkey)
19331941
end
19341942
while true
19351943
if dep isa Module
19361944
if PkgId(dep) == modkey && module_build_id(dep) === modbuild_id
19371945
break
19381946
else
1939-
if stalecheck
1940-
@debug "Rejecting cache file $path_to_try because module $modkey is already loaded and incompatible."
1941-
@goto check_next_path
1942-
end
1947+
@debug "Rejecting cache file $path_to_try because module $modkey got loaded at a different version than expected."
1948+
@goto check_next_path
19431949
end
19441950
end
19451951
dep = start_loading(modkey)
19461952
if dep === nothing
19471953
try
19481954
set_pkgorigin_version_path(modkey, modpath)
19491955
dep = _include_from_serialized(modkey, modcachepath, modocachepath, modstaledeps)
1956+
dep isa Module && stalecheck && register_root_module(dep)
19501957
finally
19511958
end_loading(modkey, dep)
19521959
end
@@ -1960,7 +1967,11 @@ end
19601967
end
19611968
staledeps[i] = dep
19621969
end
1963-
restored = _include_from_serialized(pkg, path_to_try, ocachefile, staledeps)
1970+
restored = get(loaded_precompiles, pkg => build_id, nothing)
1971+
if !isa(restored, Module)
1972+
restored = _include_from_serialized(pkg, path_to_try, ocachefile, staledeps)
1973+
end
1974+
isa(restored, Module) && stalecheck && register_root_module(restored)
19641975
isa(restored, Module) && return restored
19651976
@debug "Deserialization checks failed while attempting to load cache from $path_to_try" exception=restored
19661977
@label check_next_path
@@ -2046,7 +2057,7 @@ const package_callbacks = Any[]
20462057
const include_callbacks = Any[]
20472058

20482059
# used to optionally track dependencies when requiring a module:
2049-
const _concrete_dependencies = Pair{PkgId,UInt128}[] # these dependency versions are "set in stone", and the process should try to avoid invalidating them
2060+
const _concrete_dependencies = Pair{PkgId,UInt128}[] # these dependency versions are "set in stone", because they are explicitly loaded, and the process should try to avoid invalidating them
20502061
const _require_dependencies = Any[] # a list of (mod, abspath, fsize, hash, mtime) tuples that are the file dependencies of the module currently being precompiled
20512062
const _track_dependencies = Ref(false) # set this to true to track the list of file dependencies
20522063
function _include_dependency(mod::Module, _path::AbstractString; track_content=true,
@@ -2296,14 +2307,19 @@ end
22962307
PkgOrigin() = PkgOrigin(nothing, nothing, nothing)
22972308
const pkgorigins = Dict{PkgId,PkgOrigin}()
22982309

2299-
const loaded_modules = Dict{PkgId,Module}()
2300-
# Emptied on Julia start
2301-
const explicit_loaded_modules = Dict{PkgId,Module}()
2310+
const explicit_loaded_modules = Dict{PkgId,Module}() # Emptied on Julia start
2311+
const loaded_modules = Dict{PkgId,Module}() # available to be explicitly loaded
2312+
const loaded_precompiles = Dict{Pair{PkgId,UInt128},Module}() # extended (complete) list of modules, available to be loaded
23022313
const loaded_modules_order = Vector{Module}()
2303-
const module_keys = IdDict{Module,PkgId}() # the reverse
2314+
const module_keys = IdDict{Module,PkgId}() # the reverse of loaded_modules
23042315

23052316
root_module_key(m::Module) = @lock require_lock module_keys[m]
23062317

2318+
function module_build_id(m::Module)
2319+
hi, lo = ccall(:jl_module_build_id, NTuple{2,UInt64}, (Any,), m)
2320+
return (UInt128(hi) << 64) | lo
2321+
end
2322+
23072323
@constprop :none function register_root_module(m::Module)
23082324
# n.b. This is called from C after creating a new module in `Base.__toplevel__`,
23092325
# instead of adding them to the binding table there.
@@ -2319,7 +2335,7 @@ root_module_key(m::Module) = @lock require_lock module_keys[m]
23192335
end
23202336
end
23212337
end
2322-
push!(loaded_modules_order, m)
2338+
haskey(loaded_precompiles, key => module_build_id(m)) || push!(loaded_modules_order, m)
23232339
loaded_modules[key] = m
23242340
explicit_loaded_modules[key] = m
23252341
module_keys[m] = key
@@ -2351,6 +2367,9 @@ root_module_exists(key::PkgId) = @lock require_lock haskey(loaded_modules, key)
23512367
loaded_modules_array() = @lock require_lock copy(loaded_modules_order)
23522368

23532369
function unreference_module(key::PkgId)
2370+
if haskey(explicit_loaded_modules, key)
2371+
m = pop!(explicit_loaded_modules, key)
2372+
end
23542373
if haskey(loaded_modules, key)
23552374
m = pop!(loaded_modules, key)
23562375
# need to ensure all modules are GC rooted; will still be referenced
@@ -2495,7 +2514,7 @@ function _require(pkg::PkgId, env=nothing)
24952514
return loaded
24962515
end
24972516

2498-
# load a serialized file directly
2517+
# load a serialized file directly, including dependencies (without checking staleness except for immediate conflicts)
24992518
function _require_from_serialized(uuidkey::PkgId, path::String, ocachepath::Union{String, Nothing}, sourcepath::String)
25002519
@lock require_lock begin
25012520
set_pkgorigin_version_path(uuidkey, sourcepath)
@@ -2929,13 +2948,15 @@ function compilecache(pkg::PkgId, path::String, internal_stderr::IO = stderr, in
29292948
cachepath = compilecache_dir(pkg)
29302949

29312950
# build up the list of modules that we want the precompile process to preserve
2932-
concrete_deps = copy(_concrete_dependencies)
29332951
if keep_loaded_modules
2934-
for mod in loaded_modules_array()
2935-
if !(mod === Main || mod === Core || mod === Base)
2936-
push!(concrete_deps, PkgId(mod) => module_build_id(mod))
2952+
concrete_deps = copy(_concrete_dependencies)
2953+
for (pkgreq, modreq) in loaded_modules # TODO: convert all relevant staleness heuristics to use explicit_loaded_modules instead
2954+
if !(pkgreq === Main || pkgreq === Core || pkgreq === Base)
2955+
push!(concrete_deps, pkgreq => module_build_id(modreq))
29372956
end
29382957
end
2958+
else
2959+
concrete_deps = empty(_concrete_dependencies)
29392960
end
29402961
# run the expression and cache the result
29412962
verbosity = isinteractive() ? CoreLogging.Info : CoreLogging.Debug
@@ -3063,11 +3084,6 @@ function rename_unique_ocachefile(tmppath_so::String, ocachefile_orig::String, o
30633084
return ocachefile
30643085
end
30653086

3066-
function module_build_id(m::Module)
3067-
hi, lo = ccall(:jl_module_build_id, NTuple{2,UInt64}, (Any,), m)
3068-
return (UInt128(hi) << 64) | lo
3069-
end
3070-
30713087
function object_build_id(obj)
30723088
mod = ccall(:jl_object_top_module, Any, (Any,), obj)
30733089
if mod === nothing
@@ -3646,7 +3662,7 @@ end
36463662
@constprop :none function stale_cachefile(modkey::PkgId, build_id::UInt128, modpath::String, cachefile::String;
36473663
ignore_loaded::Bool=false, requested_flags::CacheFlags=CacheFlags(),
36483664
reasons::Union{Dict{String,Int},Nothing}=nothing, stalecheck::Bool=true)
3649-
# XXX: this function appears to dl all of the file validation, not just those checks related to stale
3665+
# n.b.: this function does nearly all of the file validation, not just those checks related to stale, so the name is potentially unclear
36503666
io = open(cachefile, "r")
36513667
try
36523668
checksum = isvalid_cache_header(io)
@@ -3700,8 +3716,8 @@ end
37003716
record_reason(reasons, "for different pkgid")
37013717
return true
37023718
end
3719+
id_build = (UInt128(checksum) << 64) | id.second
37033720
if build_id != UInt128(0)
3704-
id_build = (UInt128(checksum) << 64) | id.second
37053721
if id_build != build_id
37063722
@debug "Ignoring cache file $cachefile for $modkey ($((UUID(id_build)))) since it does not provide desired build_id ($((UUID(build_id))))"
37073723
record_reason(reasons, "for different buildid")
@@ -3716,8 +3732,12 @@ end
37163732
depmods = Vector{Any}(undef, ndeps)
37173733
for i in 1:ndeps
37183734
req_key, req_build_id = required_modules[i]
3719-
# Module is already loaded
3720-
if root_module_exists(req_key)
3735+
# Check if module is already loaded
3736+
if !stalecheck && haskey(loaded_precompiles, req_key => req_build_id)
3737+
M = loaded_precompiles[req_key => req_build_id]
3738+
@assert PkgId(M) == req_key && module_build_id(M) === req_build_id
3739+
depmods[i] = M
3740+
elseif root_module_exists(req_key)
37213741
M = root_module(req_key)
37223742
if PkgId(M) == req_key && module_build_id(M) === req_build_id
37233743
depmods[i] = M
@@ -3748,17 +3768,19 @@ end
37483768
# check if this file is going to provide one of our concrete dependencies
37493769
# or if it provides a version that conflicts with our concrete dependencies
37503770
# or neither
3751-
for (req_key, req_build_id) in _concrete_dependencies
3752-
build_id = get(modules, req_key, UInt64(0))
3753-
if build_id !== UInt64(0)
3754-
build_id |= UInt128(checksum) << 64
3755-
if build_id === req_build_id
3756-
stalecheck = false
3757-
break
3771+
if stalecheck
3772+
for (req_key, req_build_id) in _concrete_dependencies
3773+
build_id = get(modules, req_key, UInt64(0))
3774+
if build_id !== UInt64(0)
3775+
build_id |= UInt128(checksum) << 64
3776+
if build_id === req_build_id
3777+
stalecheck = false
3778+
break
3779+
end
3780+
@debug "Rejecting cache file $cachefile because it provides the wrong build_id (got $((UUID(build_id)))) for $req_key (want $(UUID(req_build_id)))"
3781+
record_reason(reasons, "wrong dep buildid")
3782+
return true # cachefile doesn't provide the required version of the dependency
37583783
end
3759-
@debug "Rejecting cache file $cachefile because it provides the wrong build_id (got $((UUID(build_id)))) for $req_key (want $(UUID(req_build_id)))"
3760-
record_reason(reasons, "wrong dep buildid")
3761-
return true # cachefile doesn't provide the required version of the dependency
37623784
end
37633785
end
37643786

@@ -3846,7 +3868,7 @@ end
38463868
return true
38473869
end
38483870

3849-
return depmods, ocachefile # fresh cachefile
3871+
return depmods, ocachefile, id_build # fresh cachefile
38503872
finally
38513873
close(io)
38523874
end

test/precompile.jl

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -617,13 +617,13 @@ precompile_test_harness(false) do dir
617617
empty_prefs_hash = Base.get_preferences_hash(nothing, String[])
618618
@test cachefile == Base.compilecache_path(Base.PkgId("FooBar"), empty_prefs_hash)
619619
@test isfile(joinpath(cachedir, "FooBar.ji"))
620-
Tsc = Bool(Base.JLOptions().use_pkgimages) ? Tuple{<:Vector, String} : Tuple{<:Vector, Nothing}
620+
Tsc = Bool(Base.JLOptions().use_pkgimages) ? Tuple{<:Vector, String, UInt128} : Tuple{<:Vector, Nothing, UInt128}
621621
@test Base.stale_cachefile(FooBar_file, joinpath(cachedir, "FooBar.ji")) isa Tsc
622622
@test !isdefined(Main, :FooBar)
623623
@test !isdefined(Main, :FooBar1)
624624

625625
relFooBar_file = joinpath(dir, "subfolder", "..", "FooBar.jl")
626-
@test Base.stale_cachefile(relFooBar_file, joinpath(cachedir, "FooBar.ji")) isa (Sys.iswindows() ? Tuple{<:Vector, String} : Bool) # `..` is not a symlink on Windows
626+
@test Base.stale_cachefile(relFooBar_file, joinpath(cachedir, "FooBar.ji")) isa (Sys.iswindows() ? Tuple{<:Vector, String, UInt128} : Bool) # `..` is not a symlink on Windows
627627
mkdir(joinpath(dir, "subfolder"))
628628
@test Base.stale_cachefile(relFooBar_file, joinpath(cachedir, "FooBar.ji")) isa Tsc
629629

@@ -1524,6 +1524,7 @@ precompile_test_harness("Issue #26028") do load_path
15241524
module Foo26028
15251525
module Bar26028
15261526
x = 0
1527+
y = 0
15271528
end
15281529
function __init__()
15291530
include(joinpath(@__DIR__, "Baz26028.jl"))
@@ -1533,7 +1534,10 @@ precompile_test_harness("Issue #26028") do load_path
15331534
write(joinpath(load_path, "Baz26028.jl"),
15341535
"""
15351536
module Baz26028
1536-
import Foo26028.Bar26028.x
1537+
using Test
1538+
@test_throws(ConcurrencyViolationError("deadlock detected in loading Foo26028 -> Foo26028"),
1539+
@eval import Foo26028.Bar26028.x)
1540+
import ..Foo26028.Bar26028.y
15371541
end
15381542
""")
15391543
Base.compilecache(Base.PkgId("Foo26028"))

0 commit comments

Comments
 (0)