diff --git a/.gitignore b/.gitignore index 3f02ca74..327cf35c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.jl.*.cov *.jl.mem Manifest.toml +Manifest-*.toml diff --git a/Project.toml b/Project.toml index 4c63a86b..35f0f0bd 100644 --- a/Project.toml +++ b/Project.toml @@ -8,7 +8,6 @@ AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" Cthulhu = "f68482b8-f384-11e8-15f7-abe071a5a75f" FlameGraphs = "08572546-2f56-4bcf-ba4e-bab62c3a3f89" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" @@ -18,11 +17,16 @@ Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" SnoopCompileCore = "e2b509da-e806-4183-be48-004708413034" YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" +[weakdeps] +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" + +[extensions] +JETExt = "JET" + [compat] AbstractTrees = "0.3, 0.4" Cthulhu = "1.5, 2" FlameGraphs = "0.2, 1" -JET = "0.0, 0.4, 0.5, 0.6, 0.7, 0.8" OrderedCollections = "1" Requires = "1" SnoopCompileCore = "~2.10.0" @@ -33,6 +37,7 @@ julia = "1" ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" MethodAnalysis = "85b6ec6f-f7df-4429-9514-a64bcd9ee824" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" @@ -40,4 +45,4 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["ColorTypes", "PrettyTables", "Documenter", "FixedPointNumbers", "MethodAnalysis", "Pkg", "Random", "Test"] +test = ["ColorTypes", "PrettyTables", "Documenter", "FixedPointNumbers", "JET", "MethodAnalysis", "Pkg", "Random", "Test"] diff --git a/docs/src/jet.md b/docs/src/jet.md index c065dfc1..ba0a09c7 100644 --- a/docs/src/jet.md +++ b/docs/src/jet.md @@ -90,15 +90,16 @@ The key reason is that SnoopCompile is a dynamic analyzer, and is capable of bri As always, you need to do the data collection in a fresh session where the calls have not previously been inferred. After restarting Julia, we can do this: -``` +```julia julia> using SnoopCompile +julia> using JET # this is necessary to enable the integration + julia> list = Any[1,2,3]; julia> lc = Any[list]; # "hide" `list` inside a Vector{Any} -julia> callsum(listcontainer) = sum(listcontainer[1]) -callsum (generic function with 1 method) +julia> callsum(listcontainer) = sum(listcontainer[1]); julia> tinf = @snoopi_deep callsum(lc) InferenceTimingNode: 0.039239/0.046793 on Core.Compiler.Timings.ROOT() with 2 direct children @@ -109,28 +110,32 @@ julia> tinf.children InferenceTimingNode: 0.000196/0.006685 on sum(::Vector{Any}) with 1 direct children julia> report_callees(inference_triggers(tinf)) -1-element Vector{Pair{InferenceTrigger, JET.JETCallResult{JET.JETAnalyzer{JET.BasicPass{typeof(JET.basic_function_filter)}}, Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}}}}: +1-element Vector{Pair{InferenceTrigger, JET.JETCallResult{JET.JETAnalyzer{JET.BasicPass}, Base.Pairs{Symbol, Union{}, Tuple{}, @NamedTuple{}}}}}: Inference triggered to call sum(::Vector{Any}) from callsum (./REPL[5]:1) with specialization callsum(::Vector{Any}) => ═════ 1 possible error found ═════ -┌ @ reducedim.jl:889 Base.#sum#732(Base.:, Base.pairs(Core.NamedTuple()), #self#, a) -│┌ @ reducedim.jl:889 Base._sum(a, dims) -││┌ @ reducedim.jl:893 Base.#_sum#734(Base.pairs(Core.NamedTuple()), #self#, a, _3) -│││┌ @ reducedim.jl:893 Base._sum(Base.identity, a, Base.:) -││││┌ @ reducedim.jl:894 Base.#_sum#735(Base.pairs(Core.NamedTuple()), #self#, f, a, _4) -│││││┌ @ reducedim.jl:894 Base.mapreduce(f, Base.add_sum, a) -││││││┌ @ reducedim.jl:322 Base.#mapreduce#725(Base.:, Base._InitialValue(), #self#, f, op, A) -│││││││┌ @ reducedim.jl:322 Base._mapreduce_dim(f, op, init, A, dims) -││││││││┌ @ reducedim.jl:330 Base._mapreduce(f, op, Base.IndexStyle(A), A) -│││││││││┌ @ reduce.jl:402 Base.mapreduce_empty_iter(f, op, A, Base.IteratorEltype(A)) -││││││││││┌ @ reduce.jl:353 Base.reduce_empty_iter(Base.MappingRF(f, op), itr, ItrEltype) -│││││││││││┌ @ reduce.jl:357 Base.reduce_empty(op, Base.eltype(itr)) -││││││││││││┌ @ reduce.jl:331 Base.mapreduce_empty(Base.getproperty(op, :f), Base.getproperty(op, :rf), _) -│││││││││││││┌ @ reduce.jl:345 Base.reduce_empty(op, T) -││││││││││││││┌ @ reduce.jl:322 Base.reduce_empty(Base.+, _) -│││││││││││││││┌ @ reduce.jl:313 Base.zero(_) -││││││││││││││││┌ @ missing.jl:106 Base.throw(Base.MethodError(Base.zero, Core.tuple(Base.Any))) -│││││││││││││││││ MethodError: no method matching zero(::Type{Any}) -││││││││││││││││└────────────────── +┌ sum(a::Vector{Any}) @ Base ./reducedim.jl:1010 +│┌ sum(a::Vector{Any}; dims::Colon, kw::@Kwargs{}) @ Base ./reducedim.jl:1010 +││┌ _sum(a::Vector{Any}, ::Colon) @ Base ./reducedim.jl:1014 +│││┌ _sum(a::Vector{Any}, ::Colon; kw::@Kwargs{}) @ Base ./reducedim.jl:1014 +││││┌ _sum(f::typeof(identity), a::Vector{Any}, ::Colon) @ Base ./reducedim.jl:1015 +│││││┌ _sum(f::typeof(identity), a::Vector{Any}, ::Colon; kw::@Kwargs{}) @ Base ./reducedim.jl:1015 +││││││┌ mapreduce(f::typeof(identity), op::typeof(Base.add_sum), A::Vector{Any}) @ Base ./reducedim.jl:357 +│││││││┌ mapreduce(f::typeof(identity), op::typeof(Base.add_sum), A::Vector{Any}; dims::Colon, init::Base._InitialValue) @ Base ./reducedim.jl:357 +││││││││┌ _mapreduce_dim(f::typeof(identity), op::typeof(Base.add_sum), ::Base._InitialValue, A::Vector{Any}, ::Colon) @ Base ./reducedim.jl:365 +│││││││││┌ _mapreduce(f::typeof(identity), op::typeof(Base.add_sum), ::IndexLinear, A::Vector{Any}) @ Base ./reduce.jl:432 +││││││││││┌ mapreduce_empty_iter(f::typeof(identity), op::typeof(Base.add_sum), itr::Vector{Any}, ItrEltype::Base.HasEltype) @ Base ./reduce.jl:380 +│││││││││││┌ reduce_empty_iter(op::Base.MappingRF{typeof(identity), typeof(Base.add_sum)}, itr::Vector{Any}, ::Base.HasEltype) @ Base ./reduce.jl:384 +││││││││││││┌ reduce_empty(op::Base.MappingRF{typeof(identity), typeof(Base.add_sum)}, ::Type{Any}) @ Base ./reduce.jl:361 +│││││││││││││┌ mapreduce_empty(::typeof(identity), op::typeof(Base.add_sum), T::Type{Any}) @ Base ./reduce.jl:372 +││││││││││││││┌ reduce_empty(::typeof(Base.add_sum), ::Type{Any}) @ Base ./reduce.jl:352 +│││││││││││││││┌ reduce_empty(::typeof(+), ::Type{Any}) @ Base ./reduce.jl:343 +││││││││││││││││┌ zero(::Type{Any}) @ Base ./missing.jl:106 +│││││││││││││││││ MethodError: no method matching zero(::Type{Any}): Base.throw(Base.MethodError(zero, tuple(Base.Any)::Tuple{DataType})::MethodError) +││││││││││││││││└──────────────────── ``` Because SnoopCompile collected the runtime-dispatched `sum` call, we can pass it to JET. `report_callees` filters those calls which generate JET reports, allowing you to focus on potential errors. + +!!! note + JET integration is enabled only if JET.jl has been loaded into your main session. + This is why there's the `using JET` statement included in the example given. diff --git a/ext/JETExt.jl b/ext/JETExt.jl new file mode 100644 index 00000000..09ccd2d1 --- /dev/null +++ b/ext/JETExt.jl @@ -0,0 +1,86 @@ +module JETExt + +@static if isdefined(Base, :get_extension) + import SnoopCompile: report_callee, report_caller, report_callees + using SnoopCompile: SnoopCompile, InferenceTrigger, callerinstance + using SnoopCompile.Cthulhu: specTypes + using JET: report_call, get_reports +else + import ..SnoopCompile: report_callee, report_caller, report_callees + using ..SnoopCompile: SnoopCompile, InferenceTrigger, callerinstance + using ..SnoopCompile.Cthulhu: specTypes + using ..JET: report_call, get_reports +end + +""" + report_callee(itrig::InferenceTrigger) + +Return the `JET.report_call` for the callee in `itrig`. +""" +SnoopCompile.report_callee(itrig::InferenceTrigger; jetconfigs...) = report_call(specTypes(itrig); jetconfigs...) + +""" + report_caller(itrig::InferenceTrigger) + +Return the `JET.report_call` for the caller in `itrig`. +""" +SnoopCompile.report_caller(itrig::InferenceTrigger; jetconfigs...) = report_call(specTypes(callerinstance(itrig)); jetconfigs...) + +""" + report_callees(itrigs) + +Filter `itrigs` for those with a non-passing `JET` report, returning the list of `itrig => report` pairs. + +# Examples + +```jldoctest jetfib; setup=(using SnoopCompile, JET), filter=[r"\\d direct children", r"[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?/[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?"] +julia> fib(n::Integer) = n ≤ 2 ? n : fib(n-1) + fib(n-2); + +julia> function fib(str::String) + n = length(str) + return fib(m) # error is here + end +fib (generic function with 2 methods) + +julia> fib(::Dict) = 0; fib(::Vector) = 0; + +julia> list = [5, "hello"]; + +julia> mapfib(list) = map(fib, list) +mapfib (generic function with 1 method) + +julia> tinf = @snoopi_deep try mapfib(list) catch end +InferenceTimingNode: 0.049825/0.071476 on Core.Compiler.Timings.ROOT() with 5 direct children + +julia> @report_call mapfib(list) +No errors detected +``` + +JET did not catch the error because the call to `fib` is hidden behind runtime dispatch. +However, when captured by `@snoopi_deep`, we get + +```jldoctest jetfib; filter=[r"@ .*", r"REPL\\[\\d+\\]|none"] +julia> report_callees(inference_triggers(tinf)) +1-element Vector{Pair{InferenceTrigger, JET.JETCallResult{JET.JETAnalyzer{JET.BasicPass{typeof(JET.basic_function_filter)}}, Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}}}}: + Inference triggered to call fib(::String) from iterate (./generator.jl:47) inlined into Base.collect_to!(::Vector{Int64}, ::Base.Generator{Vector{Any}, typeof(fib)}, ::Int64, ::Int64) (./array.jl:782) => ═════ 1 possible error found ═════ +┌ @ none:3 fib(m) +│ variable `m` is not defined +└────────── +``` +""" +function SnoopCompile.report_callees(itrigs; jetconfigs...) + function rr(itrig) + rpt = try + report_callee(itrig; jetconfigs...) + catch err + @warn "skipping $itrig due to report_callee error" exception=err + nothing + end + return itrig => rpt + end + hasreport((itrig, report)) = report !== nothing && !isempty(get_reports(report)) + + return [itrigrpt for itrigrpt in map(rr, itrigs) if hasreport(itrigrpt)] +end + +end # module JETExt diff --git a/src/SnoopCompile.jl b/src/SnoopCompile.jl index ed83b6b7..32011555 100644 --- a/src/SnoopCompile.jl +++ b/src/SnoopCompile.jl @@ -128,6 +128,9 @@ function __init__() if isdefined(SnoopCompileCore, Symbol("@snoopr")) @require PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" include("report_invalidations.jl") end + if isdefined(SnoopCompile, :report_callee) && !isdefined(Base, :get_extension) + @require JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" include("../ext/JETExt.jl") + end return nothing end diff --git a/src/parcel_snoopi_deep.jl b/src/parcel_snoopi_deep.jl index 20a9c2c0..3ca3f881 100644 --- a/src/parcel_snoopi_deep.jl +++ b/src/parcel_snoopi_deep.jl @@ -7,7 +7,6 @@ using Core.Compiler.Timings: InferenceFrameInfo using SnoopCompileCore: InferenceTiming, InferenceTimingNode, inclusive, exclusive using Profile using Cthulhu -using JET const InferenceNode = Union{InferenceFrameInfo,InferenceTiming,InferenceTimingNode} @@ -897,76 +896,13 @@ Cthulhu.specTypes(itrig::InferenceTrigger) = Cthulhu.specTypes(Cthulhu.instance( Cthulhu.backedges(itrig::InferenceTrigger) = (itrig.callerframes,) Cthulhu.nextnode(itrig::InferenceTrigger, edge) = (ret = callingframe(itrig); return isempty(ret.callerframes) ? nothing : ret) -""" - report_callee(itrig::InferenceTrigger) - -Return the `JET.report_call` for the callee in `itrig`. -""" -report_callee(itrig::InferenceTrigger; jetconfigs...) = report_call(Cthulhu.specTypes(itrig); jetconfigs...) - -""" - report_caller(itrig::InferenceTrigger) - -Return the `JET.report_call` for the caller in `itrig`. -""" -report_caller(itrig::InferenceTrigger; jetconfigs...) = report_call(Cthulhu.specTypes(callerinstance(itrig)); jetconfigs...) - -""" - report_callees(itrigs) - -Filter `itrigs` for those with a non-passing `JET` report, returning the list of `itrig => report` pairs. - -# Examples - -```jldoctest jetfib; setup=(using SnoopCompile, JET), filter=[r"\\d direct children", r"[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?/[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?"] -julia> fib(n::Integer) = n ≤ 2 ? n : fib(n-1) + fib(n-2); - -julia> function fib(str::String) - n = length(str) - return fib(m) # error is here - end -fib (generic function with 2 methods) - -julia> fib(::Dict) = 0; fib(::Vector) = 0; - -julia> list = [5, "hello"]; - -julia> mapfib(list) = map(fib, list) -mapfib (generic function with 1 method) - -julia> tinf = @snoopi_deep try mapfib(list) catch end -InferenceTimingNode: 0.049825/0.071476 on Core.Compiler.Timings.ROOT() with 5 direct children - -julia> @report_call mapfib(list) -No errors detected -``` - -JET did not catch the error because the call to `fib` is hidden behind runtime dispatch. -However, when captured by `@snoopi_deep`, we get - -```jldoctest jetfib; filter=[r"@ .*", r"REPL\\[\\d+\\]|none"] -julia> report_callees(inference_triggers(tinf)) -1-element Vector{Pair{InferenceTrigger, JET.JETCallResult{JET.JETAnalyzer{JET.BasicPass{typeof(JET.basic_function_filter)}}, Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}}}}: - Inference triggered to call fib(::String) from iterate (./generator.jl:47) inlined into Base.collect_to!(::Vector{Int64}, ::Base.Generator{Vector{Any}, typeof(fib)}, ::Int64, ::Int64) (./array.jl:782) => ═════ 1 possible error found ═════ -┌ @ none:3 fib(m) -│ variable `m` is not defined -└────────── -``` -""" -function report_callees(itrigs; jetconfigs...) - function rr(itrig) - rpt = try - report_callee(itrig; jetconfigs...) - catch err - @warn "skipping $itrig due to report_callee error" exception=err - nothing - end - return itrig => rpt - end - hasreport((itrig, report)) = report !== nothing && !isempty(JET.get_reports(report)) - - return [itrigrpt for itrigrpt in map(rr, itrigs) if hasreport(itrigrpt)] -end +# JET integrations are implemented lazily +"To use `report_caller` do `using JET`" +function report_caller end +"To use `report_callee` do `using JET`" +function report_callee end +"To use `report_callees` do `using JET`" +function report_callees end filtermod(mod::Module, itrigs::AbstractVector{InferenceTrigger}) = filter(==(mod) ∘ callermodule, itrigs) @@ -1581,7 +1517,7 @@ function unwrapconst(@nospecialize(arg)) return arg.val elseif isa(arg, Core.PartialStruct) return arg.typ - elseif isa(arg, Core.Compiler.MaybeUndef) + elseif @static isdefined(Core.Compiler, :MaybeUndef) ? isa(arg, Core.Compiler.MaybeUndef) : false return arg.typ end return arg diff --git a/test/runtests.jl b/test/runtests.jl index d0799eb2..7ff12827 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -51,7 +51,7 @@ data = SnoopCompile.read(logfile) pc = SnoopCompile.parcel(reverse!(data[2])) @test any(startswith.(pc[:IsDef], "isdefined")) -if Base.VERSION < v"1.7.0-DEV.694" +if VERSION < v"1.7.0-DEV.694" @testset "Warning for failures to precompile" begin pcs = ["@warnpcfail precompile(fsimple, (Char,))", "@warnpcfail precompile(fsimple, (Char, Char))", @@ -121,4 +121,3 @@ end =# include("colortypes.jl") - diff --git a/test/snoopi_deep.jl b/test/snoopi_deep.jl index e3d023c9..7984fb01 100644 --- a/test/snoopi_deep.jl +++ b/test/snoopi_deep.jl @@ -336,7 +336,7 @@ end @test occursin("non-inferrable or unspecialized call", String(take!(io))) # UnspecType - if Base.VERSION < v"1.9.0-DEV" # on 1.9 there is no trigger assocated with the partial type call + if VERSION < v"1.9.0-DEV" # on 1.9 there is no trigger assocated with the partial type call M = Module() @eval M begin struct Container{L,T} x::T end @@ -638,7 +638,7 @@ end fg = SnoopCompile.flamegraph(tinf) fgnodes = collect(AbstractTrees.PreOrderDFS(fg)) - for tgtname in (Base.VERSION < v"1.7" ? (:h, :i) : (:h, :i, :+)) + for tgtname in (VERSION < v"1.7" ? (:h, :i) : (:h, :i, :+)) @test mapreduce(|, fgnodes; init=false) do node node.data.sf.linfo.def.name == tgtname end @@ -650,7 +650,7 @@ end @test leaf.data.span.stop in fg.data.span has_constprop |= leaf.data.status & FlameGraphs.gc_event != 0x0 end - Base.VERSION >= v"1.7" && @test has_constprop + VERSION >= v"1.7" && @test has_constprop frame1, frame2 = frames[1], frames[2] t1, t2 = inclusive(frame1), inclusive(frame2) @@ -805,7 +805,7 @@ end Ts = subtypes(Any) tinf_unspec = @snoopi_deep SnoopBench.mappushes(SnoopBench.spell_unspec, Ts) tf_unspec = flatten(tinf_unspec) - if Base.VERSION < v"1.8.0-DEV.1148" + if VERSION < v"1.8.0-DEV.1148" # To ensure independent data, invalidate all compiled CodeInstances mis = map(last, accumulate_by_source(MethodInstance, tf_unspec)) for mi in mis @@ -882,7 +882,7 @@ end tree = length(trees) == 1 ? only(trees) : trees[findfirst(tree -> !isempty(tree.backedges), trees)] @test tree.method == which(StaleA.stale, (String,)) # defined in StaleC @test all(be -> Core.MethodInstance(be).def == which(StaleA.stale, (Any,)), tree.backedges) - if Base.VERSION > v"1.8.0-DEV.368" + if VERSION > v"1.8.0-DEV.368" root = only(filter(tree.backedges) do be Core.MethodInstance(be).specTypes.parameters[end] === String end) @@ -897,7 +897,7 @@ end end @test isempty(SnoopCompile.StaleTree(first(smis).def, :noreason).backedges) # constructor test healed = true - if Base.VERSION < v"1.8" + if VERSION < v"1.8" healed = false # On more recent Julia, the invalidation of StaleA.stale is "healed over" by re-inferrence # within StaleC. Hence we should skip this test. @@ -959,7 +959,10 @@ end Pkg.activate(cproj) end -if Base.VERSION >= v"1.7" +if VERSION >= v"1.7" + using JET +end +@static if VERSION >= v"1.7" @testset "JET integration" begin function mysum(c) # vendor a simple version of `sum` isempty(c) && return zero(eltype(c)) @@ -973,12 +976,12 @@ if Base.VERSION >= v"1.7" cc = Any[Any[1,2,3]] tinf = @snoopi_deep call_mysum(cc) - rpt = SnoopCompile.JET.@report_call call_mysum(cc) - @test isempty(SnoopCompile.JET.get_reports(rpt)) + rpt = @report_call call_mysum(cc) + @test isempty(JET.get_reports(rpt)) itrigs = inference_triggers(tinf) irpts = report_callees(itrigs) @test only(irpts).first == last(itrigs) - @test !isempty(SnoopCompile.JET.get_reports(only(irpts).second)) - @test isempty(SnoopCompile.JET.get_reports(report_caller(itrigs[end]))) + @test !isempty(JET.get_reports(only(irpts).second)) + @test isempty(JET.get_reports(report_caller(itrigs[end]))) end end diff --git a/test/testmodules/Invalidation/Manifest.toml b/test/testmodules/Invalidation/Manifest.toml index 6af973c6..cd0c831c 100644 --- a/test/testmodules/Invalidation/Manifest.toml +++ b/test/testmodules/Invalidation/Manifest.toml @@ -1,6 +1,6 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.8.4" +julia_version = "1.10.2" manifest_format = "2.0" project_hash = "0bc27949c56990fdb6d1c736921aa6799da2d200"