diff --git a/Project.toml b/Project.toml index ab46e9a..63a0c76 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "AbbreviatedStackTraces" uuid = "ac637c84-cc71-43bf-9c33-c1b4316be3d4" authors = ["Nicholas Bauer "] -version = "0.1.14" +version = "0.2.0" [deps] Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" diff --git a/README.md b/README.md index 6217a16..ad5e4ac 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,11 @@ But in the rarer case where the issue was *not* in your code, the full trace can ## Options * `ENV["JULIA_STACKTRACE_ABBREVIATED"] = true` enables abbreviated stack traces for all traces, not just those originating from an interactive session * `ENV["JULIA_STACKTRACE_MINIMAL"] = true` omits type information for a one-line-per-frame minimal variant (see below) +* `ENV["JULIA_STACKTRACE_PUBLIC"] = true` will re-insert all functions from a module's public API (part of `names(module)`; Julia < 1.11, this will just be exported names) + +## startup.jl and VSCode +Unfortunately, startup.jl is executed before VSCodeServer loads, which means the appropriate methods won't be overwritten. +Some workarounds are discussed here: https://github.com/BioTurboNick/AbbreviatedStackTraces.jl/issues/38 ## Examples @@ -43,26 +48,35 @@ using AbbreviatedStackTraces # over-writes error-related `Base` methods using BenchmarkTools, Plots @btime plot([1,2,3], seriestype=:blah) ``` -![image](https://user-images.githubusercontent.com/1438610/115907559-0c36b300-a437-11eb-87c3-ba314ab6db72.png) + +image It aims to find the stack frames of code you don't control and excludes them by default, except for the first frame into that package. In it's place, it lists the modules called in the intervening frames. The theory is that errors in your code are much more likely than errors inside Base, the Stdlibs, or published packages, so their internals are usually superfluous. -![image](https://user-images.githubusercontent.com/1438610/116329328-1dfeba00-a799-11eb-8b86-f5c28e5b78e0.png) +image + +(Note: italics only works on Julia 1.10+) The global `err` variable stores the last error and can show the full, original stack trace easily. +You can also add back functions with public (Julia 1.11) or exported (Julia 1.9, 1.10) names be setting `ENV["JULIA_STACKTRACE_PUBLIC"] = true`. + +image + There is an optional minimal display available, accessed by setting `ENV["JULIA_STACKTRACE_MINIMAL"] = true`. -![image](https://user-images.githubusercontent.com/1438610/116329297-0b848080-a799-11eb-9d71-32650092b3a5.png) +image Here's an example a beginner might readily run into: -![image](https://user-images.githubusercontent.com/1438610/121451945-8a5e0300-c96c-11eb-9070-d431b1cadc56.png) + +image **Yikes!** With this package: -![image](https://user-images.githubusercontent.com/1438610/121452028-b4172a00-c96c-11eb-961b-300cbcbf5ad9.png) + +image **Much better!** diff --git a/src/AbbreviatedStackTraces.jl b/src/AbbreviatedStackTraces.jl index 6eb1ee6..a8189c6 100644 --- a/src/AbbreviatedStackTraces.jl +++ b/src/AbbreviatedStackTraces.jl @@ -108,11 +108,87 @@ function find_visible_frames(trace::Vector) return visible_frames_i end +function find_visible_frames_public(trace::Vector) + public_frames_i = findall(trace) do frame + framemodule = parentmodule(frame[1]) + framemodule === nothing && return false + module_public_names = names(framemodule) + frame[1].func ∈ module_public_names + end + + user_frames_i = findall(trace) do frame + file = String(frame[1].file) + !is_julia(file) && !is_ide_support(file) + end + + # construct set of visible modules + all_modules = convert(Vector{Module}, filter!(!isnothing, unique(t[1] |> parentmodule for t ∈ trace))) + user_modules = convert(Vector{Module}, filter!(!isnothing, unique(t[1] |> parentmodule for t ∈ @view trace[user_frames_i]))) + Main ∈ user_modules || push!(user_modules, Main) + + debug_entries = split(get(ENV, "JULIA_DEBUG", ""), ",") + debug_include = filter(x -> !startswith(x, "!"), debug_entries) + debug_exclude = lstrip.(filter!(x -> startswith(x, "!"), debug_entries), '!') + + debug_include_modules = filter(m -> string(m) ∈ debug_include, all_modules) + debug_exclude_modules = filter(m -> string(m) ∈ debug_exclude, all_modules) + setdiff!(union!(user_modules, debug_include_modules), debug_exclude_modules) + + # construct set of visible frames + visible_frames_i = findall(trace) do frame + file = String(frame[1].file) + filenamebase = file |> basename |> splitext |> first + mod = parentmodule(frame[1]) + return (mod ∈ user_modules || filenamebase ∈ debug_include) && + !(filenamebase ∈ debug_exclude) || + is_top_level_frame(frame[1]) && is_repl(file) || + !is_julia(file) && !is_ide_support(file) + end + + # add one additional frame above each contiguous set of user code frames, removing 0. + filter!(>(0), sort!(union!(visible_frames_i, visible_frames_i .- 1))) + + # remove Main frames that originate from internal code (e.g. BenchmarkTools) + filter!(i -> parentmodule(trace[i][1]) != Main || !is_julia(string(trace[i][1].file)), visible_frames_i) + + # for each appearance of an already-visible `materialize` broadcast frame, include + # the next immediate hidden frame after the last `broadcast` frame + broadcasti = [] + for i ∈ visible_frames_i + trace[i][1].func == :materialize || continue + push!(broadcasti, findlast(trace[1:i - 1]) do frame + !is_broadcast(String(frame[1].file)) + end) + end + sort!(union!(visible_frames_i, filter!(!isnothing, broadcasti))) + + # add back public frames + sort!(union!(visible_frames_i, public_frames_i)) + + if length(trace) > 1 && visible_frames_i[end] != length(trace) + # add back the top level if it's not included (as can happen if a macro is expanded at top-level) + push!(visible_frames_i, length(trace)) + end + + if length(visible_frames_i) > 0 && visible_frames_i[end] == length(trace) + # remove REPL-based top-level + # note: file field for top-level is different from the rest, doesn't include ./ + startswith(String(trace[end][1].file), "REPL") && pop!(visible_frames_i) + end + + if length(visible_frames_i) == 1 && trace[only(visible_frames_i)][1].func == :materialize + # remove a materialize frame if it is the only visible frame + pop!(visible_frames_i) + end + + return visible_frames_i +end + function show_compact_backtrace(io::IO, trace::Vector; print_linebreaks::Bool) #= Show the lowest stackframe and display a message telling user how to retrieve the full trace =# num_frames = length(trace) - ndigits_max = ndigits(num_frames) * 2 + 1 + ndigits_max = ndigits(num_frames) modulecolordict = copy(STACKTRACE_FIXEDCOLORS) modulecolorcycler = Iterators.Stateful(Iterators.cycle(STACKTRACE_MODULECOLORS)) @@ -120,39 +196,51 @@ function show_compact_backtrace(io::IO, trace::Vector; print_linebreaks::Bool) function print_omitted_modules(i, j) # Find modules involved in intermediate frames and print them modules = filter!(!isnothing, unique(t[1] |> parentmodule for t ∈ @view trace[i:j])) - if i < j - print(io, " " ^ (ndigits_max - ndigits(i) - ndigits(j))) - print(io, "[" * string(i) * "-" * string(j) * "] ") + print(io, " " ^ (ndigits_max + 4)) + printstyled(io, "⋮ ", bold = true) + if VERSION ≥ v"1.10" + printstyled(io, "internal", color = :light_black, italic=true) else - print(io, " " ^ (ndigits_max - ndigits(i) + 1)) - print(io, "[" * string(i) * "] ") + printstyled(io, "internal", color = :light_black) end - printstyled(io, "⋮ ", bold = true) - printstyled(io, "internal", color = :light_black) - if !parse(Bool, get(ENV, "JULIA_STACKTRACE_MINIMAL", "false")) - println(io) - print(io, " " ^ (ndigits_max + 2)) + print(io, " ") + if VERSION ≥ v"1.10" + printstyled(io, "@ ", color = :light_black, italic=true) else - print(io, " ") + printstyled(io, "@ ", color = :light_black) end - printstyled(io, "@ ", color = :light_black) if length(modules) > 0 for (i, m) ∈ enumerate(modules) modulecolor = get_modulecolor!(modulecolordict, m, modulecolorcycler) - printstyled(io, m, color = modulecolor) - i < length(modules) && print(io, ", ") + if VERSION ≥ v"1.10" + printstyled(io, m, color = modulecolor, italic=true) + i < length(modules) && printstyled(io, ", ", color = :light_black, italic=true) + else + printstyled(io, m, color = modulecolor) + i < length(modules) && printstyled(io, ", ", color = :light_black) + end + end end # indicate presence of inlined methods which lack module information # (they all do right now) if any(isnothing(parentmodule(t[1])) for t ∈ @view trace[i:j]) - length(modules) > 0 && print(io, ", ") - printstyled(io, "Unknown", color = :light_black) + if VERSION ≥ v"1.10" + length(modules) > 0 && printstyled(io, ", ", color = :light_black, italic=true) + printstyled(io, "Unknown", color = :light_black, italic=true) + else + length(modules) > 0 && printstyled(io, ", ", color = :light_black) + printstyled(io, "Unknown", color = :light_black) + end end end # select frames from user-controlled code - is = find_visible_frames(trace) + if VERSION >= v"1.11.0-DEV.511" + is = parse(Bool, get(ENV, "JULIA_STACKTRACE_PUBLIC", "false")) ? find_visible_frames_public(trace) : find_visible_frames(trace) + else + is = find_visible_frames(trace) + end num_vis_frames = length(is) diff --git a/src/override-errorshow.jl b/src/override-errorshow.jl index f831eed..b8b474d 100644 --- a/src/override-errorshow.jl +++ b/src/override-errorshow.jl @@ -6,6 +6,8 @@ import Base: fixup_stdlib_path, invokelatest, printstyled, + print_module_path_file, + print_stackframe, process_backtrace, show, show_backtrace, @@ -64,193 +66,6 @@ function show_backtrace(io::IO, t::Vector) return end -if VERSION < v"1.10-alpha1" - import Base: - _simplify_include_frames - - # Copied from v1.10-alpha1 - # Collapse frames that have the same location (in some cases) - function _collapse_repeated_frames(trace) - kept_frames = trues(length(trace)) - last_frame = nothing - for i in 1:length(trace) - frame::StackFrame, _ = trace[i] - if last_frame !== nothing && frame.file == last_frame.file && frame.line == last_frame.line - #= - Handles this case: - - f(g, a; kw...) = error(); - @inline f(a; kw...) = f(identity, a; kw...); - f(1) - - which otherwise ends up as: - - [4] #f#4 <-- useless - @ ./REPL[2]:1 [inlined] - [5] f(a::Int64) - @ Main ./REPL[2]:1 - =# - if startswith(sprint(show, last_frame), "#") - kept_frames[i-1] = false - end - - #= Handles this case - g(x, y=1, z=2) = error(); - g(1) - - which otherwise ends up as: - - [2] g(x::Int64, y::Int64, z::Int64) - @ Main ./REPL[1]:1 - [3] g(x::Int64) <-- useless - @ Main ./REPL[1]:1 - =# - if frame.linfo isa MethodInstance && last_frame.linfo isa MethodInstance && - frame.linfo.def isa Method && last_frame.linfo.def isa Method - m, last_m = frame.linfo.def::Method, last_frame.linfo.def::Method - params, last_params = Base.unwrap_unionall(m.sig).parameters, Base.unwrap_unionall(last_m.sig).parameters - if last_m.nkw != 0 - pos_sig_params = last_params[(last_m.nkw+2):end] - issame = true - if pos_sig_params == params - kept_frames[i] = false - end - end - if length(last_params) > length(params) - issame = true - for i = 1:length(params) - issame &= params[i] == last_params[i] - end - if issame - kept_frames[i] = false - end - end - end - - # TODO: Detect more cases that can be collapsed - end - last_frame = frame - end - return trace[kept_frames] - end - - if VERSION < v"1.9" - # copied just to add access to _collapse_repeated_frames, without kwcall - function process_backtrace(t::Vector, limit::Int=typemax(Int); skipC = true) - n = 0 - last_frame = StackTraces.UNKNOWN - count = 0 - ret = Any[] - for i in eachindex(t) - lkups = t[i] - if lkups isa StackFrame - lkups = [lkups] - else - lkups = StackTraces.lookup(lkups) - end - for lkup in lkups - if lkup === StackTraces.UNKNOWN - continue - end - - if (lkup.from_c && skipC) - continue - end - code = lkup.linfo - if code isa MethodInstance - def = code.def - if def isa Method && def.sig <: Tuple{NamedTuple,Any,Vararg} - # hide keyword methods, which are probably internal keyword sorter methods - # (we print the internal method instead, after demangling - # the argument list, since it has the right line number info) - continue - end - end - count += 1 - if count > limit - break - end - - if lkup.file != last_frame.file || lkup.line != last_frame.line || lkup.func != last_frame.func || lkup.linfo !== last_frame.linfo - if n > 0 - push!(ret, (last_frame, n)) - end - n = 1 - last_frame = lkup - else - n += 1 - end - end - count > limit && break - end - if n > 0 - push!(ret, (last_frame, n)) - end - trace = _simplify_include_frames(ret) - trace = _collapse_repeated_frames(trace) - return trace - end - else - # copied just to add access to _collapse_repeated_frames - function process_backtrace(t::Vector, limit::Int=typemax(Int); skipC = true) - n = 0 - last_frame = StackTraces.UNKNOWN - count = 0 - ret = Any[] - for i in eachindex(t) - lkups = t[i] - if lkups isa StackFrame - lkups = [lkups] - else - lkups = StackTraces.lookup(lkups) - end - for lkup in lkups - if lkup === StackTraces.UNKNOWN - continue - end - - if (lkup.from_c && skipC) - continue - end - code = lkup.linfo - if code isa MethodInstance - def = code.def - if def isa Method && def.name !== :kwcall && def.sig <: Tuple{typeof(Core.kwcall),NamedTuple,Any,Vararg} - # hide kwcall() methods, which are probably internal keyword sorter methods - # (we print the internal method instead, after demangling - # the argument list, since it has the right line number info) - continue - end - elseif !lkup.from_c - lkup.func === :kwcall && continue - end - count += 1 - if count > limit - break - end - - if lkup.file != last_frame.file || lkup.line != last_frame.line || lkup.func != last_frame.func || lkup.linfo !== last_frame.linfo - if n > 0 - push!(ret, (last_frame, n)) - end - n = 1 - last_frame = lkup - else - n += 1 - end - end - count > limit && break - end - if n > 0 - push!(ret, (last_frame, n)) - end - trace = _simplify_include_frames(ret) - trace = _collapse_repeated_frames(trace) - return trace - end - end -end - function print_stackframe(io, i, frame::StackFrame, n::Int, ndigits_max, modulecolor) file, line = string(frame.file), frame.line @@ -269,13 +84,19 @@ function print_stackframe(io, i, frame::StackFrame, n::Int, ndigits_max, modulec print(io, " ", lpad("[" * string(i) * "]", digit_align_width)) print(io, " ") - StackTraces.show_spec_linfo(IOContext(io, :backtrace=>true), frame) + minimal = (get(io, :compacttrace, false) || parse(Bool, get(ENV, "JULIA_STACKTRACE_ABBREVIATED", "false"))) && parse(Bool, get(ENV, "JULIA_STACKTRACE_MINIMAL", "false")) + StackTraces.show_spec_linfo(IOContext(io, :backtrace=>true), frame, minimal) if n > 1 printstyled(io, " (repeats $n times)"; color=:light_black) end # @ Module path / file : line - print_module_path_file(io, modul, file, line; modulecolor, digit_align_width) + if minimal + print_module_path_file(io, modul, file, line; modulecolor, digit_align_width = 1) + else + println(io) + print_module_path_file(io, modul, file, line; modulecolor, digit_align_width) + end # inlined printstyled(io, inlined ? " [inlined]" : "", color = :light_black) diff --git a/src/override-stacktraces.jl b/src/override-stacktraces.jl index ddfd588..c953d2c 100644 --- a/src/override-stacktraces.jl +++ b/src/override-stacktraces.jl @@ -8,13 +8,14 @@ import Base: show_tuple_as_call import Base.StackTraces: + lookup, show_spec_linfo, top_level_scope_sym if VERSION < v"1.10-alpha1" - function show_spec_linfo(io::IO, frame::StackFrame) + function show_spec_linfo(io::IO, frame::StackFrame, minimal = false) linfo = frame.linfo - if linfo === nothing || ((get(io, :compacttrace, false) || parse(Bool, get(ENV, "JULIA_STACKTRACE_ABBREVIATED", "false"))) && parse(Bool, get(ENV, "JULIA_STACKTRACE_MINIMAL", "false"))) #get(io, :minimaltrace, false)) + if linfo === nothing || minimal if frame.func === empty_sym print(io, "ip:0x", string(frame.pointer, base=16)) elseif frame.func === top_level_scope_sym @@ -61,9 +62,9 @@ else import Base.StackTraces: show_spec_sig - function show_spec_linfo(io::IO, frame::StackFrame) + function show_spec_linfo(io::IO, frame::StackFrame, minimal = false) linfo = frame.linfo - if linfo === nothing || ((get(io, :compacttrace, false) || parse(Bool, get(ENV, "JULIA_STACKTRACE_ABBREVIATED", "false"))) && parse(Bool, get(ENV, "JULIA_STACKTRACE_MINIMAL", "false"))) #get(io, :minimaltrace, false)) + if linfo === nothing || minimal if frame.func === empty_sym print(io, "ip:0x", string(frame.pointer, base=16)) elseif frame.func === top_level_scope_sym @@ -87,4 +88,34 @@ else show_spec_sig(io, m, m.sig) end end +end + +if v"1.10-DEV.0" < VERSION < v"1.10" || v"1.11-DEV.0" < VERSION < v"1.11-alpha" + # Fix inline stack frames in development 1.10 and 1.11 releases + Base.@constprop :none function lookup(pointer::Ptr{Cvoid}) + infos = ccall(:jl_lookup_code_address, Any, (Ptr{Cvoid}, Cint), pointer, false)::Core.SimpleVector + pointer = convert(UInt64, pointer) + isempty(infos) && return [Base.StackTraces.StackFrame(empty_sym, empty_sym, -1, nothing, true, false, pointer)] # this is equal to UNKNOWN + parent_linfo = infos[end][4] + inlinetable = Base.StackTraces.get_inlinetable(parent_linfo) + miroots = inlinetable === nothing ? Base.StackTraces.get_method_instance_roots(parent_linfo) : nothing # fallback if linetable missing + res = Vector{Base.StackTraces.StackFrame}(undef, length(infos)) + for i in reverse(1:length(infos)) + info = infos[i]::Core.SimpleVector + @assert(length(info) == 6) + func = info[1]::Symbol + file = info[2]::Symbol + linenum = info[3]::Int + linfo = info[4] + if i < length(infos) + if inlinetable !== nothing + linfo = Base.StackTraces.lookup_inline_frame_info(func, file, linenum, inlinetable) + elseif miroots !== nothing + linfo = Base.StackTraces.lookup_inline_frame_info(func, file, miroots) + end + end + res[i] = Base.StackTraces.StackFrame(func, file, linenum, linfo, info[5]::Bool, info[6]::Bool, pointer) + end + return res + end end \ No newline at end of file diff --git a/src/override-vscode.jl b/src/override-vscode.jl index fe09a33..37b2bea 100644 --- a/src/override-vscode.jl +++ b/src/override-vscode.jl @@ -19,7 +19,6 @@ try stacktrace function display_repl_error(io, err, bt) - ccall(:jl_set_global, Cvoid, (Any, Any, Any), Main, :err, AbbreviatedStackTraces.ExceptionStack([(exception = err, backtrace = bt)])) st = stacktrace(VSCodeServer.crop_backtrace(bt)) printstyled(io, "ERROR: "; bold=true, color=Base.error_color()) showerror(IOContext(io, :limit => true, :compacttrace => true), err, st) @@ -27,7 +26,6 @@ try end function display_repl_error(io, stack::VSCodeServer.EvalErrorStack) printstyled(io, "ERROR: "; bold = true, color = Base.error_color()) - ccall(:jl_set_global, Cvoid, (Any, Any, Any), Main, :err, AbbreviatedStackTraces.ExceptionStack(reverse(stack.stack))) for (i, (err, bt)) in enumerate(reverse(stack.stack)) i !== 1 && print(io, "\ncaused by: ") st = stacktrace(crop_backtrace(bt))