Skip to content

Commit d568d26

Browse files
committed
drop require lock when not needed during loading
Fixes _require_search_from_serialized to acquire all start_loading locks (using a deadlock-free batch-locking algorithm) before doing stalechecks and the rest, so that all the global computations happen behind the require_lock then the rest can happen behind module-specific locks.
1 parent 5b677a1 commit d568d26

File tree

1 file changed

+157
-113
lines changed

1 file changed

+157
-113
lines changed

base/loading.jl

+157-113
Original file line numberDiff line numberDiff line change
@@ -1261,47 +1261,52 @@ function _include_from_serialized(pkg::PkgId, path::String, ocachepath::Union{No
12611261
assert_havelock(require_lock)
12621262
timing_imports = TIMING_IMPORTS[] > 0
12631263
try
1264-
if timing_imports
1265-
t_before = time_ns()
1266-
cumulative_compile_timing(true)
1267-
t_comp_before = cumulative_compile_time_ns()
1268-
end
1264+
if timing_imports
1265+
t_before = time_ns()
1266+
cumulative_compile_timing(true)
1267+
t_comp_before = cumulative_compile_time_ns()
1268+
end
12691269

1270-
for i in eachindex(depmods)
1271-
dep = depmods[i]
1272-
dep isa Module && continue
1273-
_, depkey, depbuild_id = dep::Tuple{String, PkgId, UInt128}
1274-
dep = something(maybe_loaded_precompile(depkey, depbuild_id))
1275-
@assert PkgId(dep) == depkey && module_build_id(dep) === depbuild_id
1276-
depmods[i] = dep
1277-
end
1270+
for i in eachindex(depmods)
1271+
dep = depmods[i]
1272+
dep isa Module && continue
1273+
_, depkey, depbuild_id = dep::Tuple{String, PkgId, UInt128}
1274+
dep = something(maybe_loaded_precompile(depkey, depbuild_id))
1275+
@assert PkgId(dep) == depkey && module_build_id(dep) === depbuild_id
1276+
depmods[i] = dep
1277+
end
12781278

1279-
if ocachepath !== nothing
1280-
@debug "Loading object cache file $ocachepath for $(repr("text/plain", pkg))"
1281-
sv = ccall(:jl_restore_package_image_from_file, Any, (Cstring, Any, Cint, Cstring, Cint), ocachepath, depmods, false, pkg.name, ignore_native)
1282-
else
1283-
@debug "Loading cache file $path for $(repr("text/plain", pkg))"
1284-
sv = ccall(:jl_restore_incremental, Any, (Cstring, Any, Cint, Cstring), path, depmods, false, pkg.name)
1285-
end
1286-
if isa(sv, Exception)
1287-
return sv
1288-
end
1279+
unlock(require_lock) # temporarily _unlock_ during these operations
1280+
sv = try
1281+
if ocachepath !== nothing
1282+
@debug "Loading object cache file $ocachepath for $(repr("text/plain", pkg))"
1283+
ccall(:jl_restore_package_image_from_file, Any, (Cstring, Any, Cint, Cstring, Cint), ocachepath, depmods, false, pkg.name, ignore_native)
1284+
else
1285+
@debug "Loading cache file $path for $(repr("text/plain", pkg))"
1286+
ccall(:jl_restore_incremental, Any, (Cstring, Any, Cint, Cstring), path, depmods, false, pkg.name)
1287+
end
1288+
finally
1289+
lock(require_lock)
1290+
end
1291+
if isa(sv, Exception)
1292+
return sv
1293+
end
12891294

1290-
restored = register_restored_modules(sv, pkg, path)
1295+
restored = register_restored_modules(sv, pkg, path)
12911296

1292-
for M in restored
1293-
M = M::Module
1294-
if parentmodule(M) === M && PkgId(M) == pkg
1295-
register && register_root_module(M)
1296-
if timing_imports
1297-
elapsed_time = time_ns() - t_before
1298-
comp_time, recomp_time = cumulative_compile_time_ns() .- t_comp_before
1299-
print_time_imports_report(M, elapsed_time, comp_time, recomp_time)
1297+
for M in restored
1298+
M = M::Module
1299+
if parentmodule(M) === M && PkgId(M) == pkg
1300+
register && register_root_module(M)
1301+
if timing_imports
1302+
elapsed_time = time_ns() - t_before
1303+
comp_time, recomp_time = cumulative_compile_time_ns() .- t_comp_before
1304+
print_time_imports_report(M, elapsed_time, comp_time, recomp_time)
1305+
end
1306+
return M
13001307
end
1301-
return M
13021308
end
1303-
end
1304-
return ErrorException("Required dependency $(repr("text/plain", pkg)) failed to load from a cache file.")
1309+
return ErrorException("Required dependency $(repr("text/plain", pkg)) failed to load from a cache file.")
13051310

13061311
finally
13071312
timing_imports && cumulative_compile_timing(false)
@@ -2020,13 +2025,46 @@ end
20202025
if staledeps === true
20212026
continue
20222027
end
2023-
try
2024-
staledeps, ocachefile, newbuild_id = staledeps::Tuple{Vector{Any}, Union{Nothing, String}, UInt128}
2025-
# finish checking staledeps module graph
2026-
for i in eachindex(staledeps)
2028+
staledeps, ocachefile, newbuild_id = staledeps::Tuple{Vector{Any}, Union{Nothing, String}, UInt128}
2029+
startedloading = length(staledeps) + 1
2030+
try # any exit from here (goto, break, continue, return) will end_loading
2031+
# finish checking staledeps module graph, while acquiring all start_loading locks
2032+
# so that concurrent require calls won't make any different decisions that might conflict with the decisions here
2033+
# note that start_loading will drop the loading lock if necessary
2034+
let i = 1
2035+
# start_loading here has a deadlock problem if we try to load `A,B,C` and `B,A,D` at the same time:
2036+
# it will claim A,B have a cycle, but really they just have an ambiguous order and need to be batch-acquired rather than singly
2037+
# solve that by making sure we can start_loading everything before allocating each of those and doing all the stale checks
2038+
while i < length(staledeps)
2039+
dep = staledeps[i]
2040+
i += 1
2041+
dep isa Module && continue
2042+
_, modkey, modbuild_id = dep::Tuple{String, PkgId, UInt128}
2043+
dep = canstart_loading(modkey, modbuild_id, stalecheck)
2044+
if dep isa Module
2045+
if PkgId(dep) == modkey && module_build_id(dep) === modbuild_id
2046+
staledeps[i] = dep
2047+
continue
2048+
elseif stalecheck
2049+
@debug "Rejecting cache file $path_to_try because module $modkey got loaded at a different version than expected."
2050+
@goto check_next_path
2051+
end
2052+
continue
2053+
elseif dep === nothing
2054+
continue
2055+
end
2056+
wait(dep) # releases require_lock, so requires restarting this loop
2057+
i = 1
2058+
end
2059+
end
2060+
for i in reverse(eachindex(staledeps))
20272061
dep = staledeps[i]
20282062
dep isa Module && continue
20292063
modpath, modkey, modbuild_id = dep::Tuple{String, PkgId, UInt128}
2064+
# inline a call to start_loading here
2065+
@assert canstart_loading(modkey, modbuild_id, stalecheck) === nothing
2066+
package_locks[modkey] = current_task() => Threads.Condition(require_lock)
2067+
startedloading = i
20302068
modpaths = find_all_in_cache_path(modkey, DEPOT_PATH)
20312069
for modpath_to_try in modpaths
20322070
modstaledeps = stale_cachefile(modkey, modbuild_id, modpath, modpath_to_try; stalecheck)
@@ -2054,37 +2092,22 @@ end
20542092
end
20552093
end
20562094
# finish loading module graph into staledeps
2057-
# TODO: call all start_loading calls (in reverse order) before calling any _include_from_serialized, since start_loading will drop the loading lock
2095+
# n.b. this runs __init__ methods too early, so it is very unwise to have those, as they may see inconsistent loading state, causing them to fail unpredictably here
20582096
for i in eachindex(staledeps)
20592097
dep = staledeps[i]
20602098
dep isa Module && continue
20612099
modpath, modkey, modbuild_id, modcachepath, modstaledeps, modocachepath = dep::Tuple{String, PkgId, UInt128, String, Vector{Any}, Union{Nothing, String}}
2062-
dep = start_loading(modkey, modbuild_id, stalecheck)
2063-
while true
2064-
if dep isa Module
2065-
if PkgId(dep) == modkey && module_build_id(dep) === modbuild_id
2066-
break
2067-
else
2068-
@debug "Rejecting cache file $path_to_try because module $modkey got loaded at a different version than expected."
2069-
@goto check_next_path
2070-
end
2071-
end
2072-
if dep === nothing
2073-
try
2074-
set_pkgorigin_version_path(modkey, modpath)
2075-
dep = _include_from_serialized(modkey, modcachepath, modocachepath, modstaledeps; register = stalecheck)
2076-
finally
2077-
end_loading(modkey, dep)
2078-
end
2079-
if !isa(dep, Module)
2080-
@debug "Rejecting cache file $path_to_try because required dependency $modkey failed to load from cache file for $modcachepath." exception=dep
2081-
@goto check_next_path
2082-
else
2083-
push!(newdeps, modkey)
2084-
end
2085-
end
2100+
set_pkgorigin_version_path(modkey, modpath)
2101+
dep = _include_from_serialized(modkey, modcachepath, modocachepath, modstaledeps; register = stalecheck)
2102+
if !isa(dep, Module)
2103+
@debug "Rejecting cache file $path_to_try because required dependency $modkey failed to load from cache file for $modcachepath." exception=dep
2104+
@goto check_next_path
2105+
else
2106+
startedloading = i + 1
2107+
end_loading(modkey, dep)
2108+
staledeps[i] = dep
2109+
push!(newdeps, modkey)
20862110
end
2087-
staledeps[i] = dep
20882111
end
20892112
restored = maybe_loaded_precompile(pkg, newbuild_id)
20902113
if !isa(restored, Module)
@@ -2094,11 +2117,21 @@ end
20942117
@debug "Deserialization checks failed while attempting to load cache from $path_to_try" exception=restored
20952118
@label check_next_path
20962119
finally
2120+
# cancel all start_loading locks that were taken but not fulfilled before failing
2121+
for i in startedloading:length(staledeps)
2122+
dep = staledeps[i]
2123+
dep isa Module && continue
2124+
if dep isa Tuple{String, PkgId, UInt128}
2125+
_, modkey, _ = dep
2126+
else
2127+
_, modkey, _ = dep::Tuple{String, PkgId, UInt128, String, Vector{Any}, Union{Nothing, String}}
2128+
end
2129+
end_loading(modkey, nothing)
2130+
end
20972131
for modkey in newdeps
20982132
insert_extension_triggers(modkey)
20992133
stalecheck && run_package_callbacks(modkey)
21002134
end
2101-
empty!(newdeps)
21022135
end
21032136
end
21042137
end
@@ -2111,66 +2144,76 @@ const package_locks = Dict{PkgId,Pair{Task,Threads.Condition}}()
21112144
debug_loading_deadlocks::Bool = true # Enable a slightly more expensive, but more complete algorithm that can handle simultaneous tasks.
21122145
# This only triggers if you have multiple tasks trying to load the same package at the same time,
21132146
# so it is unlikely to make a performance difference normally.
2114-
function start_loading(modkey::PkgId, build_id::UInt128, stalecheck::Bool)
2115-
# handle recursive and concurrent calls to require
2147+
2148+
function canstart_loading(modkey::PkgId, build_id::UInt128, stalecheck::Bool)
21162149
assert_havelock(require_lock)
21172150
require_lock.reentrancy_cnt == 1 || throw(ConcurrencyViolationError("recursive call to start_loading"))
2118-
while true
2119-
loaded = stalecheck ? maybe_root_module(modkey) : nothing
2151+
loaded = stalecheck ? maybe_root_module(modkey) : nothing
2152+
loaded isa Module && return loaded
2153+
if build_id != UInt128(0)
2154+
loaded = maybe_loaded_precompile(modkey, build_id)
21202155
loaded isa Module && return loaded
2121-
if build_id != UInt128(0)
2122-
loaded = maybe_loaded_precompile(modkey, build_id)
2123-
loaded isa Module && return loaded
2156+
end
2157+
loading = get(package_locks, modkey, nothing)
2158+
loading === nothing && return nothing
2159+
# load already in progress for this module on the task
2160+
task, cond = loading
2161+
deps = String[modkey.name]
2162+
pkgid = modkey
2163+
assert_havelock(cond.lock)
2164+
if debug_loading_deadlocks && current_task() !== task
2165+
waiters = Dict{Task,Pair{Task,PkgId}}() # invert to track waiting tasks => loading tasks
2166+
for each in package_locks
2167+
cond2 = each[2][2]
2168+
assert_havelock(cond2.lock)
2169+
for waiting in cond2.waitq
2170+
push!(waiters, waiting => (each[2][1] => each[1]))
2171+
end
21242172
end
2125-
loading = get(package_locks, modkey, nothing)
2126-
if loading === nothing
2127-
package_locks[modkey] = current_task() => Threads.Condition(require_lock)
2128-
return nothing
2173+
while true
2174+
running = get(waiters, task, nothing)
2175+
running === nothing && break
2176+
task, pkgid = running
2177+
push!(deps, pkgid.name)
2178+
task === current_task() && break
21292179
end
2130-
# load already in progress for this module on the task
2131-
task, cond = loading
2132-
deps = String[modkey.name]
2133-
pkgid = modkey
2134-
assert_havelock(cond.lock)
2135-
if debug_loading_deadlocks && current_task() !== task
2136-
waiters = Dict{Task,Pair{Task,PkgId}}() # invert to track waiting tasks => loading tasks
2137-
for each in package_locks
2138-
cond2 = each[2][2]
2139-
assert_havelock(cond2.lock)
2140-
for waiting in cond2.waitq
2141-
push!(waiters, waiting => (each[2][1] => each[1]))
2142-
end
2143-
end
2144-
while true
2145-
running = get(waiters, task, nothing)
2146-
running === nothing && break
2147-
task, pkgid = running
2148-
push!(deps, pkgid.name)
2149-
task === current_task() && break
2180+
end
2181+
if current_task() === task
2182+
others = String[modkey.name] # repeat this to emphasize the cycle here
2183+
for each in package_locks # list the rest of the packages being loaded too
2184+
if each[2][1] === task
2185+
other = each[1].name
2186+
other == modkey.name || other == pkgid.name || push!(others, other)
21502187
end
21512188
end
2152-
if current_task() === task
2153-
others = String[modkey.name] # repeat this to emphasize the cycle here
2154-
for each in package_locks # list the rest of the packages being loaded too
2155-
if each[2][1] === task
2156-
other = each[1].name
2157-
other == modkey.name || other == pkgid.name || push!(others, other)
2158-
end
2159-
end
2160-
msg = sprint(deps, others) do io, deps, others
2161-
print(io, "deadlock detected in loading ")
2162-
join(io, deps, " -> ")
2163-
print(io, " -> ")
2164-
join(io, others, " && ")
2165-
end
2166-
throw(ConcurrencyViolationError(msg))
2189+
msg = sprint(deps, others) do io, deps, others
2190+
print(io, "deadlock detected in loading ")
2191+
join(io, deps, " -> ")
2192+
print(io, " -> ")
2193+
join(io, others, " && ")
21672194
end
2168-
loaded = wait(cond)
2195+
throw(ConcurrencyViolationError(msg))
2196+
end
2197+
return cond
2198+
end
2199+
2200+
function start_loading(modkey::PkgId, build_id::UInt128, stalecheck::Bool)
2201+
# handle recursive and concurrent calls to require
2202+
while true
2203+
loaded = canstart_loading(modkey, build_id, stalecheck)
2204+
if loaded === nothing
2205+
package_locks[modkey] = current_task() => Threads.Condition(require_lock)
2206+
return nothing
2207+
elseif loaded isa Module
2208+
return loaded
2209+
end
2210+
loaded = wait(loaded)
21692211
loaded isa Module && return loaded
21702212
end
21712213
end
21722214

21732215
function end_loading(modkey::PkgId, @nospecialize loaded)
2216+
assert_havelock(require_lock)
21742217
loading = pop!(package_locks, modkey)
21752218
notify(loading[2], loaded, all=true)
21762219
nothing
@@ -2650,6 +2693,7 @@ function _require(pkg::PkgId, env=nothing)
26502693
end
26512694

26522695
# load a serialized file directly, including dependencies (without checking staleness except for immediate conflicts)
2696+
# this does not call start_loading / end_loading, so can lead to some odd behaviors
26532697
function _require_from_serialized(uuidkey::PkgId, path::String, ocachepath::Union{String, Nothing}, sourcepath::String)
26542698
@lock require_lock begin
26552699
set_pkgorigin_version_path(uuidkey, sourcepath)

0 commit comments

Comments
 (0)