Skip to content

Commit eadec43

Browse files
REPLCompletions: PATH caching tweaks (#52893)
1 parent 77652fd commit eadec43

File tree

2 files changed

+45
-21
lines changed

2 files changed

+45
-21
lines changed

stdlib/REPL/src/REPLCompletions.jl

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -271,25 +271,34 @@ end
271271

272272
const PATH_cache_lock = Base.ReentrantLock()
273273
const PATH_cache = Set{String}()
274-
cached_PATH_string::Union{String,Nothing} = nothing
275-
function cached_PATH_changed()
276-
global cached_PATH_string
277-
@lock(PATH_cache_lock, cached_PATH_string) !== get(ENV, "PATH", nothing)
274+
PATH_cache_task::Union{Task,Nothing} = nothing # used for sync in tests
275+
next_cache_update::Float64 = 0.0
276+
function maybe_spawn_cache_PATH()
277+
global PATH_cache_task, next_cache_update
278+
@lock PATH_cache_lock begin
279+
PATH_cache_task isa Task && !istaskdone(PATH_cache_task) && return
280+
time() < next_cache_update && return
281+
PATH_cache_task = Threads.@spawn REPLCompletions.cache_PATH()
282+
Base.errormonitor(PATH_cache_task)
283+
end
278284
end
279-
const PATH_cache_finished = Base.Condition() # used for sync in tests
280285

281286
# caches all reachable files in PATH dirs
282287
function cache_PATH()
283-
global cached_PATH_string
284-
path = @lock PATH_cache_lock begin
285-
empty!(PATH_cache)
286-
cached_PATH_string = get(ENV, "PATH", nothing)
287-
end
288+
path = get(ENV, "PATH", nothing)
288289
path isa String || return
289290

291+
global next_cache_update
292+
293+
# Calling empty! on PATH_cache would be annoying for async typing hints as completions would temporarily disappear.
294+
# So keep track of what's added this time and at the end remove any that didn't appear this time from the global cache.
295+
this_PATH_cache = Set{String}()
296+
290297
@debug "caching PATH files" PATH=path
291298
pathdirs = split(path, @static Sys.iswindows() ? ";" : ":")
292299

300+
next_yield_time = time() + 0.01
301+
293302
t = @elapsed for pathdir in pathdirs
294303
actualpath = try
295304
realpath(pathdir)
@@ -322,6 +331,7 @@ function cache_PATH()
322331
try
323332
if isfile(joinpath(pathdir, file))
324333
@lock PATH_cache_lock push!(PATH_cache, file)
334+
push!(this_PATH_cache, file)
325335
end
326336
catch e
327337
# `isfile()` can throw in rare cases such as when probing a
@@ -333,10 +343,18 @@ function cache_PATH()
333343
rethrow()
334344
end
335345
end
336-
yield() # so startup doesn't block when -t1
346+
if time() >= next_yield_time
347+
yield() # to avoid blocking typing when -t1
348+
next_yield_time = time() + 0.01
349+
end
337350
end
338351
end
339-
notify(PATH_cache_finished)
352+
353+
@lock PATH_cache_lock begin
354+
intersect!(PATH_cache, this_PATH_cache) # remove entries from PATH_cache that weren't found this time
355+
next_cache_update = time() + 10 # earliest next update can run is 10s after
356+
end
357+
340358
@debug "caching PATH files took $t seconds" length(pathdirs) length(PATH_cache)
341359
return PATH_cache
342360
end
@@ -380,15 +398,13 @@ function complete_path(path::AbstractString;
380398
end
381399

382400
if use_envpath && isempty(dir)
383-
# Look for files in PATH as well. These are cached in `cache_PATH` in a separate task in REPL init.
384-
# If we cannot get lock because its still caching just pass over this so that initial
385-
# typing isn't laggy. If the PATH string has changed since last cache re-cache it
386-
cached_PATH_changed() && Base.errormonitor(Threads.@spawn REPLCompletions.cache_PATH())
387-
if trylock(PATH_cache_lock)
401+
# Look for files in PATH as well. These are cached in `cache_PATH` in an async task to not block typing.
402+
# If we cannot get lock because its still caching just pass over this so that typing isn't laggy.
403+
maybe_spawn_cache_PATH() # only spawns if enough time has passed and the previous caching task has completed
404+
@lock PATH_cache_lock begin
388405
for file in PATH_cache
389406
startswith(file, prefix) && push!(matches, file)
390407
end
391-
unlock(PATH_cache_lock)
392408
end
393409
end
394410

stdlib/REPL/test/replcompletions.jl

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,11 +1146,15 @@ let s, c, r
11461146
# PATH can also contain folders which we aren't actually allowed to read.
11471147
withenv("PATH" => string(path, ":", unreadable)) do
11481148
s = "tmp-execu"
1149-
c,r = test_scomplete(s)
11501149
# Files reachable by PATH are cached async when PATH is seen to have been changed by `complete_path`
11511150
# so changes are unlikely to appear in the first complete. For testing purposes we can wait for
11521151
# caching to finish
1153-
wait(REPL.REPLCompletions.PATH_cache_finished)
1152+
@lock REPL.REPLCompletions.PATH_cache_lock begin
1153+
# force the next cache update to happen immediately
1154+
REPL.REPLCompletions.next_cache_update = 0
1155+
end
1156+
c,r = test_scomplete(s)
1157+
wait(REPL.REPLCompletions.PATH_cache_task::Task) # wait for caching to complete
11541158
c,r = test_scomplete(s)
11551159
@test "tmp-executable" in c
11561160
@test r == 1:9
@@ -1179,8 +1183,12 @@ let s, c, r
11791183

11801184
withenv("PATH" => string(tempdir(), ":", dir)) do
11811185
s = string("repl-completio")
1186+
@lock REPL.REPLCompletions.PATH_cache_lock begin
1187+
# force the next cache update to happen immediately
1188+
REPL.REPLCompletions.next_cache_update = 0
1189+
end
11821190
c,r = test_scomplete(s)
1183-
wait(REPL.REPLCompletions.PATH_cache_finished) # wait for caching to complete
1191+
wait(REPL.REPLCompletions.PATH_cache_task::Task) # wait for caching to complete
11841192
c,r = test_scomplete(s)
11851193
@test ["repl-completion"] == c
11861194
@test s[r] == "repl-completio"

0 commit comments

Comments
 (0)