-
Notifications
You must be signed in to change notification settings - Fork 57
Expose changes to Julia via @snoopi_deep to report timing profile breakdown of _all MethodInstances_ that are inferred during type inference
#139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
timholy
merged 50 commits into
JuliaDebug:master
from
NHDaly:nhd-snoopi-inferrable-set--TimerOutputs-profile
Nov 7, 2020
Merged
Changes from all commits
Commits
Show all changes
50 commits
Select commit
Hold shift + click to select a range
d19deab
First pass at snoopi collecting the full inferrable set from julia
NHDaly ac15b22
Update to use bool to control julia "collect inference callees" feature
NHDaly b9f07fe
Use commit `97b5cde769` from Julia to return dep graph from snoopi
NHDaly 9b5f04a
Almost working parcel per_method_instance_timings -- need to fix seri…
NHDaly 1eb4214
Working per-method timings, i think (though overly complicated and ov…
NHDaly bb83243
Fix now that i've disabled my first attempt in julia
NHDaly faa3eb8
Got per-method timing working on helper process! :)
NHDaly 9e441cd
Switch to BFS instead of (recursive) DFS
NHDaly 4cad634
Fix `code_typed()` to `code_typed_by_type(tt)` to ACTUALLY measure in…
NHDaly 6a62f74
Try randomly shuffling the leaves to get a (hopefully) different walk…
NHDaly 4820a24
Second attempt at per-method inf timings: TimerOutputs
NHDaly 21bae6d
(silly) fixups to get the linfos to print to a string
NHDaly bc20040
Fix double-counting, using "$linfo" for timer name
NHDaly b290ec4
Start precompiling the TimerOutputs snoopi approach
NHDaly d77e22d
Revert changes to SnoopCompile snoopi
NHDaly 5b8603a
Rewrite snoopi on top of custom Core.Compiler.Timings
NHDaly 615b1b6
Add `exclusive_timings()` to compute and list the per-method-instance…
NHDaly bc19fed
Now julia itself reports exclusive times
NHDaly c91c7d2
Add basic flame graph printing for per-method inference timings!
NHDaly 95b3fc2
Close "root" timer so `root` is an accurate measurement as well! :)
NHDaly 3b34dd6
Split out `@snoopi_deep`: for the per-method-instance snoopi timings
NHDaly 6c9de36
Get `to_flamegraph(::Timing)` working! :grin:
NHDaly 316b8bc
Print snoopi_deep flamegraph with pretty function syntax
NHDaly fb48e8a
Update to work with latest version of https://github.com/JuliaLang/ju…
NHDaly 0974619
Move `flatten_times()` for at-snoopi_deep to parcel_snoopi.jl
NHDaly 493ae50
Reorganized parcel_snoopi.jl
NHDaly abfc173
Handle Type Tuples that can't be printed to a string
NHDaly 1e4eda2
Add tests for `@snoopi_deep`, including AbstractTrees tests of FlameG…
NHDaly 1b63149
Add docstring for macro snoopi_deep
NHDaly 3f21529
Add TODO to add `tmin=` to `to_flamegraph()`
NHDaly 52b414e
Add `tmin_secs=` param to `to_flamegraphs()` to filter frames by incl…
NHDaly 1e20614
Switch back to nice MethodInstance printing for frame names!
NHDaly 7bb6a28
Add file and line info to SnoopCompile.to_flamegraph
NHDaly f1fbd1a
Clean up: Remove debugging and experimentation cruft
NHDaly 2d84f1d
Fix `tmin_secs` for `to_flamegraph()` and `flatten_times()`
NHDaly 3adda6c
Improve docstrings and add Examples
NHDaly 11cab16
Clean up old cruft
NHDaly 54c3462
Add tests for `tmin_secs=` param
NHDaly ceaecb2
Set fail-fast: false for easier CI for code review
NHDaly 37ad8be
Add compat bounds for FlameGraphs and LeftChildRightSiblingTrees
NHDaly bfa969e
Apply suggestions from code review
NHDaly 421593d
Fix small type stability of `range=` in `_flamegraph_frame()`
NHDaly 6d3ff30
Simplify Project.toml: Use `FlameGraphs.LeftChildRightSiblingTrees`
NHDaly 74e7f69
Improve docstrings and comments (PR Review suggestions)
NHDaly 4e6b74f
Split out `snoopi_deep` into separate, version-guarded files:
NHDaly 67c447c
Merge branch 'master' into nhd-snoopi-inferrable-set--TimerOutputs-pr…
NHDaly 49ce9f2
Simplify the logic in `SnoopCompile.max_end_time()` and add comments
NHDaly 2723e48
Change `to_flamegraph()` to `flamegraph()`
NHDaly 53d6bf8
Remove qualified names from docstring examples
NHDaly ba358cc
Update src/parcel_snoopi_deep.jl
NHDaly File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| function start_deep_timing() | ||
| Core.Compiler.Timings.reset_timings() | ||
| Core.Compiler.__set_measure_typeinf(true) | ||
| end | ||
| function stop_deep_timing() | ||
| Core.Compiler.__set_measure_typeinf(false) | ||
| Core.Compiler.Timings.close_current_timer() | ||
| end | ||
|
|
||
| function finish_snoopi_deep() | ||
| return Core.Compiler.Timings._timings[1] | ||
| end | ||
|
|
||
| function _snoopi_deep(cmd::Expr) | ||
| return quote | ||
| start_deep_timing() | ||
| try | ||
| $(esc(cmd)) | ||
| finally | ||
| stop_deep_timing() | ||
| end | ||
| finish_snoopi_deep() | ||
| end | ||
| end | ||
|
|
||
| """ | ||
| timing_tree = @snoopi_deep commands | ||
|
|
||
| Produce a profile of julia's type inference, containing the amount of time spent inferring | ||
| every `MethodInstance` processed while executing `commands`. | ||
|
|
||
| The top-level node in this profile tree is `ROOT`, which contains the time spent _not_ in | ||
| julia's type inference (codegen, llvm_opt, runtime, etc). | ||
|
|
||
| To make use of these results, see the processing functions in SnoopCompile: | ||
| - [`SnoopCompile.flatten_times(timing_tree)`](@ref) | ||
| - [`SnoopCompile.flamegraph(timing_tree)`](@ref) | ||
|
|
||
| # Examples | ||
| ```julia | ||
| julia> timing = @snoopi_deep begin | ||
| @eval sort(rand(100)) # Evaluate some code and profile julia's type inference | ||
| end; | ||
|
|
||
| julia> using SnoopCompile, ProfileView | ||
|
|
||
| julia> times = flatten_times(timing, tmin_secs=0.001) | ||
| 4-element Vector{Any}: | ||
| 0.001088448 => Core.Compiler.Timings.InferenceFrameInfo(MethodInstance for fpsort!(... | ||
| 0.001618478 => Core.Compiler.Timings.InferenceFrameInfo(MethodInstance for rand!(... | ||
| 0.002289655 => Core.Compiler.Timings.InferenceFrameInfo(MethodInstance for _rand_max383!(... | ||
| 0.093143594 => Core.Compiler.Timings.InferenceFrameInfo(MethodInstance for ROOT(), ... | ||
|
|
||
| julia> fg = flamegraph(timing) | ||
| Node(FlameGraphs.NodeData(ROOT() at typeinfer.jl:70, 0x00, 0:15355670)) | ||
|
|
||
| julia> ProfileView.view(fg); # Display the FlameGraph in a package that supports it | ||
|
|
||
| julia> fg = flamegraph(timing; tmin_secs=0.0001) # Skip very tiny frames | ||
| Node(FlameGraphs.NodeData(ROOT() at typeinfer.jl:70, 0x00, 0:15355670)) | ||
| ``` | ||
| """ | ||
| macro snoopi_deep(cmd) | ||
| return _snoopi_deep(cmd) | ||
| end | ||
|
|
||
| # These are okay to come at the top-level because we're only measuring inference, and | ||
| # inference results will be cached in a `.ji` file. | ||
| @assert precompile(Core.Compiler.Timings.reset_timings, ()) | ||
| @assert precompile(start_deep_timing, ()) | ||
| @assert precompile(stop_deep_timing, ()) | ||
| @assert precompile(finish_snoopi_deep, ()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| import FlameGraphs | ||
|
|
||
| using Base.StackTraces: StackFrame | ||
| using FlameGraphs.LeftChildRightSiblingTrees: Node, addchild | ||
| using Core.Compiler.Timings: Timing | ||
|
|
||
| const flamegraph = FlameGraphs.flamegraph # For re-export | ||
|
|
||
| """ | ||
| flatten_times(timing::Core.Compiler.Timings.Timing; tmin_secs = 0.0) | ||
|
|
||
| Flatten the execution graph of Timings returned from `@snoopi_deep` into a Vector of pairs, | ||
| with the exclusive time for each invocation of type inference, skipping any frames that | ||
| took less than `tmin_secs` seconds. Results are sorted by time. | ||
| """ | ||
| function flatten_times(timing::Core.Compiler.Timings.Timing; tmin_secs = 0.0) | ||
| out = Pair{Float64,Core.Compiler.Timings.InferenceFrameInfo}[] | ||
| frontier = [timing] | ||
| while !isempty(frontier) | ||
| t = popfirst!(frontier) | ||
| exclusive_time = (t.time / 1e9) | ||
| if exclusive_time >= tmin_secs | ||
| push!(out, exclusive_time => t.mi_info) | ||
| end | ||
| append!(frontier, t.children) | ||
| end | ||
| return sort(out; by=tl->tl[1]) | ||
| end | ||
|
|
||
| struct InclusiveTiming | ||
| mi_info::Core.Compiler.Timings.InferenceFrameInfo | ||
| inclusive_time::UInt64 | ||
| start_time::UInt64 | ||
| children::Vector{InclusiveTiming} | ||
| end | ||
|
|
||
| inclusive_time(t::InclusiveTiming) = t.inclusive_time | ||
|
|
||
| function build_inclusive_times(t::Timing) | ||
| child_times = InclusiveTiming[ | ||
| build_inclusive_times(child) | ||
| for child in t.children | ||
| ] | ||
| incl_time = t.time + sum(inclusive_time, child_times; init=UInt64(0)) | ||
| return InclusiveTiming(t.mi_info, incl_time, t.start_time, child_times) | ||
| end | ||
|
|
||
| """ | ||
| flamegraph(t::Core.Compiler.Timings.Timing; tmin_secs=0.0) | ||
| flamegraph(t::SnoopCompile.InclusiveTiming; tmin_secs=0.0) | ||
|
|
||
| Convert the call tree of inference timings returned from `@snoopi_deep` into a FlameGraph. | ||
| Returns a FlameGraphs.FlameGraph structure that represents the timing trace recorded for | ||
| type inference. | ||
|
|
||
| Frames that take less than `tmin_secs` seconds of _inclusive time_ will not be included | ||
| in the resultant FlameGraph (meaning total time including it and all of its children). | ||
| This can be helpful if you have a very big profile, to save on processing time. | ||
|
|
||
| # Examples | ||
| ```julia | ||
| julia> timing = @snoopi_deep begin | ||
| @eval sort(rand(100)) # Evaluate some code and profile julia's type inference | ||
| end; | ||
|
|
||
| julia> fg = flamegraph(timing) | ||
| Node(FlameGraphs.NodeData(ROOT() at typeinfer.jl:70, 0x00, 0:15355670)) | ||
|
|
||
| julia> ProfileView.view(fg); # Display the FlameGraph in a package that supports it | ||
|
|
||
| julia> fg = flamegraph(timing; tmin_secs=0.0001) # Skip very tiny frames | ||
| Node(FlameGraphs.NodeData(ROOT() at typeinfer.jl:70, 0x00, 0:15355670)) | ||
| ``` | ||
|
|
||
| NOTE: This function must touch every frame in the provided `Timing` to build inclusive | ||
| timing information (`InclusiveTiming`). If you have a very large profile, and you plan to | ||
| call this function multiple times (say with different values for `tmin_secs`), you can save | ||
| some intermediate time by first calling [`SnoopCompile.build_inclusive_times(t)`](@ref), only once, | ||
| and then passing in the `InclusiveTiming` object for all subsequent calls. | ||
| """ | ||
| function FlameGraphs.flamegraph(t::Timing; tmin_secs = 0.0) | ||
| it = build_inclusive_times(t) | ||
| flamegraph(it; tmin_secs=tmin_secs) | ||
| end | ||
|
|
||
| function FlameGraphs.flamegraph(to::InclusiveTiming; tmin_secs = 0.0) | ||
| tmin_ns = UInt64(round(tmin_secs * 1e9)) | ||
|
|
||
| # Compute a "root" frame for the top-level node, to cover the whole profile | ||
| node_data = _flamegraph_frame(to, to.start_time; toplevel=true) | ||
| root = Node(node_data) | ||
| return _build_flamegraph!(root, to, to.start_time, tmin_ns) | ||
| end | ||
| function _build_flamegraph!(root, to::InclusiveTiming, start_ns, tmin_ns) | ||
| for child in to.children | ||
| if child.inclusive_time > tmin_ns | ||
| node_data = _flamegraph_frame(child, start_ns; toplevel=false) | ||
| node = addchild(root, node_data) | ||
| _build_flamegraph!(node, child, start_ns, tmin_ns) | ||
| end | ||
| end | ||
| return root | ||
| end | ||
|
|
||
| function frame_name(mi_info::Core.Compiler.Timings.InferenceFrameInfo) | ||
| frame_name(mi_info.mi::Core.Compiler.MethodInstance) | ||
| end | ||
| function frame_name(mi::Core.Compiler.MethodInstance) | ||
| frame_name(mi.def.name, mi.specTypes) | ||
| end | ||
| # Special printing for Type Tuples so they're less ugly in the FlameGraph | ||
| function frame_name(name, ::Type{TT}) where TT<:Tuple | ||
| io = IOBuffer() | ||
| Base.show_tuple_as_call(io, name, TT) | ||
| v = String(take!(io)) | ||
| return v | ||
| end | ||
|
|
||
| # NOTE: The "root" node doesn't cover the whole profile, because it's only the _complement_ | ||
| # of the inference times (so it's missing the _overhead_ from the measurement). | ||
| # SO we need to manually create a root node that covers the whole thing. | ||
| function max_end_time(t::InclusiveTiming) | ||
| # It's possible that t is already the longest-reaching node. | ||
| t_end = UInt64(t.start_time + t.inclusive_time) | ||
| # It's also possible that the last child extends past the end of t. (I think this is | ||
| # possible because of the small unmeasured overhead in computing these measurements.) | ||
| last_node = length(t.children) > 0 ? t.children[end] : t | ||
| child_end = last_node.start_time + last_node.inclusive_time | ||
| # Return the maximum end time to make sure the top node covers the entire graph. | ||
| return max(t_end, child_end) | ||
| end | ||
|
|
||
| # Make a flat frame for this Timing | ||
| function _flamegraph_frame(to::InclusiveTiming, start_ns; toplevel) | ||
| mi = to.mi_info.mi | ||
| tt = Symbol(frame_name(to.mi_info)) | ||
| sf = StackFrame(tt, mi.def.file, mi.def.line, mi, false, false, UInt64(0x0)) | ||
| status = 0x0 # "default" status -- See FlameGraphs.jl | ||
| start = to.start_time - start_ns | ||
| if toplevel | ||
| # Compute a range over the whole profile for the top node. | ||
| range = Int(start) : Int(max_end_time(to) - start_ns) | ||
| else | ||
| range = Int(start) : Int(start + to.inclusive_time) | ||
| end | ||
| return FlameGraphs.NodeData(sf, status, range) | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| using SnoopCompile | ||
| using SnoopCompile.SnoopCompileCore | ||
| using Test | ||
|
|
||
| using AbstractTrees # For FlameGraphs tests | ||
|
|
||
| @testset "@snoopi_deep" begin | ||
| # WARMUP (to compile all the small, reachable methods) | ||
| @eval module M # Example with some functions that include type instability | ||
| i(x) = x+5 | ||
| h(a::Array) = i(a[1]::Integer) + 2 | ||
| g(y::Integer) = h(Any[y]) | ||
| end | ||
| M.g(2) # Warmup all deeply reachable functions | ||
|
|
||
| # Redefine the module, so the snoop will only show these functions: | ||
| @eval module M # Example with some functions that include type instability | ||
| i(x) = x+5 | ||
| h(a::Array) = i(a[1]::Integer) + 2 | ||
| g(y::Integer) = h(Any[y]) | ||
| end | ||
|
|
||
| timing = SnoopCompileCore.@snoopi_deep begin | ||
| M.g(2) | ||
| end | ||
| times = SnoopCompile.flatten_times(timing) | ||
| @test length(times) == 5 # ROOT, g(...), h(...), i(::Integer), i(::Int) | ||
| names = [mi_info.mi.def.name for (time, mi_info) in times] | ||
| @test sort(names) == [:ROOT, :g, :h, :i, :i] | ||
|
|
||
| longest_frame_time = times[end][1] | ||
| @test length(SnoopCompile.flatten_times(timing, tmin_secs=longest_frame_time)) == 1 | ||
| end | ||
|
|
||
| @testset "flamegraph_export" begin | ||
| @eval module M # Take another timing | ||
| i(x) = x+5 | ||
| h(a::Array) = i(a[1]::Integer) + 2 | ||
| g(y::Integer) = h(Any[y]) | ||
| end | ||
|
|
||
| timing = SnoopCompileCore.@snoopi_deep begin | ||
| M.g(2) | ||
| end | ||
| times = SnoopCompile.flatten_times(timing) | ||
|
|
||
| fg = SnoopCompile.flamegraph(timing) | ||
| @test length(collect(AbstractTrees.PreOrderDFS(fg))) == 5 | ||
| # Test that the span covers the whole tree. | ||
| for leaf in AbstractTrees.PreOrderDFS(fg) | ||
| @test leaf.data.span.start in fg.data.span | ||
| @test leaf.data.span.stop in fg.data.span | ||
| end | ||
|
|
||
| cutoff_bottom_frame = (times[1][1] + times[2][1]) / 2 | ||
| fg2 = SnoopCompile.flamegraph(timing, tmin_secs = cutoff_bottom_frame) | ||
| @test length(collect(AbstractTrees.PreOrderDFS(fg2))) == (length(collect(AbstractTrees.PreOrderDFS(fg))) - 1) | ||
| end |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.