Skip to content

Commit 9808816

Browse files
Precompile out of env deps in serial within precompilepkgs (#59122)
1 parent a60814f commit 9808816

File tree

6 files changed

+64
-29
lines changed

6 files changed

+64
-29
lines changed

base/loading.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2625,8 +2625,7 @@ function __require_prelocked(pkg::PkgId, env)
26252625
if JLOptions().use_compiled_modules == 1
26262626
if !generating_output(#=incremental=#false)
26272627
project = active_project()
2628-
if !generating_output() && !parallel_precompile_attempted && !disable_parallel_precompile && @isdefined(Precompilation) && project !== nothing &&
2629-
isfile(project) && project_file_manifest_path(project) !== nothing
2628+
if !generating_output() && !parallel_precompile_attempted && !disable_parallel_precompile && @isdefined(Precompilation)
26302629
parallel_precompile_attempted = true
26312630
unlock(require_lock)
26322631
try

base/precompilation.jl

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,24 @@ struct ExplicitEnv
2626
#local_prefs::Union{Nothing, Dict{String, Any}}
2727
end
2828

29-
function ExplicitEnv(envpath::String=Base.active_project())
29+
ExplicitEnv() = ExplicitEnv(Base.active_project())
30+
function ExplicitEnv(::Nothing, envpath::String="")
31+
ExplicitEnv(envpath,
32+
Dict{String, UUID}(), # project_deps
33+
Dict{String, UUID}(), # project_weakdeps
34+
Dict{String, UUID}(), # project_extras
35+
Dict{String, Vector{UUID}}(), # project_extensions
36+
Dict{UUID, Vector{UUID}}(), # deps
37+
Dict{UUID, Vector{UUID}}(), # weakdeps
38+
Dict{UUID, Dict{String, Vector{UUID}}}(), # extensions
39+
Dict{UUID, String}(), # names
40+
Dict{UUID, Union{SHA1, String, Nothing, Missing}}())
41+
end
42+
function ExplicitEnv(envpath::String)
43+
# Handle missing project file by creating an empty environment
3044
if !isfile(envpath)
31-
error("expected a project file at $(repr(envpath))")
45+
envpath = abspath(envpath)
46+
return ExplicitEnv(nothing, envpath)
3247
end
3348
envpath = abspath(envpath)
3449
project_d = parsed_toml(envpath)
@@ -468,6 +483,7 @@ function precompilepkgs(pkgs::Vector{String}=String[];
468483
fancyprint::Bool = can_fancyprint(io) && !timing,
469484
manifest::Bool=false,
470485
ignore_loaded::Bool=true)
486+
@debug "precompilepkgs called with" pkgs internal_call strict warn_loaded timing _from_loading configs fancyprint manifest ignore_loaded
471487
# monomorphize this to avoid latency problems
472488
_precompilepkgs(pkgs, internal_call, strict, warn_loaded, timing, _from_loading,
473489
configs isa Vector{Config} ? configs : [configs],
@@ -518,9 +534,12 @@ function _precompilepkgs(pkgs::Vector{String},
518534
# inverse map of `parent_to_ext` above (ext → parent)
519535
ext_to_parent = Dict{Base.PkgId, Base.PkgId}()
520536

521-
function describe_pkg(pkg::PkgId, is_project_dep::Bool, flags::Cmd, cacheflags::Base.CacheFlags)
537+
function describe_pkg(pkg::PkgId, is_project_dep::Bool, is_serial_dep::Bool, flags::Cmd, cacheflags::Base.CacheFlags)
522538
name = full_name(ext_to_parent, pkg)
523539
name = is_project_dep ? name : color_string(name, :light_black)
540+
if is_serial_dep
541+
name *= color_string(" (serial)", :light_black)
542+
end
524543
if nconfigs > 1 && !isempty(flags)
525544
config_str = join(flags, " ")
526545
name *= color_string(" `$config_str`", :light_black)
@@ -630,15 +649,28 @@ function _precompilepkgs(pkgs::Vector{String},
630649
end
631650
@debug "precompile: extensions collected"
632651

652+
serial_deps = Base.PkgId[] # packages that are being precompiled in serial
653+
654+
if _from_loading
655+
# if called from loading precompilation it may be a package from another environment stack
656+
# where we don't have access to the dep graph, so just add as a single package and do serial
657+
# precompilation of its deps within the job.
658+
for pkg in requested_pkgs # In case loading asks for multiple packages
659+
pkgid = Base.identify_package(pkg)
660+
pkgid === nothing && continue
661+
if !haskey(direct_deps, pkgid)
662+
@debug "precompile: package `$(pkgid)` is outside of the environment, so adding as single package serial job"
663+
direct_deps[pkgid] = Base.PkgId[] # no deps, do them in serial in the job
664+
push!(project_deps, pkgid) # add to project_deps so it doesn't show up in gray
665+
push!(serial_deps, pkgid)
666+
end
667+
end
668+
end
669+
633670
# return early if no deps
634671
if isempty(direct_deps)
635672
if isempty(pkgs)
636673
return
637-
elseif _from_loading
638-
# if called from loading precompilation it may be a package from another environment stack so
639-
# don't error and allow serial precompilation to try
640-
# TODO: actually handle packages from other envs in the stack
641-
return
642674
else
643675
error("No direct dependencies outside of the sysimage found matching $(pkgs)")
644676
end
@@ -846,7 +878,7 @@ function _precompilepkgs(pkgs::Vector{String},
846878
dep, config = pkg_config
847879
loaded = warn_loaded && haskey(Base.loaded_modules, dep)
848880
flags, cacheflags = config
849-
name = describe_pkg(dep, dep in project_deps, flags, cacheflags)
881+
name = describe_pkg(dep, dep in project_deps, dep in serial_deps, flags, cacheflags)
850882
line = if pkg_config in precomperr_deps
851883
string(color_string(" ? ", Base.warn_color()), name)
852884
elseif haskey(failed_deps, pkg_config)
@@ -929,12 +961,13 @@ function _precompilepkgs(pkgs::Vector{String},
929961
if !circular && is_stale
930962
Base.acquire(parallel_limiter)
931963
is_project_dep = pkg in project_deps
964+
is_serial_dep = pkg in serial_deps
932965

933966
# std monitoring
934967
std_pipe = Base.link_pipe!(Pipe(); reader_supports_async=true, writer_supports_async=true)
935968
t_monitor = @async monitor_std(pkg_config, std_pipe; single_requested_pkg)
936969

937-
name = describe_pkg(pkg, is_project_dep, flags, cacheflags)
970+
name = describe_pkg(pkg, is_project_dep, is_serial_dep, flags, cacheflags)
938971
@lock print_lock begin
939972
if !fancyprint && isempty(pkg_queue)
940973
printpkgstyle(io, :Precompiling, something(target[], "packages..."))

doc/src/manual/code-loading.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ These environments each serve a different purpose:
4242
* Package directories provide **convenience** when a full carefully-tracked project environment is unnecessary. They are useful when you want to put a set of packages somewhere and be able to directly use them, without needing to create a project environment for them.
4343
* Stacked environments allow for **adding** tools to the primary environment. You can push an environment of development tools onto the end of the stack to make them available from the REPL and scripts, but not from inside packages.
4444

45+
!!! note
46+
When loading a package from another environment in the stack other than the active environment the package is loaded in the context of the active environment. This means that the package will be loaded as if it were imported in the active environment, which may affect how its dependencies versions are resolved. When such a package is precompiling it will be marked as a `(serial)` precompile job, which means that its dependencies will be precompiled in series within the same job, which will likely be slower.
47+
4548
At a high-level, each environment conceptually defines three maps: roots, graph and paths. When resolving the meaning of `import X`, the roots and graph maps are used to determine the identity of `X`, while the paths map is used to locate the source code of `X`. The specific roles of the three maps are:
4649

4750
- **roots:** `name::Symbol``uuid::UUID`

test/embedding/embedding-test.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@ end
3232
@test lines[9] == "called bar"
3333
@test lines[10] == "calling new bar"
3434
@test lines[11] == " From worker 2:\tTaking over the world..."
35-
@test readline(err) == "exception caught from C"
35+
@test "exception caught from C" in readlines(err)
3636
end

test/loading.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1495,7 +1495,7 @@ end
14951495

14961496
# helper function to load a package and return the output
14971497
function load_package(name, args=``)
1498-
code = "using $name"
1498+
code = "Base.disable_parallel_precompile = true; using $name"
14991499
cmd = addenv(`$(Base.julia_cmd()) -e $code $args`,
15001500
"JULIA_LOAD_PATH" => dir,
15011501
"JULIA_DEPOT_PATH" => depot_path,

test/precompile.jl

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -686,16 +686,15 @@ precompile_test_harness(false) do dir
686686
error("break me")
687687
end
688688
""")
689-
@test_warn r"LoadError: break me\nStacktrace:\n[ ]*\[1\] [\e01m\[]*error" try
690-
Base.require(Main, :FooBar2)
691-
error("the \"break me\" test failed")
692-
catch exc
693-
isa(exc, ErrorException) || rethrow()
694-
# The LoadError shouldn't be surfaced but is printed to stderr, hence the `@test_warn` capture tests
695-
occursin("LoadError: break me", exc.msg) && rethrow()
696-
# The actual error that is thrown
697-
occursin("Failed to precompile FooBar2", exc.msg) || rethrow()
698-
end
689+
try
690+
Base.require(Main, :FooBar2)
691+
error("the \"break me\" test failed")
692+
catch exc
693+
isa(exc, Base.Precompilation.PkgPrecompileError) || rethrow()
694+
occursin("Failed to precompile FooBar2", exc.msg) || rethrow()
695+
# The LoadError is printed to stderr in the precompilepkgs worker and captured in the PkgPrecompileError msg
696+
occursin("LoadError: break me", exc.msg) || rethrow()
697+
end
699698

700699
# Test that trying to eval into closed modules during precompilation is an error
701700
FooBar3_file = joinpath(dir, "FooBar3.jl")
@@ -707,11 +706,12 @@ precompile_test_harness(false) do dir
707706
$code
708707
end
709708
""")
710-
@test_warn "Evaluation into the closed module `Base` breaks incremental compilation" try
711-
Base.require(Main, :FooBar3)
712-
catch exc
713-
isa(exc, ErrorException) || rethrow()
714-
end
709+
try
710+
Base.require(Main, :FooBar3)
711+
catch exc
712+
isa(exc, Base.Precompilation.PkgPrecompileError) || rethrow()
713+
occursin("Evaluation into the closed module `Base` breaks incremental compilation", exc.msg) || rethrow()
714+
end
715715
end
716716

717717
# Test transitive dependency for #21266

0 commit comments

Comments
 (0)