Skip to content
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

Integrate StackTraces.jl into Base #14469

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ Library improvements

* Improve performance of `quantile` ([#14413]).

* The new `Base.StackTraces` module makes stack traces easier to use programmatically. ([#14469])

Deprecated or removed
---------------------

Expand Down Expand Up @@ -1771,3 +1773,4 @@ Too numerous to mention.
[#14424]: https://github.com/JuliaLang/julia/issues/14424
[#14759]: https://github.com/JuliaLang/julia/issues/14759
[#14114]: https://github.com/JuliaLang/julia/issues/14114
[#14469]: https://github.com/JuliaLang/julia/issues/14469
7 changes: 7 additions & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export
Pkg,
Git,
LibGit2,
StackTraces,
Profile,
Dates,
Sys,
Expand Down Expand Up @@ -1047,6 +1048,12 @@ export
rethrow,
systemerror,

# stack traces
StackTrace,
StackFrame,
stacktrace,
catch_stacktrace,

# types
convert,
fieldoffset,
Expand Down
79 changes: 22 additions & 57 deletions base/profile.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module Profile

import Base: hash, ==
import Base.StackTraces: lookup, UNKNOWN

export @profile

Expand Down Expand Up @@ -117,7 +117,7 @@ end

function getdict(data::Vector{UInt})
uip = unique(data)
Dict{UInt, LineInfo}([ip=>lookup(ip) for ip in uip])
Dict{UInt, StackFrame}([ip=>lookup(ip) for ip in uip])
end

# TODO update signature in docstring.
Expand Down Expand Up @@ -166,35 +166,6 @@ Julia, and examine the resulting `*.mem` files.
clear_malloc_data() = ccall(:jl_clear_malloc_data, Void, ())


####
#### Internal interface
####
immutable LineInfo
func::ByteString
file::ByteString
line::Int
inlined_file::ByteString
inlined_line::Int
fromC::Bool
ip::Int64 # large enough that this struct can be losslessly read on any machine (32 or 64 bit)
end

const UNKNOWN = LineInfo("?", "?", -1, "?", -1, true, 0)

#
# If the LineInfo has function and line information, we consider two of them the same
# if they share the same function/line information. For unknown functions, line==ip
# so we never actually need to consider the .ip field.
#
==(a::LineInfo, b::LineInfo) = a.line == b.line && a.fromC == b.fromC && a.func == b.func && a.file == b.file

function hash(li::LineInfo, h::UInt)
h += 0xf4fbda67fe20ce88 % UInt
h = hash(li.line, h)
h = hash(li.file, h)
h = hash(li.func, h)
end

# C wrappers
start_timer() = ccall(:jl_profile_start_timer, Cint, ())

Expand All @@ -208,16 +179,6 @@ len_data() = convert(Int, ccall(:jl_profile_len_data, Csize_t, ()))

maxlen_data() = convert(Int, ccall(:jl_profile_maxlen_data, Csize_t, ()))

function lookup(ip::Ptr{Void})
info = ccall(:jl_lookup_code_address, Any, (Ptr{Void},Cint), ip, false)
if length(info) == 7
return LineInfo(string(info[1]), string(info[2]), Int(info[3]), string(info[4]), Int(info[5]), info[6], Int64(info[7]))
else
return UNKNOWN
end
end
lookup(ip::UInt) = lookup(convert(Ptr{Void},ip))

error_codes = Dict{Int,ASCIIString}(
-1=>"cannot specify signal action for profiling",
-2=>"cannot create the timer for profiling",
Expand Down Expand Up @@ -282,7 +243,7 @@ function parse_flat(iplist, n, lidict, C::Bool)
# The ones with no line number might appear multiple times in a single
# backtrace, giving the wrong impression about the total number of backtraces.
# Delete them too.
keep = !Bool[x == UNKNOWN || x.line == 0 || (x.fromC && !C) for x in lilist]
keep = !Bool[x == UNKNOWN || x.line == 0 || (x.from_c && !C) for x in lilist]
n = n[keep]
lilist = lilist[keep]
lilist, n
Expand All @@ -301,7 +262,7 @@ function flat{T<:Unsigned}(io::IO, data::Vector{T}, lidict::Dict, C::Bool, combi
print_flat(io, lilist, n, combine, cols, sortedby)
end

function print_flat(io::IO, lilist::Vector{LineInfo}, n::Vector{Int}, combine::Bool, cols::Integer, sortedby)
function print_flat(io::IO, lilist::Vector{StackFrame}, n::Vector{Int}, combine::Bool, cols::Integer, sortedby)
p = liperm(lilist)
lilist = lilist[p]
n = n[p]
Expand Down Expand Up @@ -330,8 +291,8 @@ function print_flat(io::IO, lilist::Vector{LineInfo}, n::Vector{Int}, combine::B
maxfunc = 0
for li in lilist
maxline = max(maxline, li.line)
maxfile = max(maxfile, length(li.file))
maxfunc = max(maxfunc, length(li.func))
maxfile = max(maxfile, length(string(li.file)))
maxfunc = max(maxfunc, length(string(li.func)))
end
wline = max(5, ndigits(maxline))
ntext = cols - wcounts - wline - 3
Expand Down Expand Up @@ -371,9 +332,9 @@ function tree_aggregate{T<:Unsigned}(data::Vector{T})
bt, counts
end

tree_format_linewidth(x::LineInfo) = ndigits(x.line)+6
tree_format_linewidth(x::StackFrame) = ndigits(x.line)+6

function tree_format(lilist::Vector{LineInfo}, counts::Vector{Int}, level::Int, cols::Integer)
function tree_format(lilist::Vector{StackFrame}, counts::Vector{Int}, level::Int, cols::Integer)
nindent = min(cols>>1, level)
ndigcounts = ndigits(maximum(counts))
ndigline = maximum([tree_format_linewidth(x) for x in lilist])
Expand All @@ -394,18 +355,20 @@ function tree_format(lilist::Vector{LineInfo}, counts::Vector{Int}, level::Int,
if showextra
base = string(base, "+", nextra, " ")
end
if li.line == li.ip
if li.line == li.pointer
strs[i] = string(base,
rpad(string(counts[i]), ndigcounts, " "),
" ","unknown function (ip: 0x",hex(li.ip,2*sizeof(Ptr{Void})),
" ",
"unknown function (pointer: 0x",
hex(li.pointer,2*sizeof(Ptr{Void})),
")")
else
base = string(base,
rpad(string(counts[i]), ndigcounts, " "),
" ",
truncto(string(li.file), widthfile),
truncto(li.file, widthfile),
"; ",
truncto(string(li.func), widthfunc),
truncto(li.func, widthfunc),
"; ")
if li.line == -1
strs[i] = string(base, "(unknown line)")
Expand All @@ -428,7 +391,7 @@ function tree{T<:Unsigned}(io::IO, bt::Vector{Vector{T}}, counts::Vector{Int}, l
# Organize backtraces into groups that are identical up to this level
if combine
# Combine based on the line information
d = Dict{LineInfo,Vector{Int}}()
d = Dict{StackFrame,Vector{Int}}()
for i = 1:length(bt)
ip = bt[i][level+1]
key = lidict[ip]
Expand All @@ -441,7 +404,7 @@ function tree{T<:Unsigned}(io::IO, bt::Vector{Vector{T}}, counts::Vector{Int}, l
end
# Generate counts
dlen = length(d)
lilist = Array(LineInfo, dlen)
lilist = Array(StackFrame, dlen)
group = Array(Vector{Int}, dlen)
n = Array(Int, dlen)
i = 1
Expand All @@ -465,7 +428,7 @@ function tree{T<:Unsigned}(io::IO, bt::Vector{Vector{T}}, counts::Vector{Int}, l
end
# Generate counts, and do the code lookup
dlen = length(d)
lilist = Array(LineInfo, dlen)
lilist = Array(StackFrame, dlen)
group = Array(Vector{Int}, dlen)
n = Array(Int, dlen)
i = 1
Expand Down Expand Up @@ -516,7 +479,7 @@ function tree{T<:Unsigned}(io::IO, data::Vector{T}, lidict::Dict, C::Bool, combi
end

function callersf(matchfunc::Function, bt::Vector{UInt}, lidict)
counts = Dict{LineInfo, Int}()
counts = Dict{StackFrame, Int}()
lastmatched = false
for id in bt
if id == 0
Expand Down Expand Up @@ -548,8 +511,10 @@ function truncto(str::ByteString, w::Int)
ret
end

truncto(str::Symbol, w::Int) = truncto(string(str), w)

# Order alphabetically (file, function) and then by line number
function liperm(lilist::Vector{LineInfo})
function liperm(lilist::Vector{StackFrame})
comb = Array(ByteString, length(lilist))
for i = 1:length(lilist)
li = lilist[i]
Expand All @@ -568,7 +533,7 @@ warning_empty() = warn("""
Profile.init().""")

function purgeC(data, lidict)
keep = Bool[d == 0 || lidict[d].fromC == false for d in data]
keep = Bool[d == 0 || lidict[d].from_c == false for d in data]
data[keep]
end

Expand Down
139 changes: 139 additions & 0 deletions base/stacktraces.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# This file is a part of Julia. License is MIT: http://julialang.org/license

module StackTraces


import Base: hash, ==, show

export StackTrace, StackFrame, stacktrace, catch_stacktrace

"""
StackFrame

Stack information representing execution context.
"""
immutable StackFrame
"the name of the function containing the execution context"
func::Symbol
"the path to the file containing the execution context"
file::Symbol
"the line number in the file containing the execution context"
line::Int
"the path to the file containing the context for inlined code"
inlined_file::Symbol
"the line number in the file containing the context for inlined code"
inlined_line::Int
"true if the code is from C"
from_c::Bool
"representation of the pointer to the execution context as returned by `backtrace`"
pointer::Int64 # Large enough to be read losslessly on 32- and 64-bit machines.
end

StackFrame(func, file, line) = StackFrame(func, file, line, symbol(""), -1, false, 0)

"""
StackTrace

An alias for `Vector{StackFrame}` provided for convenience; returned by calls to
`stacktrace` and `catch_stacktrace`.
"""
typealias StackTrace Vector{StackFrame}


const UNKNOWN = StackFrame(symbol("???"), symbol("???"), 0, symbol(""), -1, true, 0)


#=
If the StackFrame has function and line information, we consider two of them the same if
they share the same function/line information. For unknown functions, line == pointer, so we
never actually need to consider the pointer field.
=#
function ==(a::StackFrame, b::StackFrame)
a.line == b.line && a.from_c == b.from_c && a.func == b.func && a.file == b.file
end

function hash(frame::StackFrame, h::UInt)
h += 0xf4fbda67fe20ce88 % UInt
h = hash(frame.line, h)
h = hash(frame.file, h)
h = hash(frame.func, h)
end


"""
lookup(pointer::Union{Ptr{Void}, UInt}) -> StackFrame

Given a pointer to an execution context (usually generated by a call to `backtrace`), looks
up stack frame context information.
"""
function lookup(pointer::Ptr{Void})
return StackFrame(ccall(:jl_lookup_code_address, Any, (Ptr{Void}, Cint), pointer, 0)...)
end

lookup(pointer::UInt) = lookup(convert(Ptr{Void}, pointer))

"""
stacktrace([trace::Vector{Ptr{Void}},] [c_funcs::Bool=false]) -> StackTrace

Returns a stack trace in the form of a vector of `StackFrame`s. (By default stacktrace
doesn't return C functions, but this can be enabled.) When called without specifying a
trace, `stacktrace` first calls `backtrace`.
"""
function stacktrace(trace::Vector{Ptr{Void}}, c_funcs::Bool=false)
stack = map(lookup, trace)::StackTrace

# Remove frames that come from C calls.
if !c_funcs
filter!(frame -> !frame.from_c, stack)
end

# Remove frame for this function (and any functions called by this function).
remove_frames!(stack, :stacktrace)
end

stacktrace(c_funcs::Bool=false) = stacktrace(backtrace(), c_funcs)

"""
catch_stacktrace([c_funcs::Bool=false]) -> StackTrace

Returns the stack trace for the most recent error thrown, rather than the current execution
context.
"""
catch_stacktrace(c_funcs::Bool=false) = stacktrace(catch_backtrace(), c_funcs)

"""
remove_frames!(stack::StackTrace, name::Symbol)

Takes a `StackTrace` (a vector of `StackFrames`) and a function name (a `Symbol`) and
removes the `StackFrame` specified by the function name from the `StackTrace` (also removing
all frames above the specified function). Primarily used to remove `StackTraces` functions
from the `StackTrace` prior to returning it.
"""
function remove_frames!(stack::StackTrace, name::Symbol)
splice!(stack, 1:findlast(frame -> frame.func == name, stack))
return stack
end

function remove_frames!(stack::StackTrace, names::Vector{Symbol})
splice!(stack, 1:findlast(frame -> frame.func in names, stack))
return stack
end

function show(io::IO, frame::StackFrame; full_path::Bool=false)
file_info = "$(full_path ? frame.file : basename(string(frame.file))):$(frame.line)"

if frame.inlined_file != Symbol("")
inline_info = string("[inlined code from ", file_info, "] ")
file_info = string(
full_path ? frame.inlined_file : basename(string(frame.inlined_file)),
":", frame.inlined_line
)
else
inline_info = ""
end

print(io, string(inline_info, frame.func != "" ? frame.func : "?", " at ", file_info))
end


end
4 changes: 4 additions & 0 deletions base/sysimg.jl
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,10 @@ include("libgit2.jl")
include("pkg.jl")
const Git = Pkg.Git

# Stack frames and traces
include("stacktraces.jl")
importall .StackTraces

# profiler
include("profile.jl")
importall .Profile
Expand Down
2 changes: 2 additions & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
manual/embedding
manual/packages
manual/profile
manual/stacktraces
manual/performance-tips
manual/workflow-tips
manual/style-guide
Expand Down Expand Up @@ -79,6 +80,7 @@
stdlib/test
stdlib/c
stdlib/profile
stdlib/stacktraces

.. _devdocs:

Expand Down
Loading