Skip to content

Commit adce356

Browse files
spurllvtjnash
authored andcommitted
Incorporating StackTraces into Base.
Tests and documentation for stacktraces. Updated NEWS.md with stacktraces functionality. Refactored, doctests, show_stacktrace is now show. Added doctests to the manual. Removed show_stacktrace in favour of show(x::StackFrame). Removed format_stacktrace and format_stackframe. Additional test cases (which currently fail due to difficult to track down line number issues) Test for invalid frame. Removed failing Win32 test. Reintroduced UNKNOWN constant stack frame from profile.jl. Added test case for looking up an invalid stack frame. Selectively removed some tests that consistently fail in Appveyor's 32-bit Windows LVM.
1 parent 9ce59c8 commit adce356

File tree

12 files changed

+600
-72
lines changed

12 files changed

+600
-72
lines changed

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ Library improvements
9999

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

102+
* The new `Base.StackTraces` module makes stack traces easier to use programmatically. ([#14469])
103+
102104
Deprecated or removed
103105
---------------------
104106

@@ -1779,3 +1781,4 @@ Too numerous to mention.
17791781
[#14424]: https://github.com/JuliaLang/julia/issues/14424
17801782
[#14759]: https://github.com/JuliaLang/julia/issues/14759
17811783
[#14114]: https://github.com/JuliaLang/julia/issues/14114
1784+
[#14469]: https://github.com/JuliaLang/julia/issues/14469

base/exports.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export
99
Pkg,
1010
Git,
1111
LibGit2,
12+
StackTraces,
1213
Profile,
1314
Dates,
1415
Sys,
@@ -1046,6 +1047,12 @@ export
10461047
rethrow,
10471048
systemerror,
10481049

1050+
# stack traces
1051+
StackTrace,
1052+
StackFrame,
1053+
stacktrace,
1054+
catch_stacktrace,
1055+
10491056
# types
10501057
convert,
10511058
fieldoffset,

base/profile.jl

Lines changed: 23 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
module Profile
44

5-
import Base: hash, ==, show_spec_linfo
5+
import Base: show_spec_linfo
6+
import Base.StackTraces: lookup, UNKNOWN
67

78
export @profile
89

@@ -117,7 +118,7 @@ end
117118

118119
function getdict(data::Vector{UInt})
119120
uip = unique(data)
120-
Dict{UInt, LineInfo}([ip=>lookup(ip) for ip in uip])
121+
Dict{UInt, StackFrame}([ip=>lookup(ip) for ip in uip])
121122
end
122123

123124
# TODO update signature in docstring.
@@ -165,37 +166,6 @@ Julia, and examine the resulting `*.mem` files.
165166
"""
166167
clear_malloc_data() = ccall(:jl_clear_malloc_data, Void, ())
167168

168-
169-
####
170-
#### Internal interface
171-
####
172-
immutable LineInfo
173-
func::ByteString
174-
inlined_file::ByteString
175-
inlined_line::Int
176-
file::ByteString
177-
line::Int
178-
outer_linfo::Nullable{LambdaStaticData}
179-
fromC::Bool
180-
ip::Int64 # large enough that this struct can be losslessly read on any machine (32 or 64 bit)
181-
end
182-
183-
const UNKNOWN = LineInfo("?", "?", -1, "?", -1, Nullable{LambdaStaticData}(), true, 0)
184-
185-
#
186-
# If the LineInfo has function and line information, we consider two of them the same
187-
# if they share the same function/line information. For unknown functions, line==ip
188-
# so we never actually need to consider the .ip field.
189-
#
190-
==(a::LineInfo, b::LineInfo) = a.line == b.line && a.fromC == b.fromC && a.func == b.func && a.file == b.file
191-
192-
function hash(li::LineInfo, h::UInt)
193-
h += 0xf4fbda67fe20ce88 % UInt
194-
h = hash(li.line, h)
195-
h = hash(li.file, h)
196-
h = hash(li.func, h)
197-
end
198-
199169
# C wrappers
200170
start_timer() = ccall(:jl_profile_start_timer, Cint, ())
201171

@@ -209,24 +179,6 @@ len_data() = convert(Int, ccall(:jl_profile_len_data, Csize_t, ()))
209179

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

212-
const empty_sym = Symbol("")
213-
function lookup(ip::Ptr{Void})
214-
info = ccall(:jl_lookup_code_address, Any, (Ptr{Void},Cint), ip, false)
215-
if length(info) == 8
216-
is_inlined = (info[4] !== empty_sym)
217-
return LineInfo(string(info[1]),
218-
is_inlined ? string(info[2]) : "",
219-
is_inlined ? Int(info[3]) : -1,
220-
string(is_inlined ? info[4] : info[2]),
221-
Int(is_inlined ? info[5] : info[3]),
222-
info[6] === nothing ? Nullable{LambdaStaticData}() : Nullable{LambdaStaticData}(info[6]),
223-
info[7], Int64(info[8]))
224-
else
225-
return UNKNOWN
226-
end
227-
end
228-
lookup(ip::UInt) = lookup(convert(Ptr{Void},ip))
229-
230182
error_codes = Dict{Int,ASCIIString}(
231183
-1=>"cannot specify signal action for profiling",
232184
-2=>"cannot create the timer for profiling",
@@ -291,7 +243,7 @@ function parse_flat(iplist, n, lidict, C::Bool)
291243
# The ones with no line number might appear multiple times in a single
292244
# backtrace, giving the wrong impression about the total number of backtraces.
293245
# Delete them too.
294-
keep = !Bool[x == UNKNOWN || x.line == 0 || (x.fromC && !C) for x in lilist]
246+
keep = !Bool[x == UNKNOWN || x.line == 0 || (x.from_c && !C) for x in lilist]
295247
n = n[keep]
296248
lilist = lilist[keep]
297249
lilist, n
@@ -310,7 +262,7 @@ function flat{T<:Unsigned}(io::IO, data::Vector{T}, lidict::Dict, C::Bool, combi
310262
print_flat(io, lilist, n, combine, cols, sortedby)
311263
end
312264

313-
function print_flat(io::IO, lilist::Vector{LineInfo}, n::Vector{Int}, combine::Bool, cols::Integer, sortedby)
265+
function print_flat(io::IO, lilist::Vector{StackFrame}, n::Vector{Int}, combine::Bool, cols::Integer, sortedby)
314266
p = liperm(lilist)
315267
lilist = lilist[p]
316268
n = n[p]
@@ -339,8 +291,8 @@ function print_flat(io::IO, lilist::Vector{LineInfo}, n::Vector{Int}, combine::B
339291
maxfunc = 10
340292
for li in lilist
341293
maxline = max(maxline, li.line)
342-
maxfile = max(maxfile, length(li.file))
343-
maxfunc = max(maxfunc, length(li.func))
294+
maxfile = max(maxfile, length(string(li.file)))
295+
maxfunc = max(maxfunc, length(string(li.func)))
344296
end
345297
wline = max(5, ndigits(maxline))
346298
ntext = cols - wcounts - wline - 3
@@ -389,9 +341,9 @@ function tree_aggregate{T<:Unsigned}(data::Vector{T})
389341
bt, counts
390342
end
391343

392-
tree_format_linewidth(x::LineInfo) = ndigits(x.line)+6
344+
tree_format_linewidth(x::StackFrame) = ndigits(x.line)+6
393345

394-
function tree_format(lilist::Vector{LineInfo}, counts::Vector{Int}, level::Int, cols::Integer)
346+
function tree_format(lilist::Vector{StackFrame}, counts::Vector{Int}, level::Int, cols::Integer)
395347
nindent = min(cols>>1, level)
396348
ndigcounts = ndigits(maximum(counts))
397349
ndigline = maximum([tree_format_linewidth(x) for x in lilist])
@@ -412,20 +364,22 @@ function tree_format(lilist::Vector{LineInfo}, counts::Vector{Int}, level::Int,
412364
if showextra
413365
base = string(base, "+", nextra, " ")
414366
end
415-
if li.line == li.ip
367+
if li.line == li.pointer
416368
strs[i] = string(base,
417369
rpad(string(counts[i]), ndigcounts, " "),
418-
" ","unknown function (ip: 0x",hex(li.ip,2*sizeof(Ptr{Void})),
370+
" ",
371+
"unknown function (pointer: 0x",
372+
hex(li.pointer,2*sizeof(Ptr{Void})),
419373
")")
420374
else
421-
fname = li.func
375+
fname = string(li.func)
422376
if !li.fromC && !isnull(li.outer_linfo)
423377
fname = sprint(show_spec_linfo, Symbol(li.func), get(li.outer_linfo))
424378
end
425379
strs[i] = string(base,
426380
rpad(string(counts[i]), ndigcounts, " "),
427381
" ",
428-
rtruncto(li.file, widthfile),
382+
rtruncto(string(li.file), widthfile),
429383
":",
430384
li.line == -1 ? "?" : string(li.line),
431385
"; ",
@@ -446,7 +400,7 @@ function tree{T<:Unsigned}(io::IO, bt::Vector{Vector{T}}, counts::Vector{Int}, l
446400
# Organize backtraces into groups that are identical up to this level
447401
if combine
448402
# Combine based on the line information
449-
d = Dict{LineInfo,Vector{Int}}()
403+
d = Dict{StackFrame,Vector{Int}}()
450404
for i = 1:length(bt)
451405
ip = bt[i][level+1]
452406
key = lidict[ip]
@@ -459,7 +413,7 @@ function tree{T<:Unsigned}(io::IO, bt::Vector{Vector{T}}, counts::Vector{Int}, l
459413
end
460414
# Generate counts
461415
dlen = length(d)
462-
lilist = Array(LineInfo, dlen)
416+
lilist = Array(StackFrame, dlen)
463417
group = Array(Vector{Int}, dlen)
464418
n = Array(Int, dlen)
465419
i = 1
@@ -483,7 +437,7 @@ function tree{T<:Unsigned}(io::IO, bt::Vector{Vector{T}}, counts::Vector{Int}, l
483437
end
484438
# Generate counts, and do the code lookup
485439
dlen = length(d)
486-
lilist = Array(LineInfo, dlen)
440+
lilist = Array(StackFrame, dlen)
487441
group = Array(Vector{Int}, dlen)
488442
n = Array(Int, dlen)
489443
i = 1
@@ -534,7 +488,7 @@ function tree{T<:Unsigned}(io::IO, data::Vector{T}, lidict::Dict, C::Bool, combi
534488
end
535489

536490
function callersf(matchfunc::Function, bt::Vector{UInt}, lidict)
537-
counts = Dict{LineInfo, Int}()
491+
counts = Dict{StackFrame, Int}()
538492
lastmatched = false
539493
for id in bt
540494
if id == 0
@@ -574,8 +528,10 @@ function ltruncto(str::ByteString, w::Int)
574528
end
575529

576530

531+
truncto(str::Symbol, w::Int) = truncto(string(str), w)
532+
577533
# Order alphabetically (file, function) and then by line number
578-
function liperm(lilist::Vector{LineInfo})
534+
function liperm(lilist::Vector{StackFrame})
579535
comb = Array(ByteString, length(lilist))
580536
for i = 1:length(lilist)
581537
li = lilist[i]
@@ -594,7 +550,7 @@ warning_empty() = warn("""
594550
Profile.init().""")
595551

596552
function purgeC(data, lidict)
597-
keep = Bool[d == 0 || lidict[d].fromC == false for d in data]
553+
keep = Bool[d == 0 || lidict[d].from_c == false for d in data]
598554
data[keep]
599555
end
600556

base/stacktraces.jl

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# This file is a part of Julia. License is MIT: http://julialang.org/license
2+
3+
module StackTraces
4+
5+
6+
import Base: hash, ==, show
7+
8+
export StackTrace, StackFrame, stacktrace, catch_stacktrace
9+
10+
"""
11+
StackFrame
12+
13+
Stack information representing execution context.
14+
"""
15+
immutable StackFrame
16+
"the name of the function containing the execution context"
17+
func::Symbol
18+
"the LambdaStaticData for the the execution context"
19+
outer_linfo::Nullable{LambdaStaticData}
20+
"the path to the file containing the execution context"
21+
file::Symbol
22+
"the line number in the file containing the execution context"
23+
line::Int
24+
"the path to the file containing the context for code inlined into the execution context"
25+
inlined_file::Symbol
26+
"the line number in the file containing the context for code inlined into the execution context"
27+
inlined_line::Int
28+
"true if the code is from C"
29+
from_c::Bool
30+
"representation of the pointer to the execution context as returned by `backtrace`"
31+
pointer::Int64 # Large enough to be read losslessly on 32- and 64-bit machines.
32+
end
33+
34+
StackFrame(func, file, line) = StackFrame(func, file, line, symbol(""), -1, false, 0)
35+
36+
"""
37+
StackTrace
38+
39+
An alias for `Vector{StackFrame}` provided for convenience; returned by calls to
40+
`stacktrace` and `catch_stacktrace`.
41+
"""
42+
typealias StackTrace Vector{StackFrame}
43+
44+
const empty_symbol = symbol("")
45+
const unk_symbol = symbol("???")
46+
const UNKNOWN = StackFrame(unk_symbol, Nullable{LambdaStaticData}(), unk_symbol, 0, empty_symbol, -1, true, 0)
47+
48+
49+
#=
50+
If the StackFrame has function and line information, we consider two of them the same if
51+
they share the same function/line information. For unknown functions, line == pointer, so we
52+
never actually need to consider the pointer field.
53+
=#
54+
function ==(a::StackFrame, b::StackFrame)
55+
a.line == b.line && a.from_c == b.from_c && a.func == b.func && a.file == b.file
56+
end
57+
58+
function hash(frame::StackFrame, h::UInt)
59+
h += 0xf4fbda67fe20ce88 % UInt
60+
h = hash(frame.line, h)
61+
h = hash(frame.file, h)
62+
h = hash(frame.func, h)
63+
end
64+
65+
66+
"""
67+
lookup(pointer::Union{Ptr{Void}, UInt}) -> StackFrame
68+
69+
Given a pointer to an execution context (usually generated by a call to `backtrace`), looks
70+
up stack frame context information.
71+
"""
72+
function lookup(pointer::Ptr{Void})
73+
info = ccall(:jl_lookup_code_address, Any, (Ptr{Void}, Cint), ip, false)
74+
if length(info) == 8
75+
is_inlined = (info[4] !== empty_sym)
76+
return LineInfo(string(info[1]),
77+
info[6] === nothing ? Nullable{LambdaStaticData}() : Nullable{LambdaStaticData}(info[6]),
78+
is_inlined ? info[4] : info[2],
79+
Int(is_inlined ? info[5] : info[3]),
80+
is_inlined ? info[2] : "",
81+
is_inlined ? Int(info[3]) : -1,
82+
info[7], Int64(info[8]))
83+
else
84+
return UNKNOWN
85+
end
86+
end
87+
88+
lookup(pointer::UInt) = lookup(convert(Ptr{Void}, pointer))
89+
90+
"""
91+
stacktrace([trace::Vector{Ptr{Void}},] [c_funcs::Bool=false]) -> StackTrace
92+
93+
Returns a stack trace in the form of a vector of `StackFrame`s. (By default stacktrace
94+
doesn't return C functions, but this can be enabled.) When called without specifying a
95+
trace, `stacktrace` first calls `backtrace`.
96+
"""
97+
function stacktrace(trace::Vector{Ptr{Void}}, c_funcs::Bool=false)
98+
stack = map(lookup, trace)::StackTrace
99+
100+
# Remove frames that come from C calls.
101+
if !c_funcs
102+
filter!(frame -> !frame.from_c, stack)
103+
end
104+
105+
# Remove frame for this function (and any functions called by this function).
106+
remove_frames!(stack, :stacktrace)
107+
end
108+
109+
stacktrace(c_funcs::Bool=false) = stacktrace(backtrace(), c_funcs)
110+
111+
"""
112+
catch_stacktrace([c_funcs::Bool=false]) -> StackTrace
113+
114+
Returns the stack trace for the most recent error thrown, rather than the current execution
115+
context.
116+
"""
117+
catch_stacktrace(c_funcs::Bool=false) = stacktrace(catch_backtrace(), c_funcs)
118+
119+
"""
120+
remove_frames!(stack::StackTrace, name::Symbol)
121+
122+
Takes a `StackTrace` (a vector of `StackFrames`) and a function name (a `Symbol`) and
123+
removes the `StackFrame` specified by the function name from the `StackTrace` (also removing
124+
all frames above the specified function). Primarily used to remove `StackTraces` functions
125+
from the `StackTrace` prior to returning it.
126+
"""
127+
function remove_frames!(stack::StackTrace, name::Symbol)
128+
splice!(stack, 1:findlast(frame -> frame.func == name, stack))
129+
return stack
130+
end
131+
132+
function remove_frames!(stack::StackTrace, names::Vector{Symbol})
133+
splice!(stack, 1:findlast(frame -> frame.func in names, stack))
134+
return stack
135+
end
136+
137+
function show(io::IO, frame::StackFrame; full_path::Bool=false)
138+
file_info = "$(full_path ? frame.file : basename(string(frame.file))):$(frame.line)"
139+
140+
if frame.inlined_file != Symbol("")
141+
inline_info = string("[inlined code from ", file_info, "] ")
142+
file_info = string(
143+
full_path ? frame.inlined_file : basename(string(frame.inlined_file)),
144+
":", frame.inlined_line
145+
)
146+
else
147+
inline_info = ""
148+
end
149+
150+
print(io, string(inline_info, frame.func != "" ? frame.func : "?", " at ", file_info))
151+
end
152+
153+
154+
end

0 commit comments

Comments
 (0)