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

v0.2.0 new features and fixes #64

Merged
merged 14 commits into from
Sep 25, 2023
Merged
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
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "AbbreviatedStackTraces"
uuid = "ac637c84-cc71-43bf-9c33-c1b4316be3d4"
authors = ["Nicholas Bauer <[email protected]>"]
version = "0.1.14"
version = "0.2.0"

[deps]
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
Expand Down
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)

<img width="848" alt="image" src="https://github.com/BioTurboNick/AbbreviatedStackTraces.jl/assets/1438610/7d32ab8d-ff92-47d1-93b1-d683edc6cb85">

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)
<img width="736" alt="image" src="https://github.com/BioTurboNick/AbbreviatedStackTraces.jl/assets/1438610/a03ff4ca-9113-4546-9269-00526b7323b4">

(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`.

<img width="737" alt="image" src="https://github.com/BioTurboNick/AbbreviatedStackTraces.jl/assets/1438610/66b77163-e3a1-424a-9c28-7df52caa1ebb">

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)

<img width="838" alt="image" src="https://github.com/BioTurboNick/AbbreviatedStackTraces.jl/assets/1438610/9379b2a9-7880-4122-8727-64cd6c5fed18">



Here's an example a beginner might readily run into:
![image](https://user-images.githubusercontent.com/1438610/121451945-8a5e0300-c96c-11eb-9070-d431b1cadc56.png)

<img width="845" alt="image" src="https://github.com/BioTurboNick/AbbreviatedStackTraces.jl/assets/1438610/b6af91a2-bff2-4a0f-91fd-c33b8727e165">

**Yikes!**

With this package:
![image](https://user-images.githubusercontent.com/1438610/121452028-b4172a00-c96c-11eb-961b-300cbcbf5ad9.png)

<img width="845" alt="image" src="https://github.com/BioTurboNick/AbbreviatedStackTraces.jl/assets/1438610/ec413046-bb1e-43e6-bc93-ca29852a69c7">

**Much better!**
124 changes: 106 additions & 18 deletions src/AbbreviatedStackTraces.jl
Original file line number Diff line number Diff line change
Expand Up @@ -108,51 +108,139 @@ 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))

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)

Expand Down
Loading