Skip to content

Commit

Permalink
[WIP] include ClearStacktrace.jl in Base (#36134)
Browse files Browse the repository at this point in the history
* tweak how stacktraces are printed


Co-authored-by: KristofferC <kcarlsson89@gmail.com>
  • Loading branch information
jkrumbiegel and KristofferC authored Jul 2, 2020
1 parent 7dba988 commit 30b09df
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 90 deletions.
189 changes: 151 additions & 38 deletions base/errorshow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -538,15 +538,6 @@ function show_method_candidates(io::IO, ex::MethodError, @nospecialize kwargs=()
end
end

function show_trace_entry(io, frame, n; prefix = "")
if haskey(io, :LAST_SHOWN_LINE_INFOS)
push!(io[:LAST_SHOWN_LINE_INFOS], (string(frame.file), frame.line))
end
print(io, "\n", prefix)
show(io, frame, full_path=true)
n > 1 && print(io, " (repeats ", n, " times)")
end

# In case the line numbers in the source code have changed since the code was compiled,
# allow packages to set a callback function that corrects them.
# (Used by Revise and perhaps other packages.)
Expand All @@ -558,9 +549,44 @@ end
# replace `sf` as needed.
const update_stackframes_callback = Ref{Function}(identity)

function replaceuserpath(str)
str = replace(str, homedir() => "~")
# seems to be necessary for some paths with small letter drive c:// etc
str = replace(str, lowercasefirst(homedir()) => "~")
return str
end

const STACKTRACE_MODULECOLORS = [:light_blue, :light_yellow,
:light_magenta, :light_green, :light_cyan, :light_red,
:blue, :yellow, :magenta, :green, :cyan, :red]
stacktrace_expand_basepaths()::Bool =
tryparse(Bool, get(ENV, "JULIA_STACKTRACE_EXPAND_BASEPATHS", "false")) === true
stacktrace_contract_userdir()::Bool =
tryparse(Bool, get(ENV, "JULIA_STACKTRACE_CONTRACT_HOMEDIR", "true")) === true
stacktrace_linebreaks()::Bool =
tryparse(Bool, get(ENV, "JULIA_STACKTRACE_LINEBREAKS", "false")) === true

function show_full_backtrace(io::IO, trace; print_linebreaks::Bool)
n = length(trace)
ndigits_max = ndigits(n)

modulecolordict = Dict{Module, Symbol}()
modulecolorcycler = Iterators.Stateful(Iterators.cycle(STACKTRACE_MODULECOLORS))

println(io, "\nStacktrace:")

for (i, frame) in enumerate(trace)
print_stackframe(io, i, frame, 1, ndigits_max, modulecolordict, modulecolorcycler)
if i < n
println(io)
print_linebreaks && println(io)
end
end
end

const BIG_STACKTRACE_SIZE = 50 # Arbitrary constant chosen here

function show_reduced_backtrace(io::IO, t::Vector, with_prefix::Bool)
function show_reduced_backtrace(io::IO, t::Vector)
recorded_positions = IdDict{UInt, Vector{Int}}()
#= For each frame of hash h, recorded_positions[h] is the list of indices i
such that hash(t[i-1]) == h, ie the list of positions in which the
Expand Down Expand Up @@ -606,32 +632,133 @@ function show_reduced_backtrace(io::IO, t::Vector, with_prefix::Bool)

try invokelatest(update_stackframes_callback[], displayed_stackframes) catch end

println(io, "\nStacktrace:")

ndigits_max = ndigits(length(t))

modulecolordict = Dict{Module, Symbol}()
modulecolorcycler = Iterators.Stateful(Iterators.cycle(STACKTRACE_MODULECOLORS))

push!(repeated_cycle, (0,0,0)) # repeated_cycle is never empty
frame_counter = 1
for i in 1:length(displayed_stackframes)
(frame, n) = displayed_stackframes[i]
if with_prefix
show_trace_entry(io, frame, n, prefix = string(" [", frame_counter, "] "))
else
show_trace_entry(io, frame, n)

print_stackframe(io, frame_counter, frame, n, ndigits_max, modulecolordict, modulecolorcycler)

if i < length(displayed_stackframes)
println(io)
stacktrace_linebreaks() && println(io)
end

while repeated_cycle[1][1] == i # never empty because of the initial (0,0,0)
cycle_length = repeated_cycle[1][2]
repetitions = repeated_cycle[1][3]
popfirst!(repeated_cycle)
print(io, "\n ... (the last ", cycle_length, " lines are repeated ",
repetitions, " more time", repetitions>1 ? "s)" : ")")
printstyled(io,
"--- the last ", cycle_length, " lines are repeated ",
repetitions, " more time", repetitions>1 ? "s" : "", " ---", color = :light_black)
if i < length(displayed_stackframes)
println(io)
stacktrace_linebreaks() && println(io)
end
frame_counter += cycle_length * repetitions
end
frame_counter += 1
end
end


# Print a stack frame where the module color is determined by looking up the parent module in
# `modulecolordict`. If the module does not have a color, yet, a new one can be drawn
# from `modulecolorcycler`.
function print_stackframe(io, i, frame, n, digit_align_width, modulecolordict, modulecolorcycler)
m = Base.parentmodule(frame)
if m !== nothing
while parentmodule(m) !== m
pm = parentmodule(m)
pm == Main && break
m = pm
end
if !haskey(modulecolordict, m)
modulecolordict[m] = popfirst!(modulecolorcycler)
end
modulecolor = modulecolordict[m]
else
modulecolor = :default
end
print_stackframe(io, i, frame, n, digit_align_width, modulecolor)
end


# Print a stack frame where the module color is set manually with `modulecolor`.
function print_stackframe(io, i, frame, n, digit_align_width, modulecolor)
file, line = string(frame.file), frame.line
stacktrace_expand_basepaths() && (file = something(find_source_file(file), file))
stacktrace_contract_userdir() && (file = replaceuserpath(file))

# Used by the REPL to make it possible to open
# the location of a stackframe/method in the editor.
if haskey(io, :LAST_SHOWN_LINE_INFOS)
push!(io[:LAST_SHOWN_LINE_INFOS], (string(frame.file), frame.line))
end

inlined = getfield(frame, :inlined)
modul = parentmodule(frame)

# frame number
print(io, " ", lpad("[" * string(i) * "]", digit_align_width + 2))
print(io, " ")

StackTraces.show_spec_linfo(IOContext(io, :backtrace=>true), frame)
if n > 1
printstyled(io, " (repeats $n times)"; color=:light_black)
end
println(io)

# @
printstyled(io, " " ^ (digit_align_width + 2) * "@ ", color = :light_black)

# module
if modul !== nothing
printstyled(io, modul, color = modulecolor)
print(io, " ")
end

# filepath
pathparts = splitpath(file)
folderparts = pathparts[1:end-1]
if !isempty(folderparts)
printstyled(io, joinpath(folderparts...) * (Sys.iswindows() ? "\\" : "/"), color = :light_black)
end

# filename, separator, line
# use escape codes for formatting, printstyled can't do underlined and color
# codes are bright black (90) and underlined (4)
function print_underlined(io::IO, s...)
colored = get(io, :color, false)::Bool
start_s = colored ? "\033[90;4m" : ""
end_s = colored ? "\033[0m" : ""
print(io, start_s, s..., end_s)
end
print_underlined(io, pathparts[end], ":", line)

# inlined
printstyled(io, inlined ? " [inlined]" : "", color = :light_black)
end


function show_backtrace(io::IO, t::Vector)
if haskey(io, :LAST_SHOWN_LINE_INFOS)
resize!(io[:LAST_SHOWN_LINE_INFOS], 0)
end
filtered = process_backtrace(t)

# t is a pre-processed backtrace (ref #12856)
if t isa Vector{Any}
filtered = t
else
filtered = process_backtrace(t)
end
isempty(filtered) && return

if length(filtered) == 1 && StackTraces.is_top_level_frame(filtered[1][1])
Expand All @@ -642,32 +769,18 @@ function show_backtrace(io::IO, t::Vector)
end
end

print(io, "\nStacktrace:")
if length(filtered) < BIG_STACKTRACE_SIZE
# Fast track: no duplicate stack frame detection.
try invokelatest(update_stackframes_callback[], filtered) catch end
frame_counter = 0
for (last_frame, n) in filtered
frame_counter += 1
show_trace_entry(IOContext(io, :backtrace => true), last_frame, n, prefix = string(" [", frame_counter, "] "))
end
if length(filtered) > BIG_STACKTRACE_SIZE
show_reduced_backtrace(IOContext(io, :backtrace => true), filtered)
return
end

show_reduced_backtrace(IOContext(io, :backtrace => true), filtered, true)
try invokelatest(update_stackframes_callback[], filtered) catch end
# process_backtrace returns a Vector{Tuple{Frame, Int}}
frames = first.(filtered)
show_full_backtrace(io, frames; print_linebreaks = stacktrace_linebreaks())
return
end

function show_backtrace(io::IO, t::Vector{Any})
# t is a pre-processed backtrace (ref #12856)
if length(t) < BIG_STACKTRACE_SIZE
try invokelatest(update_stackframes_callback[], t) catch end
for entry in t
show_trace_entry(io, entry...)
end
else
show_reduced_backtrace(io, t, false)
end
end

function is_kw_sorter_name(name::Symbol)
sn = string(name)
Expand Down
2 changes: 1 addition & 1 deletion base/loading.jl
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,7 @@ end
function find_source_file(path::AbstractString)
(isabspath(path) || isfile(path)) && return path
base_path = joinpath(Sys.BINDIR::String, DATAROOTDIR, "julia", "base", path)
return isfile(base_path) ? base_path : nothing
return isfile(base_path) ? normpath(base_path) : nothing
end

cache_file_entry(pkg::PkgId) = joinpath(
Expand Down
33 changes: 21 additions & 12 deletions base/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2011,11 +2011,17 @@ function show_signature_function(io::IO, @nospecialize(ft), demangle=false, farg
nothing
end

function show_tuple_as_call(io::IO, name::Symbol, sig::Type, demangle=false, kwargs=nothing)
function print_within_stacktrace(io, s...; color, bold=false)
if get(io, :backtrace, false)::Bool
printstyled(io, s...; color, bold)
else
print(io, s...)
end
end
function show_tuple_as_call(io::IO, name::Symbol, sig::Type, demangle=false, kwargs=nothing, argnames=nothing)
# print a method signature tuple for a lambda definition
color = get(io, :color, false) && get(io, :backtrace, false) ? stackframe_function_color() : :nothing
if sig === Tuple
printstyled(io, demangle ? demangle_function_name(name) : name, "(...)", color=color)
print(io, demangle ? demangle_function_name(name) : name, "(...)")
return
end
tv = Any[]
Expand All @@ -2026,28 +2032,31 @@ function show_tuple_as_call(io::IO, name::Symbol, sig::Type, demangle=false, kwa
sig = sig.body
end
sig = sig.parameters
with_output_color(color, env_io) do io
show_signature_function(io, sig[1], demangle)
end
show_signature_function(env_io, sig[1], demangle)
first = true
print_style = get(io, :color, false) && get(io, :backtrace, false) ? :bold : :nothing
printstyled(io, "(", color=print_style)
print_within_stacktrace(io, "(", color=:light_black)
show_argnames = argnames !== nothing && length(argnames) == length(sig)
for i = 2:length(sig) # fixme (iter): `eachindex` with offset?
first || print(io, ", ")
first = false
print(env_io, "::", sig[i])
if show_argnames
print_within_stacktrace(io, argnames[i]; bold=true, color=:light_black)
end
print(io, "::")
print_within_stacktrace(env_io, sig[i]; color=:light_black)
end
if kwargs !== nothing
print(io, "; ")
first = true
for (k, t) in kwargs
first || print(io, ", ")
first = false
print(io, k, "::")
show(io, t)
print_within_stacktrace(io, k; bold=true, color=:light_black)
print(io, "::")
print_within_stacktrace(io, t; color=:light_black)
end
end
printstyled(io, ")", color=print_style)
print_within_stacktrace(io, ")", color=:light_black)
show_method_params(io, tv)
nothing
end
Expand Down
28 changes: 12 additions & 16 deletions base/stacktraces.jl
Original file line number Diff line number Diff line change
Expand Up @@ -216,30 +216,28 @@ function show_spec_linfo(io::IO, frame::StackFrame)
elseif frame.func === top_level_scope_sym
print(io, "top-level scope")
else
color = get(io, :color, false) && get(io, :backtrace, false) ?
Base.stackframe_function_color() :
:nothing
printstyled(io, Base.demangle_function_name(string(frame.func)), color=color)
print(io, Base.demangle_function_name(string(frame.func)))
end
elseif frame.linfo isa MethodInstance
def = frame.linfo.def
if isa(def, Method)
sig = frame.linfo.specTypes
argnames = Base.method_argnames(def)
if def.nkw > 0
# rearrange call kw_impl(kw_args..., func, pos_args...) to func(pos_args...)
kwarg_types = Any[ fieldtype(sig, i) for i = 2:(1+def.nkw) ]
uw = Base.unwrap_unionall(sig)
pos_sig = Base.rewrap_unionall(Tuple{uw.parameters[(def.nkw+2):end]...}, sig)
kwnames = Base.method_argnames(def)[2:(def.nkw+1)]
kwnames = argnames[2:(def.nkw+1)]
for i = 1:length(kwnames)
str = string(kwnames[i])
if endswith(str, "...")
kwnames[i] = Symbol(str[1:end-3])
end
end
Base.show_tuple_as_call(io, def.name, pos_sig, true, zip(kwnames, kwarg_types))
Base.show_tuple_as_call(io, def.name, pos_sig, true, zip(kwnames, kwarg_types), argnames[def.nkw+2:end])
else
Base.show_tuple_as_call(io, def.name, sig, true)
Base.show_tuple_as_call(io, def.name, sig, true, nothing, argnames)
end
else
Base.show(io, frame.linfo)
Expand All @@ -249,18 +247,16 @@ function show_spec_linfo(io::IO, frame::StackFrame)
end
end

function show(io::IO, frame::StackFrame; full_path::Bool=false)
function show(io::IO, frame::StackFrame)
show_spec_linfo(io, frame)
if frame.file !== empty_sym
file_info = full_path ? string(frame.file) : basename(string(frame.file))
file_info = basename(string(frame.file))
print(io, " at ")
Base.with_output_color(get(io, :color, false) && get(io, :backtrace, false) ? Base.stackframe_lineinfo_color() : :nothing, io) do io
print(io, file_info, ":")
if frame.line >= 0
print(io, frame.line)
else
print(io, "?")
end
print(io, file_info, ":")
if frame.line >= 0
print(io, frame.line)
else
print(io, "?")
end
end
if frame.inlined
Expand Down
10 changes: 0 additions & 10 deletions doc/src/manual/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,16 +245,6 @@ should have at the terminal.
The formatting `Base.answer_color()` (default: normal, `"\033[0m"`) that output
should have at the terminal.

### `JULIA_STACKFRAME_LINEINFO_COLOR`

The formatting `Base.stackframe_lineinfo_color()` (default: bold, `"\033[1m"`)
that line info should have during a stack trace at the terminal.

### `JULIA_STACKFRAME_FUNCTION_COLOR`

The formatting `Base.stackframe_function_color()` (default: bold, `"\033[1m"`)
that function calls should have during a stack trace at the terminal.

## Debugging and profiling

### `JULIA_DEBUG`
Expand Down
Loading

0 comments on commit 30b09df

Please sign in to comment.