Skip to content

Commit 69f8bce

Browse files
Add macro for timing package and dependency imports (#41612)
1 parent e5ce472 commit 69f8bce

File tree

6 files changed

+97
-5
lines changed

6 files changed

+97
-5
lines changed

NEWS.md

+3
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ Standard library changes
4545
arithmetic to error if the result may be wrapping. Or use a package such as SaferIntegers.jl when
4646
constructing the range. ([#40382])
4747

48+
#### InteractiveUtils
49+
* A new macro `@time_imports` for reporting any time spent importing packages and their dependencies ([#41612])
50+
4851
#### Package Manager
4952

5053
#### LinearAlgebra

base/loading.jl

+15-4
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,7 @@ function _include_from_serialized(path::String, depmods::Vector{Any})
769769
return restored
770770
end
771771

772-
function _tryrequire_from_serialized(modkey::PkgId, build_id::UInt64, modpath::Union{Nothing, String})
772+
function _tryrequire_from_serialized(modkey::PkgId, build_id::UInt64, modpath::Union{Nothing, String}, depth::Int = 0)
773773
if root_module_exists(modkey)
774774
M = root_module(modkey)
775775
if PkgId(M) == modkey && module_build_id(M) === build_id
@@ -780,7 +780,7 @@ function _tryrequire_from_serialized(modkey::PkgId, build_id::UInt64, modpath::U
780780
modpath = locate_package(modkey)
781781
modpath === nothing && return nothing
782782
end
783-
mod = _require_search_from_serialized(modkey, String(modpath))
783+
mod = _require_search_from_serialized(modkey, String(modpath), depth)
784784
get!(PkgOrigin, pkgorigins, modkey).path = modpath
785785
if !isa(mod, Bool)
786786
for callback in package_callbacks
@@ -821,10 +821,14 @@ function _require_from_serialized(path::String)
821821
return _include_from_serialized(path, depmods)
822822
end
823823

824+
# use an Int counter so that nested @time_imports calls all remain open
825+
const TIMING_IMPORTS = Threads.Atomic{Int}(0)
826+
824827
# returns `true` if require found a precompile cache for this sourcepath, but couldn't load it
825828
# returns `false` if the module isn't known to be precompilable
826829
# returns the set of modules restored if the cache load succeeded
827-
function _require_search_from_serialized(pkg::PkgId, sourcepath::String)
830+
function _require_search_from_serialized(pkg::PkgId, sourcepath::String, depth::Int = 0)
831+
t_before = time_ns()
828832
paths = find_all_in_cache_path(pkg)
829833
for path_to_try in paths::Vector{String}
830834
staledeps = stale_cachefile(sourcepath, path_to_try)
@@ -840,7 +844,7 @@ function _require_search_from_serialized(pkg::PkgId, sourcepath::String)
840844
dep = staledeps[i]
841845
dep isa Module && continue
842846
modpath, modkey, build_id = dep::Tuple{String, PkgId, UInt64}
843-
dep = _tryrequire_from_serialized(modkey, build_id, modpath)
847+
dep = _tryrequire_from_serialized(modkey, build_id, modpath, depth + 1)
844848
if dep === nothing
845849
@debug "Required dependency $modkey failed to load from cache file for $modpath."
846850
staledeps = true
@@ -855,6 +859,13 @@ function _require_search_from_serialized(pkg::PkgId, sourcepath::String)
855859
if isa(restored, Exception)
856860
@debug "Deserialization checks failed while attempting to load cache from $path_to_try" exception=restored
857861
else
862+
if TIMING_IMPORTS[] > 0
863+
elapsed = round((time_ns() - t_before) / 1e6, digits = 1)
864+
tree_prefix = depth == 0 ? "" : "$(" "^(depth-1))"
865+
print("$(lpad(elapsed, 9)) ms ")
866+
printstyled(tree_prefix, color = :light_black)
867+
println(pkg.name)
868+
end
858869
return restored
859870
end
860871
end

stdlib/InteractiveUtils/docs/src/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,6 @@ InteractiveUtils.code_llvm
2626
InteractiveUtils.@code_llvm
2727
InteractiveUtils.code_native
2828
InteractiveUtils.@code_native
29+
InteractiveUtils.@time_imports
2930
InteractiveUtils.clipboard
3031
```

stdlib/InteractiveUtils/src/InteractiveUtils.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Base.Experimental.@optlevel 1
66

77
export apropos, edit, less, code_warntype, code_llvm, code_native, methodswith, varinfo,
88
versioninfo, subtypes, supertypes, @which, @edit, @less, @functionloc, @code_warntype,
9-
@code_typed, @code_lowered, @code_llvm, @code_native, clipboard
9+
@code_typed, @code_lowered, @code_llvm, @code_native, @time_imports, clipboard
1010

1111
import Base.Docs.apropos
1212

stdlib/InteractiveUtils/src/macros.jl

+44
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,17 @@ macro code_lowered(ex0...)
232232
end
233233
end
234234

235+
macro time_imports(ex)
236+
quote
237+
try
238+
Base.TIMING_IMPORTS[] += 1
239+
$(esc(ex))
240+
finally
241+
Base.TIMING_IMPORTS[] -= 1
242+
end
243+
end
244+
end
245+
235246
"""
236247
@functionloc
237248
@@ -332,3 +343,36 @@ Set the optional keyword argument `debuginfo` by putting it before the function
332343
`debuginfo` may be one of `:source` (default) or `:none`, to specify the verbosity of code comments.
333344
"""
334345
:@code_native
346+
347+
"""
348+
@time_imports
349+
350+
A macro to execute an expression and produce a report of any time spent importing packages and their
351+
dependencies.
352+
353+
If a package's dependencies have already been imported either globally or by another dependency they will
354+
not appear under that package and the package will accurately report a faster load time than if it were to
355+
be loaded in isolation.
356+
357+
```julia-repl
358+
julia> @time_imports using CSV
359+
3.5 ms ┌ IteratorInterfaceExtensions
360+
27.4 ms ┌ TableTraits
361+
614.0 ms ┌ SentinelArrays
362+
138.6 ms ┌ Parsers
363+
2.7 ms ┌ DataValueInterfaces
364+
3.4 ms ┌ DataAPI
365+
59.0 ms ┌ WeakRefStrings
366+
35.4 ms ┌ Tables
367+
49.5 ms ┌ PooledArrays
368+
972.1 ms CSV
369+
```
370+
371+
!!! note
372+
During the load process a package sequentially imports where necessary all of its dependencies, not just
373+
its direct dependencies. That is also true for the dependencies themselves so nested importing will likely
374+
occur, but not always. Therefore the nesting shown in this output report is not equivalent to the dependency
375+
tree, but does indicate where import time has accumulated.
376+
377+
"""
378+
:@time_imports

stdlib/InteractiveUtils/test/runtests.jl

+33
Original file line numberDiff line numberDiff line change
@@ -586,3 +586,36 @@ let
586586
opt = false
587587
@test !(first(@code_typed optimize=opt sum(1:10)).inferred)
588588
end
589+
590+
@testset "@time_imports" begin
591+
mktempdir() do dir
592+
cd(dir) do
593+
try
594+
pushfirst!(LOAD_PATH, dir)
595+
foo_file = joinpath(dir, "Foo3242.jl")
596+
write(foo_file,
597+
"""
598+
module Foo3242
599+
foo() = 1
600+
end
601+
""")
602+
603+
Base.compilecache(Base.PkgId("Foo3242"))
604+
605+
fname = tempname()
606+
f = open(fname, "w")
607+
redirect_stdout(f) do
608+
@eval @time_imports using Foo3242
609+
end
610+
close(f)
611+
buf = read(fname)
612+
rm(fname)
613+
614+
@test occursin("ms Foo3242\n", String(buf))
615+
616+
finally
617+
filter!(()(dir), LOAD_PATH)
618+
end
619+
end
620+
end
621+
end

0 commit comments

Comments
 (0)