From 7614d63d865f1116242ba51c2bce59caab8d9a5e Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Thu, 26 Feb 2026 08:15:36 -0600 Subject: [PATCH] Update type-cache after each user interaction The intent here is to keep the type-cache up to date, so that if and when a type is redefined, there isn't a large latency before the revision can start. This is the last piece of the puzzle needed to Fix #988 --- .github/workflows/ci.yml | 2 +- src/packagedef.jl | 58 +++++++++++++++++++++++++++++----------- src/precompile.jl | 2 +- test/runtests.jl | 19 ++++++++++--- test/start_late.jl | 2 +- 5 files changed, 62 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bfdaa4c..d677e27a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,7 @@ jobs: Base.run_main_repl(true, true, false, true, false)) isdefined(Base, :errormonitor) && Base.errormonitor(t) while (!isdefined(Base, :active_repl_backend) || isnothing(Base.active_repl_backend)) sleep(0.1) end - pushfirst!(Base.active_repl_backend.ast_transforms, Revise.revise_first) + pushfirst!(Base.active_repl_backend.ast_transforms, Revise.revise_first_scan_last) include(joinpath("test", "runtests.jl")) if Base.VERSION.major == 1 && Base.VERSION.minor >= 9 REPL.eval_user_input(:(exit()), Base.active_repl_backend, Main) diff --git a/src/packagedef.jl b/src/packagedef.jl index 28dade24..01d66544 100644 --- a/src/packagedef.jl +++ b/src/packagedef.jl @@ -1483,12 +1483,9 @@ function maybe_set_prompt_color(color) return nothing end -# `revise_first` gets called by the REPL prior to executing the next command (by having been pushed +# `revise_first_scan_last` gets called by the REPL prior to executing the next command (by having been pushed # onto the `ast_transform` list). -# This uses invokelatest not for reasons of world age but to ensure that the call is made at runtime. -# This allows `revise_first` to be compiled without compiling `revise` itself, and greatly -# reduces the overhead of using Revise. -function revise_first(ex) +function revise_first_scan_last(ex) # Special-case `exit()` (issue #562) if isa(ex, Expr) exu = unwrap(ex) @@ -1517,8 +1514,28 @@ function revise_first(ex) end end end - # Check for queued revisions, and if so call `revise` first before executing the expression - return Expr(:toplevel, :($isempty($revision_queue) || $(Base.invokelatest)($revise)), ex) + return revise_first_scan_last_expr(ex) +end + +function revise_first_scan_last_expr(ex) + # Modify the user-supplied expression `ex` to: + # 1. Check for queued revisions, and if any call `revise` first + # 2. Execute the user's expression and store the result in a temporary variable + # 3. Spawn a task to update the type cache after the expression has been executed + # 4. Return the result of the user's expression + result = gensym("result") + return Expr(:toplevel, + # This uses invokelatest not for reasons of world age but to ensure that the call is made at runtime. + # This allows `revise_first_scan_last` to be compiled without compiling `revise` itself, and greatly + # reduces the overhead of using Revise. + :($isempty($revision_queue) || $(Base.invokelatest)($revise)), + quote + let $result = $ex + Base.Threads.@spawn :default $(repopulate_typecache)() + $result + end + end + ) end steal_repl_backend(_...) = @warn """ @@ -1633,10 +1650,10 @@ function __init__() mode = get(ENV, "JULIA_REVISE", "auto") if mode == "auto" - pushfirst!(REPL.repl_ast_transforms, revise_first) + pushfirst!(REPL.repl_ast_transforms, revise_first_scan_last) # #664: once a REPL is started, it no longer interacts with REPL.repl_ast_transforms if active_repl_backend_available() - push!(Base.active_repl_backend.ast_transforms, revise_first) + push!(Base.active_repl_backend.ast_transforms, revise_first_scan_last) else # wait for active_repl_backend to exist # #719: do this async in case Revise is being loaded from startup.jl @@ -1647,7 +1664,7 @@ function __init__() iter += 1 end if active_repl_backend_available() - push!(Base.active_repl_backend.ast_transforms, revise_first) + push!(Base.active_repl_backend.ast_transforms, revise_first_scan_last) end end isdefined(Base, :errormonitor) && Base.errormonitor(t) @@ -1665,11 +1682,22 @@ function __init__() # This feature needs to be disabled on Apple Silicon for Julia v1.12 and earlier # due to the Julia runtime side issue (https://github.com/JuliaLang/julia/issues/60721) @static if !(VERSION < v"1.13-" && Sys.isapple()) - if __bpart__[] && (isnothing(distributed_module) || distributed_module.myid() == 1) - Threads.@spawn :default foreach_subtype(Any) do @nospecialize type - # Populating this cache can be time consuming (eg, 30s on an - # i7-7700HQ) so do this incrementally and yield() to the scheduler - # regularly so this thread gets a chance to exit if the user quits early + if (isnothing(distributed_module) || distributed_module.myid() == 1) + Threads.@spawn :default repopulate_typecache() + end + end + return nothing +end + +const _repopulating = ReentrantLock() +function repopulate_typecache() + if __bpart__[] + @lock _repopulating begin + foreach_subtype(Any) do @nospecialize type + # Populating this cache can may take a few seconds on large + # codebases, so do this incrementally and yield() to the + # scheduler regularly so this thread gets a chance to exit if + # the user quits early yield() fieldtypes_cached(type) end diff --git a/src/precompile.jl b/src/precompile.jl index 507f71a1..27120ecc 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -37,7 +37,7 @@ function _precompile_() @warnpcfail precompile(Tuple{typeof(watch_package_callback), PkgId}) @warnpcfail precompile(Tuple{typeof(revise)}) - @warnpcfail precompile(Tuple{typeof(revise_first), Expr}) + @warnpcfail precompile(Tuple{typeof(revise_first_scan_last), Expr}) @warnpcfail precompile(Tuple{typeof(includet), String}) @warnpcfail precompile(Tuple{typeof(track), Module, String}) # setindex! doesn't fully precompile, but it's still beneficial to do it diff --git a/test/runtests.jl b/test/runtests.jl index 183fe07e..ad4a01a2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -182,9 +182,18 @@ end do_test("REPL input") && @testset "REPL input" begin # issue #573 - retex = Revise.revise_first(nothing) + retex = Revise.revise_first_scan_last(nothing) @test retex.head === :toplevel - @test length(retex.args) == 2 && retex.args[end] === nothing + @test length(retex.args) == 2 + retarg = retex.args[end] + if Meta.isexpr(retarg, :block) + retarg = retarg.args[end] + end + @test isexpr(retarg, :let) + ex1 = retarg.args[1] + @test Meta.isexpr(ex1, :(=)) + @test ex1.args[2] === nothing + @test retarg.args[2].args[end] == ex1.args[1] end do_test("Signature extraction") && @testset "Signature extraction" begin @@ -2561,7 +2570,11 @@ end end end - Revise.__bpart__[] && do_test("visit") && @testset "visit" include("test_visit.jl") + Revise.__bpart__[] && do_test("visit") && @testset "visit" begin + @lock Revise._repopulating begin + include("test_visit.jl") + end + end if Revise.__bpart__[] && do_test("struct revision (simple)") # can we revise types and constants? @testset "struct revision (simple)" begin diff --git a/test/start_late.jl b/test/start_late.jl index efca4c53..9d1a53ee 100644 --- a/test/start_late.jl +++ b/test/start_late.jl @@ -13,6 +13,6 @@ while !isdefined(Base, :active_repl_backend) || isnothing(Base.active_repl_backe end using Revise -@test Revise.revise_first ∈ Base.active_repl_backend.ast_transforms +@test Revise.revise_first_scan_last ∈ Base.active_repl_backend.ast_transforms exit()