Skip to content

Commit ab42218

Browse files
IanButterworthKristofferC
authored andcommitted
Precompile out of env deps in serial within precompilepkgs (#59122)
(cherry picked from commit 9808816)
1 parent 48d6e46 commit ab42218

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
@@ -2622,8 +2622,7 @@ function __require_prelocked(pkg::PkgId, env)
26222622
if JLOptions().use_compiled_modules == 1
26232623
if !generating_output(#=incremental=#false)
26242624
project = active_project()
2625-
if !generating_output() && !parallel_precompile_attempted && !disable_parallel_precompile && @isdefined(Precompilation) && project !== nothing &&
2626-
isfile(project) && project_file_manifest_path(project) !== nothing
2625+
if !generating_output() && !parallel_precompile_attempted && !disable_parallel_precompile && @isdefined(Precompilation)
26272626
parallel_precompile_attempted = true
26282627
unlock(require_lock)
26292628
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 environment 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
@@ -1483,7 +1483,7 @@ end
14831483

14841484
# helper function to load a package and return the output
14851485
function load_package(name, args=``)
1486-
code = "using $name"
1486+
code = "Base.disable_parallel_precompile = true; using $name"
14871487
cmd = addenv(`$(Base.julia_cmd()) -e $code $args`,
14881488
"JULIA_LOAD_PATH" => dir,
14891489
"JULIA_DEPOT_PATH" => depot_path,

test/precompile.jl

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

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

718718
# Test transitive dependency for #21266

0 commit comments

Comments
 (0)