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

Profile.print: color Base/Core & packages. Make paths clickable #55335

Merged
merged 14 commits into from
Sep 4, 2024
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ Standard library changes
* `Profile.take_heap_snapshot` takes a new keyword argument, `redact_data::Bool`,
that is `true` by default. When set, the contents of Julia objects are not emitted
in the heap snapshot. This currently only applies to strings. ([#55326])
* `Profile.print()` now colors Base/Core/Package modules similarly to how they are in stacktraces.
Also paths, even if truncated, are now clickable in terminals that support URI links
to take you to the specified `JULIA_EDITOR` for the given file & line number. ([#55335])

#### Random

Expand Down
6 changes: 6 additions & 0 deletions stdlib/Profile/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ name = "Profile"
uuid = "9abbd945-dff8-562f-b5e8-e1ebf5ef1b79"
version = "1.11.0"

[deps]
StyledStrings = "f489334b-da3d-4c2e-b8f0-e476e12c162b"

[compat]
StyledStrings = "1.11.0"

[extras]
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
Expand Down
2 changes: 1 addition & 1 deletion stdlib/Profile/src/Allocs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ end
function flat(io::IO, data::Vector{Alloc}, cols::Int, fmt::ProfileFormat)
fmt.combine || error(ArgumentError("combine=false"))
lilist, n, m, totalbytes = parse_flat(fmt.combine ? StackFrame : UInt64, data, fmt.C)
filenamemap = Dict{Symbol,String}()
filenamemap = FileNameMap()
IanButterworth marked this conversation as resolved.
Show resolved Hide resolved
if isempty(lilist)
warning_empty()
return true
Expand Down
147 changes: 93 additions & 54 deletions stdlib/Profile/src/Profile.jl
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public clear,
Allocs

import Base.StackTraces: lookup, UNKNOWN, show_spec_linfo, StackFrame
import Base: AnnotatedString
using StyledStrings: @styled_str

const nmeta = 4 # number of metadata fields per block (threadid, taskid, cpu_cycle_clock, thread_sleeping)

Expand All @@ -63,10 +65,10 @@ end

# An internal function called to show the report after an information request (SIGINFO or SIGUSR1).
function _peek_report()
iob = IOBuffer()
iob = Base.AnnotatedIOBuffer()
ioc = IOContext(IOContext(iob, stderr), :displaysize=>displaysize(stderr))
print(ioc, groupby = [:thread, :task])
Base.print(stderr, String(take!(iob)))
Base.print(stderr, read(seekstart(iob), AnnotatedString))
end
# This is a ref so that it can be overridden by other profile info consumers.
const peek_report = Ref{Function}(_peek_report)
Expand Down Expand Up @@ -266,7 +268,7 @@ function print(io::IO,
end
any_nosamples = true
if format === :tree
Base.print(io, "Overhead ╎ [+additional indent] Count File:Line; Function\n")
Base.print(io, "Overhead ╎ [+additional indent] Count File:Line Function\n")
Base.print(io, "=========================================================\n")
end
if groupby == [:task, :thread]
Expand Down Expand Up @@ -503,9 +505,10 @@ end

# Take a file-system path and try to form a concise representation of it
# based on the package ecosystem
function short_path(spath::Symbol, filenamecache::Dict{Symbol, String})
function short_path(spath::Symbol, filenamecache::Dict{Symbol, Tuple{String,String,String}})
return get!(filenamecache, spath) do
path = Base.fixup_stdlib_path(string(spath))
possible_base_path = normpath(joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia", "base", path))
if isabspath(path)
if ispath(path)
# try to replace the file-system prefix with a short "@Module" one,
Expand All @@ -522,20 +525,21 @@ function short_path(spath::Symbol, filenamecache::Dict{Symbol, String})
pkgid = Base.project_file_name_uuid(project_file, "")
isempty(pkgid.name) && return path # bad Project file
# return the joined the module name prefix and path suffix
path = path[nextind(path, sizeof(root)):end]
return string("@", pkgid.name, path)
_short_path = path[nextind(path, sizeof(root)):end]
return path, string("@", pkgid.name), _short_path
end
end
end
end
return path
elseif isfile(joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia", "base", path))
return path, "", path
elseif isfile(possible_base_path)
# do the same mechanic for Base (or Core/Compiler) files as above,
# but they start from a relative path
return joinpath("@Base", normpath(path))
return possible_base_path, "@Base", normpath(path)
else
# for non-existent relative paths (such as "REPL[1]"), just consider simplifying them
return normpath(path) # drop leading "./"
path = normpath(path)
return "", "", path # drop leading "./"
end
end
end
Expand Down Expand Up @@ -678,7 +682,7 @@ function add_fake_meta(data; threadid = 1, taskid = 0xf0f0f0f0)
!isempty(data) && has_meta(data) && error("input already has metadata")
cpu_clock_cycle = UInt64(99)
data_with_meta = similar(data, 0)
for i = 1:length(data)
for i in eachindex(data)
val = data[i]
if iszero(val)
# META_OFFSET_THREADID, META_OFFSET_TASKID, META_OFFSET_CPUCYCLECLOCK, META_OFFSET_SLEEPSTATE
Expand Down Expand Up @@ -756,6 +760,8 @@ function parse_flat(::Type{T}, data::Vector{UInt64}, lidict::Union{LineInfoDict,
return (lilist, n, m, totalshots, nsleeping)
end

const FileNameMap = Dict{Symbol,Tuple{String,String,String}}

function flat(io::IO, data::Vector{UInt64}, lidict::Union{LineInfoDict, LineInfoFlatDict}, cols::Int, fmt::ProfileFormat,
threads::Union{Int,AbstractVector{Int}}, tasks::Union{UInt,AbstractVector{UInt}}, is_subsection::Bool)
lilist, n, m, totalshots, nsleeping = parse_flat(fmt.combine ? StackFrame : UInt64, data, lidict, fmt.C, threads, tasks)
Expand All @@ -766,7 +772,7 @@ function flat(io::IO, data::Vector{UInt64}, lidict::Union{LineInfoDict, LineInfo
m = m[keep]
end
util_perc = (1 - (nsleeping / totalshots)) * 100
filenamemap = Dict{Symbol,String}()
filenamemap = FileNameMap()
if isempty(lilist)
if is_subsection
Base.print(io, "Total snapshots: ")
Expand All @@ -788,9 +794,34 @@ function flat(io::IO, data::Vector{UInt64}, lidict::Union{LineInfoDict, LineInfo
return false
end

# make a terminal-clickable link to the file and linenum.
# Similar to `define_default_editors` in `Base.Filesystem` but for creating URIs not commands
function editor_link(path::String, linenum::Int)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At a glance it looks like path is directly interpolated? This looks a little dodgy to me, what about a path like /tmp/?file=redherring&line=42/file.jl? Will that become say idea://open?file=/tmp/?file=redherring&line=42/file.jl&line=1

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That path isn't a file path? and especially not something that the profiler could return, as far as I understand...?
Though escaping the path generally seems like a good idea.

Also it'd be great to have #55454 finished to use in this function for the generic case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see what you're saying.. a dir named ?file=redherring&line=42. Ok

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also it'd be great to have #55454 finished to use in this function for the generic case.

From my perspective, that PR is good to go, just awaiting further comment or merging.

editor = get(ENV, "JULIA_EDITOR", "")

if editor == "code"
return "vscode://file/$path:$linenum"
elseif editor == "subl" || editor == "sublime_text"
return "subl://$path:$linenum"
elseif editor == "idea" || occursin("idea", editor)
return "idea://open?file=$path&line=$linenum"
elseif editor == "pycharm"
return "pycharm://open?file=$path&line=$linenum"
elseif editor == "atom"
return "atom://core/open/file?filename=$path&line=$linenum"
elseif editor == "emacsclient"
return "emacs://open?file=$path&line=$linenum"
elseif editor == "vim" || editor == "nvim"
return "vim://open?file=$path&line=$linenum"
else
# TODO: convert the path to a generic URI (line numbers are not supported by generic URI)
return path
end
end

function print_flat(io::IO, lilist::Vector{StackFrame},
n::Vector{Int}, m::Vector{Int},
cols::Int, filenamemap::Dict{Symbol,String},
cols::Int, filenamemap::FileNameMap,
fmt::ProfileFormat)
if fmt.sortedby === :count
p = sortperm(n)
Expand All @@ -802,18 +833,18 @@ function print_flat(io::IO, lilist::Vector{StackFrame},
lilist = lilist[p]
n = n[p]
m = m[p]
filenames = String[short_path(li.file, filenamemap) for li in lilist]
pkgnames_filenames = Tuple{String,String,String}[short_path(li.file, filenamemap) for li in lilist]
funcnames = String[string(li.func) for li in lilist]
wcounts = max(6, ndigits(maximum(n)))
wself = max(9, ndigits(maximum(m)))
maxline = 1
maxfile = 6
maxfunc = 10
for i in 1:length(lilist)
for i in eachindex(lilist)
li = lilist[i]
maxline = max(maxline, li.line)
maxfunc = max(maxfunc, length(funcnames[i]))
maxfile = max(maxfile, length(filenames[i]))
maxfunc = max(maxfunc, textwidth(funcnames[i]))
maxfile = max(maxfile, sum(textwidth, pkgnames_filenames[i][2:3]) + 1)
end
wline = max(5, ndigits(maxline))
ntext = max(20, cols - wcounts - wself - wline - 3)
Expand All @@ -829,7 +860,7 @@ function print_flat(io::IO, lilist::Vector{StackFrame},
rpad("File", wfile, " "), " ", lpad("Line", wline, " "), " Function")
println(io, lpad("=====", wcounts, " "), " ", lpad("========", wself, " "), " ",
rpad("====", wfile, " "), " ", lpad("====", wline, " "), " ========")
for i = 1:length(n)
for i in eachindex(n)
n[i] < fmt.mincount && continue
li = lilist[i]
Base.print(io, lpad(string(n[i]), wcounts, " "), " ")
Expand All @@ -841,16 +872,29 @@ function print_flat(io::IO, lilist::Vector{StackFrame},
Base.print(io, "[any unknown stackframes]")
end
else
file = filenames[i]
path, pkgname, file = pkgnames_filenames[i]
isempty(file) && (file = "[unknown file]")
Base.print(io, rpad(rtruncto(file, wfile), wfile, " "), " ")
pkgcolor = get!(() -> popfirst!(Base.STACKTRACE_MODULECOLORS), PACKAGE_FIXEDCOLORS, pkgname)
Base.printstyled(io, pkgname, color=pkgcolor)
file_trunc = ltruncate(file, wfile)
wpad = wfile - textwidth(pkgname)
if !isempty(pkgname) && !startswith(file_trunc, "/")
Base.print(io, "/")
wpad -= 1
end
if isempty(path)
Base.print(io, rpad(file_trunc, wpad, " "))
else
link = editor_link(path, li.line)
Base.print(io, rpad(styled"{link=$link:$file_trunc}", wpad, " "))
end
Base.print(io, lpad(li.line > 0 ? string(li.line) : "?", wline, " "), " ")
fname = funcnames[i]
if !li.from_c && li.linfo !== nothing
fname = sprint(show_spec_linfo, li)
end
isempty(fname) && (fname = "[unknown function]")
Base.print(io, ltruncto(fname, wfunc))
Base.print(io, rtruncate(fname, wfunc))
end
println(io)
end
Expand Down Expand Up @@ -889,21 +933,24 @@ function indent(depth::Int)
return indent
end

function tree_format(frames::Vector{<:StackFrameTree}, level::Int, cols::Int, maxes, filenamemap::Dict{Symbol,String}, showpointer::Bool)
# mimics Stacktraces
const PACKAGE_FIXEDCOLORS = Dict{String, Any}("@Base" => :gray, "@Core" => :gray)

function tree_format(frames::Vector{<:StackFrameTree}, level::Int, cols::Int, maxes, filenamemap::FileNameMap, showpointer::Bool)
nindent = min(cols>>1, level)
ndigoverhead = ndigits(maxes.overhead)
ndigcounts = ndigits(maxes.count)
ndigline = ndigits(maximum(frame.frame.line for frame in frames)) + 6
ntext = max(30, cols - ndigoverhead - nindent - ndigcounts - ndigline - 6)
widthfile = 2*ntext÷5 # min 12
strs = Vector{String}(undef, length(frames))
strs = Vector{AnnotatedString{String}}(undef, length(frames))
showextra = false
if level > nindent
nextra = level - nindent
nindent -= ndigits(nextra) + 2
showextra = true
end
for i = 1:length(frames)
for i in eachindex(frames)
frame = frames[i]
li = frame.frame
stroverhead = lpad(frame.overhead > 0 ? string(frame.overhead) : "", ndigoverhead, " ")
Expand All @@ -924,25 +971,34 @@ function tree_format(frames::Vector{<:StackFrameTree}, level::Int, cols::Int, ma
else
fname = string(li.func)
end
filename = short_path(li.file, filenamemap)
path, pkgname, filename = short_path(li.file, filenamemap)
if showpointer
fname = string(
"0x",
string(li.pointer, base = 16, pad = 2*sizeof(Ptr{Cvoid})),
" ",
fname)
end
strs[i] = string(stroverhead, "╎", base, strcount, " ",
rtruncto(filename, widthfile),
":",
li.line == -1 ? "?" : string(li.line),
"; ",
fname)
pkgcolor = get!(() -> popfirst!(Base.STACKTRACE_MODULECOLORS), PACKAGE_FIXEDCOLORS, pkgname)
remaining_path = ltruncate(filename, widthfile - textwidth(pkgname) - 1)
linenum = li.line == -1 ? "?" : string(li.line)
slash = (!isempty(pkgname) && !startswith(remaining_path, "/")) ? "/" : ""
styled_path = styled"{$pkgcolor:$pkgname}$slash$remaining_path:$linenum"
rich_file = if isempty(path)
styled_path
else
link = editor_link(path, li.line)
styled"{link=$link:$styled_path}"
end
strs[i] = Base.annotatedstring(stroverhead, "╎", base, strcount, " ", rich_file, " ", fname)
if frame.overhead > 0
strs[i] = styled"{bold:$(strs[i])}"
end
end
else
strs[i] = string(stroverhead, "╎", base, strcount, " [unknown stackframe]")
end
strs[i] = ltruncto(strs[i], cols)
strs[i] = rtruncate(strs[i], cols)
end
return strs
end
Expand Down Expand Up @@ -1101,10 +1157,10 @@ end
# avoid stack overflows.
function print_tree(io::IO, bt::StackFrameTree{T}, cols::Int, fmt::ProfileFormat, is_subsection::Bool) where T
maxes = maxstats(bt)
filenamemap = Dict{Symbol,String}()
worklist = [(bt, 0, 0, "")]
filenamemap = FileNameMap()
worklist = [(bt, 0, 0, AnnotatedString(""))]
if !is_subsection
Base.print(io, "Overhead ╎ [+additional indent] Count File:Line; Function\n")
Base.print(io, "Overhead ╎ [+additional indent] Count File:Line Function\n")
Base.print(io, "=========================================================\n")
end
while !isempty(worklist)
Expand Down Expand Up @@ -1135,7 +1191,7 @@ function print_tree(io::IO, bt::StackFrameTree{T}, cols::Int, fmt::ProfileFormat
count = down.count
count < fmt.mincount && continue
count < noisefloor && continue
str = strs[i]
str = strs[i]::AnnotatedString
noisefloor_down = fmt.noisefloor > 0 ? floor(Int, fmt.noisefloor * sqrt(count)) : 0
pushfirst!(worklist, (down, level + 1, noisefloor_down, str))
end
Expand Down Expand Up @@ -1196,24 +1252,7 @@ function callersf(matchfunc::Function, bt::Vector, lidict::LineInfoFlatDict)
return [(v[i], k[i]) for i in p]
end

# Utilities
function rtruncto(str::String, w::Int)
if textwidth(str) <= w
return str
else
return string("…", str[prevind(str, end, w-2):end])
end
end
function ltruncto(str::String, w::Int)
if textwidth(str) <= w
return str
else
return string(str[1:nextind(str, 1, w-2)], "…")
end
end


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

# Order alphabetically (file, function) and then by line number
function liperm(lilist::Vector{StackFrame})
Expand Down