Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function index_project_request(params::JuliaDynamicAnalysisProtocol.IndexProject
TestEnv.activate(params.package);
end

SymbolServer.get_store(params.storePath, nothing)
SymbolServer.get_store(params.storePath, nothing; used_packages=params.usedPackages)

return dirname(Base.active_project())
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ else
is_stdlib(uuid::UUID) = uuid in keys(ctx.stdlibs)
end

function get_store(store_path::String, progress_callback)
function get_store(store_path::String, progress_callback; used_packages=nothing)
loading_bay = Module(:LoadingBay)

ctx = try
Expand All @@ -34,7 +34,7 @@ function get_store(store_path::String, progress_callback)
@info "Package environment can't be read."
exit()
end

server = Server(store_path, ctx, Dict{UUID,Package}())

written_caches = String[] # List of caches that have already been written
Expand All @@ -52,6 +52,19 @@ function get_store(store_path::String, progress_callback)
@info "$pk_name not in manifest, skipping."
continue
end

# Only load registry packages the workspace actually `using`/`import`s.
# `used_packages === nothing` means "index everything" (the default used
# by the cloud indexer and standalone-project indexing). In-workspace
# `develop`ed packages are always indexed, so a package's own symbols and
# those of workspace deps are never dropped. Dependencies of an imported
# package are still cached: importing it loads them, and the whole-process
# `getenvtree`/`symbols` walk below picks them up.
if !isnothing(used_packages) && pk_name ∉ used_packages && !is_package_deved(manifest(ctx), uuid)
@debug "$pk_name ($uuid) not imported by the workspace, skipping load."
continue
end

pe = frommanifest(manifest(ctx), uuid)
cache_path = joinpath(server.storedir, SymbolServer.get_cache_path(manifest(ctx), uuid)...)

Expand Down
1 change: 1 addition & 0 deletions shared/julia_dynamic_analysis_process_protocol.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ using ..JSONRPC: @dict_readable, RequestType, NotificationType, Outbound
projectPath::String
package::Union{Nothing,String}
storePath::String
usedPackages::Union{Nothing,Vector{String}}
end

@dict_readable struct CreateStandaloneProjectParams <: JSONRPC.Outbound
Expand Down
111 changes: 84 additions & 27 deletions src/dynamic_feature/dynamic_feature.jl
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,18 @@ mutable struct DynamicJuliaProcess
end

function index_project(djp::DynamicJuliaProcess, store_path::String)
# For a watch-environment process the key carries the workspace's imported
# package set, so the child only loads those (plus deved packages). For a
# test-environment process there is no such set, so `nothing` = index all.
used_packages = djp.key isa WatchEnvironmentKey ? djp.key.imported_packages : nothing
JSONRPC.send(
djp.endpoint,
JuliaDynamicAnalysisProtocol.index_project_request_type,
JuliaDynamicAnalysisProtocol.IndexProjectParams(
djp.project_path,
djp.package,
store_path
store_path,
used_packages
)
)
end
Expand Down Expand Up @@ -357,14 +362,49 @@ end

const MissingPackage = @NamedTuple{name::String, uuid::UUID, version::String, git_tree_sha1::Union{String,Nothing}}

# Names of every manifest package reachable from `roots` by following `deps`
# edges (roots included). `manifest_deps` is keyed by package name, and each
# entry's dependencies are listed either as an array of names or a name⇒uuid
# table. Used to restrict the missing-cache check to the packages the child will
# actually load (the closure of the workspace's imported and deved packages).
function _packages_reachable_from(manifest_deps, roots)
reachable = Set{String}()
stack = collect(String, roots)
while !isempty(stack)
name = pop!(stack)
name in reachable && continue
push!(reachable, name)
v_entry = get(manifest_deps, name, nothing)
(v_entry isa Vector && length(v_entry) == 1 && v_entry[1] isa Dict) || continue
d = get(v_entry[1], "deps", nothing)
depnames = if d isa Vector
String[x for x in d if x isa AbstractString]
elseif d isa Dict
collect(keys(d))
else
String[]
end
for dn in depnames
dn in reachable || push!(stack, dn)
end
end
return reachable
end

"""
_get_missing_packages(project_path, store_path) -> Vector{MissingPackage}
_get_missing_packages(project_path, store_path, imported_packages=nothing) -> Vector{MissingPackage}

Parse the Manifest.toml at `project_path` and return a list of regular and stdlib
packages whose .jstore cache files do not yet exist on disk. Deved packages are
skipped entirely (they have no git_tree_sha1 and are handled by StaticLint).

When `imported_packages` is given, only packages in the dependency closure of the
workspace's imported and deved packages are considered — i.e. exactly the set the
child indexer will load. This prevents a never-loaded dependency from being
reported "missing" forever, which would otherwise spawn an indexing child every
session that could never satisfy it. `nothing` considers the whole manifest.
"""
function _get_missing_packages(project_path::String, store_path::String)
function _get_missing_packages(project_path::String, store_path::String, imported_packages=nothing)
manifest_path = joinpath(project_path, "Manifest.toml")
isfile(manifest_path) || return MissingPackage[]

Expand All @@ -386,6 +426,20 @@ function _get_missing_packages(project_path::String, store_path::String)
return MissingPackage[]
end

# Restrict to the packages the child will actually load: the dependency
# closure of the workspace's imported packages plus all deved packages
# (which the child always loads). `nothing` means "consider everything".
allowed = if isnothing(imported_packages)
nothing
else
deved_names = Set{String}(
k for (k, v) in pairs(manifest_deps)
if v isa Vector && length(v) == 1 && v[1] isa Dict && haskey(v[1], "path")
)
roots = union(Set{String}(imported_packages), deved_names)
_packages_reachable_from(manifest_deps, roots)
end

missing = MissingPackage[]

for (k_entry, v_entry) in pairs(manifest_deps)
Expand All @@ -394,6 +448,9 @@ function _get_missing_packages(project_path::String, store_path::String)
v_entry[1] isa Dict || continue
entry = v_entry[1]

# Skip packages the workspace never loads.
allowed === nothing || k_entry in allowed || continue

# Skip deved packages (have "path" key)
haskey(entry, "path") && continue

Expand Down Expand Up @@ -623,7 +680,7 @@ end
# ─── Work messages ──────────────────────────────────────────────────────────

function handle!(df::DynamicFeature, msg::WatchEnvironmentMsg)
key = WatchEnvironmentKey(msg.project_path, msg.content_hash)
key = msg.key
push!(df.inflight, key)

if key in df.failed_projects
Expand All @@ -636,18 +693,18 @@ function handle!(df::DynamicFeature, msg::WatchEnvironmentMsg)
# Offload the (potentially slow) missing-package check + cloud download to a
# task so the reactor stays responsive. The result is fed back as an
# `EnvironmentPrepDoneMsg`, so all state mutation stays on the reactor.
project_path = msg.project_path
content_hash = msg.content_hash
project_path = key.project_path
imported_packages = key.imported_packages
Threads.@async try
missing_pkgs = _get_missing_packages(project_path, df.store_path)
missing_pkgs = _get_missing_packages(project_path, df.store_path, imported_packages)

if !isempty(missing_pkgs) && df.download_enabled
@info "Downloading missing package caches" count=length(missing_pkgs)
_report_progress(df, "Downloading caches for $(basename(project_path))...")
missing_pkgs = _download_missing_caches(missing_pkgs, df.store_path, df.upstream_url; df=df)
end

put!(df.in_channel, EnvironmentPrepDoneMsg(project_path, content_hash, !isempty(missing_pkgs)))
put!(df.in_channel, EnvironmentPrepDoneMsg(key, !isempty(missing_pkgs)))
catch err
@error "Environment prep failed" project_path=project_path exception=(err, catch_backtrace())
put!(df.in_channel, ProcessIndexFailedMsg(key, err))
Expand All @@ -657,24 +714,24 @@ function handle!(df::DynamicFeature, msg::WatchEnvironmentMsg)
end

function handle!(df::DynamicFeature, msg::EnvironmentPrepDoneMsg)
key = WatchEnvironmentKey(msg.project_path, msg.content_hash)
key = msg.key
df.progress_state.current_sub_progress = 0.5

if !msg.still_missing
@info "All package caches available, skipping DJP" project_path=msg.project_path
_report_progress(df, "All caches available for $(basename(msg.project_path))")
put!(df.out_channel, EnvironmentReadyResult(msg.project_path, msg.content_hash))
@info "All package caches available, skipping DJP" project_path=key.project_path
_report_progress(df, "All caches available for $(basename(key.project_path))")
put!(df.out_channel, EnvironmentReadyResult(key))
push!(df.done, key)
_complete_work_item!(df, key)
elseif df.djp_mode != DynamicOff
@info "Launching DJP for remaining missing packages" project_path=msg.project_path
_report_progress(df, "Indexing $(basename(msg.project_path))...")
djp = DynamicJuliaProcess(key, msg.project_path, nothing, :watch_environment)
@info "Launching DJP for remaining missing packages" project_path=key.project_path
_report_progress(df, "Indexing $(basename(key.project_path))...")
djp = DynamicJuliaProcess(key, key.project_path, nothing, :watch_environment)
df.procs[key] = djp
_launch_process!(df, djp)
else
@info "Some packages missing but DJP disabled, proceeding with best-effort" project_path=msg.project_path
put!(df.out_channel, EnvironmentReadyResult(msg.project_path, msg.content_hash))
@info "Some packages missing but DJP disabled, proceeding with best-effort" project_path=key.project_path
put!(df.out_channel, EnvironmentReadyResult(key))
push!(df.done, key)
_complete_work_item!(df, key)
end
Expand All @@ -683,7 +740,7 @@ function handle!(df::DynamicFeature, msg::EnvironmentPrepDoneMsg)
end

function handle!(df::DynamicFeature, msg::WatchTestEnvironmentMsg)
key = WatchTestEnvironmentKey(msg.project_path, msg.package, msg.content_hash)
key = msg.key
push!(df.inflight, key)

if key in df.failed_projects
Expand All @@ -693,16 +750,16 @@ function handle!(df::DynamicFeature, msg::WatchTestEnvironmentMsg)
return false
end

_report_progress(df, "Indexing test environment for $(msg.package)...")
djp = DynamicJuliaProcess(key, msg.project_path, msg.package, :watch_test_environment)
_report_progress(df, "Indexing test environment for $(key.package_name)...")
djp = DynamicJuliaProcess(key, key.project_path, key.package_name, :watch_test_environment)
df.procs[key] = djp
_launch_process!(df, djp)

return false
end

function handle!(df::DynamicFeature, msg::CreateStandaloneProjectMsg)
key = CreateStandaloneProjectKey(msg.package_path, msg.content_hash)
key = msg.key
push!(df.inflight, key)

if key in df.failed_projects
Expand All @@ -712,8 +769,8 @@ function handle!(df::DynamicFeature, msg::CreateStandaloneProjectMsg)
return false
end

_report_progress(df, "Creating standalone project for $(basename(msg.package_path))...")
djp = DynamicJuliaProcess(key, msg.package_path, nothing, :create_standalone_project)
_report_progress(df, "Creating standalone project for $(basename(key.package_path))...")
djp = DynamicJuliaProcess(key, key.package_path, nothing, :create_standalone_project)
df.procs[key] = djp
_launch_process!(df, djp)

Expand Down Expand Up @@ -766,7 +823,7 @@ function handle!(df::DynamicFeature, msg::ProcessIndexedMsg)
end

if key isa WatchEnvironmentKey
put!(df.out_channel, EnvironmentReadyResult(key.project_path, key.content_hash))
put!(df.out_channel, EnvironmentReadyResult(key))
elseif key isa WatchTestEnvironmentKey
test_project_uri = filepath2uri(msg.result_dir)
put!(df.out_channel, TestEnvironmentReadyResult(filepath2uri(key.project_path), key.package_name, test_project_uri, key.content_hash))
Expand Down Expand Up @@ -899,11 +956,11 @@ function handle!(df::DynamicFeature, msg::ReconcileMsg)
_report_progress(df, "Preparing to index...")

if key isa WatchEnvironmentKey
handle!(df, WatchEnvironmentMsg(key.project_path, key.content_hash))
handle!(df, WatchEnvironmentMsg(key))
elseif key isa WatchTestEnvironmentKey
handle!(df, WatchTestEnvironmentMsg(key.project_path, key.package_name, key.content_hash))
handle!(df, WatchTestEnvironmentMsg(key))
elseif key isa CreateStandaloneProjectKey
handle!(df, CreateStandaloneProjectMsg(key.package_path, key.content_hash))
handle!(df, CreateStandaloneProjectMsg(key))
end
end

Expand Down
22 changes: 11 additions & 11 deletions src/dynamic_feature/dynamic_messages.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@
# `@auto_hash_equals` gives them value-based `==`/`hash` (the default for plain
# structs compares `String` fields by identity, which is unsafe for use as
# `Set`/`Dict` keys).
# `imported_packages` is the sorted set of top-level package names the
# workspace's own source `using`/`import`s within this project. It is part of
# the key's identity so that a change to which packages are used (re)triggers
# indexing via the normal reconcile diff, and it is forwarded to the child so it
# only loads packages the workspace actually references.
@auto_hash_equals struct WatchEnvironmentKey
project_path::String
content_hash::UInt64
imported_packages::Vector{String}
end

@auto_hash_equals struct WatchTestEnvironmentKey
Expand Down Expand Up @@ -49,21 +55,17 @@ abstract type DynamicReactorMessage end

"""Request to index/watch the environment of a project."""
struct WatchEnvironmentMsg <: DynamicReactorMessage
project_path::String
content_hash::UInt64
key::WatchEnvironmentKey
end

"""Request to index/watch the test environment of a project + package."""
struct WatchTestEnvironmentMsg <: DynamicReactorMessage
project_path::String
package::String
content_hash::UInt64
key::WatchTestEnvironmentKey
end

"""Request to create a standalone project for a package folder."""
struct CreateStandaloneProjectMsg <: DynamicReactorMessage
package_path::String
content_hash::UInt64
key::CreateStandaloneProjectKey
end

"""Request an orderly shutdown of the reactor."""
Expand All @@ -90,8 +92,7 @@ once the (potentially slow) missing-package check + cloud download finished.
`still_missing` indicates whether a DJP is still required afterwards.
"""
struct EnvironmentPrepDoneMsg <: DynamicReactorMessage
project_path::String
content_hash::UInt64
key::WatchEnvironmentKey
still_missing::Bool
end

Expand Down Expand Up @@ -140,8 +141,7 @@ end

"""The environment for a project has been fully processed."""
struct EnvironmentReadyResult <: DynamicResultMessage
project_path::String
content_hash::UInt64
key::WatchEnvironmentKey
end

"""The test environment for a project + package is ready."""
Expand Down
Loading
Loading