Skip to content

Commit 667ab89

Browse files
authored
Track chosen cachefile and load the module path (#37421)
These changes allow Revise to leverage precompiled Base machinery, dropping about 150ms from Revise's extra latency penalty for the first package load.
1 parent 72854dc commit 667ab89

File tree

3 files changed

+116
-14
lines changed

3 files changed

+116
-14
lines changed

base/loading.jl

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -824,9 +824,19 @@ function require(into::Module, mod::Symbol)
824824
return require(uuidkey, cache)
825825
end
826826

827+
struct PkgOrigin
828+
# version::VersionNumber
829+
# path::String
830+
cachepath::Union{String,Nothing}
831+
end
832+
const pkgorigins = Dict{PkgId,PkgOrigin}()
833+
827834
function require(uuidkey::PkgId, cache::TOMLCache=TOMLCache())
828835
if !root_module_exists(uuidkey)
829-
_require(uuidkey, cache)
836+
cachefile = _require(uuidkey, cache)
837+
if cachefile !== nothing
838+
pkgorigins[uuidkey] = PkgOrigin(cachefile)
839+
end
830840
# After successfully loading, notify downstream consumers
831841
for callback in package_callbacks
832842
invokelatest(callback, uuidkey)
@@ -881,6 +891,7 @@ function unreference_module(key::PkgId)
881891
end
882892
end
883893

894+
# Returns `nothing` or the name of the newly-created cachefile
884895
function _require(pkg::PkgId, cache::TOMLCache)
885896
# handle recursive calls to require
886897
loading = get(package_locks, pkg, false)
@@ -942,7 +953,7 @@ function _require(pkg::PkgId, cache::TOMLCache)
942953
if isa(m, Exception)
943954
@warn "The call to compilecache failed to create a usable precompiled cache file for $pkg" exception=m
944955
else
945-
return
956+
return cachefile
946957
end
947958
end
948959
end
@@ -1257,6 +1268,13 @@ module_build_id(m::Module) = ccall(:jl_module_build_id, UInt64, (Any,), m)
12571268
isvalid_cache_header(f::IOStream) = (0 != ccall(:jl_read_verify_header, Cint, (Ptr{Cvoid},), f.ios))
12581269
isvalid_file_crc(f::IOStream) = (_crc32c(seekstart(f), filesize(f) - 4) == read(f, UInt32))
12591270

1271+
struct CacheHeaderIncludes
1272+
id::PkgId
1273+
filename::String
1274+
mtime::Float64
1275+
modpath::Vector{String} # seemingly not needed in Base, but used by Revise
1276+
end
1277+
12601278
function parse_cache_header(f::IO)
12611279
modules = Vector{Pair{PkgId, UInt64}}()
12621280
while true
@@ -1270,7 +1288,7 @@ function parse_cache_header(f::IO)
12701288
totbytes = read(f, Int64) # total bytes for file dependencies
12711289
# read the list of requirements
12721290
# and split the list into include and requires statements
1273-
includes = Tuple{PkgId, String, Float64}[]
1291+
includes = CacheHeaderIncludes[]
12741292
requires = Pair{PkgId, PkgId}[]
12751293
while true
12761294
n2 = read(f, Int32)
@@ -1280,20 +1298,21 @@ function parse_cache_header(f::IO)
12801298
n1 = read(f, Int32)
12811299
# map ids to keys
12821300
modkey = (n1 == 0) ? PkgId("") : modules[n1].first
1301+
modpath = String[]
12831302
if n1 != 0
1284-
# consume (and ignore) the module path too
1303+
# determine the complete module path
12851304
while true
12861305
n1 = read(f, Int32)
12871306
totbytes -= 4
12881307
n1 == 0 && break
1289-
skip(f, n1) # String(read(f, n1))
1308+
push!(modpath, String(read(f, n1)))
12901309
totbytes -= n1
12911310
end
12921311
end
12931312
if depname[1] == '\0'
12941313
push!(requires, modkey => binunpack(depname))
12951314
else
1296-
push!(includes, (modkey, depname, mtime))
1315+
push!(includes, CacheHeaderIncludes(modkey, depname, mtime, modpath))
12971316
end
12981317
totbytes -= 4 + 4 + n2 + 8
12991318
end
@@ -1312,19 +1331,28 @@ function parse_cache_header(f::IO)
13121331
return modules, (includes, requires), required_modules, srctextpos
13131332
end
13141333

1315-
function parse_cache_header(cachefile::String)
1334+
function parse_cache_header(cachefile::String; srcfiles_only::Bool=false)
13161335
io = open(cachefile, "r")
13171336
try
13181337
!isvalid_cache_header(io) && throw(ArgumentError("Invalid header in cache file $cachefile."))
1319-
return parse_cache_header(io)
1338+
ret = parse_cache_header(io)
1339+
srcfiles_only || return ret
1340+
modules, (includes, requires), required_modules, srctextpos = ret
1341+
srcfiles = srctext_files(io, srctextpos)
1342+
delidx = Int[]
1343+
for (i, chi) in enumerate(includes)
1344+
chi.filename srcfiles || push!(delidx, i)
1345+
end
1346+
deleteat!(includes, delidx)
1347+
return modules, (includes, requires), required_modules, srctextpos
13201348
finally
13211349
close(io)
13221350
end
13231351
end
13241352

13251353
function cache_dependencies(f::IO)
13261354
defs, (includes, requires), modules = parse_cache_header(f)
1327-
return modules, map(mod_fl_mt -> (mod_fl_mt[2], mod_fl_mt[3]), includes) # discard the module
1355+
return modules, map(chi -> (chi.filename, chi.mtime), includes) # return just filename and mtime
13281356
end
13291357

13301358
function cache_dependencies(cachefile::String)
@@ -1368,6 +1396,21 @@ function read_dependency_src(cachefile::String, filename::AbstractString)
13681396
end
13691397
end
13701398

1399+
function srctext_files(f::IO, srctextpos::Int64)
1400+
files = Set{String}()
1401+
srctextpos == 0 && return files
1402+
seek(f, srctextpos)
1403+
while !eof(f)
1404+
filenamelen = read(f, Int32)
1405+
filenamelen == 0 && break
1406+
fn = String(read(f, filenamelen))
1407+
len = read(f, UInt64)
1408+
push!(files, fn)
1409+
seek(f, position(f) + len)
1410+
end
1411+
return files
1412+
end
1413+
13711414
# returns true if it "cachefile.ji" is stale relative to "modpath.jl"
13721415
# otherwise returns the list of dependencies to also check
13731416
stale_cachefile(modpath::String, cachefile::String) = stale_cachefile(modpath, cachefile, TOMLCache())
@@ -1379,6 +1422,7 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
13791422
return true # invalid cache file
13801423
end
13811424
(modules, (includes, requires), required_modules) = parse_cache_header(io)
1425+
id = isempty(modules) ? nothing : first(modules).first
13821426
modules = Dict{PkgId, UInt64}(modules)
13831427

13841428
# Check if transitive dependencies can be fulfilled
@@ -1423,8 +1467,8 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
14231467

14241468
# now check if this file is fresh relative to its source files
14251469
if !skip_timecheck
1426-
if !samefile(includes[1][2], modpath)
1427-
@debug "Rejecting cache file $cachefile because it is for file $(includes[1][2])) not file $modpath"
1470+
if !samefile(includes[1].filename, modpath)
1471+
@debug "Rejecting cache file $cachefile because it is for file $(includes[1].filename)) not file $modpath"
14281472
return true # cache file was compiled from a different path
14291473
end
14301474
for (modkey, req_modkey) in requires
@@ -1434,7 +1478,8 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
14341478
return true
14351479
end
14361480
end
1437-
for (_, f, ftime_req) in includes
1481+
for chi in includes
1482+
f, ftime_req = chi.filename, chi.mtime
14381483
# Issue #13606: compensate for Docker images rounding mtimes
14391484
# Issue #20837: compensate for GlusterFS truncating mtimes to microseconds
14401485
ftime = mtime(f)
@@ -1450,6 +1495,10 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
14501495
return true
14511496
end
14521497

1498+
if isa(id, PkgId)
1499+
pkgorigins[id] = PkgOrigin(cachefile)
1500+
end
1501+
14531502
return depmods # fresh cachefile
14541503
finally
14551504
close(io)

contrib/generate_precompile.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ hardcoded_precompile_statements = """
1919
@assert precompile(Tuple{typeof(push!), Set{Module}, Module})
2020
@assert precompile(Tuple{typeof(push!), Set{Method}, Method})
2121
@assert precompile(Tuple{typeof(push!), Set{Base.PkgId}, Base.PkgId})
22+
@assert precompile(Tuple{typeof(getindex), Dict{Base.PkgId,String}, Base.PkgId})
2223
@assert precompile(Tuple{typeof(setindex!), Dict{String,Base.PkgId}, Base.PkgId, String})
2324
@assert precompile(Tuple{typeof(get!), Type{Vector{Function}}, Dict{Base.PkgId,Vector{Function}}, Base.PkgId})
2425
@assert precompile(Tuple{typeof(isassigned), Core.SimpleVector, Int})
2526
@assert precompile(Tuple{typeof(pushfirst!), Vector{Any}, Function})
27+
@assert precompile(Tuple{typeof(Base.parse_cache_header), String})
2628
"""
2729

2830
precompile_script = """

test/precompile.jl

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,9 +264,9 @@ try
264264
@test string(Base.Docs.doc(Foo.Bar.bar)) == "bar function\n"
265265

266266
modules, (deps, requires), required_modules = Base.parse_cache_header(cachefile)
267-
discard_module = mod_fl_mt -> (mod_fl_mt[2], mod_fl_mt[3])
267+
discard_module = mod_fl_mt -> (mod_fl_mt.filename, mod_fl_mt.mtime)
268268
@test modules == [ Base.PkgId(Foo) => Base.module_build_id(Foo) ]
269-
@test map(x -> x[2], deps) == [ Foo_file, joinpath(dir, "foo.jl"), joinpath(dir, "bar.jl") ]
269+
@test map(x -> x.filename, deps) == [ Foo_file, joinpath(dir, "foo.jl"), joinpath(dir, "bar.jl") ]
270270
@test requires == [ Base.PkgId(Foo) => Base.PkgId(string(FooBase_module)),
271271
Base.PkgId(Foo) => Base.PkgId(Foo2),
272272
Base.PkgId(Foo) => Base.PkgId(Test),
@@ -299,6 +299,8 @@ try
299299
end
300300
)
301301
@test discard_module.(deps) == deps1
302+
modules, (deps, requires), required_modules = Base.parse_cache_header(cachefile; srcfiles_only=true)
303+
@test map(x -> x.filename, deps) == [Foo_file]
302304

303305
@test current_task()(0x01, 0x4000, 0x30031234) == 2
304306
@test sin(0x01, 0x4000, 0x30031234) == 52
@@ -339,6 +341,55 @@ try
339341
@test pointer_from_objref(PV) === pointer_from_objref(ft(ft(PV)[1].parameters[1])[1])
340342
end
341343

344+
Nest_module = :Nest4b3a94a1a081a8cb
345+
Nest_file = joinpath(dir, "$Nest_module.jl")
346+
NestInner_file = joinpath(dir, "$(Nest_module)Inner.jl")
347+
NestInner2_file = joinpath(dir, "$(Nest_module)Inner2.jl")
348+
write(Nest_file,
349+
"""
350+
module $Nest_module
351+
include("$(escape_string(NestInner_file))")
352+
end
353+
""")
354+
write(NestInner_file,
355+
"""
356+
module NestInner
357+
include("$(escape_string(NestInner2_file))")
358+
end
359+
""")
360+
write(NestInner2_file,
361+
"""
362+
f() = 22
363+
""")
364+
Nest = Base.require(Main, Nest_module)
365+
cachefile = joinpath(cachedir, "$Nest_module.ji")
366+
modules, (deps, requires), required_modules = Base.parse_cache_header(cachefile)
367+
@test last(deps).modpath == ["NestInner"]
368+
369+
UsesB_module = :UsesB4b3a94a1a081a8cb
370+
B_module = :UsesB4b3a94a1a081a8cb_B
371+
UsesB_file = joinpath(dir, "$UsesB_module.jl")
372+
B_file = joinpath(dir, "$(B_module).jl")
373+
write(UsesB_file,
374+
"""
375+
module $UsesB_module
376+
using $B_module
377+
end
378+
""")
379+
write(B_file,
380+
"""
381+
module $B_module
382+
export bfunc
383+
bfunc() = 33
384+
end
385+
""")
386+
UsesB = Base.require(Main, UsesB_module)
387+
cachefile = joinpath(cachedir, "$UsesB_module.ji")
388+
modules, (deps, requires), required_modules = Base.parse_cache_header(cachefile)
389+
id1, id2 = only(requires)
390+
@test Base.pkgorigins[id1].cachepath == cachefile
391+
@test Base.pkgorigins[id2].cachepath == joinpath(cachedir, "$B_module.ji")
392+
342393
Baz_file = joinpath(dir, "Baz.jl")
343394
write(Baz_file,
344395
"""

0 commit comments

Comments
 (0)