Skip to content

Commit e0e19a6

Browse files
authored
pkg: Add activate_with_early_release for early lock release (#348)
Add a variant of activate_do that allows early release of ACTIVATE_LOCK before the callback completes. The caller can notify the passed event to signal that the activated environment is no longer needed, enabling the environment to be restored and the lock released while the callback continues executing. Use this in full-analysis to release the activation lock after the code loading phase finishes, allowing other analysis requests to proceed while type inference continues in the background.
1 parent 78adf6b commit e0e19a6

File tree

3 files changed

+82
-28
lines changed

3 files changed

+82
-28
lines changed

src/analysis/Interpreter.jl

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,38 @@ struct LSInterpreter{S<:Server} <: JET.ConcreteInterpreter
2323
request::AnalysisRequest
2424
analyzer::LSAnalyzer
2525
counter::Counter
26+
activation_done::Union{Nothing,Base.Event}
2627
state::JET.InterpretationState
27-
function LSInterpreter(server::S, request::AnalysisRequest, analyzer::LSAnalyzer, counter::Counter) where S<:Server
28-
return new{S}(server, request, analyzer, counter)
28+
function LSInterpreter(
29+
server::S, request::AnalysisRequest, analyzer::LSAnalyzer, counter::Counter,
30+
activation_done::Union{Nothing,Base.Event}
31+
) where S<:Server
32+
return new{S}(server, request, analyzer, counter, activation_done)
2933
end
30-
function LSInterpreter(server::S, request::AnalysisRequest, analyzer::LSAnalyzer, counter::Counter, state::JET.InterpretationState) where S<:Server
31-
return new{S}(server, request, analyzer, counter, state)
34+
function LSInterpreter(
35+
server::S, request::AnalysisRequest, analyzer::LSAnalyzer, counter::Counter,
36+
activation_done::Union{Nothing,Base.Event}, state::JET.InterpretationState
37+
) where S<:Server
38+
return new{S}(server, request, analyzer, counter, activation_done, state)
3239
end
3340
end
3441

3542
# The main constructor
36-
LSInterpreter(server::Server, request::AnalysisRequest) = LSInterpreter(server, request, LSAnalyzer(request.entry), Counter())
43+
function LSInterpreter(
44+
server::Server, request::AnalysisRequest;
45+
activation_done::Union{Nothing,Base.Event} = nothing
46+
)
47+
return LSInterpreter(server, request, LSAnalyzer(request.entry), Counter(), activation_done)
48+
end
3749

3850
# `JET.ConcreteInterpreter` interface
3951
JET.InterpretationState(interp::LSInterpreter) = interp.state
4052
function JET.ConcreteInterpreter(interp::LSInterpreter, state::JET.InterpretationState)
4153
# add `state` to `interp`, and update `interp.analyzer.cache`
4254
initialize_cache!(interp.analyzer, state.res.analyzed_files)
43-
return LSInterpreter(interp.server, interp.request, interp.analyzer, interp.counter, state)
55+
return LSInterpreter(
56+
interp.server, interp.request, interp.analyzer, interp.counter,
57+
interp.activation_done, state)
4458
end
4559
JET.ToplevelAbstractAnalyzer(interp::LSInterpreter) = interp.analyzer
4660

@@ -58,6 +72,13 @@ function cache_intermediate_analysis_result!(interp::LSInterpreter)
5872
end
5973

6074
function JET.analyze_from_definitions!(interp::LSInterpreter, config::JET.ToplevelConfig)
75+
activation_done = interp.activation_done
76+
if activation_done !== nothing
77+
# The phase that requires code loading has finished, so this environment is no
78+
# longer needed, so let's release the `ACTIVATION_LOCK`
79+
notify(activation_done)
80+
end
81+
6182
# Cache intermediate analysis results after file analysis completes
6283
# This makes module context information available immediately for LS features
6384
cache_intermediate_analysis_result!(interp)

src/analysis/full-analysis.jl

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -386,18 +386,18 @@ function execute_analysis_request(server::Server, request::AnalysisRequest)
386386
result = analyze_parsed_if_exist(server, request)
387387

388388
elseif entry isa ScriptInEnvAnalysisEntry
389-
result = activate_do(entry.env_path) do
390-
analyze_parsed_if_exist(server, request)
389+
result = activate_with_early_release(entry.env_path) do activation_done::Base.Event
390+
analyze_parsed_if_exist(server, request; activation_done)
391391
end
392392

393393
elseif entry isa PackageSourceAnalysisEntry
394-
result = activate_do(entry.env_path) do
395-
analyze_parsed_if_exist(server, request, entry.pkgid)
394+
result = activate_with_early_release(entry.env_path) do activation_done::Base.Event
395+
analyze_parsed_if_exist(server, request, entry.pkgid; activation_done)
396396
end
397397

398398
elseif entry isa PackageTestAnalysisEntry
399-
result = activate_do(entry.env_path) do
400-
analyze_parsed_if_exist(server, request)
399+
result = activate_with_early_release(entry.env_path) do activation_done::Base.Event
400+
analyze_parsed_if_exist(server, request; activation_done)
401401
end
402402

403403
else error("Unsupported analysis entry $entry") end
@@ -428,16 +428,21 @@ function end_full_analysis_progress(server::Server, cancellable_token::Cancellab
428428
WorkDoneProgressEnd(; message = "Analysis completed"))
429429
end
430430

431-
function analyze_parsed_if_exist(server::Server, request::AnalysisRequest, args...)
431+
function analyze_parsed_if_exist(
432+
server::Server, request::AnalysisRequest, args...;
433+
activation_done::Union{Nothing,Base.Event} = nothing
434+
)
432435
uri = entryuri(request.entry)
433436
jetconfigs = getjetconfigs(request.entry)
434437
fi = get_saved_file_info(server.state, uri)
435438
if !isnothing(fi)
436439
filename = @something uri2filename(uri) error(lazy"Unsupported URI: $uri")
437-
return JET.analyze_and_report_expr!(LSInterpreter(server, request), fi.syntax_node, filename, args...; jetconfigs...)
440+
interp = LSInterpreter(server, request; activation_done)
441+
return JET.analyze_and_report_expr!(interp, fi.syntax_node, filename, args...; jetconfigs...)
438442
else
439443
filepath = @something uri2filepath(uri) error(lazy"Unsupported URI: $uri")
440-
return JET.analyze_and_report_file!(LSInterpreter(server, request), filepath, args...; jetconfigs...)
444+
interp = LSInterpreter(server, request; activation_done)
445+
return JET.analyze_and_report_file!(interp, filepath, args...; jetconfigs...)
441446
end
442447
end
443448

@@ -675,23 +680,25 @@ end
675680

676681
function ensure_instantiated!(server::Server, env_path::String)
677682
if get_config(server.state.config_manager, :full_analysis, :auto_instantiate)
683+
manifest_name = "Manifest-v$(VERSION.major).$(VERSION.minor).toml"
684+
manifest_path = joinpath(dirname(env_path), manifest_name)
685+
io = IOBuffer()
678686
try
679-
manifest_name = "Manifest-v$(VERSION.major).$(VERSION.minor).toml"
680-
manifest_path = joinpath(dirname(env_path), manifest_name)
681687
if !isfile(manifest_path)
682688
JETLS_DEV_MODE && @info "Touching versioned manifest file" env_path
683689
touch(manifest_path)
684690
end
685691
JETLS_DEV_MODE && @info "Resolving package environment" env_path
686-
Pkg.resolve()
692+
Pkg.resolve(; io)
687693
JETLS_DEV_MODE && @info "Instantiating package environment" env_path
688-
Pkg.instantiate()
694+
Pkg.instantiate(; io)
689695
catch e
690696
@error """Failed to instantiate package environment;
691697
Unable to instantiate the environment of the target package for analysis,
692698
so this package will be analyzed as a script instead.
693699
This may cause various features such as diagnostics to not function properly.
694700
It is recommended to fix the problem by referring to the following error""" env_path
701+
print(stderr, String(take!(io)))
695702
Base.showerror(stderr, e, catch_backtrace())
696703
show_warning_message(server, """
697704
Failed to instantiate package environment at $env_path.

src/utils/pkg.jl

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function find_pkg_name(env_path::AbstractString)
4242
return pkg_name isa String ? pkg_name : nothing
4343
end
4444

45-
const ACTIVATE_LOCK = ReentrantLock()
45+
const PKG_ACTIVATION_LOCK = ReentrantLock()
4646

4747
"""
4848
activate_do(func, env_path::String)
@@ -51,15 +51,41 @@ Temporarily activate the environment at `env_path`, execute `func`, and restore
5151
previous environment. Uses a global lock to prevent concurrent environment switching.
5252
"""
5353
function activate_do(func, env_path::String)
54-
@lock ACTIVATE_LOCK begin
55-
old_env = Pkg.project().path
56-
try
57-
Pkg.activate(env_path; io=devnull)
58-
func()
59-
finally
60-
Pkg.activate(old_env; io=devnull)
61-
end
54+
lock(PKG_ACTIVATION_LOCK)
55+
old_env = Pkg.project().path
56+
try
57+
Pkg.activate(env_path; io=devnull)
58+
return func()
59+
finally
60+
Pkg.activate(old_env; io=devnull)
61+
unlock(PKG_ACTIVATION_LOCK)
62+
end
63+
end
64+
65+
"""
66+
activate_with_early_release(func, env_path::String)
67+
68+
Temporarily activate the environment at `env_path` and execute `func(activation_done)`.
69+
Unlike [`activate_do`](@ref), this allows early release of `PKG_ACTIVATION_LOCK` before `func`
70+
completes: the caller can `notify(activation_done)` to signal that the activated environment
71+
is no longer needed, allowing the environment to be restored and the lock released while
72+
`func` continues executing. `func` is allowed to return without notifying, in which case
73+
the event is automatically notified in the `finally` block.
74+
"""
75+
function activate_with_early_release(func, env_path::String)
76+
activation_done = Base.Event()
77+
lock(PKG_ACTIVATION_LOCK)
78+
old_env = Pkg.project().path
79+
Pkg.activate(env_path; io=devnull)
80+
t = Threads.@spawn try
81+
func(activation_done)
82+
finally
83+
notify(activation_done)
6284
end
85+
wait(activation_done)
86+
Pkg.activate(old_env; io=devnull)
87+
unlock(PKG_ACTIVATION_LOCK)
88+
return fetch(t)
6389
end
6490

6591
function find_package_directory(path::String, env_path::String)

0 commit comments

Comments
 (0)