mirror of
https://github.com/JuliaLang/julia.git
synced 2026-05-28 03:10:33 +08:00
2502 lines
108 KiB
Julia
2502 lines
108 KiB
Julia
module Precompilation
|
||
|
||
using Base: CoreLogging, PkgId, UUID, SHA1, StaleCacheKey, parsed_toml, project_file_name_uuid, project_names,
|
||
project_file_manifest_path, get_deps, preferences_names, isaccessibledir, isfile_casesensitive,
|
||
base_project, env_project_file, isdefined
|
||
|
||
const Config = Pair{Cmd, Base.CacheFlags}
|
||
const PkgConfig = Tuple{PkgId,Config}
|
||
|
||
## PrecompileJob
|
||
|
||
# Per-package compilation status and associated state.
|
||
@enum JobStatus::UInt8 begin
|
||
JOB_PENDING # not yet started
|
||
JOB_STARTED # compilation in progress
|
||
JOB_RECOMPILED # successfully compiled
|
||
JOB_SOFT_ERROR # compilecache returned an Exception (may work after restart)
|
||
JOB_FAILED # hard compile error
|
||
end
|
||
|
||
mutable struct PrecompileJob
|
||
status::JobStatus
|
||
started_at::Float64
|
||
error_msg::String
|
||
output::IOBuffer
|
||
pid::Int32
|
||
had_pid::Bool # sticky: true once a subprocess was actually spawned for this job
|
||
lock_holder::String
|
||
waiting_for_bg::Bool
|
||
PrecompileJob() = new(JOB_PENDING, 0.0, "", IOBuffer(), Int32(0), false, "", false)
|
||
end
|
||
|
||
is_pending(j::PrecompileJob) = j.status == JOB_PENDING
|
||
is_started(j::PrecompileJob) = j.status == JOB_STARTED
|
||
is_recompiled(j::PrecompileJob) = j.status == JOB_RECOMPILED
|
||
is_soft_error(j::PrecompileJob) = j.status == JOB_SOFT_ERROR
|
||
is_failed(j::PrecompileJob) = j.status == JOB_FAILED
|
||
has_pid(j::PrecompileJob) = j.pid > 0
|
||
had_pid(j::PrecompileJob) = j.had_pid
|
||
is_locked(j::PrecompileJob) = !isempty(j.lock_holder)
|
||
is_waiting(j::PrecompileJob) = j.waiting_for_bg
|
||
|
||
mark_started!(j::PrecompileJob, t::Float64=time()) = (j.status = JOB_STARTED; j.started_at = t)
|
||
mark_recompiled!(j::PrecompileJob) = (j.status = JOB_RECOMPILED)
|
||
mark_soft_error!(j::PrecompileJob) = (j.status = JOB_SOFT_ERROR)
|
||
mark_failed!(j::PrecompileJob, msg::String) = (j.status = JOB_FAILED; j.error_msg = msg)
|
||
set_pid!(j::PrecompileJob, pid::Int32) = (j.pid = pid; j.had_pid = true)
|
||
clear_pid!(j::PrecompileJob) = (j.pid = Int32(0))
|
||
|
||
function clear_failure!(j::PrecompileJob)
|
||
j.status = JOB_PENDING
|
||
j.error_msg = ""
|
||
truncate(j.output, 0)
|
||
end
|
||
|
||
# A request to do precompilation work, either in a new session or merged into a running one.
|
||
struct PrecompileRequest
|
||
pkgs::Vector{String}
|
||
internal_call::Bool
|
||
strict::Bool
|
||
warn_loaded::Bool
|
||
timing::Bool
|
||
_from_loading::Bool
|
||
configs::Vector{Config}
|
||
io::IOContext
|
||
fancyprint::Bool
|
||
manifest::Bool
|
||
ignore_loaded::Bool
|
||
detachable::Bool
|
||
result::Channel{Any}
|
||
end
|
||
|
||
# Shared mutable state for a precompilation session.
|
||
Base.@kwdef mutable struct PrecompileSession
|
||
# Configuration (set once, read-only after construction)
|
||
configs::Vector{Config}
|
||
io::IOContext
|
||
logio::IOContext
|
||
logcalls::Union{Nothing, CoreLogging.LogLevel}
|
||
fancyprint::Bool
|
||
hascolor::Bool
|
||
warn_loaded::Bool
|
||
ignore_loaded::Bool
|
||
internal_call::Bool
|
||
strict::Bool
|
||
_from_loading::Bool
|
||
time_start::UInt64
|
||
print_lock::ReentrantLock
|
||
parallel_limiter::Base.Semaphore
|
||
num_tasks::Int
|
||
start_loaded_modules::Set{PkgId}
|
||
requested_pkgids::Vector{PkgId}
|
||
|
||
# Dependency graph (built by setup, extended by drainer)
|
||
direct_deps::Dict{PkgId, Vector{PkgId}}
|
||
ext_to_parent::Dict{PkgId, PkgId}
|
||
parent_to_exts::Dict{PkgId, Vector{PkgId}}
|
||
triggers::Dict{PkgId, Vector{PkgId}}
|
||
project_deps::Vector{PkgId}
|
||
serial_deps::Vector{PkgId}
|
||
circular_deps::Vector{PkgId}
|
||
|
||
# Progress counters and flags
|
||
n_done::Int = 0
|
||
n_already_precomp::Int = 0
|
||
n_loaded::Int = 0
|
||
loaded_pkgs::Vector{PkgId} = PkgId[]
|
||
n_total::Int
|
||
n_batches::Int = 1
|
||
interrupted::Bool = false
|
||
canceled::Bool = false
|
||
interrupted_or_done::Bool = false
|
||
printloop_should_exit::Bool
|
||
target::String
|
||
pkg_liveprinted::Union{Nothing, PkgId} = nothing
|
||
|
||
# Per-package tracking
|
||
jobs::Dict{PkgConfig, PrecompileJob} = Dict{PkgConfig,PrecompileJob}()
|
||
was_processed::Dict{PkgConfig, Base.Event}
|
||
stale_cache::Dict{StaleCacheKey, Bool} = Dict{StaleCacheKey,Bool}()
|
||
cachepath_cache::Dict{PkgId, Vector{String}} = Dict{PkgId,Vector{String}}()
|
||
pkg_queue::Vector{PkgConfig} = PkgConfig[]
|
||
prev_cpu_times::Dict{Int32, UInt64} = Dict{Int32,UInt64}()
|
||
|
||
# Synchronization
|
||
first_started::Base.Event = Base.Event()
|
||
cache_lock::ReentrantLock = ReentrantLock()
|
||
|
||
# Task tracking
|
||
tasks::Vector{Task} = Task[]
|
||
injected_tasks::Vector{Task} = Task[]
|
||
result_waiters::Vector{Task} = Task[]
|
||
end
|
||
|
||
# Background precompilation state
|
||
#
|
||
# Lock ordering (outermost first): BG.lock → BG.pkg_done → BG.task_done
|
||
# All three have independent ReentrantLocks. Always acquire in this order to avoid deadlocks.
|
||
mutable struct BackgroundPrecompileState
|
||
task::Union{Nothing, Task}
|
||
interrupt_requested::Bool
|
||
cancel_requested::Bool
|
||
monitoring::Bool
|
||
completed_at::Union{Nothing, Float64}
|
||
result::Union{Nothing, String}
|
||
return_value::Any
|
||
exception::Any
|
||
lock::ReentrantLock
|
||
task_done::Threads.Condition
|
||
signal_channels::Vector{Channel{Int32}} # channels for broadcasting signals to all subprocesses
|
||
pending_pkgids::Dict{PkgId, Int} # packages queued and currently being precompiled (refcount for multi-config)
|
||
completed_pkgids::Set{PkgId} # packages that have finished precompiling (for fast-finish detection)
|
||
pkg_done::Threads.Condition # notified when a package finishes precompiling
|
||
work_channel::Channel{PrecompileRequest} # channel for injecting requests into running task
|
||
verbose::Bool # show PIDs and CPU% for each worker
|
||
detachable::Bool # whether the monitor can be detached
|
||
confirming::Symbol # :none, :cancel, or :info — action awaiting Enter to confirm
|
||
confirm_deadline::Float64 # time() deadline for confirmation
|
||
info_requested::Bool # whether SIGINFO/SIGUSR1 has been broadcast at least once
|
||
key_listening::Bool # whether a key listener task is currently consuming stdin
|
||
end
|
||
Base.lock(f, bg::BackgroundPrecompileState) = lock(f, bg.lock)
|
||
Base.lock(bg::BackgroundPrecompileState) = lock(bg.lock)
|
||
Base.unlock(bg::BackgroundPrecompileState) = unlock(bg.lock)
|
||
|
||
const BG = BackgroundPrecompileState(nothing, false, false, false, nothing, nothing, nothing, nothing, ReentrantLock(), Threads.Condition(), Channel{Int32}[], Dict{PkgId, Int}(), Set{PkgId}(), Threads.Condition(), Channel{PrecompileRequest}(Inf), false, false, :none, 0.0, false, false)
|
||
|
||
## Constants and formatting utilities
|
||
|
||
const ansi_movecol1 = "\e[1G"
|
||
const ansi_cleartoend = "\e[0J"
|
||
const ansi_cleartoendofline = "\e[0K"
|
||
const ansi_enablecursor = "\e[?25h"
|
||
const ansi_disablecursor = "\e[?25l"
|
||
const ansi_clearline = "\e[2K"
|
||
ansi_moveup(n::Int) = string("\e[", n, "A")
|
||
|
||
struct PkgPrecompileError <: Exception
|
||
msg::String
|
||
end
|
||
Base.showerror(io::IO, err::PkgPrecompileError) = print(io, err.msg)
|
||
Base.showerror(io::IO, err::PkgPrecompileError, bt; kw...) = Base.showerror(io, err) # hide stacktrace
|
||
|
||
# This needs a show method to make `julia> err` show nicely
|
||
Base.show(io::IO, err::PkgPrecompileError) = print(io, "PkgPrecompileError: ", err.msg)
|
||
|
||
can_fancyprint(io::IO) = @something(get(io, :force_fancyprint, nothing), (io isa Base.TTY && (get(ENV, "CI", nothing) != "true")))
|
||
|
||
function printpkgstyle(io, header, msg; color=:green)
|
||
return @lock io begin
|
||
printstyled(io, header; color, bold=true)
|
||
println(io, " ", msg)
|
||
end
|
||
end
|
||
|
||
timing_string(t) = string(lpad(round(t, digits = 1), 6), " s")
|
||
|
||
|
||
function color_string(cstr::String, col::Union{Int64, Symbol}, hascolor)
|
||
if hascolor
|
||
enable_ansi = get(Base.text_colors, col, Base.text_colors[:default])
|
||
disable_ansi = get(Base.disable_text_style, col, Base.text_colors[:default])
|
||
return string(enable_ansi, cstr, disable_ansi)
|
||
else
|
||
return cstr
|
||
end
|
||
end
|
||
|
||
# using Printf
|
||
Base.@kwdef mutable struct MiniProgressBar
|
||
max::Int = 1
|
||
header::String = ""
|
||
color::Symbol = :nothing
|
||
width::Int = 40
|
||
current::Int = 0
|
||
prev::Int = 0
|
||
has_shown::Bool = false
|
||
time_shown::Float64 = 0.0
|
||
percentage::Bool = true
|
||
always_reprint::Bool = false
|
||
indent::Int = 4
|
||
end
|
||
|
||
const PROGRESS_BAR_TIME_GRANULARITY = Ref(1 / 30.0) # 30 fps
|
||
const PROGRESS_BAR_PERCENTAGE_GRANULARITY = Ref(0.1)
|
||
|
||
start_progress(io::IO, _::MiniProgressBar) = print(io, ansi_disablecursor)
|
||
|
||
function show_progress(io::IO, p::MiniProgressBar; termwidth=nothing, carriagereturn=true)
|
||
if p.max == 0
|
||
perc = 0.0
|
||
prev_perc = 0.0
|
||
else
|
||
perc = p.current / p.max * 100
|
||
prev_perc = p.prev / p.max * 100
|
||
end
|
||
# Bail early if we are not updating the progress bar,
|
||
# Saves printing to the terminal
|
||
if !p.always_reprint && p.has_shown && !((perc - prev_perc) > PROGRESS_BAR_PERCENTAGE_GRANULARITY[])
|
||
return
|
||
end
|
||
t = time()
|
||
if !p.always_reprint && p.has_shown && (t - p.time_shown) < PROGRESS_BAR_TIME_GRANULARITY[]
|
||
return
|
||
end
|
||
p.time_shown = t
|
||
p.prev = p.current
|
||
p.has_shown = true
|
||
|
||
progress_text = string(p.current, "/", p.max)
|
||
termwidth = @something termwidth (displaysize(io)::Tuple{Int,Int})[2]
|
||
max_progress_width = max(0, min(termwidth - textwidth(p.header) - textwidth(progress_text) - 10 , p.width))
|
||
filled = max_progress_width * clamp(perc / 100, 0.0, 1.0)
|
||
(partial_filled, n_filled::Int64) = modf(filled) # get fractional / integer part
|
||
n_left = max_progress_width - n_filled
|
||
headers = split(p.header, ' ')
|
||
to_print = sprint(; context=io) do io
|
||
print(io, " "^p.indent)
|
||
printstyled(io, headers[1], " "; color=:green, bold=true)
|
||
printstyled(io, join(headers[2:end], ' '))
|
||
print(io, " ")
|
||
printstyled(io, "━"^n_filled; color=p.color)
|
||
if n_left > 0
|
||
if partial_filled > 0.5
|
||
printstyled(io, "╸"; color=p.color) # More filled, use ╸
|
||
else
|
||
printstyled(io, "╺"; color=:light_black) # Less filled, use ╺
|
||
end
|
||
printstyled(io, "━"^(n_left-1); color=:light_black)
|
||
end
|
||
printstyled(io, " "; color=:light_black)
|
||
print(io, progress_text)
|
||
carriagereturn && print(io, "\r")
|
||
end
|
||
# Print everything in one call
|
||
print(io, to_print)
|
||
end
|
||
|
||
end_progress(io, p::MiniProgressBar) = print(io, ansi_enablecursor, ansi_clearline)
|
||
|
||
print_progress_bottom(io::IO) = print(io, "\e[S", ansi_moveup(1), ansi_clearline, ansi_movecol1)
|
||
|
||
## ExplicitEnv
|
||
|
||
# This is currently only used for pkgprecompile but the plan is to use this in code loading in the future
|
||
# see the `kc/codeloading2.0` branch
|
||
struct ExplicitEnv
|
||
path::String
|
||
project_deps::Dict{String, UUID} # [deps] in the active project's Project.toml
|
||
project_weakdeps::Dict{String, UUID} # [weakdeps] in the active project's Project.toml
|
||
project_extras::Dict{String, UUID} # [extras] in the active project's Project.toml
|
||
project_extensions::Dict{String, Vector{UUID}} # [extensions] in the active project's Project.toml
|
||
workspace_deps::Dict{String, UUID} # union of [deps] from all workspace member Project.tomls
|
||
deps::Dict{UUID, Vector{UUID}} # full dependency graph from Manifest.toml
|
||
weakdeps::Dict{UUID, Vector{UUID}} # full weak dependency graph from Manifest.toml
|
||
extensions::Dict{UUID, Dict{String, Vector{UUID}}}
|
||
# Lookup name for a UUID
|
||
names::Dict{UUID, String}
|
||
lookup_strategy::Dict{UUID, Union{
|
||
SHA1, # `git-tree-sha1` entry
|
||
String, # `path` entry
|
||
Nothing, # stdlib (no `path` nor `git-tree-sha1`)
|
||
Missing}} # not present in the manifest
|
||
#prefs::Union{Nothing, Dict{String, Any}}
|
||
#local_prefs::Union{Nothing, Dict{String, Any}}
|
||
end
|
||
|
||
ExplicitEnv() = ExplicitEnv(Base.active_project())
|
||
function ExplicitEnv(::Nothing, envpath::String="")
|
||
ExplicitEnv(envpath,
|
||
Dict{String, UUID}(), # project_deps
|
||
Dict{String, UUID}(), # project_weakdeps
|
||
Dict{String, UUID}(), # project_extras
|
||
Dict{String, Vector{UUID}}(), # project_extensions
|
||
Dict{String, UUID}(), # workspace_deps
|
||
Dict{UUID, Vector{UUID}}(), # deps
|
||
Dict{UUID, Vector{UUID}}(), # weakdeps
|
||
Dict{UUID, Dict{String, Vector{UUID}}}(), # extensions
|
||
Dict{UUID, String}(), # names
|
||
Dict{UUID, Union{SHA1, String, Nothing, Missing}}())
|
||
end
|
||
function ExplicitEnv(envpath::String)
|
||
# Handle missing project file by creating an empty environment
|
||
if !isfile(envpath) || project_file_manifest_path(envpath) === nothing
|
||
envpath = abspath(envpath)
|
||
return ExplicitEnv(nothing, envpath)
|
||
end
|
||
envpath = abspath(envpath)
|
||
project_d = parsed_toml(envpath)
|
||
|
||
# TODO: Perhaps verify that two packages with the same UUID do not have different names?
|
||
names = Dict{UUID, String}()
|
||
project_uuid_to_name = Dict{String, UUID}()
|
||
|
||
project_deps = Dict{String, UUID}()
|
||
project_weakdeps = Dict{String, UUID}()
|
||
project_extras = Dict{String, UUID}()
|
||
|
||
# Collect all direct dependencies of the project
|
||
for key in ["deps", "weakdeps", "extras"]
|
||
for (name, _uuid) in get(Dict{String, Any}, project_d, key)::Dict{String, Any}
|
||
v = key == "deps" ? project_deps :
|
||
key == "weakdeps" ? project_weakdeps :
|
||
key == "extras" ? project_extras :
|
||
error()
|
||
uuid = UUID(_uuid::String)
|
||
v[name] = uuid
|
||
names[uuid] = name
|
||
project_uuid_to_name[name] = uuid
|
||
end
|
||
end
|
||
|
||
# A package in both deps and weakdeps is in fact only a weakdep
|
||
for (name, _) in project_weakdeps
|
||
delete!(project_deps, name)
|
||
end
|
||
|
||
# This project might be a package, in that case, that is also a "dependency"
|
||
# of the project.
|
||
proj_name = get(project_d, "name", nothing)::Union{String, Nothing}
|
||
_proj_uuid = get(project_d, "uuid", nothing)::Union{String, Nothing}
|
||
proj_uuid = _proj_uuid === nothing ? nothing : UUID(_proj_uuid)
|
||
|
||
project_is_package = proj_name !== nothing && proj_uuid !== nothing
|
||
if project_is_package
|
||
# TODO: Error on missing uuid?
|
||
project_deps[proj_name] = proj_uuid
|
||
names[proj_uuid] = proj_name
|
||
end
|
||
|
||
project_extensions = Dict{String, Vector{UUID}}()
|
||
# Collect all extensions of the project
|
||
for (name, triggers) in get(Dict{String, Any}, project_d, "extensions")::Dict{String, Any}
|
||
if triggers isa String
|
||
triggers = [triggers]
|
||
else
|
||
triggers = triggers::Vector{String}
|
||
end
|
||
uuids = UUID[]
|
||
for trigger in triggers
|
||
uuid = get(project_uuid_to_name, trigger, nothing)
|
||
if uuid === nothing
|
||
error("Trigger $trigger for extension $name not found in project")
|
||
end
|
||
push!(uuids, uuid)
|
||
end
|
||
project_extensions[name] = uuids
|
||
end
|
||
|
||
manifest = project_file_manifest_path(envpath)
|
||
manifest_d = manifest === nothing ? Dict{String, Any}() : parsed_toml(manifest)
|
||
|
||
# Dependencies in a manifest can either be stored compressed (when name is unique among all packages)
|
||
# in which case it is a `Vector{String}` or expanded where it is a `name => uuid` mapping.
|
||
deps = Dict{UUID, Union{Vector{String}, Vector{UUID}}}()
|
||
weakdeps = Dict{UUID, Union{Vector{String}, Vector{UUID}}}()
|
||
extensions = Dict{UUID, Dict{String, Vector{String}}}()
|
||
name_to_uuid = Dict{String, UUID}()
|
||
lookup_strategy = Dict{UUID, Union{SHA1, String, Nothing, Missing}}()
|
||
|
||
sizehint!(deps, length(manifest_d))
|
||
sizehint!(weakdeps, length(manifest_d))
|
||
sizehint!(extensions, length(manifest_d))
|
||
sizehint!(name_to_uuid, length(manifest_d))
|
||
sizehint!(lookup_strategy, length(manifest_d))
|
||
|
||
for (name, pkg_infos) in get_deps(manifest_d)
|
||
for pkg_info in pkg_infos::Vector{Any}
|
||
pkg_info = pkg_info::Dict{String, Any}
|
||
m_uuid = UUID(pkg_info["uuid"]::String)
|
||
|
||
# If we have multiple packages with the same name we will overwrite things here
|
||
# but that is fine since we will only use the information in here for packages
|
||
# with unique names
|
||
names[m_uuid] = name
|
||
name_to_uuid[name] = m_uuid
|
||
|
||
for key in ["deps", "weakdeps"]
|
||
deps_pkg = get(Vector{String}, pkg_info, key)::Union{Vector{String}, Dict{String, Any}}
|
||
d = key == "deps" ? deps :
|
||
key == "weakdeps" ? weakdeps :
|
||
error()
|
||
|
||
# Compressed format with unique names:
|
||
if deps_pkg isa Vector{String}
|
||
d[m_uuid] = deps_pkg
|
||
# Expanded format:
|
||
else
|
||
uuids = UUID[]
|
||
for (name_dep, _dep_uuid) in deps_pkg
|
||
dep_uuid = UUID(_dep_uuid::String)
|
||
push!(uuids, dep_uuid)
|
||
names[dep_uuid] = name_dep
|
||
end
|
||
d[m_uuid] = uuids
|
||
end
|
||
end
|
||
|
||
# Extensions
|
||
deps_pkg = get(Dict{String, Any}, pkg_info, "extensions")::Dict{String, Any}
|
||
deps_pkg_concrete = Dict{String, Vector{String}}()
|
||
for (ext, triggers) in deps_pkg
|
||
if triggers isa String
|
||
triggers = [triggers]
|
||
else
|
||
triggers = triggers::Vector{String}
|
||
end
|
||
deps_pkg_concrete[ext] = triggers
|
||
end
|
||
extensions[m_uuid] = deps_pkg_concrete
|
||
|
||
# Determine strategy to find package
|
||
lookup_strat = begin
|
||
if (path = get(pkg_info, "path", nothing)::Union{String, Nothing}) !== nothing
|
||
path
|
||
elseif (git_tree_sha_str = get(pkg_info, "git-tree-sha1", nothing)::Union{String, Nothing}) !== nothing
|
||
SHA1(git_tree_sha_str)
|
||
else
|
||
nothing
|
||
end
|
||
end
|
||
lookup_strategy[m_uuid] = lookup_strat
|
||
end
|
||
end
|
||
|
||
# No matter if the deps were stored compressed or not in the manifest,
|
||
# we internally store them expanded
|
||
deps_expanded = Dict{UUID, Vector{UUID}}()
|
||
weakdeps_expanded = Dict{UUID, Vector{UUID}}()
|
||
extensions_expanded = Dict{UUID, Dict{String, Vector{UUID}}}()
|
||
sizehint!(deps_expanded, length(deps))
|
||
sizehint!(weakdeps_expanded, length(deps))
|
||
sizehint!(extensions_expanded, length(deps))
|
||
|
||
if proj_name !== nothing && proj_uuid !== nothing
|
||
deps_expanded[proj_uuid] = filter!(!=(proj_uuid), collect(values(project_deps)))
|
||
extensions_expanded[proj_uuid] = project_extensions
|
||
path = get(project_d, "path", nothing)::Union{String, Nothing}
|
||
entry_point = path !== nothing ? path : dirname(envpath)
|
||
lookup_strategy[proj_uuid] = entry_point
|
||
end
|
||
|
||
for key in ["deps", "weakdeps"]
|
||
d = key == "deps" ? deps :
|
||
key == "weakdeps" ? weakdeps :
|
||
error()
|
||
d_expanded = key == "deps" ? deps_expanded :
|
||
key == "weakdeps" ? weakdeps_expanded :
|
||
error()
|
||
for (pkg, deps) in d
|
||
# dependencies was already expanded so use it directly:
|
||
if deps isa Vector{UUID}
|
||
d_expanded[pkg] = deps
|
||
for dep in deps
|
||
name_to_uuid[names[dep]] = dep
|
||
end
|
||
# find the (unique) UUID associated with the name
|
||
else
|
||
deps_pkg = UUID[]
|
||
sizehint!(deps_pkg, length(deps))
|
||
for dep in deps
|
||
push!(deps_pkg, name_to_uuid[dep])
|
||
end
|
||
d_expanded[pkg] = deps_pkg
|
||
end
|
||
end
|
||
end
|
||
|
||
for (pkg, exts) in extensions
|
||
exts_expanded = Dict{String, Vector{UUID}}()
|
||
for (ext, triggers) in exts
|
||
triggers_expanded = UUID[]
|
||
sizehint!(triggers_expanded, length(triggers))
|
||
for trigger in triggers
|
||
push!(triggers_expanded, name_to_uuid[trigger])
|
||
end
|
||
exts_expanded[ext] = triggers_expanded
|
||
end
|
||
extensions_expanded[pkg] = exts_expanded
|
||
end
|
||
|
||
# Everything that does not yet have a lookup_strategy is missing from the manifest
|
||
for (_, uuid) in project_deps
|
||
get!(lookup_strategy, uuid, missing)
|
||
end
|
||
|
||
#=
|
||
# Preferences:
|
||
prefs = get(project_d, "preferences", nothing)
|
||
|
||
# `(Julia)LocalPreferences.toml`
|
||
project_dir = dirname(envpath)
|
||
local_prefs = nothing
|
||
for name in preferences_names
|
||
toml_path = joinpath(project_dir, name)
|
||
if isfile(toml_path)
|
||
local_prefs = parsed_toml(toml_path)
|
||
break
|
||
end
|
||
end
|
||
=#
|
||
|
||
# Collect the union of [deps] from all workspace member projects.
|
||
# For non-workspace projects, this is the same as project_deps.
|
||
workspace_deps = copy(project_deps)
|
||
base = base_project(envpath)
|
||
if base !== nothing
|
||
base_d = parsed_toml(base)
|
||
# Add deps from the workspace root project
|
||
for (name, _uuid) in get(Dict{String, Any}, base_d, "deps")::Dict{String, Any}
|
||
workspace_deps[name] = UUID(_uuid::String)
|
||
end
|
||
# Add deps from each workspace member project
|
||
ws = get(base_d, "workspace", nothing)::Union{Dict{String, Any}, Nothing}
|
||
if ws !== nothing
|
||
ws_projects = get(ws, "projects", nothing)::Union{Vector{String}, Nothing, String}
|
||
if ws_projects isa Vector
|
||
ws_root = dirname(base)
|
||
for ws_proj in ws_projects
|
||
ws_proj_dir = joinpath(ws_root, ws_proj)
|
||
ws_proj_file = Base.env_project_file(ws_proj_dir)
|
||
ws_proj_file isa String || continue
|
||
ws_d = parsed_toml(ws_proj_file)
|
||
for (name, _uuid) in get(Dict{String, Any}, ws_d, "deps")::Dict{String, Any}
|
||
workspace_deps[name] = UUID(_uuid::String)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
return ExplicitEnv(envpath, project_deps, project_weakdeps, project_extras,
|
||
project_extensions, workspace_deps,
|
||
deps_expanded, weakdeps_expanded, extensions_expanded,
|
||
names, lookup_strategy, #=prefs, local_prefs=#)
|
||
end
|
||
|
||
## Dependency graph
|
||
|
||
# name or parent → ext
|
||
function full_name(ext_to_parent::Dict{PkgId, PkgId}, pkg::PkgId)
|
||
if haskey(ext_to_parent, pkg)
|
||
return string(ext_to_parent[pkg].name, " → ", pkg.name)
|
||
else
|
||
return pkg.name
|
||
end
|
||
end
|
||
|
||
function excluded_circular_deps_explanation(io::IOContext, ext_to_parent::Dict{PkgId, PkgId}, circular_deps, cycles)
|
||
outer_deps = copy(circular_deps)
|
||
cycles_names = ""
|
||
hascolor = get(io, :color, false)::Bool
|
||
for cycle in cycles
|
||
filter!(!in(cycle), outer_deps)
|
||
cycle_str = ""
|
||
for (i, pkg) in enumerate(cycle)
|
||
j = max(0, i - 1)
|
||
if length(cycle) == 1
|
||
line = " ─ "
|
||
elseif i == 1
|
||
line = " ┌ "
|
||
elseif i < length(cycle)
|
||
line = " │ " * " " ^j
|
||
else
|
||
line = " └" * "─" ^j * " "
|
||
end
|
||
line = color_string(line, :light_black, hascolor) * full_name(ext_to_parent, pkg) * "\n"
|
||
cycle_str *= line
|
||
end
|
||
cycles_names *= cycle_str
|
||
end
|
||
plural1 = length(cycles) > 1 ? "these cycles" : "this cycle"
|
||
plural2 = length(cycles) > 1 ? "cycles" : "cycle"
|
||
msg = """Circular dependency detected.
|
||
Precompilation will be skipped for dependencies in $plural1:
|
||
$cycles_names"""
|
||
if !isempty(outer_deps)
|
||
msg *= "Precompilation will also be skipped for the following, which depend on the above $plural2:\n"
|
||
msg *= join((" " * full_name(ext_to_parent, pkg) for pkg in outer_deps), "\n")
|
||
end
|
||
return msg
|
||
end
|
||
|
||
|
||
function scan_pkg!(stack, could_be_cycle, cycles, pkg, dmap)
|
||
if haskey(could_be_cycle, pkg)
|
||
return could_be_cycle[pkg]
|
||
else
|
||
return scan_deps!(stack, could_be_cycle, cycles, pkg, dmap)
|
||
end
|
||
end
|
||
function scan_deps!(stack, could_be_cycle, cycles, pkg, dmap)
|
||
push!(stack, pkg)
|
||
cycle = nothing
|
||
for dep in dmap[pkg]
|
||
if dep in stack
|
||
# Created fresh cycle
|
||
cycle′ = stack[findlast(==(dep), stack):end]
|
||
if cycle === nothing || length(cycle′) < length(cycle)
|
||
cycle = cycle′ # try to report smallest cycle possible
|
||
end
|
||
elseif scan_pkg!(stack, could_be_cycle, cycles, dep, dmap)
|
||
# Reaches an existing cycle
|
||
could_be_cycle[pkg] = true
|
||
pop!(stack)
|
||
return true
|
||
end
|
||
end
|
||
pop!(stack)
|
||
if cycle !== nothing
|
||
push!(cycles, cycle)
|
||
could_be_cycle[pkg] = true
|
||
return true
|
||
end
|
||
could_be_cycle[pkg] = false
|
||
return false
|
||
end
|
||
|
||
# restrict to dependencies of given packages
|
||
function collect_all_deps(direct_deps, dep, alldeps=Set{Base.PkgId}())
|
||
for _dep in direct_deps[dep]
|
||
if !(_dep in alldeps)
|
||
push!(alldeps, _dep)
|
||
collect_all_deps(direct_deps, _dep, alldeps)
|
||
end
|
||
end
|
||
return alldeps
|
||
end
|
||
|
||
function visit_indirect_deps!(direct_deps::Dict{PkgId, Vector{PkgId}}, visited::Set{PkgId},
|
||
node::PkgId, all_deps::Set{PkgId})
|
||
if node in visited
|
||
return
|
||
end
|
||
push!(visited, node)
|
||
for dep in get(Set{PkgId}, direct_deps, node)
|
||
if !(dep in all_deps)
|
||
push!(all_deps, dep)
|
||
visit_indirect_deps!(direct_deps, visited, dep, all_deps)
|
||
end
|
||
end
|
||
return
|
||
end
|
||
|
||
# Build dependency graph from an ExplicitEnv.
|
||
# Returns a NamedTuple of (direct_deps, ext_to_parent, parent_to_exts, triggers, project_deps, serial_deps).
|
||
function build_dep_graph(env::ExplicitEnv, manifest::Bool, _from_loading::Bool, requested_pkgids::Vector{PkgId})
|
||
direct_deps = Dict{PkgId, Vector{PkgId}}()
|
||
parent_to_exts = Dict{PkgId, Vector{PkgId}}()
|
||
ext_to_parent = Dict{PkgId, PkgId}()
|
||
triggers = Dict{PkgId, Vector{PkgId}}()
|
||
|
||
# Determine which packages to consider for precompilation by walking
|
||
# transitive dependencies from the appropriate roots.
|
||
# `manifest` controls the scope: workspace_deps (all members) vs project_deps (current project).
|
||
roots = manifest ? env.workspace_deps : env.project_deps
|
||
pkg_uuids = Set{UUID}()
|
||
for (_, uuid) in roots
|
||
_collect_reachable!(pkg_uuids, env.deps, uuid)
|
||
end
|
||
|
||
for dep in pkg_uuids
|
||
haskey(env.deps, dep) || continue
|
||
pkg = PkgId(dep, env.names[dep])
|
||
Base.in_sysimage(pkg) && continue
|
||
deps = [PkgId(x, env.names[x]) for x in env.deps[dep]]
|
||
direct_deps[pkg] = filter!(!Base.in_sysimage, deps)
|
||
for (ext_name, trigger_uuids) in get(Dict{String, Vector{UUID}}, env.extensions, dep)
|
||
ext_uuid = Base.uuid5(pkg.uuid, ext_name)
|
||
ext = PkgId(ext_uuid, ext_name)
|
||
triggers[ext] = PkgId[pkg]
|
||
all_triggers_available = true
|
||
for trigger_uuid in trigger_uuids
|
||
trigger_name = PkgId(trigger_uuid, env.names[trigger_uuid])
|
||
if trigger_uuid in pkg_uuids || Base.in_sysimage(trigger_name)
|
||
push!(triggers[ext], trigger_name)
|
||
else
|
||
all_triggers_available = false
|
||
break
|
||
end
|
||
end
|
||
all_triggers_available || continue
|
||
ext_to_parent[ext] = pkg
|
||
direct_deps[ext] = filter(!Base.in_sysimage, triggers[ext])
|
||
if !haskey(parent_to_exts, pkg)
|
||
parent_to_exts[pkg] = PkgId[ext]
|
||
else
|
||
push!(parent_to_exts[pkg], ext)
|
||
end
|
||
end
|
||
end
|
||
|
||
project_deps = [
|
||
PkgId(uuid, name)
|
||
for (name, uuid) in env.project_deps if !Base.in_sysimage(PkgId(uuid, name))
|
||
]
|
||
# consider exts of project deps to be project deps so that errors are reported
|
||
append!(project_deps, keys(filter(d->last(d).name in keys(env.project_deps), ext_to_parent)))
|
||
|
||
# An extension effectively depends on another extension if it has a strict superset of its triggers
|
||
for ext_a in keys(ext_to_parent)
|
||
for ext_b in keys(ext_to_parent)
|
||
if triggers[ext_a] ⊋ triggers[ext_b]
|
||
push!(triggers[ext_a], ext_b)
|
||
push!(direct_deps[ext_a], ext_b)
|
||
end
|
||
end
|
||
end
|
||
|
||
# A package depends on an extension if it (indirectly) depends on all extension triggers
|
||
# Iterate to a fixed point because adding an extension edge (e.g. ExtA → TopPkg)
|
||
# may cause another extension (e.g. ExtAB, which depends on ExtA) to become
|
||
# loadable in TopPkg on the next iteration.
|
||
changed = true
|
||
while changed
|
||
changed = false
|
||
indirect_deps = Dict{PkgId, Set{PkgId}}()
|
||
for package in keys(direct_deps)
|
||
all_deps = Set{PkgId}()
|
||
visited = Set{PkgId}()
|
||
visit_indirect_deps!(direct_deps, visited, package, all_deps)
|
||
indirect_deps[package] = all_deps
|
||
end
|
||
for ext in keys(ext_to_parent)
|
||
ext_loadable_in_pkg = Dict{PkgId,Bool}()
|
||
for pkg in keys(direct_deps)
|
||
is_trigger = in(pkg, direct_deps[ext])
|
||
is_extension = in(pkg, keys(ext_to_parent))
|
||
has_triggers = issubset(direct_deps[ext], indirect_deps[pkg])
|
||
ext_loadable_in_pkg[pkg] = !is_extension && has_triggers && !is_trigger
|
||
end
|
||
for (pkg, ext_loadable) in ext_loadable_in_pkg
|
||
if ext_loadable && !any((dep)->ext_loadable_in_pkg[dep], direct_deps[pkg])
|
||
if ext ∉ direct_deps[pkg]
|
||
# add an edge if the extension is loadable by pkg, and was not loadable in any
|
||
# of the pkg's dependencies
|
||
push!(direct_deps[pkg], ext)
|
||
changed = true
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
serial_deps = PkgId[]
|
||
if _from_loading
|
||
for pkgid in requested_pkgids
|
||
pkgid === nothing && continue
|
||
if !haskey(direct_deps, pkgid)
|
||
@debug "precompile: package `$(pkgid)` is outside of the environment, so adding as single package serial job"
|
||
direct_deps[pkgid] = PkgId[]
|
||
push!(project_deps, pkgid)
|
||
push!(serial_deps, pkgid)
|
||
end
|
||
end
|
||
end
|
||
|
||
return (; direct_deps, ext_to_parent, parent_to_exts, triggers, project_deps, serial_deps)
|
||
end
|
||
|
||
# Detect circular dependencies and notify their Events so waiting tasks can skip them.
|
||
# Returns the list of packages involved in or dependent on cycles.
|
||
function detect_circular_deps!(direct_deps, serial_deps, was_processed, io, ext_to_parent)
|
||
cycles = Vector{PkgId}[]
|
||
could_be_cycle = Dict{PkgId, Bool}()
|
||
stack = PkgId[]
|
||
circular_deps = PkgId[]
|
||
for pkg in keys(direct_deps)
|
||
@assert isempty(stack)
|
||
pkg in serial_deps && continue
|
||
if scan_pkg!(stack, could_be_cycle, cycles, pkg, direct_deps)
|
||
push!(circular_deps, pkg)
|
||
for (pkg_config, evt) in was_processed
|
||
pkg_config[1] == pkg && notify(evt)
|
||
end
|
||
end
|
||
end
|
||
if !isempty(circular_deps)
|
||
printpkgstyle(io, :Warning, excluded_circular_deps_explanation(io, ext_to_parent, circular_deps, cycles), color=Base.warn_color())
|
||
end
|
||
return circular_deps
|
||
end
|
||
|
||
# Filter the dependency graph to only include requested packages and their transitive deps.
|
||
# Returns true if the graph became empty (caller should return early).
|
||
function filter_dep_graph!(direct_deps, pkg_names, manifest, project_deps, ext_to_parent, requested_pkgids)
|
||
isempty(pkg_names) && return false
|
||
keep = Set{PkgId}()
|
||
for dep_pkgid in keys(direct_deps)
|
||
if dep_pkgid.name in pkg_names
|
||
push!(keep, dep_pkgid)
|
||
collect_all_deps(direct_deps, dep_pkgid, keep)
|
||
end
|
||
end
|
||
for requested_pkgid in requested_pkgids
|
||
if haskey(direct_deps, requested_pkgid)
|
||
push!(keep, requested_pkgid)
|
||
collect_all_deps(direct_deps, requested_pkgid, keep)
|
||
end
|
||
end
|
||
for ext in keys(ext_to_parent)
|
||
if issubset(collect_all_deps(direct_deps, ext), keep)
|
||
push!(keep, ext)
|
||
end
|
||
end
|
||
filter!(d->in(first(d), keep), direct_deps)
|
||
return isempty(direct_deps)
|
||
end
|
||
|
||
## Public API
|
||
|
||
"""
|
||
precompilepkgs(pkgs; kwargs...)
|
||
|
||
Precompile packages and their dependencies, with support for parallel compilation,
|
||
progress tracking, and various compilation configurations.
|
||
|
||
`pkgs::Union{Vector{String}, Vector{PkgId}}`: Packages to precompile. When
|
||
empty (default), precompiles all project dependencies. When specified,
|
||
precompiles only the given packages and their dependencies (unless
|
||
`manifest=true`).
|
||
|
||
!!! note
|
||
Errors will only throw when precompiling the top-level dependencies, given that
|
||
not all manifest dependencies may be loaded by the top-level dependencies on the given system.
|
||
This can be overridden to make errors in all dependencies throw by setting the kwarg `strict` to `true`
|
||
|
||
# Keyword Arguments
|
||
- `internal_call::Bool`: Indicates this is an automatic precompilation call
|
||
from somewhere external (e.g. Pkg). Do not use this parameter.
|
||
|
||
- `strict::Bool`: Controls error reporting scope. When `false` (default), only reports
|
||
errors for direct project dependencies. Only relevant when `manifest=true`.
|
||
|
||
- `warn_loaded::Bool`: When `true` (default), checks for and warns about packages that are
|
||
precompiled but already loaded with a different version. Displays a warning that Julia
|
||
needs to be restarted to use the newly precompiled versions.
|
||
|
||
- `timing::Bool`: When `true` (not default), displays timing information for
|
||
each package compilation, but only if compilation might have succeeded.
|
||
Disables fancy progress bar output (timing is shown in simple text mode).
|
||
|
||
- `_from_loading::Bool`: Internal flag indicating the call originated from the
|
||
package loading system. When `true` (not default): returns early instead of
|
||
throwing when packages are not found; suppresses progress messages when not
|
||
in an interactive session; allows packages outside the current environment to
|
||
be added as serial precompilation jobs; skips LOADING_CACHE initialization;
|
||
and changes cachefile locking behavior.
|
||
|
||
- `configs::Union{Config,Vector{Config}}`: Compilation configurations to use. Each Config
|
||
is a `Pair{Cmd, Base.CacheFlags}` specifying command flags and cache flags. When
|
||
multiple configs are provided, each package is precompiled for each configuration.
|
||
|
||
- `io::IO`: Output stream for progress messages, warnings, and errors. Can be
|
||
redirected (e.g., to `devnull` when called from loading in non-interactive mode).
|
||
|
||
- `fancyprint::Bool`: Controls output format. When `true`, displays an animated progress
|
||
bar with spinners. When `false`, instead enables `timing` mode. Automatically
|
||
disabled when `timing=true` or when called from loading in non-interactive mode.
|
||
|
||
- `manifest::Bool`: Controls the scope of packages to precompile. When `false` (default),
|
||
precompiles only packages specified in `pkgs` and their dependencies. When `true`,
|
||
precompiles all packages in the manifest (workspace mode), typically used by Pkg for
|
||
workspace precompile requests.
|
||
|
||
- `ignore_loaded::Bool`: Controls whether already-loaded packages affect cache
|
||
freshness checks. When `false` (not default), loaded package versions are considered when
|
||
determining if cache files are fresh.
|
||
|
||
- `detachable::Bool`: When `true` (not default), allows detaching from the
|
||
precompilation monitor with the `d` key, letting precompilation continue in
|
||
the background. The monitor can be reattached later via
|
||
[`Base.Precompilation.monitor_background_precompile`](@ref). Pkg.jl passes
|
||
`detachable=true` in interactive sessions.
|
||
|
||
# Keyboard Controls
|
||
|
||
When running interactively in a TTY, the following keys are available during
|
||
precompilation:
|
||
|
||
- **`c`** — Cancel precompilation. Prompts for Enter to confirm; ignored after
|
||
5 seconds or if any other key is pressed.
|
||
- **`d`/`q`/`]`** — Detach (only when `detachable=true`). Returns to the REPL while
|
||
precompilation continues in the background.
|
||
- **`i`** — Info. Sends a profiling signal (SIGINFO on macOS/BSD, SIGUSR1 on
|
||
Linux) to subprocesses, triggering a profile peek without interrupting
|
||
compilation. Prompts for Enter to confirm; ignored after 5 seconds or if any
|
||
other key is pressed.
|
||
- **`v`** — Toggle verbose mode. Shows elapsed time and worker PID for each actively
|
||
compiling package, plus CPU% and memory (RSS) on Linux and macOS.
|
||
- **`?`/`h`** — Show keyboard shortcut help.
|
||
- **Ctrl-C** — Interrupt. Sends SIGINT to subprocesses and displays their output.
|
||
|
||
# Return
|
||
- `Vector{String}`: Paths to cache files for the requested packages.
|
||
- `Nothing`: precompilation should be skipped
|
||
|
||
# Notes
|
||
- Packages in circular dependency cycles are skipped with a warning.
|
||
- Packages with `__precompile__(false)` are skipped if they are from loading to
|
||
avoid repeated work on every session.
|
||
- Parallel compilation is controlled by `JULIA_NUM_PRECOMPILE_TASKS` environment variable
|
||
(defaults to CPU_THREADS + 1, capped at 16, halved on Windows).
|
||
- Extensions are precompiled when all their triggers are available in the environment.
|
||
"""
|
||
function precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}=String[];
|
||
internal_call::Bool=false,
|
||
strict::Bool = false,
|
||
warn_loaded::Bool = true,
|
||
timing::Bool = false,
|
||
_from_loading::Bool=false,
|
||
configs::Union{Config,Vector{Config}}=(``=>Base.CacheFlags()),
|
||
io::IO=stderr,
|
||
# asking for timing disables fancy mode, as timing is shown in non-fancy mode
|
||
fancyprint::Bool = can_fancyprint(io) && !timing,
|
||
manifest::Bool=false,
|
||
ignore_loaded::Bool=true,
|
||
detachable::Bool=false)
|
||
@debug "precompilepkgs called with" pkgs internal_call strict warn_loaded timing _from_loading configs fancyprint manifest ignore_loaded detachable
|
||
# monomorphize this to avoid latency problems
|
||
_precompilepkgs(pkgs, internal_call, strict, warn_loaded, timing, _from_loading,
|
||
configs isa Vector{Config} ? configs : [configs],
|
||
io isa IOContext ? io : IOContext(io), fancyprint, manifest, ignore_loaded, detachable)
|
||
end
|
||
|
||
## Background lifecycle
|
||
|
||
function is_precompiling_in_background()
|
||
@lock BG (BG.task !== nothing && !istaskdone(BG.task))
|
||
end
|
||
|
||
# Check if `pkg` is currently being precompiled by a background task.
|
||
# This should be called while holding require_lock to avoid races.
|
||
# Returns true if the package is pending.
|
||
function is_package_pending(pkg::PkgId)
|
||
@lock BG begin
|
||
BG.task === nothing && return false
|
||
istaskdone(BG.task) && return false
|
||
return get(BG.pending_pkgids, pkg, 0) > 0
|
||
end
|
||
end
|
||
|
||
# Monitor the precompile progress for `pkg` until that package finishes.
|
||
# This should be called after unlocking require_lock.
|
||
function wait_for_pending_package(pkg::PkgId)
|
||
is_package_pending(pkg) || return false
|
||
printpkgstyle(stderr, :Info, "$(pkg.name) is currently being precompiled in the background. Waiting for it to finish...", color = Base.info_color())
|
||
monitor_background_precompile(stderr, false, pkg)
|
||
return true
|
||
end
|
||
|
||
# Broadcast a signal to all active precompilation subprocesses.
|
||
function broadcast_signal(sig::Int32)
|
||
@lock BG begin
|
||
for ch in BG.signal_channels
|
||
try; isopen(ch) && put!(ch, sig); catch e; e isa InvalidStateException || rethrow(); end
|
||
end
|
||
end
|
||
end
|
||
broadcast_signal(sig::Integer) = broadcast_signal(Int32(sig))
|
||
|
||
function stop_background_precompile(; graceful::Bool = true)
|
||
@lock BG begin
|
||
if BG.task !== nothing && !istaskdone(BG.task)
|
||
if graceful
|
||
BG.interrupt_requested = true
|
||
else
|
||
BG.cancel_requested = true
|
||
broadcast_signal(Base.SIGKILL) # re-entrant: broadcast_signal also @lock's BG
|
||
end
|
||
try; close(BG.work_channel); catch; end
|
||
return true
|
||
end
|
||
return false
|
||
end
|
||
end
|
||
|
||
const register_atexit_hook = Base.OncePerProcess{Nothing}() do
|
||
Base.atexit() do
|
||
task = @lock BG BG.task
|
||
task === nothing && return
|
||
istaskdone(task) && return
|
||
stop_background_precompile(; graceful=false)
|
||
wait(task)
|
||
end
|
||
nothing
|
||
end
|
||
|
||
const _confirm_messages = Dict{Symbol, String}(
|
||
:cancel => "cancel precompilation",
|
||
:info => "send profiling signal",
|
||
)
|
||
|
||
function keyboard_tip(s::BackgroundPrecompileState)
|
||
s.monitoring || return "", :default
|
||
s.key_listening || return "", :default
|
||
if s.confirming !== :none
|
||
remaining = max(0, ceil(Int, s.confirm_deadline - time()))
|
||
msg = get(_confirm_messages, s.confirming, string(s.confirming))
|
||
return "Press Enter to $(msg) (ignoring in $(remaining)s)", Base.info_color()
|
||
end
|
||
if s.detachable
|
||
return "Press `?` for help, `c` to cancel, `d` to detach.", :default
|
||
else
|
||
return "Press `?` for help, `c` to cancel.", :default
|
||
end
|
||
end
|
||
|
||
function monitor_background_precompile(io::IO = stderr, detachable::Bool = true, wait_for_pkg::Union{Nothing, PkgId} = nothing;
|
||
key_controls::Union{Bool, Nothing} = nothing)
|
||
# By default only enable key controls when this task is the foreground task (see #61563, #61698).
|
||
# Falls back to roottask when no foreground task is registered (e.g. non-REPL interactive scripts).
|
||
key_controls = @something key_controls current_task() === something(Base.foreground_task(), Base.roottask)
|
||
local completed_at::Union{Nothing, Float64}
|
||
local task
|
||
|
||
@lock BG begin
|
||
completed_at = BG.completed_at
|
||
task = BG.task
|
||
end
|
||
|
||
if task === nothing || istaskdone(task)
|
||
if completed_at !== nothing
|
||
elapsed = time() - completed_at
|
||
time_str = if elapsed < 60
|
||
"$(round(Int, elapsed)) seconds ago"
|
||
elseif elapsed < 3600
|
||
"$(round(Int, elapsed / 60)) minutes ago"
|
||
elseif elapsed < 86400
|
||
"$(round(Int, elapsed / 3600)) hours ago"
|
||
else
|
||
"$(round(Int, elapsed / 86400)) days ago"
|
||
end
|
||
printpkgstyle(io, :Info, "Background precompilation completed $time_str", color = Base.info_color())
|
||
result = @lock BG BG.result
|
||
if result !== nothing && !isempty(result)
|
||
println(io, " ", result)
|
||
end
|
||
else
|
||
printpkgstyle(io, :Info, "No background precompilation is running or has been run in this session", color = Base.info_color())
|
||
end
|
||
return
|
||
end
|
||
|
||
# Enable output from do_precompile
|
||
@lock BG BG.monitoring = true
|
||
|
||
exit_requested = Ref(false)
|
||
cancel_requested = Ref(false)
|
||
interrupt_requested = Ref(false)
|
||
|
||
# Start a task to listen for keypresses. Skipped if another reader already holds
|
||
# raw mode on stdin (e.g. runtests.jl's stdin_monitor).
|
||
key_task = if key_controls && stdin isa Base.TTY
|
||
Threads.@spawn :samepool try
|
||
trylock(stdin.raw_lock) || return
|
||
@lock BG begin
|
||
BG.detachable = detachable
|
||
BG.confirming = :none
|
||
BG.key_listening = true
|
||
end
|
||
buffered_input = UInt8[]
|
||
try
|
||
term = Base.Terminals.TTYTerminal(get(ENV, "TERM", "dumb"), stdin, stdout, stderr)
|
||
Base.Terminals.raw!(term, true)
|
||
# Drain any pre-existing input (e.g. pasted or pre-typed text)
|
||
# before entering the key listener. This avoids accidentally
|
||
# triggering menu actions from stale input (see #61520).
|
||
# The drained bytes are replayed into stdin on exit.
|
||
# We must start_reading + yield first so libuv delivers any
|
||
# bytes sitting in the kernel tty buffer into stdin.buffer.
|
||
Base.start_reading(stdin)
|
||
yield()
|
||
while bytesavailable(stdin) > 0
|
||
push!(buffered_input, read(stdin, UInt8))
|
||
end
|
||
try
|
||
while true
|
||
completed = @lock BG (BG.completed_at !== nothing)
|
||
if completed || exit_requested[] || cancel_requested[] || interrupt_requested[]
|
||
break
|
||
end
|
||
Base.wait_readnb(stdin, 1)
|
||
completed = @lock BG (BG.completed_at !== nothing)
|
||
if completed || exit_requested[] || cancel_requested[] || interrupt_requested[]
|
||
break
|
||
end
|
||
bytesavailable(stdin) > 0 || continue
|
||
c = read(stdin, Char)
|
||
# If waiting for confirmation, Enter confirms, anything else aborts
|
||
confirmed_action = @lock BG begin
|
||
prev = BG.confirming
|
||
BG.confirming = :none
|
||
(prev !== :none && c in ('\r', '\n')) ? prev : :none
|
||
end
|
||
if confirmed_action == :cancel
|
||
cancel_requested[] = true
|
||
println(io)
|
||
@lock BG BG.cancel_requested = true
|
||
broadcast_signal(Base.SIGKILL)
|
||
break
|
||
elseif confirmed_action == :info
|
||
@lock BG BG.info_requested = true
|
||
broadcast_signal(Sys.isapple() ? Base.SIGINFO : Base.SIGUSR1)
|
||
continue
|
||
end
|
||
if c in ('c', 'C')
|
||
@lock BG begin
|
||
BG.confirming = :cancel
|
||
BG.confirm_deadline = time() + 5.0
|
||
end
|
||
elseif detachable && c in ('d', 'D', 'q', 'Q', ']')
|
||
exit_requested[] = true
|
||
println(io) # newline after keypress
|
||
break
|
||
elseif c == '\x03' # Ctrl-C
|
||
interrupt_requested[] = true
|
||
println(io) # newline after keypress
|
||
@lock BG BG.interrupt_requested = true
|
||
broadcast_signal(Base.SIGINT)
|
||
break
|
||
elseif c in ('i', 'I')
|
||
@lock BG begin
|
||
BG.confirming = :info
|
||
BG.confirm_deadline = time() + 5.0
|
||
end
|
||
elseif c in ('v', 'V')
|
||
@lock BG BG.verbose = !BG.verbose
|
||
elseif c in ('?', 'h', 'H')
|
||
@lock io begin
|
||
println(io, " Keyboard shortcuts:", ansi_cleartoendofline)
|
||
println(io, " c Cancel precompilation via killing subprocesses (press Enter to confirm)", ansi_cleartoendofline)
|
||
if detachable
|
||
println(io, " d/q/] Detach (precompilation continues in background)", ansi_cleartoendofline)
|
||
end
|
||
println(io, " i Send profiling signal to subprocesses (press Enter to confirm)", ansi_cleartoendofline)
|
||
fields = Sys.iswindows() ? "elapsed time and PID" : "elapsed time, PID, CPU% and memory"
|
||
println(io, " v Toggle verbose mode (show $(fields) for each worker)", ansi_cleartoendofline)
|
||
println(io, " Ctrl-C Interrupt (sends SIGINT, shows output)", ansi_cleartoendofline)
|
||
println(io, " ?/h Show this help", ansi_cleartoendofline)
|
||
end
|
||
end
|
||
end
|
||
finally
|
||
Base.Terminals.raw!(term, false)
|
||
end
|
||
catch err
|
||
err isa EOFError && return
|
||
exit_requested[] = true
|
||
rethrow()
|
||
finally
|
||
# Replay any buffered input back into stdin so the REPL
|
||
# (or whatever reads stdin next) sees it as typed text.
|
||
if !isempty(buffered_input)
|
||
lock(stdin.cond)
|
||
try
|
||
write(stdin.buffer, buffered_input)
|
||
notify(stdin.cond)
|
||
finally
|
||
unlock(stdin.cond)
|
||
end
|
||
end
|
||
Base.reseteof(stdin)
|
||
@lock BG begin
|
||
BG.confirming = :none
|
||
BG.key_listening = false
|
||
end
|
||
unlock(stdin.raw_lock)
|
||
end
|
||
finally
|
||
@lock BG.task_done notify(BG.task_done)
|
||
end
|
||
else
|
||
nothing
|
||
end
|
||
|
||
# Wake up key_task by signaling EOF on stdin so wait_readnb returns
|
||
wake_key_task = () -> begin
|
||
if key_task !== nothing && !istaskdone(key_task)
|
||
lock(stdin.cond)
|
||
try
|
||
stdin.status = Base.StatusEOF
|
||
notify(stdin.cond)
|
||
finally
|
||
unlock(stdin.cond)
|
||
end
|
||
end
|
||
end
|
||
|
||
# If waiting for a specific package, spawn a watcher that exits when it's done
|
||
pkg_watcher = if wait_for_pkg !== nothing
|
||
Threads.@spawn :samepool begin
|
||
@lock BG.pkg_done begin
|
||
# Wait for the package to appear in pending_pkgids (it may not be
|
||
# registered yet if the request was just injected via work_channel
|
||
# and drain_work_channel! hasn't processed it).
|
||
# Also check completed_pkgids in case the package was added and
|
||
# removed before we started watching.
|
||
while get(BG.pending_pkgids, wait_for_pkg, 0) == 0 && wait_for_pkg ∉ BG.completed_pkgids && BG.completed_at === nothing
|
||
wait(BG.pkg_done)
|
||
end
|
||
# Now wait for it to finish
|
||
while get(BG.pending_pkgids, wait_for_pkg, 0) > 0
|
||
wait(BG.pkg_done)
|
||
end
|
||
end
|
||
exit_requested[] = true
|
||
@lock BG.task_done notify(BG.task_done)
|
||
end
|
||
else
|
||
nothing
|
||
end
|
||
|
||
return try
|
||
# Wait for task completion or user action
|
||
@lock BG.task_done begin
|
||
while !exit_requested[] && !cancel_requested[] && !interrupt_requested[]
|
||
BG.completed_at !== nothing && break
|
||
wait(BG.task_done)
|
||
end
|
||
end
|
||
|
||
# If user requested cancel, stop the background task
|
||
if cancel_requested[]
|
||
key_task !== nothing && wait(key_task)
|
||
print(io, ansi_enablecursor, ansi_cleartoend)
|
||
printpkgstyle(io, :Info, "Canceling precompilation...$(ansi_cleartoend)", color = Base.info_color())
|
||
# Wait for the task to emit its final report before clearing
|
||
# `BG.monitoring`, which gates that report's output.
|
||
wait(task; throw=false)
|
||
@lock BG BG.monitoring = false
|
||
return
|
||
end
|
||
|
||
# If user requested interrupt, wait for background task to finish cleanly
|
||
if interrupt_requested[]
|
||
key_task !== nothing && wait(key_task)
|
||
# Escalate to SIGKILL if background task doesn't finish promptly
|
||
escalation = Timer(5) do _
|
||
broadcast_signal(Base.SIGKILL)
|
||
end
|
||
wait(task; throw=false)
|
||
close(escalation)
|
||
return
|
||
end
|
||
|
||
# If we were waiting for a specific package and it finished, clean up silently
|
||
if exit_requested[] && wait_for_pkg !== nothing
|
||
@lock BG BG.monitoring = false
|
||
if key_task !== nothing
|
||
wake_key_task()
|
||
wait(key_task)
|
||
end
|
||
print(io, ansi_enablecursor, ansi_cleartoend)
|
||
return
|
||
end
|
||
|
||
# If user requested exit, clean up and return
|
||
if exit_requested[]
|
||
@lock BG BG.monitoring = false
|
||
key_task !== nothing && wait(key_task)
|
||
print(io, ansi_enablecursor, ansi_cleartoend)
|
||
n_pending = @lock BG length(BG.pending_pkgids)
|
||
progress = n_pending > 0 ? " ($n_pending packages remaining)." : "."
|
||
printpkgstyle(io, :Precompiling, "detached$(progress) Precompilation will continue in the background. Monitor with `precompile --monitor`.$(ansi_cleartoend)", color = Base.info_color())
|
||
return
|
||
end
|
||
|
||
# Normal completion - signal key_task to exit and wait
|
||
if key_task !== nothing
|
||
wake_key_task()
|
||
wait(key_task)
|
||
end
|
||
|
||
wait(task; throw=false)
|
||
catch e
|
||
# Clean up on error
|
||
@lock BG BG.monitoring = false
|
||
if key_task !== nothing
|
||
exit_requested[] = true
|
||
wake_key_task()
|
||
try; wait(key_task); catch; end
|
||
end
|
||
rethrow()
|
||
end
|
||
end
|
||
|
||
function launch_background_precompile(pkgs::Union{Vector{String}, Vector{PkgId}},
|
||
internal_call::Bool,
|
||
strict::Bool,
|
||
warn_loaded::Bool,
|
||
timing::Bool,
|
||
_from_loading::Bool,
|
||
configs::Vector{Config},
|
||
io::IOContext,
|
||
fancyprint::Bool,
|
||
manifest::Bool,
|
||
ignore_loaded::Bool,
|
||
detachable::Bool)
|
||
# Stop any existing background precompilation
|
||
@lock BG begin
|
||
if BG.task !== nothing && !istaskdone(BG.task)
|
||
BG.interrupt_requested = true
|
||
@lock BG.task_done notify(BG.task_done)
|
||
end
|
||
end
|
||
|
||
# Wait for previous task to complete
|
||
old_task = @lock BG BG.task
|
||
if old_task !== nothing
|
||
wait(old_task)
|
||
end
|
||
|
||
@lock BG begin
|
||
BG.interrupt_requested = false
|
||
BG.cancel_requested = false
|
||
BG.info_requested = false
|
||
empty!(BG.signal_channels)
|
||
BG.monitoring = true
|
||
BG.completed_at = nothing
|
||
BG.result = nothing
|
||
BG.return_value = nothing
|
||
BG.exception = nothing
|
||
empty!(BG.pending_pkgids)
|
||
empty!(BG.completed_pkgids)
|
||
BG.work_channel = Channel{PrecompileRequest}(Inf)
|
||
end
|
||
|
||
# Capture necessary context for background task
|
||
pkg_names = pkgs isa Vector{String} ? copy(pkgs) : String[pkg.name for pkg in pkgs]
|
||
|
||
# Register an atexit hook (once) to cleanly shut down background precompilation
|
||
# before the event loop is torn down.
|
||
register_atexit_hook()
|
||
|
||
# Launch new background precompilation
|
||
@lock BG begin
|
||
wc = BG.work_channel
|
||
BG.task = Threads.@spawn :samepool begin
|
||
try
|
||
ret = do_precompile(pkg_names, internal_call, strict, warn_loaded, timing, _from_loading,
|
||
configs, io, fancyprint, manifest, ignore_loaded, detachable, wc)
|
||
|
||
@lock BG begin
|
||
BG.return_value = ret
|
||
end
|
||
catch e
|
||
@lock BG begin
|
||
if BG.interrupt_requested || BG.cancel_requested
|
||
BG.result = "Background precompilation was interrupted"
|
||
else
|
||
BG.exception = e
|
||
BG.result = "Background precompilation failed: $(sprint(showerror, e))"
|
||
end
|
||
end
|
||
finally
|
||
close(wc)
|
||
# Drain pending requests with error
|
||
while isready(wc)
|
||
req = try; take!(wc); catch; break; end
|
||
try; put!(req.result, InterruptException()); catch; end
|
||
end
|
||
@lock BG begin
|
||
BG.task = nothing
|
||
BG.interrupt_requested = false
|
||
BG.cancel_requested = false
|
||
BG.info_requested = false
|
||
foreach(close, BG.signal_channels)
|
||
empty!(BG.signal_channels)
|
||
@lock BG.pkg_done begin
|
||
empty!(BG.pending_pkgids)
|
||
notify(BG.pkg_done)
|
||
end
|
||
BG.monitoring = false
|
||
BG.completed_at = time()
|
||
@lock BG.task_done notify(BG.task_done)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
return nothing
|
||
end
|
||
|
||
function _collect_reachable!(pkg_uuids::Set{UUID}, deps::Dict{UUID, Vector{UUID}}, uuid::UUID)
|
||
uuid in pkg_uuids && return
|
||
push!(pkg_uuids, uuid)
|
||
for dep_uuid in get(Vector{UUID}, deps, uuid)
|
||
_collect_reachable!(pkg_uuids, deps, dep_uuid)
|
||
end
|
||
end
|
||
|
||
function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}},
|
||
internal_call::Bool,
|
||
strict::Bool,
|
||
warn_loaded::Bool,
|
||
timing::Bool,
|
||
_from_loading::Bool,
|
||
configs::Vector{Config},
|
||
io::IOContext,
|
||
fancyprint′::Bool,
|
||
manifest::Bool,
|
||
ignore_loaded::Bool,
|
||
detachable::Bool)
|
||
# Try to inject into a running background task
|
||
local req = nothing
|
||
injected = @lock BG begin
|
||
if BG.task !== nothing && !istaskdone(BG.task) &&
|
||
isopen(BG.work_channel)
|
||
pkg_names = pkgs isa Vector{String} ? copy(pkgs) : String[pkg.name for pkg in pkgs]
|
||
req = PrecompileRequest(pkg_names, internal_call, strict, warn_loaded, timing, _from_loading,
|
||
configs, io, fancyprint′, manifest, ignore_loaded, detachable,
|
||
Channel{Any}(1))
|
||
try
|
||
put!(BG.work_channel, req)
|
||
true
|
||
catch
|
||
req = nothing
|
||
false
|
||
end
|
||
else
|
||
false
|
||
end
|
||
end
|
||
if injected
|
||
printpkgstyle(io, :Precompiling, "Merging precompilation request into existing run...", color = Base.info_color())
|
||
else
|
||
launch_background_precompile(pkgs, internal_call, strict, warn_loaded, timing, _from_loading,
|
||
configs, io, fancyprint′, manifest, ignore_loaded, detachable)
|
||
end
|
||
|
||
if req !== nothing
|
||
# Injected into existing task — monitor until our package finishes, then read result
|
||
wait_for_pkg = if _from_loading && length(pkgs) == 1
|
||
pkgs[1] isa PkgId ? pkgs[1] : Base.identify_package(pkgs[1])
|
||
else
|
||
nothing
|
||
end
|
||
monitor_background_precompile(io.io, detachable, wait_for_pkg)
|
||
if _from_loading
|
||
# _from_loading: package just left pending_pkgids, waiter will put result shortly
|
||
result = take!(req.result)
|
||
result isa Exception && throw(result)
|
||
return result
|
||
end
|
||
# Interactive: if result is ready (task completed), use it; if detached, return nothing
|
||
if isready(req.result)
|
||
result = take!(req.result)
|
||
result isa Exception && throw(result)
|
||
return result
|
||
end
|
||
return nothing
|
||
end
|
||
|
||
# Launched new task — wait for full completion
|
||
monitor_background_precompile(io.io, detachable)
|
||
|
||
local ret_val, ret_ex
|
||
@lock BG begin
|
||
ret_val = BG.return_value
|
||
ret_ex = BG.exception
|
||
end
|
||
ret_ex !== nothing && throw(ret_ex)
|
||
return ret_val
|
||
end
|
||
|
||
## Session implementation
|
||
|
||
# Mach timebase info for converting Mach absolute time → nanoseconds on macOS.
|
||
# Queried once per process at runtime (not at sysimage build time).
|
||
@static if Sys.isapple()
|
||
const _mach_timebase = Base.OncePerProcess{Tuple{UInt64,UInt64}}() do
|
||
buf = zeros(UInt32, 2)
|
||
ccall(:mach_timebase_info, Cvoid, (Ptr{UInt32},), buf)
|
||
(UInt64(buf[1]), UInt64(buf[2]))
|
||
end
|
||
end
|
||
|
||
# Read cumulative CPU time (user+system) in nanoseconds and RSS in bytes for a process.
|
||
# Returns (cpu_ns=0, rss_bytes=0) if the process no longer exists or on unsupported platforms.
|
||
function process_stats(pid::Int32)
|
||
@static if Sys.islinux()
|
||
try
|
||
stat = read("/proc/$(pid)/stat", String)
|
||
# Field 2 (comm) is in parens and may contain spaces; skip past it
|
||
i = findlast(')', stat)
|
||
i === nothing && return (cpu_ns=UInt64(0), rss_bytes=UInt64(0))
|
||
fields = split(@view(stat[nextind(stat, i):end]))
|
||
# fields[1] = state (field 3), so utime=field 14 is at index 12,
|
||
# stime=field 15 at index 13, rss=field 24 at index 22
|
||
length(fields) >= 22 || return (cpu_ns=UInt64(0), rss_bytes=UInt64(0))
|
||
utime = parse(UInt64, fields[12])
|
||
stime = parse(UInt64, fields[13])
|
||
rss_pages = parse(UInt64, fields[22])
|
||
# CLK_TCK is almost always 100 on Linux; 1 tick = 10ms = 10_000_000 ns
|
||
cpu_ns = (utime + stime) * UInt64(10_000_000)
|
||
rss_bytes = rss_pages * UInt64(ccall(:getpagesize, Cint, ()))
|
||
return (; cpu_ns, rss_bytes)
|
||
catch
|
||
return (cpu_ns=UInt64(0), rss_bytes=UInt64(0))
|
||
end
|
||
elseif Sys.isapple()
|
||
try
|
||
buf = Vector{UInt8}(undef, 96) # sizeof(struct proc_taskinfo)
|
||
ret = ccall((:proc_pidinfo, "libproc"), Cint,
|
||
(Cint, Cint, UInt64, Ptr{UInt8}, Cint),
|
||
pid, Cint(4), UInt64(0), buf, Cint(96))
|
||
ret <= 0 && return (cpu_ns=UInt64(0), rss_bytes=UInt64(0))
|
||
# pti_resident_size at offset 8 (bytes)
|
||
rss_bytes = GC.@preserve buf unsafe_load(Ptr{UInt64}(pointer(buf) + 8))
|
||
# pti_total_user at offset 16, pti_total_system at offset 24 (Mach absolute time)
|
||
user = GC.@preserve buf unsafe_load(Ptr{UInt64}(pointer(buf) + 16))
|
||
sys = GC.@preserve buf unsafe_load(Ptr{UInt64}(pointer(buf) + 24))
|
||
# Convert Mach absolute time to nanoseconds via mach_timebase_info
|
||
numer, denom = _mach_timebase()
|
||
cpu_ns = div((user + sys) * numer, denom)
|
||
return (; cpu_ns, rss_bytes)
|
||
catch
|
||
return (cpu_ns=UInt64(0), rss_bytes=UInt64(0))
|
||
end
|
||
else
|
||
return (cpu_ns=UInt64(0), rss_bytes=UInt64(0))
|
||
end
|
||
end
|
||
|
||
# Compute CPU% and RSS for all active PIDs since last poll.
|
||
# Mutates cpu_pcts and rss in place.
|
||
function poll_process_stats!(cpu_pcts::Dict{Int32, Float64}, rss::Dict{Int32, UInt64},
|
||
prev_cpu_times::Dict{Int32, UInt64}, jobs::Dict{PkgConfig, PrecompileJob}, dt::Float64)
|
||
empty!(cpu_pcts)
|
||
empty!(rss)
|
||
@static if !(Sys.islinux() || Sys.isapple())
|
||
return
|
||
end
|
||
for (_, job) in jobs
|
||
has_pid(job) || continue
|
||
pid = job.pid
|
||
haskey(cpu_pcts, pid) && continue
|
||
stats = process_stats(pid)
|
||
stats.cpu_ns == 0 && continue
|
||
prev = get(prev_cpu_times, pid, stats.cpu_ns)
|
||
delta = stats.cpu_ns >= prev ? stats.cpu_ns - prev : UInt64(0)
|
||
prev_cpu_times[pid] = stats.cpu_ns
|
||
pct = dt > 0 ? (delta / 1.0e9) / dt * 100.0 : 0.0
|
||
cpu_pcts[pid] = min(pct, 999.9)
|
||
stats.rss_bytes > 0 && (rss[pid] = stats.rss_bytes)
|
||
end
|
||
pids_set = Set(job.pid for (_, job) in jobs if has_pid(job))
|
||
for pid in keys(prev_cpu_times)
|
||
pid in pids_set || delete!(prev_cpu_times, pid)
|
||
end
|
||
return
|
||
end
|
||
|
||
function should_stop(s::PrecompileSession)
|
||
ir, cr = @lock BG begin
|
||
BG.interrupt_requested, BG.cancel_requested
|
||
end
|
||
if ir || cr
|
||
@lock s.print_lock begin
|
||
s.interrupted = s.interrupted || ir
|
||
s.canceled = s.canceled || cr
|
||
if !s.interrupted_or_done
|
||
s.interrupted_or_done = true
|
||
foreach(notify, values(s.was_processed))
|
||
notify(s.first_started)
|
||
end
|
||
end
|
||
return true
|
||
end
|
||
return s.interrupted_or_done
|
||
end
|
||
|
||
function make_signal_channel()
|
||
ch = Channel{Int32}(32)
|
||
@lock BG push!(BG.signal_channels, ch)
|
||
return ch
|
||
end
|
||
|
||
function describe_pkg(s::PrecompileSession, pkg::PkgId, is_project_dep::Bool, is_serial_dep::Bool, flags::Cmd, cacheflags::Base.CacheFlags)
|
||
name = full_name(s.ext_to_parent, pkg)
|
||
name = is_project_dep ? name : color_string(name, :light_black, s.hascolor)
|
||
if is_serial_dep
|
||
name *= color_string(" (serial)", :light_black, s.hascolor)
|
||
end
|
||
if length(s.configs) > 1 && !isempty(flags)
|
||
config_str = join(flags, " ")
|
||
name *= color_string(" `$config_str`", :light_black, s.hascolor)
|
||
end
|
||
if length(s.configs) > 1
|
||
config_str = join(Base.translate_cache_flags(cacheflags, Base.DefaultCacheFlags), " ")
|
||
name *= color_string(" $config_str", :light_black, s.hascolor)
|
||
end
|
||
return name
|
||
end
|
||
|
||
function spawn_print_loop!(s::PrecompileSession)
|
||
Threads.@spawn :samepool begin
|
||
cursor_disabled = false
|
||
try
|
||
wait(s.first_started)
|
||
(isempty(s.pkg_queue) || s.interrupted_or_done) && return
|
||
@lock s.print_lock begin
|
||
if BG.monitoring
|
||
printpkgstyle(s.logio, :Precompiling, s.target)
|
||
end
|
||
end
|
||
t = Timer(0; interval=1/10)
|
||
anim_chars = ["◐","◓","◑","◒"]
|
||
i = 1
|
||
last_length = 0
|
||
bar = MiniProgressBar(; indent=0, header = "Precompiling packages ", color = :green, percentage=false, always_reprint=true)
|
||
bar.max = s.n_total - s.n_already_precomp
|
||
final_loop = false
|
||
n_print_rows = 0
|
||
last_poll_time = time()
|
||
cpu_pcts = Dict{Int32, Float64}()
|
||
rss_bytes = Dict{Int32, UInt64}()
|
||
while !s.printloop_should_exit
|
||
# Propagate cancel/interrupt requested via BG into the session so the loop exits.
|
||
should_stop(s)
|
||
@lock s.print_lock begin
|
||
verbose = BG.verbose
|
||
now_time = time()
|
||
dt = now_time - last_poll_time
|
||
if verbose && dt >= 0.5
|
||
poll_process_stats!(cpu_pcts, rss_bytes, s.prev_cpu_times, s.jobs, dt)
|
||
last_poll_time = now_time
|
||
end
|
||
term_size = displaysize(s.logio)::Tuple{Int, Int}
|
||
num_deps_show = max(term_size[1] - 3, 2) # show at least 2 deps
|
||
pkg_queue_show = if !s.interrupted_or_done && length(s.pkg_queue) > num_deps_show
|
||
last(s.pkg_queue, num_deps_show)
|
||
else
|
||
s.pkg_queue
|
||
end
|
||
i_local = i
|
||
final_loop_local = final_loop
|
||
tip, tip_color = @lock BG begin
|
||
if BG.confirming !== :none && time() >= BG.confirm_deadline
|
||
BG.confirming = :none
|
||
end
|
||
keyboard_tip(BG)
|
||
end
|
||
str_ = sprint(; context=s.logio) do iostr
|
||
if i_local > 1
|
||
print(iostr, ansi_cleartoend)
|
||
end
|
||
bar.header = verbose ? "Precompiling ($(s.num_tasks) tasks) " : "Precompiling packages "
|
||
# max(0,...) guards against a race where the print loop runs after
|
||
# n_already_precomp is incremented but before n_done is incremented,
|
||
# which would otherwise produce a negative value and crash repeat().
|
||
bar.current = max(0, s.n_done - s.n_already_precomp)
|
||
bar.max = max(0, s.n_total - s.n_already_precomp)
|
||
termwidth = (displaysize(s.logio)::Tuple{Int,Int})[2]
|
||
if !final_loop_local
|
||
tip_width = isempty(tip) ? 0 : textwidth(tip) + 1
|
||
bar_termwidth = termwidth - tip_width
|
||
s_bar = sprint(io -> show_progress(io, bar; termwidth=bar_termwidth, carriagereturn=false); context=s.logio)
|
||
if !isempty(tip)
|
||
s_bar = string(s_bar, " ", color_string(tip, tip_color, s.hascolor))
|
||
end
|
||
print(iostr, Base._truncate_at_width_or_chars(true, s_bar, termwidth), "\n")
|
||
end
|
||
for pkg_config in pkg_queue_show
|
||
dep, config = pkg_config
|
||
loaded = s.warn_loaded && (dep in s.start_loaded_modules)
|
||
flags, cacheflags = config
|
||
name = describe_pkg(s, dep, dep in s.project_deps, dep in s.serial_deps, flags, cacheflags)
|
||
job = s.jobs[pkg_config]
|
||
line = if is_soft_error(job)
|
||
string(color_string(" ? ", Base.warn_color(), s.hascolor), name)
|
||
elseif is_failed(job)
|
||
string(color_string(" ✗ ", Base.error_color(), s.hascolor), name)
|
||
elseif is_recompiled(job)
|
||
!loaded && s.interrupted_or_done && continue
|
||
loaded || Base.errormonitor(Threads.@spawn :samepool begin
|
||
sleep(1);
|
||
@lock s.print_lock filter!(!isequal(pkg_config), s.pkg_queue)
|
||
end)
|
||
string(color_string(" ✓ ", loaded ? Base.warn_color() : :green, s.hascolor), name)
|
||
elseif is_started(job) && s.interrupted_or_done
|
||
# Cancel/interrupt: show a static marker for jobs that actually had
|
||
# a subprocess running, so the user sees what was in flight without
|
||
# it looking still-running. Skip jobs that hadn't reached subprocess
|
||
# spawn yet (e.g. still acquiring the parallel_limiter).
|
||
had_pid(job) || continue
|
||
string(color_string(" - ", :light_black, s.hascolor), name)
|
||
elseif is_started(job)
|
||
anim_char = anim_chars[(i_local + Int(dep.name[1])) % length(anim_chars) + 1]
|
||
anim_char_colored = dep in s.project_deps ? anim_char : color_string(anim_char, :light_black, s.hascolor)
|
||
waiting = if is_locked(job)
|
||
color_string(" Being precompiled by $(job.lock_holder)", Base.info_color(), s.hascolor)
|
||
elseif is_waiting(job)
|
||
color_string(" Waiting for background task / IO / timer. Interrupt to inspect", Base.warn_color(), s.hascolor)
|
||
else
|
||
""
|
||
end
|
||
|
||
pid_info = if verbose && has_pid(job)
|
||
elapsed_str = string(round(Int, now_time - job.started_at), "s")
|
||
pid = job.pid
|
||
pct = get(cpu_pcts, pid, -1.0)
|
||
pct_str = pct >= 0 ? string(" | cpu ", round(Int, pct), "%") : ""
|
||
mem = get(rss_bytes, pid, UInt64(0))
|
||
mem_str = mem > 0 ? string(" | ", Base.format_bytes(mem)) : ""
|
||
color_string(" $(elapsed_str) | pid $(pid)$(pct_str)$(mem_str)", Base.info_color(), s.hascolor)
|
||
else
|
||
""
|
||
end
|
||
string(" ", anim_char_colored, " ", name, pid_info, waiting)
|
||
else
|
||
string(" ", name)
|
||
end
|
||
println(iostr, Base._truncate_at_width_or_chars(true, line, termwidth))
|
||
end
|
||
end
|
||
last_length = length(pkg_queue_show)
|
||
n_print_rows = count("\n", str_)
|
||
s.printloop_should_exit = s.interrupted_or_done && final_loop
|
||
final_loop = s.interrupted_or_done
|
||
i += 1
|
||
if BG.monitoring
|
||
if s.fancyprint && !cursor_disabled
|
||
print(s.logio, ansi_disablecursor)
|
||
cursor_disabled = true
|
||
end
|
||
if s.printloop_should_exit
|
||
print(s.logio, str_)
|
||
else
|
||
print(s.logio, str_, ansi_moveup(n_print_rows), ansi_movecol1)
|
||
end
|
||
elseif cursor_disabled
|
||
print(s.logio, ansi_enablecursor)
|
||
cursor_disabled = false
|
||
end
|
||
end
|
||
wait(t)
|
||
end
|
||
finally
|
||
cursor_disabled && print(s.logio, ansi_enablecursor)
|
||
end
|
||
end
|
||
end
|
||
|
||
function precompilepkgs_monitor_std(s::PrecompileSession, pkg_config, pipe, single_requested_pkg::Bool)
|
||
pkg, config = pkg_config
|
||
liveprinting = false
|
||
thistaskwaiting = false
|
||
while !eof(pipe)
|
||
str = readline(pipe, keep=true)
|
||
if single_requested_pkg && (liveprinting || !isempty(str))
|
||
BG.monitoring && @lock s.print_lock begin
|
||
if !liveprinting
|
||
liveprinting = true
|
||
s.pkg_liveprinted = pkg
|
||
end
|
||
print(s.io, ansi_cleartoendofline, str)
|
||
end
|
||
end
|
||
write(s.jobs[pkg_config].output, str)
|
||
if !thistaskwaiting && occursin("Waiting for background task / IO / timer", str)
|
||
thistaskwaiting = true
|
||
!liveprinting && !s.fancyprint && BG.monitoring && @lock s.print_lock begin
|
||
println(s.io, full_name(s.ext_to_parent, pkg), color_string(str, Base.warn_color(), s.hascolor))
|
||
end
|
||
s.jobs[pkg_config].waiting_for_bg = true
|
||
elseif !thistaskwaiting
|
||
# XXX: don't just re-enable IO for random packages without printing the context for them first
|
||
!liveprinting && !s.fancyprint && BG.monitoring && @lock s.print_lock begin
|
||
print(s.io, ansi_cleartoendofline, str)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
# Can be merged with `maybe_cachefile_lock` in loading?
|
||
# Wraps the precompilation function `f` with cachefile lock handling.
|
||
# Returns the result from `f()`, which can be:
|
||
# - `nothing`: cache already existed
|
||
# - `Tuple{String, Union{Nothing, String}}`: this process just compiled
|
||
# - `Exception`: compilation failed
|
||
function precompile_pkgs_maybe_cachefile_lock(f, s::PrecompileSession, pkg_config, fullname)
|
||
if !(isdefined(Base, :mkpidlock_hook) && isdefined(Base, :trymkpidlock_hook) && Base.isdefined(Base, :parse_pidfile_hook))
|
||
return f()
|
||
end
|
||
pkg, config = pkg_config
|
||
flags, cacheflags = config
|
||
stale_age = Base.compilecache_pidlock_stale_age
|
||
pidfile = Base.compilecache_pidfile_path(pkg, flags=cacheflags)
|
||
cachefile = @invokelatest Base.trymkpidlock_hook(f, pidfile; stale_age)
|
||
if cachefile === false
|
||
pid, hostname, age = @invokelatest Base.parse_pidfile_hook(pidfile)
|
||
job = s.jobs[pkg_config]
|
||
job.lock_holder = if isempty(hostname) || hostname == gethostname()
|
||
if pid == getpid()
|
||
"an async task in this process (pidfile: $pidfile)"
|
||
else
|
||
"another process (pid: $pid, pidfile: $pidfile)"
|
||
end
|
||
else
|
||
"another machine (hostname: $hostname, pid: $pid, pidfile: $pidfile)"
|
||
end
|
||
!s.fancyprint && BG.monitoring && @lock s.print_lock begin
|
||
println(s.io, " ", fullname, color_string(" Being precompiled by $(job.lock_holder)", Base.info_color(), s.hascolor))
|
||
end
|
||
Base.release(s.parallel_limiter) # release so other work can be done while waiting
|
||
try
|
||
# wait until the lock is available
|
||
cachefile = @invokelatest Base.mkpidlock_hook(() -> begin
|
||
job.lock_holder = ""
|
||
Base.acquire(f, s.parallel_limiter)
|
||
end,
|
||
pidfile; stale_age)
|
||
finally
|
||
Base.acquire(s.parallel_limiter) # re-acquire so the outer release is balanced
|
||
end
|
||
end
|
||
return cachefile
|
||
end
|
||
|
||
function spawn_precompile_tasks!(s::PrecompileSession;
|
||
direct_deps, was_processed, configs, circular_deps,
|
||
requested_pkgids, pkg_names, requested_pkgs, from_loading)
|
||
batch_tasks = Task[]
|
||
for (pkg, deps) in direct_deps
|
||
cachepaths = Base.find_all_in_cache_path(pkg)
|
||
freshpaths = String[]
|
||
@lock s.cache_lock s.cachepath_cache[pkg] = freshpaths
|
||
sourcespec = Base.locate_package_load_spec(pkg)
|
||
single_requested_pkg = length(requested_pkgs) == 1 &&
|
||
(pkg in requested_pkgids || pkg.name in pkg_names)
|
||
for config in configs
|
||
pkg_config = (pkg, config)
|
||
if sourcespec === nothing
|
||
mark_failed!(s.jobs[pkg_config], "Error: Missing source file for $(pkg)")
|
||
notify(was_processed[pkg_config])
|
||
continue
|
||
end
|
||
if from_loading && single_requested_pkg && occursin(r"\b__precompile__\(\s*false\s*\)", read(sourcespec.path, String))
|
||
@lock s.print_lock begin
|
||
Base.@logmsg s.logcalls "Disabled precompiling $(repr("text/plain", pkg)) since the text `__precompile__(false)` was found in file."
|
||
end
|
||
notify(was_processed[pkg_config])
|
||
continue
|
||
end
|
||
@lock BG @lock BG.pkg_done begin
|
||
BG.pending_pkgids[pkg] = get(BG.pending_pkgids, pkg, 0) + 1
|
||
notify(BG.pkg_done)
|
||
end
|
||
flags, cacheflags = config
|
||
task = Threads.@spawn :samepool try
|
||
loaded = s.warn_loaded && (pkg in s.start_loaded_modules)
|
||
for dep in deps
|
||
wait(was_processed[(dep,config)])
|
||
if should_stop(s)
|
||
return
|
||
end
|
||
end
|
||
circular = pkg in circular_deps
|
||
freshpath = @lock s.cache_lock Base.compilecache_freshest_path(pkg; ignore_loaded=s.ignore_loaded,
|
||
stale_cache=s.stale_cache, cachepath_cache=s.cachepath_cache, cachepaths, sourcespec, flags=cacheflags)
|
||
is_stale = freshpath === nothing
|
||
if !is_stale
|
||
push!(freshpaths, freshpath)
|
||
end
|
||
if !circular && is_stale
|
||
Base.acquire(s.parallel_limiter)
|
||
is_serial_dep = pkg in s.serial_deps
|
||
is_project_dep = pkg in s.project_deps
|
||
|
||
std_pipe = Base.link_pipe!(Pipe(); reader_supports_async=true, writer_supports_async=true)
|
||
t_monitor = Threads.@spawn :samepool precompilepkgs_monitor_std(s, pkg_config, std_pipe,
|
||
single_requested_pkg)
|
||
|
||
name = describe_pkg(s, pkg, is_project_dep, is_serial_dep, flags, cacheflags)
|
||
try
|
||
@lock s.print_lock begin
|
||
if !s.fancyprint && isempty(s.pkg_queue) && BG.monitoring
|
||
printpkgstyle(s.logio, :Precompiling, s.target)
|
||
end
|
||
push!(s.pkg_queue, pkg_config)
|
||
end
|
||
mark_started!(s.jobs[pkg_config])
|
||
s.fancyprint && notify(s.first_started)
|
||
if should_stop(s)
|
||
return
|
||
end
|
||
loadable_exts = haskey(s.ext_to_parent, pkg) ? filter((dep)->haskey(s.ext_to_parent, dep), s.triggers[pkg]) : nothing
|
||
|
||
flags_ = if !isempty(deps)
|
||
`$flags --compiled-modules=strict`
|
||
else
|
||
flags
|
||
end
|
||
|
||
pid_ch = Channel{Int32}(1)
|
||
if from_loading && pkg in requested_pkgids
|
||
Base.errormonitor(Threads.@spawn :samepool begin
|
||
pid = try; take!(pid_ch); catch; Int32(0); end
|
||
pid > 0 && @lock s.print_lock set_pid!(s.jobs[pkg_config], pid)
|
||
end)
|
||
t = @elapsed ret = begin
|
||
Base.compilecache(pkg, sourcespec, std_pipe, std_pipe, !s.ignore_loaded;
|
||
flags=flags_, cacheflags, loadable_exts, signal_channel=make_signal_channel(),
|
||
pid_channel=pid_ch)
|
||
end
|
||
else
|
||
fullname = full_name(s.ext_to_parent, pkg)
|
||
Base.errormonitor(Threads.@spawn :samepool begin
|
||
pid = try; take!(pid_ch); catch; Int32(0); end
|
||
pid > 0 && @lock s.print_lock set_pid!(s.jobs[pkg_config], pid)
|
||
end)
|
||
t = @elapsed ret = precompile_pkgs_maybe_cachefile_lock(s, pkg_config, fullname) do
|
||
if should_stop(s)
|
||
return ErrorException("canceled")
|
||
end
|
||
local cachepaths = Base.find_all_in_cache_path(pkg)
|
||
local freshpath = @lock s.cache_lock Base.compilecache_freshest_path(pkg; ignore_loaded=s.ignore_loaded,
|
||
stale_cache=s.stale_cache, cachepath_cache=s.cachepath_cache, cachepaths, sourcespec, flags=cacheflags)
|
||
local is_stale = freshpath === nothing
|
||
if !is_stale
|
||
push!(freshpaths, freshpath)
|
||
return nothing
|
||
end
|
||
s.logcalls === CoreLogging.Debug && @lock s.print_lock begin
|
||
@debug "Precompiling $(repr("text/plain", pkg))"
|
||
end
|
||
Base.compilecache(pkg, sourcespec, std_pipe, std_pipe, !s.ignore_loaded;
|
||
flags=flags_, cacheflags, loadable_exts, signal_channel=make_signal_channel(),
|
||
pid_channel=pid_ch)
|
||
end
|
||
end
|
||
if ret isa Exception
|
||
mark_soft_error!(s.jobs[pkg_config])
|
||
!s.fancyprint && BG.monitoring && @lock s.print_lock begin
|
||
println(s.logio, timing_string(t), color_string(" ? ", Base.warn_color(), s.hascolor), name)
|
||
end
|
||
else
|
||
!s.fancyprint && BG.monitoring && @lock s.print_lock begin
|
||
println(s.logio, timing_string(t), color_string(" ✓ ", loaded ? Base.warn_color() : :green, s.hascolor), name)
|
||
end
|
||
if ret !== nothing
|
||
mark_recompiled!(s.jobs[pkg_config])
|
||
cachefile, _ = ret::Tuple{String, Union{Nothing, String}}
|
||
push!(freshpaths, cachefile)
|
||
build_id, _ = Base.parse_cache_buildid(cachefile)
|
||
stale_cache_key = (pkg, build_id, sourcespec, cachefile, s.ignore_loaded, cacheflags)::StaleCacheKey
|
||
@lock s.cache_lock s.stale_cache[stale_cache_key] = false
|
||
if loaded && Base.module_build_id(Base.loaded_modules[pkg]) != build_id
|
||
@lock s.print_lock begin
|
||
s.n_loaded += 1
|
||
push!(s.loaded_pkgs, pkg)
|
||
end
|
||
end
|
||
elseif loaded
|
||
# another process compiled this package; conservatively warn
|
||
@lock s.print_lock begin
|
||
s.n_loaded += 1
|
||
push!(s.loaded_pkgs, pkg)
|
||
end
|
||
end
|
||
end
|
||
catch err
|
||
close(std_pipe.in)
|
||
wait(t_monitor)
|
||
err isa InterruptException && rethrow()
|
||
# If cancel was requested, this failure is almost certainly the
|
||
# subprocess being SIGKILL'd by the cancel; don't report it as
|
||
# a precompile failure.
|
||
if !(@lock BG BG.cancel_requested)
|
||
mark_failed!(s.jobs[pkg_config], sprint(showerror, err))
|
||
!s.fancyprint && BG.monitoring && @lock s.print_lock begin
|
||
println(s.logio, " "^12, color_string(" ✗ ", Base.error_color(), s.hascolor), name)
|
||
end
|
||
end
|
||
finally
|
||
isopen(std_pipe.in) && close(std_pipe.in)
|
||
wait(t_monitor)
|
||
try; close(pid_ch); catch; end
|
||
@lock s.print_lock clear_pid!(s.jobs[pkg_config])
|
||
Base.release(s.parallel_limiter)
|
||
end
|
||
else
|
||
if !is_stale
|
||
@lock s.print_lock begin
|
||
s.n_already_precomp += 1
|
||
if loaded
|
||
fresh_build_id, _ = Base.parse_cache_buildid(freshpath)
|
||
if Base.module_build_id(Base.loaded_modules[pkg]) != fresh_build_id
|
||
s.n_loaded += 1
|
||
push!(s.loaded_pkgs, pkg)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
finally
|
||
@lock BG @lock BG.pkg_done begin
|
||
n = get(BG.pending_pkgids, pkg, 0) - 1
|
||
if n <= 0
|
||
delete!(BG.pending_pkgids, pkg)
|
||
push!(BG.completed_pkgids, pkg)
|
||
else
|
||
BG.pending_pkgids[pkg] = n
|
||
end
|
||
notify(BG.pkg_done)
|
||
end
|
||
@lock s.print_lock (s.n_done += 1)
|
||
notify(was_processed[pkg_config])
|
||
end
|
||
push!(batch_tasks, task)
|
||
end
|
||
end
|
||
return batch_tasks
|
||
end
|
||
|
||
function drain_work_channel!(s::PrecompileSession, work_channel::Channel{PrecompileRequest})
|
||
drainer = Threads.@spawn :samepool begin
|
||
while true
|
||
isopen(work_channel) || break
|
||
should_stop(s) && break
|
||
request = try; take!(work_channel); catch; break; end
|
||
waiter_spawned = false
|
||
try
|
||
new_env = ExplicitEnv()
|
||
req_pkgids = PkgId[]
|
||
for name in request.pkgs
|
||
pkgid = Base.identify_package(name)
|
||
pkgid !== nothing && push!(req_pkgids, pkgid)
|
||
end
|
||
new_graph = build_dep_graph(new_env, request.manifest, request._from_loading, req_pkgids)
|
||
@lock s.print_lock begin
|
||
merge!(s.direct_deps, new_graph.direct_deps)
|
||
merge!(s.ext_to_parent, new_graph.ext_to_parent)
|
||
merge!(s.parent_to_exts, new_graph.parent_to_exts)
|
||
merge!(s.triggers, new_graph.triggers)
|
||
union!(s.project_deps, new_graph.project_deps)
|
||
union!(s.serial_deps, new_graph.serial_deps)
|
||
# When no specific packages were requested, treat project deps as the requested set
|
||
union!(s.requested_pkgids, isempty(req_pkgids) ? new_graph.project_deps : req_pkgids)
|
||
end
|
||
new_pkg_names = copy(request.pkgs)
|
||
new_dd = new_graph.direct_deps
|
||
filter_dep_graph!(new_dd, new_pkg_names, request.manifest, new_graph.project_deps, new_graph.ext_to_parent, req_pkgids)
|
||
skip_pkgs = Set{PkgId}()
|
||
@lock BG begin
|
||
for pkgid in keys(new_dd)
|
||
if haskey(BG.pending_pkgids, pkgid)
|
||
push!(skip_pkgs, pkgid)
|
||
end
|
||
end
|
||
end
|
||
# Replace skip_pkgs with leaf nodes so circular dep detection
|
||
# can traverse through them, but no tasks will be spawned.
|
||
for pkgid in skip_pkgs
|
||
new_dd[pkgid] = PkgId[]
|
||
end
|
||
new_wp = Dict{PkgConfig, Base.Event}()
|
||
for config in request.configs
|
||
for pkgid in keys(new_dd)
|
||
pkg_config = (pkgid, config)
|
||
if pkgid in skip_pkgs
|
||
# Wire existing event so new tasks wait for the already-compiling dep
|
||
if haskey(s.was_processed, pkg_config)
|
||
new_wp[pkg_config] = s.was_processed[pkg_config]
|
||
else
|
||
evt = Base.Event()
|
||
notify(evt)
|
||
new_wp[pkg_config] = evt
|
||
end
|
||
else
|
||
@lock s.print_lock begin
|
||
get!(PrecompileJob, s.jobs, pkg_config)
|
||
end
|
||
evt = Base.Event()
|
||
new_wp[pkg_config] = evt
|
||
s.was_processed[pkg_config] = evt
|
||
end
|
||
end
|
||
end
|
||
new_circular = detect_circular_deps!(new_dd, new_graph.serial_deps, new_wp, s.io, s.ext_to_parent)
|
||
for pkgid in skip_pkgs
|
||
delete!(new_dd, pkgid)
|
||
end
|
||
if isempty(new_dd)
|
||
try; put!(request.result, String[]); catch; end
|
||
continue
|
||
end
|
||
@lock s.print_lock begin
|
||
s.n_total += length(new_dd) * length(request.configs)
|
||
s.n_batches += 1
|
||
end
|
||
new_tasks = spawn_precompile_tasks!(s;
|
||
direct_deps=new_dd, was_processed=new_wp, configs=request.configs,
|
||
circular_deps=new_circular, requested_pkgids=req_pkgids,
|
||
pkg_names=request.pkgs, requested_pkgs=request.pkgs,
|
||
from_loading=request._from_loading)
|
||
append!(s.injected_tasks, new_tasks)
|
||
waiter = Threads.@spawn :samepool begin
|
||
try
|
||
waitall(new_tasks; failfast=false, throw=false)
|
||
paths = @lock s.cache_lock collect(String, Iterators.flatten((v for (pkgid, v) in s.cachepath_cache if pkgid in req_pkgids)))
|
||
try; put!(request.result, paths); catch; end
|
||
finally
|
||
isready(request.result) || try; put!(request.result, String[]); catch; end
|
||
end
|
||
end
|
||
push!(s.result_waiters, waiter)
|
||
waiter_spawned = true
|
||
catch e
|
||
try; put!(request.result, e); catch; end
|
||
finally
|
||
# Only write the fallback here if no waiter was spawned — the waiter
|
||
# has its own finally that guarantees a result is written after tasks finish.
|
||
if !waiter_spawned && !isready(request.result)
|
||
try; put!(request.result, String[]); catch; end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
return drainer
|
||
end
|
||
|
||
function report_precompile_results!(s::PrecompileSession)
|
||
if !s._from_loading
|
||
@lock Base.require_lock begin
|
||
Base.LOADING_CACHE[] = nothing
|
||
end
|
||
end
|
||
notify(s.first_started) # in cases of no-op or !fancyprint
|
||
|
||
quick_exit = any(t -> !istaskdone(t) || istaskfailed(t), s.tasks) || s.interrupted || s.canceled
|
||
seconds_elapsed = round(Int, (s.time_start > 0 ? (time_ns() - s.time_start) : 0) / 1e9)
|
||
ndeps = count(j -> is_recompiled(j), values(s.jobs))
|
||
|
||
requested_errs = false
|
||
for ((dep, _), job) in s.jobs
|
||
if is_failed(job) && dep in s.requested_pkgids
|
||
requested_errs = true
|
||
break
|
||
end
|
||
end
|
||
if !s.strict && !requested_errs && !s.interrupted && !s.canceled
|
||
for (_, job) in s.jobs
|
||
is_failed(job) && clear_failure!(job)
|
||
end
|
||
end
|
||
if s.canceled && !(@lock BG BG.info_requested)
|
||
# Drop captured stdout/stderr from jobs that didn't fail before cancel,
|
||
# since their output is just truncated cancel-induced noise. If the user
|
||
# asked for info (SIGINFO/SIGUSR1) at any point, keep the output so the
|
||
# profiling info gets surfaced in the cancel report. Soft errors
|
||
# (e.g. `?` packages) are completed jobs whose captured output is the
|
||
# actual precompile error message, so preserve it as well.
|
||
for (_, job) in s.jobs
|
||
(is_failed(job) || is_soft_error(job)) && continue
|
||
job.output.size > 0 && truncate(job.output, 0)
|
||
end
|
||
end
|
||
n_failed = count(j -> is_failed(j), values(s.jobs))
|
||
if ndeps > 0 || n_failed > 0
|
||
if !quick_exit
|
||
logstr = sprint(context=s.logio) do iostr
|
||
if s.fancyprint
|
||
what = if s.n_batches > 1
|
||
"done."
|
||
elseif isempty(s.requested_pkgids)
|
||
"packages finished."
|
||
else
|
||
"$(join((full_name(s.ext_to_parent, p) for p in s.requested_pkgids), ", ", " and ")) finished."
|
||
end
|
||
printpkgstyle(iostr, :Precompiling, what)
|
||
end
|
||
plural = length(s.configs) > 1 ? "dependency configurations" : ndeps == 1 ? "dependency" : "dependencies"
|
||
print(iostr, " $(ndeps) $(plural) successfully precompiled in $(seconds_elapsed) seconds")
|
||
if s.n_already_precomp > 0 || !isempty(s.circular_deps)
|
||
s.n_already_precomp > 0 && (print(iostr, ". $(s.n_already_precomp) already precompiled"))
|
||
!isempty(s.circular_deps) && (print(iostr, ". $(length(s.circular_deps)) skipped due to circular dependency"))
|
||
print(iostr, ".")
|
||
end
|
||
if s.n_loaded > 0
|
||
plural1 = length(s.configs) > 1 ? "dependency configurations" : s.n_loaded == 1 ? "dependency" : "dependencies"
|
||
plural2 = s.n_loaded == 1 ? "a different version is" : "different versions are"
|
||
plural3 = s.n_loaded == 1 ? "" : "s"
|
||
loaded_names = join(sort!([full_name(s.ext_to_parent, p) for p in s.loaded_pkgs]), ", ", " and ")
|
||
# compute how many precompiled packages transitively depend on the loaded packages
|
||
loaded_set = Set{PkgId}(s.loaded_pkgs)
|
||
n_affected = let reverse_deps = Dict{PkgId, Vector{PkgId}}()
|
||
for (p, deps) in s.direct_deps
|
||
for d in deps
|
||
push!(get!(Vector{PkgId}, reverse_deps, d), p)
|
||
end
|
||
end
|
||
affected = Set{PkgId}()
|
||
frontier = PkgId[p for p in loaded_set]
|
||
while !isempty(frontier)
|
||
p = pop!(frontier)
|
||
for rdep in get(reverse_deps, p, PkgId[])
|
||
if rdep ∉ affected && rdep ∉ loaded_set
|
||
push!(affected, rdep)
|
||
push!(frontier, rdep)
|
||
end
|
||
end
|
||
end
|
||
length(affected)
|
||
end
|
||
print(iostr, "\n ",
|
||
color_string(string(s.n_loaded), Base.warn_color(), s.hascolor),
|
||
" $(plural1) precompiled but ",
|
||
color_string("$(plural2) currently loaded", Base.warn_color(), s.hascolor),
|
||
" (", loaded_names, ")",
|
||
". Restart julia to access the new version$(plural3)."
|
||
)
|
||
if n_affected > 0
|
||
affected_plural = length(s.configs) > 1 ? "dependency configurations" : n_affected == 1 ? "dependent" : "dependents"
|
||
print(iostr,
|
||
" Otherwise, $(n_affected) $(affected_plural) of ",
|
||
s.n_loaded == 1 ? "this package" : "these packages",
|
||
" may trigger further precompilation to work with the unexpected version$(plural3)."
|
||
)
|
||
end
|
||
end
|
||
n_soft_errors = count(j -> is_soft_error(j), values(s.jobs))
|
||
if n_soft_errors > 0
|
||
pluralpc = length(s.configs) > 1 ? "dependency configurations" : n_soft_errors == 1 ? "dependency" : "dependencies"
|
||
print(iostr, "\n ",
|
||
color_string(string(n_soft_errors), Base.warn_color(), s.hascolor),
|
||
" $(pluralpc) failed but may be precompilable after restarting julia"
|
||
)
|
||
end
|
||
end
|
||
@lock BG BG.result = logstr
|
||
BG.monitoring && @lock s.print_lock begin
|
||
println(s.logio, logstr)
|
||
end
|
||
elseif s.interrupted || s.canceled
|
||
istr = sprint(context=s.logio) do iostr
|
||
if s.fancyprint
|
||
printpkgstyle(iostr, :Precompiling, s.canceled && !s.interrupted ? "canceled." : "interrupted.")
|
||
end
|
||
# On cancel we don't mark in-flight jobs as failed (their subprocesses
|
||
# were killed by the cancel itself), so report them as the "canceled" count.
|
||
# Use the sticky `had_pid` flag so we count jobs that actually spawned a
|
||
# subprocess, not ones that were just past mark_started!. Soft errors are
|
||
# completed jobs that errored before cancel; report them separately.
|
||
n_soft_errors = count(j -> is_soft_error(j), values(s.jobs))
|
||
n_canceled_i = s.canceled && !s.interrupted ?
|
||
count(j -> had_pid(j) && !is_recompiled(j) && !is_soft_error(j), values(s.jobs)) :
|
||
n_failed
|
||
verb = s.canceled && !s.interrupted ? "canceled" : "interrupted"
|
||
print(iostr, " $(ndeps) dependenc$(ndeps == 1 ? "y" : "ies") precompiled, ",
|
||
color_string("$(n_canceled_i)", Base.error_color(), s.hascolor),
|
||
" $verb after $(seconds_elapsed) seconds")
|
||
if n_soft_errors > 0
|
||
pluralpc = length(s.configs) > 1 ? "dependency configurations" : n_soft_errors == 1 ? "dependency" : "dependencies"
|
||
print(iostr, "\n ",
|
||
color_string(string(n_soft_errors), Base.warn_color(), s.hascolor),
|
||
" $(pluralpc) failed but may be precompilable after restarting julia"
|
||
)
|
||
end
|
||
end
|
||
@lock BG BG.result = istr
|
||
BG.monitoring && @lock s.print_lock begin
|
||
println(s.logio, istr)
|
||
end
|
||
end
|
||
end
|
||
if any(j -> j.output.size > 0, values(s.jobs))
|
||
str = sprint(context=s.io) do iostr
|
||
let std_outputs = Tuple{PkgConfig,SubString{String}}[(pc, strip(String(take!(job.output)))) for (pc, job) in s.jobs]
|
||
filter!(!isempty∘last, std_outputs)
|
||
if !isempty(std_outputs)
|
||
plural1 = length(std_outputs) == 1 ? "y" : "ies"
|
||
print(iostr, " ", color_string("$(length(std_outputs))", Base.warn_color(), s.hascolor), " dependenc$(plural1) had output during precompilation:")
|
||
for (pkg_config, err) in std_outputs
|
||
pkg, config = pkg_config
|
||
err = if pkg == s.pkg_liveprinted
|
||
"[Output was shown above]"
|
||
else
|
||
join(split(err, "\n"), color_string("\n│ ", Base.warn_color(), s.hascolor))
|
||
end
|
||
name = full_name(s.ext_to_parent, pkg)
|
||
print(iostr, color_string("\n┌ ", Base.warn_color(), s.hascolor), name, color_string("\n│ ", Base.warn_color(), s.hascolor), err, color_string("\n└ ", Base.warn_color(), s.hascolor))
|
||
end
|
||
end
|
||
end
|
||
end
|
||
!isempty(str) && BG.monitoring && @lock s.print_lock begin
|
||
println(s.io, str)
|
||
end
|
||
end
|
||
if n_failed > 0
|
||
err_str = IOBuffer()
|
||
for ((dep, config), job) in s.jobs
|
||
is_failed(job) || continue
|
||
write(err_str, "\n")
|
||
print(err_str, "\n", full_name(s.ext_to_parent, dep), " ")
|
||
join(err_str, config[1], " ")
|
||
print(err_str, "\n", job.error_msg)
|
||
end
|
||
pluraled = n_failed == 1 ? "" : "s"
|
||
err_msg = "The following $n_failed package$(pluraled) failed to precompile:$(String(take!(err_str)))\n"
|
||
if s.internal_call
|
||
print(s.io, err_msg)
|
||
else
|
||
throw(PkgPrecompileError(err_msg))
|
||
end
|
||
end
|
||
if s.interrupted
|
||
throw(InterruptException())
|
||
end
|
||
return @lock s.cache_lock collect(String, Iterators.flatten((v for (pkgid, v) in s.cachepath_cache if pkgid in s.requested_pkgids)))
|
||
end
|
||
|
||
# The actual precompilation implementation (mode-agnostic)
|
||
function do_precompile(pkgs::Union{Vector{String}, Vector{PkgId}},
|
||
internal_call::Bool,
|
||
strict::Bool,
|
||
warn_loaded::Bool,
|
||
timing::Bool,
|
||
_from_loading::Bool,
|
||
configs::Vector{Config},
|
||
io::IOContext,
|
||
fancyprint′::Bool,
|
||
manifest::Bool,
|
||
ignore_loaded::Bool,
|
||
detachable::Bool,
|
||
work_channel::Channel{PrecompileRequest})
|
||
requested_pkgs = copy(pkgs)
|
||
pkg_names = pkgs isa Vector{String} ? copy(pkgs) : String[pkg.name for pkg in pkgs]
|
||
if pkgs isa Vector{PkgId}
|
||
requested_pkgids = copy(pkgs)
|
||
else
|
||
requested_pkgids = PkgId[]
|
||
for name in pkgs
|
||
pkgid = Base.identify_package(name)
|
||
if pkgid === nothing
|
||
_from_loading && return
|
||
throw(PkgPrecompileError("Unknown package: $name"))
|
||
end
|
||
push!(requested_pkgids, pkgid)
|
||
end
|
||
end
|
||
|
||
time_start = time_ns()
|
||
env = ExplicitEnv()
|
||
|
||
# Windows sometimes hits a ReadOnlyMemoryError, so we halve the default number of tasks. Pkg.jl#2323
|
||
# TODO: Investigate why this happens in windows and restore the full task limit
|
||
default_num_tasks = Sys.iswindows() ? div(Sys.EFFECTIVE_CPU_THREADS::Int, 2) + 1 : Sys.EFFECTIVE_CPU_THREADS::Int + 1
|
||
default_num_tasks = min(default_num_tasks, 16) # limit for better stability on shared resource systems
|
||
num_tasks = max(1, something(tryparse(Int, get(ENV, "JULIA_NUM_PRECOMPILE_TASKS", string(default_num_tasks))), 1))
|
||
|
||
# Suppress precompilation progress messages when precompiling for loading packages, except during
|
||
# interactive sessions, since the complicated IO can have disastrous consequences in the background (#59599)
|
||
logio = io
|
||
logcalls = nothing
|
||
if _from_loading
|
||
if isinteractive()
|
||
logcalls = CoreLogging.Info
|
||
else
|
||
logio = IOContext{IO}(devnull)
|
||
fancyprint′ = false
|
||
logcalls = CoreLogging.Debug
|
||
end
|
||
end
|
||
fancyprint = fancyprint′
|
||
hascolor = get(logio, :color, false)::Bool
|
||
|
||
# Build dependency graph
|
||
graph = build_dep_graph(env, manifest, _from_loading, requested_pkgids)
|
||
|
||
# Return early if no deps
|
||
if isempty(graph.direct_deps)
|
||
isempty(pkgs) && return
|
||
error("No direct dependencies outside of the sysimage found matching $(pkgs)")
|
||
end
|
||
|
||
# Initialize signalling
|
||
was_processed = Dict{PkgConfig,Base.Event}()
|
||
jobs = Dict{PkgConfig,PrecompileJob}()
|
||
for config in configs
|
||
for pkgid in keys(graph.direct_deps)
|
||
pkg_config = (pkgid, config)
|
||
jobs[pkg_config] = PrecompileJob()
|
||
was_processed[pkg_config] = Base.Event()
|
||
end
|
||
end
|
||
|
||
circular_deps = detect_circular_deps!(graph.direct_deps, graph.serial_deps, was_processed, io, graph.ext_to_parent)
|
||
|
||
if filter_dep_graph!(graph.direct_deps, pkg_names, manifest, graph.project_deps, graph.ext_to_parent, requested_pkgids)
|
||
@lock BG BG.result = ""
|
||
return
|
||
end
|
||
|
||
nconfigs = length(configs)
|
||
target = if nconfigs == 1
|
||
flags = only(configs)[1]
|
||
isempty(flags) ? "project..." : "for configuration $(join(flags, " "))"
|
||
else
|
||
"for $nconfigs compilation configurations"
|
||
end
|
||
|
||
print_lock = io.io isa Base.LibuvStream ? io.io.lock::ReentrantLock : ReentrantLock()
|
||
|
||
s = PrecompileSession(;
|
||
configs, io, logio, logcalls, fancyprint, hascolor,
|
||
warn_loaded, ignore_loaded, internal_call, strict, _from_loading,
|
||
time_start, print_lock, parallel_limiter=Base.Semaphore(num_tasks), num_tasks,
|
||
start_loaded_modules=Set{PkgId}(keys(Base.loaded_modules)), requested_pkgids,
|
||
direct_deps=graph.direct_deps,
|
||
ext_to_parent=graph.ext_to_parent, parent_to_exts=graph.parent_to_exts,
|
||
triggers=graph.triggers, project_deps=graph.project_deps,
|
||
serial_deps=graph.serial_deps, circular_deps,
|
||
n_total=length(graph.direct_deps) * nconfigs,
|
||
printloop_should_exit=!fancyprint, target,
|
||
jobs, was_processed,
|
||
)
|
||
|
||
# Start print loop
|
||
t_print = spawn_print_loop!(s)
|
||
|
||
try
|
||
if !_from_loading
|
||
@lock Base.require_lock begin
|
||
Base.LOADING_CACHE[] = Base.LoadingCache()
|
||
end
|
||
end
|
||
@debug "precompile: starting precompilation loop" graph.direct_deps graph.project_deps
|
||
|
||
# Initial pass
|
||
initial_tasks = spawn_precompile_tasks!(s;
|
||
direct_deps=graph.direct_deps, was_processed, configs, circular_deps,
|
||
requested_pkgids, pkg_names, requested_pkgs, from_loading=_from_loading)
|
||
append!(s.tasks, initial_tasks)
|
||
|
||
# Concurrent drainer for injected requests
|
||
drainer = drain_work_channel!(s, work_channel)
|
||
|
||
waitall(initial_tasks; failfast=false, throw=false)
|
||
@lock BG close(work_channel)
|
||
wait(drainer)
|
||
append!(s.tasks, s.injected_tasks)
|
||
waitall(s.injected_tasks; failfast=false, throw=false)
|
||
waitall(s.result_waiters; failfast=false, throw=false)
|
||
|
||
# Final output
|
||
s.interrupted_or_done = true
|
||
notify(s.first_started) # unblock print loop if nothing ever started (no-op case)
|
||
fancyprint && wait(t_print)
|
||
return report_precompile_results!(s)
|
||
finally
|
||
# Ensure print loop exits even on exception
|
||
s.interrupted_or_done = true
|
||
notify(s.first_started)
|
||
end
|
||
end
|
||
|
||
end
|