• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JuliaLang / julia / #38182

15 Aug 2025 03:55AM UTC coverage: 77.87% (-0.4%) from 78.28%
#38182

push

local

web-flow
🤖 [master] Bump the SparseArrays stdlib from 30201ab to bb5ecc0 (#59263)

Stdlib: SparseArrays
URL: https://github.com/JuliaSparse/SparseArrays.jl.git
Stdlib branch: main
Julia branch: master
Old commit: 30201ab
New commit: bb5ecc0
Julia version: 1.13.0-DEV
SparseArrays version: 1.13.0
Bump invoked by: @ViralBShah
Powered by:
[BumpStdlibs.jl](https://github.com/JuliaLang/BumpStdlibs.jl)

Diff:
https://github.com/JuliaSparse/SparseArrays.jl/compare/30201abcb...bb5ecc091

```
$ git log --oneline 30201ab..bb5ecc0
bb5ecc0 fast quadratic form for dense matrix, sparse vectors (#640)
34ece87 Extend 3-arg `dot` to generic `HermOrSym` sparse matrices (#643)
095b685 Exclude unintended complex symmetric sparse matrices from 3-arg `dot` (#642)
8049287 Fix signature for 2-arg matrix-matrix `dot` (#641)
cff971d Make cond(::SparseMatrix, 1 / Inf) discoverable from 2-norm error (#629)
```

Co-authored-by: ViralBShah <744411+ViralBShah@users.noreply.github.com>

48274 of 61993 relevant lines covered (77.87%)

9571166.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.89
/stdlib/Profile/src/Profile.jl
1
# This file is a part of Julia. License is MIT: https://julialang.org/license
2

3
"""
4
    Profile
5

6
Profiling support.
7

8
## CPU profiling
9
- `@profile foo()` to profile a specific call.
10
- `Profile.print()` to print the report. Paths are clickable links in supported terminals and specialized for JULIA_EDITOR etc.
11
- `Profile.clear()` to clear the buffer.
12
- Send a SIGUSR1 (on linux) or SIGINFO (on macOS/BSD) signal to the process to automatically trigger a profile and print. i.e. `kill -s SIGUSR1/SIGINFO 1234`, where 1234 is the pid of the julia process. On macOS & BSD platforms `ctrl-t` can be used directly.
13

14
## Memory profiling
15
- `Profile.Allocs.@profile [sample_rate=0.1] foo()` to sample allocations within a specific call. A sample rate of 1.0 will record everything; 0.0 will record nothing.
16
- `Profile.Allocs.print()` to print the report.
17
- `Profile.Allocs.clear()` to clear the buffer.
18

19
## Heap profiling
20
- `Profile.take_heap_snapshot()` to record a `.heapsnapshot` record of the heap.
21
- Set `JULIA_PROFILE_PEEK_HEAP_SNAPSHOT=true` to capture a heap snapshot when signal $(Sys.isbsd() ? "SIGINFO (ctrl-t)" : "SIGUSR1") is sent.
22
"""
23
module Profile
24

25
global print
26
export @profile, @profile_walltime
27
public clear,
28
    print,
29
    fetch,
30
    retrieve,
31
    add_fake_meta,
32
    flatten,
33
    callers,
34
    init,
35
    take_heap_snapshot,
36
    take_page_profile,
37
    clear_malloc_data,
38
    Allocs
39

40
import Base.StackTraces: lookup, UNKNOWN, show_spec_linfo, StackFrame
41
import Base: AnnotatedString
42
using StyledStrings: @styled_str
43

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

46
const slash = Sys.iswindows() ? "\\" : "/"
47

48
# deprecated functions: use `getdict` instead
49
lookup(ip::UInt) = lookup(convert(Ptr{Cvoid}, ip))
5,677✔
50

51
"""
52
    @profile
53

54
`@profile <expression>` runs your expression while taking periodic backtraces. These are
55
appended to an internal buffer of backtraces.
56
"""
57
macro profile(ex)
9✔
58
    return quote
9✔
59
        start_timer()
1✔
60
        Base.@__tryfinally(
1✔
61
            $(esc(ex))
62
            ,
63
            stop_timer()
64
        )
65
    end
66
end
67

68
"""
69
    @profile_walltime
70

71
`@profile_walltime <expression>` runs your expression while taking periodic backtraces of a sample of all live tasks (both running and not running).
72
These are appended to an internal buffer of backtraces.
73

74
It can be configured via `Profile.init`, same as the `Profile.@profile`, and that you can't use `@profile` simultaneously with `@profile_walltime`.
75

76
As mentioned above, since this tool sample not only running tasks, but also sleeping tasks and tasks performing IO,
77
it can be used to diagnose performance issues such as lock contention, IO bottlenecks, and other issues that are not visible in the CPU profile.
78
"""
79
macro profile_walltime(ex)
3✔
80
    return quote
3✔
81
        start_timer(true);
82
        Base.@__tryfinally(
83
            $(esc(ex))
84
            ,
85
            stop_timer()
86
        )
87
    end
88
end
89

90
# An internal function called to show the report after an information request (SIGINFO or SIGUSR1).
91
function _peek_report()
2✔
92
    iob = Base.AnnotatedIOBuffer()
2✔
93
    ioc = IOContext(IOContext(iob, stderr), :displaysize=>displaysize(stderr))
4✔
94
    print(ioc, groupby = [:thread, :task])
4✔
95
    Base.print(stderr, read(seekstart(iob), AnnotatedString))
2✔
96
end
97
# This is a ref so that it can be overridden by other profile info consumers.
98
const peek_report = Ref{Function}(_peek_report)
99

100
"""
101
    get_peek_duration()
102

103
Get the duration in seconds of the profile "peek" that is triggered via `SIGINFO` or `SIGUSR1`, depending on platform.
104
"""
105
get_peek_duration() = ccall(:jl_get_profile_peek_duration, Float64, ())
×
106
"""
107
    set_peek_duration(t::Float64)
108

109
Set the duration in seconds of the profile "peek" that is triggered via `SIGINFO` or `SIGUSR1`, depending on platform.
110
"""
111
set_peek_duration(t::Float64) = ccall(:jl_set_profile_peek_duration, Cvoid, (Float64,), t)
×
112

113

114

115
####
116
#### User-level functions
117
####
118

119
"""
120
    init(; n::Integer, delay::Real)
121

122
Configure the `delay` between backtraces (measured in seconds), and the number `n` of instruction pointers that may be
123
stored per thread. Each instruction pointer corresponds to a single line of code; backtraces generally consist of a long
124
list of instruction pointers. Note that 6 spaces for instruction pointers per backtrace are used to store metadata and two
125
NULL end markers. Current settings can be obtained by calling this function with no arguments, and each can be set independently
126
using keywords or in the order `(n, delay)`.
127
"""
128
function init(; n::Union{Nothing,Integer} = nothing, delay::Union{Nothing,Real} = nothing, limitwarn::Bool = true)
12✔
129
    n_cur = ccall(:jl_profile_maxlen_data, Csize_t, ())
8✔
130
    if n_cur == 0 && isnothing(n) && isnothing(delay)
8✔
131
        # indicates that the buffer hasn't been initialized at all, so set the default
132
        default_init()
1✔
133
        n_cur = ccall(:jl_profile_maxlen_data, Csize_t, ())
1✔
134
    end
135
    delay_cur = ccall(:jl_profile_delay_nsec, UInt64, ())/10^9
8✔
136
    if n === nothing && delay === nothing
8✔
137
        return n_cur, delay_cur
4✔
138
    end
139
    nnew = (n === nothing) ? n_cur : n
4✔
140
    delaynew = (delay === nothing) ? delay_cur : delay
4✔
141
    init(nnew, delaynew; limitwarn)
4✔
142
end
143

144
function init(n::Integer, delay::Real; limitwarn::Bool = true)
17✔
145
    sample_size_bytes = sizeof(Ptr) # == Sys.WORD_SIZE / 8
9✔
146
    buffer_samples = n
9✔
147
    buffer_size_bytes = buffer_samples * sample_size_bytes
9✔
148
    if buffer_size_bytes > 2^29 && Sys.WORD_SIZE == 32
9✔
149
        buffer_samples = floor(Int, 2^29 / sample_size_bytes)
×
150
        buffer_size_bytes = buffer_samples * sample_size_bytes
×
151
        limitwarn && @warn "Requested profile buffer limited to 512MB (n = $buffer_samples) given that this system is 32-bit"
×
152
    end
153
    status = ccall(:jl_profile_init, Cint, (Csize_t, UInt64), buffer_samples, round(UInt64, 10^9*delay))
9✔
154
    if status == -1
9✔
155
        error("could not allocate space for ", n, " instruction pointers ($(Base.format_bytes(buffer_size_bytes)))")
×
156
    end
157
end
158

159
function default_init()
160
    # init with default values
161
    # Use a max size of 10M profile samples, and fire timer every 1ms
162
    # (that should typically give around 100 seconds of record)
163
    @static if Sys.iswindows() && Sys.WORD_SIZE == 32
5✔
164
        # The Win32 unwinder is 1000x slower than elsewhere (around 1ms/frame),
165
        # so we don't want to slow the program down by quite that much
166
        n = 1_000_000
167
        delay = 0.01
168
    else
169
        # Keep these values synchronized with trigger_profile_peek
170
        n = 10_000_000
5✔
171
        delay = 0.001
5✔
172
    end
173
    init(n, delay, limitwarn = false)
5✔
174
end
175

176
# Checks whether the profile buffer has been initialized. If not, initializes it with the default size.
177
function check_init()
178
    buffer_size = @ccall jl_profile_maxlen_data()::Int
12✔
179
    if buffer_size == 0
12✔
180
        default_init()
4✔
181
    end
182
end
183

184
"""
185
    clear()
186

187
Clear any existing backtraces from the internal buffer.
188
"""
189
clear() = ccall(:jl_profile_clear_data, Cvoid, ())
7✔
190

191
const LineInfoDict = Dict{UInt64, Vector{StackFrame}}
192
const LineInfoFlatDict = Dict{UInt64, StackFrame}
193

194
struct ProfileFormat
195
    maxdepth::Int
196
    mincount::Int
197
    noisefloor::Float64
198
    sortedby::Symbol
199
    combine::Bool
200
    C::Bool
201
    recur::Symbol
202
    function ProfileFormat(;
77✔
203
        C = false,
204
        combine = true,
205
        maxdepth::Int = typemax(Int),
206
        mincount::Int = 0,
207
        noisefloor = 0,
208
        sortedby::Symbol = :filefuncline,
209
        recur::Symbol = :off)
210
        return new(maxdepth, mincount, noisefloor, sortedby, combine, C, recur)
77✔
211
    end
212
end
213

214
# offsets of the metadata in the data stream
215
const META_OFFSET_SLEEPSTATE = 2
216
const META_OFFSET_CPUCYCLECLOCK = 3
217
const META_OFFSET_TASKID = 4
218
const META_OFFSET_THREADID = 5
219

220
"""
221
    print([io::IO = stdout,] [data::Vector = fetch()], [lidict::Union{LineInfoDict, LineInfoFlatDict} = getdict(data)]; kwargs...)
222
    print(path::String, [cols::Int = 1000], [data::Vector = fetch()], [lidict::Union{LineInfoDict, LineInfoFlatDict} = getdict(data)]; kwargs...)
223

224
Prints profiling results to `io` (by default, `stdout`). If you do not
225
supply a `data` vector, the internal buffer of accumulated backtraces
226
will be used. Paths are clickable links in supported terminals and
227
specialized for [`JULIA_EDITOR`](@ref) with line numbers, or just file
228
links if no editor is set.
229

230
The keyword arguments can be any combination of:
231

232
 - `format` -- Determines whether backtraces are printed with (default, `:tree`) or without (`:flat`)
233
   indentation indicating tree structure.
234

235
 - `C` -- If `true`, backtraces from C and Fortran code are shown (normally they are excluded).
236

237
 - `combine` -- If `true` (default), instruction pointers are merged that correspond to the same line of code.
238

239
 - `maxdepth` -- Limits the depth higher than `maxdepth` in the `:tree` format.
240

241
 - `sortedby` -- Controls the order in `:flat` format. `:filefuncline` (default) sorts by the source
242
    line, `:count` sorts in order of number of collected samples, and `:overhead` sorts by the number of samples
243
    incurred by each function by itself.
244

245
 - `groupby` -- Controls grouping over tasks and threads, or no grouping. Options are `:none` (default), `:thread`, `:task`,
246
    `[:thread, :task]`, or `[:task, :thread]` where the last two provide nested grouping.
247

248
 - `noisefloor` -- Limits frames that exceed the heuristic noise floor of the sample (only applies to format `:tree`).
249
    A suggested value to try for this is 2.0 (the default is 0). This parameter hides samples for which `n <= noisefloor * √N`,
250
    where `n` is the number of samples on this line, and `N` is the number of samples for the callee.
251

252
 - `mincount` -- Limits the printout to only those lines with at least `mincount` occurrences.
253

254
 - `recur` -- Controls the recursion handling in `:tree` format. `:off` (default) prints the tree as normal. `:flat` instead
255
    compresses any recursion (by ip), showing the approximate effect of converting any self-recursion into an iterator.
256
    `:flatc` does the same but also includes collapsing of C frames (may do odd things around `jl_apply`).
257

258
 - `threads::Union{Int,AbstractVector{Int}}` -- Specify which threads to include snapshots from in the report. Note that
259
    this does not control which threads samples are collected on (which may also have been collected on another machine).
260

261
 - `tasks::Union{Int,AbstractVector{Int}}` -- Specify which tasks to include snapshots from in the report. Note that this
262
    does not control which tasks samples are collected within.
263

264
!!! compat "Julia 1.8"
265
    The `groupby`, `threads`, and `tasks` keyword arguments were introduced in Julia 1.8.
266

267
!!! note
268
    Profiling on windows is limited to the main thread. Other threads have not been sampled and will not show in the report.
269

270
"""
271
function print(io::IO,
272✔
272
        data::Vector{<:Unsigned} = fetch(),
273
        lidict::Union{LineInfoDict, LineInfoFlatDict} = getdict(data)
274
        ;
275
        format = :tree,
276
        C = false,
277
        combine = true,
278
        maxdepth::Int = typemax(Int),
279
        mincount::Int = 0,
280
        noisefloor = 0,
281
        sortedby::Symbol = :filefuncline,
282
        groupby::Union{Symbol,AbstractVector{Symbol}} = :none,
283
        recur::Symbol = :off,
284
        threads::Union{Int,AbstractVector{Int}} = 1:typemax(Int),
285
        tasks::Union{UInt,AbstractVector{UInt}} = typemin(UInt):typemax(UInt))
286

287
    pf = ProfileFormat(;C, combine, maxdepth, mincount, noisefloor, sortedby, recur)
69✔
288
    if groupby === :none
69✔
289
        print_group(io, data, lidict, pf, format, threads, tasks, false)
27✔
290
    else
291
        if !in(groupby, [:thread, :task, [:task, :thread], [:thread, :task]])
84✔
292
            error(ArgumentError("Unrecognized groupby option: $groupby. Options are :none (default), :task, :thread, [:task, :thread], or [:thread, :task]"))
×
293
        elseif Sys.iswindows() && in(groupby, [:thread, [:task, :thread], [:thread, :task]])
42✔
294
            @warn "Profiling on windows is limited to the main thread. Other threads have not been sampled and will not show in the report"
×
295
        end
296
        any_nosamples = true
42✔
297
        if format === :tree
42✔
298
            Base.print(io, "Overhead ╎ [+additional indent] Count File:Line  Function\n")
22✔
299
            Base.print(io, "=========================================================\n")
22✔
300
        end
301
        if groupby == [:task, :thread]
84✔
302
            taskids = intersect(get_task_ids(data), tasks)
10✔
303
            isempty(taskids) && (any_nosamples = true)
10✔
304
            for taskid in taskids
10✔
305
                threadids = intersect(get_thread_ids(data, taskid), threads)
10✔
306
                if length(threadids) == 0
10✔
307
                    any_nosamples = true
×
308
                else
309
                    nl = length(threadids) > 1 ? "\n" : ""
10✔
310
                    printstyled(io, "Task $(Base.repr(taskid))$nl"; bold=true, color=Base.debug_color())
20✔
311
                    for threadid in threadids
10✔
312
                        printstyled(io, " Thread $threadid ($(Threads.threadpooldescription(threadid))) "; bold=true, color=Base.info_color())
20✔
313
                        nosamples = print_group(io, data, lidict, pf, format, threadid, taskid, true)
10✔
314
                        nosamples && (any_nosamples = true)
10✔
315
                        println(io)
10✔
316
                    end
10✔
317
                end
318
            end
10✔
319
        elseif groupby == [:thread, :task]
64✔
320
            threadids = intersect(get_thread_ids(data), threads)
12✔
321
            isempty(threadids) && (any_nosamples = true)
12✔
322
            for threadid in threadids
12✔
323
                taskids = intersect(get_task_ids(data, threadid), tasks)
12✔
324
                if length(taskids) == 0
12✔
325
                    any_nosamples = true
×
326
                else
327
                    nl = length(taskids) > 1 ? "\n" : ""
12✔
328
                    printstyled(io, "Thread $threadid ($(Threads.threadpooldescription(threadid)))$nl"; bold=true, color=Base.info_color())
24✔
329
                    for taskid in taskids
12✔
330
                        printstyled(io, " Task $(Base.repr(taskid)) "; bold=true, color=Base.debug_color())
26✔
331
                        nosamples = print_group(io, data, lidict, pf, format, threadid, taskid, true)
13✔
332
                        nosamples && (any_nosamples = true)
13✔
333
                        println(io)
13✔
334
                    end
13✔
335
                end
336
            end
12✔
337
        elseif groupby === :task
20✔
338
            threads = 1:typemax(Int)
10✔
339
            taskids = intersect(get_task_ids(data), tasks)
10✔
340
            isempty(taskids) && (any_nosamples = true)
10✔
341
            for taskid in taskids
10✔
342
                printstyled(io, "Task $(Base.repr(taskid)) "; bold=true, color=Base.debug_color())
20✔
343
                nosamples = print_group(io, data, lidict, pf, format, threads, taskid, true)
10✔
344
                nosamples && (any_nosamples = true)
10✔
345
                println(io)
10✔
346
            end
10✔
347
        elseif groupby === :thread
10✔
348
            tasks = 1:typemax(UInt)
10✔
349
            threadids = intersect(get_thread_ids(data), threads)
10✔
350
            isempty(threadids) && (any_nosamples = true)
10✔
351
            for threadid in threadids
10✔
352
                printstyled(io, "Thread $threadid ($(Threads.threadpooldescription(threadid))) "; bold=true, color=Base.info_color())
20✔
353
                nosamples = print_group(io, data, lidict, pf, format, threadid, tasks, true)
10✔
354
                nosamples && (any_nosamples = true)
10✔
355
                println(io)
10✔
356
            end
10✔
357
        end
358
        any_nosamples && warning_empty(summary = true)
42✔
359
    end
360
    return
69✔
361
end
362

363
function print(path::String, cols::Int = 1000, args...; kwargs...)
12✔
364
    open(path, "w") do io
6✔
365
        ioc = IOContext(io, :displaysize=>(1000,cols))
6✔
366
        print(ioc, args...; kwargs...)
6✔
367
    end
368
end
369

370
"""
371
    print([io::IO = stdout,] data::Vector, lidict::LineInfoDict; kwargs...)
372

373
Prints profiling results to `io`. This variant is used to examine results exported by a
374
previous call to [`retrieve`](@ref). Supply the vector `data` of backtraces and
375
a dictionary `lidict` of line information.
376

377
See `Profile.print([io], data)` for an explanation of the valid keyword arguments.
378
"""
379
print(data::Vector{<:Unsigned} = fetch(), lidict::Union{LineInfoDict, LineInfoFlatDict} = getdict(data); kwargs...) =
1✔
380
    print(stdout, data, lidict; kwargs...)
381

382
function print_group(io::IO, data::Vector{<:Unsigned}, lidict::Union{LineInfoDict, LineInfoFlatDict}, fmt::ProfileFormat,
70✔
383
                format::Symbol, threads::Union{Int,AbstractVector{Int}}, tasks::Union{UInt,AbstractVector{UInt}},
384
                is_subsection::Bool = false)
385
    cols::Int = Base.displaysize(io)[2]
70✔
386
    data = convert(Vector{UInt64}, data)
70✔
387
    fmt.recur ∈ (:off, :flat, :flatc) || throw(ArgumentError("recur value not recognized"))
70✔
388
    if format === :tree
70✔
389
        nosamples = tree(io, data, lidict, cols, fmt, threads, tasks, is_subsection)
39✔
390
        return nosamples
39✔
391
    elseif format === :flat
31✔
392
        fmt.recur === :off || throw(ArgumentError("format flat only implements recur=:off"))
31✔
393
        nosamples = flat(io, data, lidict, cols, fmt, threads, tasks, is_subsection)
31✔
394
        return nosamples
31✔
395
    else
396
        throw(ArgumentError("output format $(repr(format)) not recognized"))
×
397
    end
398
end
399

400
function get_task_ids(data::Vector{<:Unsigned}, threadid = nothing)
32✔
401
    taskids = UInt[]
52✔
402
    for i in length(data):-1:1
64✔
403
        if is_block_end(data, i)
149,280✔
404
            if isnothing(threadid) || data[i - META_OFFSET_THREADID] == threadid
4,336✔
405
                taskid = data[i - META_OFFSET_TASKID]
4,336✔
406
                !in(taskid, taskids) && push!(taskids, taskid)
4,346✔
407
            end
408
        end
409
    end
149,376✔
410
    return taskids
32✔
411
end
412

413
function get_thread_ids(data::Vector{<:Unsigned}, taskid = nothing)
32✔
414
    threadids = Int[]
54✔
415
    for i in length(data):-1:1
64✔
416
        if is_block_end(data, i)
149,280✔
417
            if isnothing(taskid) || data[i - META_OFFSET_TASKID] == taskid
4,336✔
418
                threadid = data[i - META_OFFSET_THREADID]
4,336✔
419
                !in(threadid, threadids) && push!(threadids, threadid)
4,336✔
420
            end
421
        end
422
    end
149,376✔
423
    return sort(threadids)
32✔
424
end
425

426
function is_block_end(data, i)
21,144✔
427
    i < nmeta + 1 && return false
672,847✔
428
    # 32-bit linux has been seen to have rogue NULL ips, so we use two to
429
    # indicate block end, where the 2nd is the actual end index.
430
    # and we could have (though very unlikely):
431
    # 1:<stack><metadata><null><null><NULL><metadata><null><null>:end
432
    # and we want to ignore the triple NULL (which is an ip).
433
    return data[i] == 0 && data[i - 1] == 0 && data[i - META_OFFSET_SLEEPSTATE] != 0
672,128✔
434
end
435

436
function has_meta(data)
437
    for i in 6:length(data)
155✔
438
        data[i] == 0 || continue                            # first block end null
2,255✔
439
        data[i - 1] == 0 || continue                        # second block end null
704✔
440
        data[i - META_OFFSET_SLEEPSTATE] in 1:3 || continue # 1 for not sleeping, 2 for sleeping, 3 for task profiler fake state
142✔
441
                                                            # See definition in `src/julia_internal.h`
442
        data[i - META_OFFSET_CPUCYCLECLOCK] != 0 || continue
142✔
443
        data[i - META_OFFSET_TASKID] != 0 || continue
142✔
444
        data[i - META_OFFSET_THREADID] != 0 || continue
143✔
445
        return true
141✔
446
    end
4,226✔
447
    return false
9✔
448
end
449

450
"""
451
    retrieve(; kwargs...) -> data, lidict
452

453
"Exports" profiling results in a portable format, returning the set of all backtraces
454
(`data`) and a dictionary that maps the (session-specific) instruction pointers in `data` to
455
`LineInfo` values that store the file name, function name, and line number. This function
456
allows you to save profiling results for future analysis.
457
"""
458
function retrieve(; kwargs...)
9✔
459
    data = fetch(; kwargs...)
9✔
460
    return (data, getdict(data))
9✔
461
end
462

463
function getdict(data::Vector{UInt})
464
    dict = LineInfoDict()
76✔
465
    return getdict!(dict, data)
76✔
466
end
467

468
function getdict!(dict::LineInfoDict, data::Vector{UInt})
76✔
469
    # we don't want metadata here as we're just looking up ips
470
    unique_ips = unique(has_meta(data) ? strip_meta(data) : data)
77✔
471
    n_unique_ips = length(unique_ips)
76✔
472
    n_unique_ips == 0 && return dict
76✔
473
    iplookups = similar(unique_ips, Vector{StackFrame})
71✔
474
    sort!(unique_ips) # help each thread to get a disjoint set of libraries, as much if possible
141✔
475
    @sync for indexes_part in Iterators.partition(eachindex(unique_ips), div(n_unique_ips, Threads.threadpoolsize(), RoundUp))
71✔
476
        Threads.@spawn begin
142✔
477
            for i in indexes_part
142✔
478
                iplookups[i] = _lookup_corrected(unique_ips[i])
4,457✔
479
            end
4,457✔
480
        end
481
    end
482
    for i in eachindex(unique_ips)
73✔
483
        dict[unique_ips[i]] = iplookups[i]
4,457✔
484
    end
8,843✔
485
    return dict
71✔
486
end
487

488
function _lookup_corrected(ip::UInt)
4,457✔
489
    st = lookup(convert(Ptr{Cvoid}, ip))
4,457✔
490
    # To correct line numbers for moving code, put it in the form expected by
491
    # Base.update_stackframes_callback[]
492
    stn = map(x->(x, 1), st)
14,911✔
493
    # Note: Base.update_stackframes_callback[] should be data-race free
494
    try Base.invokelatest(Base.update_stackframes_callback[], stn) catch end
4,457✔
495
    return map(first, stn)
4,457✔
496
end
497

498
"""
499
    flatten(btdata::Vector, lidict::LineInfoDict) -> (newdata::Vector{UInt64}, newdict::LineInfoFlatDict)
500

501
Produces "flattened" backtrace data. Individual instruction pointers
502
sometimes correspond to a multi-frame backtrace due to inlining; in
503
such cases, this function inserts fake instruction pointers for the
504
inlined calls, and returns a dictionary that is a 1-to-1 mapping
505
between instruction pointers and a single StackFrame.
506
"""
507
function flatten(data::Vector, lidict::LineInfoDict)
5✔
508
    # Makes fake instruction pointers, counting down from typemax(UInt)
509
    newip = typemax(UInt64) - 1
5✔
510
    taken = Set(keys(lidict))  # make sure we don't pick one that's already used
7✔
511
    newdict = Dict{UInt64,StackFrame}()
5✔
512
    newmap  = Dict{UInt64,Vector{UInt64}}()
5✔
513
    for (ip, trace) in lidict
7✔
514
        if length(trace) == 1
98✔
515
            newdict[ip] = trace[1]
74✔
516
        else
517
            newm = UInt64[]
24✔
518
            for sf in trace
24✔
519
                while newip ∈ taken && newip > 0
116✔
520
                    newip -= 1
×
521
                end
×
522
                newip == 0 && error("all possible instruction pointers used")
58✔
523
                push!(newm, newip)
58✔
524
                newdict[newip] = sf
58✔
525
                newip -= 1
58✔
526
            end
58✔
527
            newmap[ip] = newm
24✔
528
        end
529
    end
194✔
530
    newdata = UInt64[]
5✔
531
    for ip::UInt64 in data
5✔
532
        if haskey(newmap, ip)
5,984✔
533
            append!(newdata, newmap[ip])
852✔
534
        else
535
            push!(newdata, ip)
2,566✔
536
        end
537
    end
2,992✔
538
    return (newdata, newdict)
5✔
539
end
540

541
const SRC_DIR = normpath(joinpath(Sys.BUILD_ROOT_PATH, "src"))
542
const COMPILER_DIR = "../usr/share/julia/Compiler/"
543

544
# Take a file-system path and try to form a concise representation of it
545
# based on the package ecosystem
546
# filenamecache is a dict of spath -> (fullpath or "" if !isfile, modulename, shortpath)
547
function short_path(spath::Symbol, filenamecache::Dict{Symbol, Tuple{String,String,String}})
548
    return get!(filenamecache, spath) do
4,333✔
549
        path = Base.fixup_stdlib_path(string(spath))
989✔
550
        path_norm = normpath(path)
989✔
551
        possible_base_path = normpath(joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia", "base", path))
989✔
552
        lib_dir = abspath(Sys.BINDIR, Base.LIBDIR)
989✔
553
        if startswith(path_norm, SRC_DIR)
989✔
554
            remainder = only(split(path_norm, SRC_DIR, keepempty=false))
72✔
555
            return (isfile(path_norm) ? path_norm : ""), "@juliasrc", remainder
72✔
556
        elseif startswith(path_norm, lib_dir)
917✔
557
            remainder = only(split(path_norm, lib_dir, keepempty=false))
6✔
558
            return (isfile(path_norm) ? path_norm : ""), "@julialib", remainder
6✔
559
        elseif contains(path, COMPILER_DIR)
911✔
560
            remainder = split(path, COMPILER_DIR, keepempty=false)[end]
23✔
561
            possible_compiler_path = normpath(joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia", "Compiler", remainder))
23✔
562
            return (isfile(possible_compiler_path) ? possible_compiler_path : ""), "@Compiler", remainder
23✔
563
        elseif isabspath(path)
1,773✔
564
            if ispath(path)
299✔
565
                # try to replace the file-system prefix with a short "@Module" one,
566
                # assuming that profile came from the current machine
567
                # (or at least has the same file-system layout)
568
                root = path
281✔
569
                while !isempty(root)
1,466✔
570
                    root, base = splitdir(root)
1,466✔
571
                    isempty(base) && break
1,466✔
572
                    @assert startswith(path, root)
1,383✔
573
                    for proj in Base.project_names
1,383✔
574
                        project_file = joinpath(root, proj)
2,766✔
575
                        if Base.isfile_casesensitive(project_file)
2,766✔
576
                            pkgid = Base.project_file_name_uuid(project_file, "")
198✔
577
                            isempty(pkgid.name) && return path, "", path # bad Project file
198✔
578
                            # return the joined the module name prefix and path suffix
579
                            _short_path = path[nextind(path, sizeof(root)):end]
396✔
580
                            return path, string("@", pkgid.name), _short_path
198✔
581
                        end
582
                    end
3,753✔
583
                end
1,185✔
584
            end
585
            return path, "", path
101✔
586
        elseif isfile(possible_base_path)
589✔
587
            # do the same mechanic for Base (or Core/Compiler) files as above,
588
            # but they start from a relative path
589
            return possible_base_path, "@Base", normpath(path)
585✔
590
        else
591
            # for non-existent relative paths (such as "REPL[1]"), just consider simplifying them
592
            path = normpath(path)
4✔
593
            return "", "", path # drop leading "./"
4✔
594
        end
595
    end
596
end
597

598
"""
599
    callers(funcname, [data, lidict], [filename=<filename>], [linerange=<start:stop>])::Vector{Tuple{count, lineinfo}}
600

601
Given a previous profiling run, determine who called a particular function. Supplying the
602
filename (and optionally, range of line numbers over which the function is defined) allows
603
you to disambiguate an overloaded method. The returned value is a vector containing a count
604
of the number of calls and line information about the caller. One can optionally supply
605
backtrace `data` obtained from [`retrieve`](@ref); otherwise, the current internal
606
profile buffer is used.
607
"""
608
function callers end
609

610
function callers(funcname::String, bt::Vector, lidict::LineInfoFlatDict; filename = nothing, linerange = nothing)
4✔
611
    if filename === nothing && linerange === nothing
3✔
612
        return callersf(li -> String(li.func) == funcname,
2✔
613
            bt, lidict)
614
    end
615
    filename === nothing && throw(ArgumentError("if supplying linerange, you must also supply the filename"))
1✔
616
    filename = String(filename)
×
617
    if linerange === nothing
×
618
        return callersf(li -> String(li.func) == funcname && String(li.file) == filename,
×
619
            bt, lidict)
620
    else
621
        return callersf(li -> String(li.func) == funcname && String(li.file) == filename && in(li.line, linerange),
×
622
            bt, lidict)
623
    end
624
end
625

626
callers(funcname::String, bt::Vector, lidict::LineInfoDict; kwargs...) =
6✔
627
    callers(funcname, flatten(bt, lidict)...; kwargs...)
628
callers(funcname::String; kwargs...) = callers(funcname, retrieve()...; kwargs...)
2✔
629
callers(func::Function, bt::Vector, lidict::LineInfoFlatDict; kwargs...) =
×
630
    callers(string(func), bt, lidict; kwargs...)
631
callers(func::Function; kwargs...) = callers(string(func), retrieve()...; kwargs...)
4✔
632

633
##
634
## For --track-allocation
635
##
636
# Reset the malloc log. Used to avoid counting memory allocated during
637
# compilation.
638

639
"""
640
    clear_malloc_data()
641

642
Clears any stored memory allocation data when running julia with `--track-allocation`.
643
Execute the command(s) you want to test (to force JIT-compilation), then call
644
[`clear_malloc_data`](@ref). Then execute your command(s) again, quit
645
Julia, and examine the resulting `*.mem` files.
646
"""
647
clear_malloc_data() = ccall(:jl_clear_malloc_data, Cvoid, ())
×
648

649
# C wrappers
650
function start_timer(all_tasks::Bool=false)
20✔
651
    check_init() # if the profile buffer hasn't been initialized, initialize with default size
21✔
652
    status = ccall(:jl_profile_start_timer, Cint, (Bool,), all_tasks)
12✔
653
    if status < 0
12✔
654
        error(error_codes[status])
×
655
    end
656
end
657

658

659
stop_timer() = ccall(:jl_profile_stop_timer, Cvoid, ())
12✔
660

661
is_running() = ccall(:jl_profile_is_running, Cint, ())!=0
×
662

663
is_buffer_full() = ccall(:jl_profile_is_buffer_full, Cint, ())!=0
80✔
664

665
get_data_pointer() = convert(Ptr{UInt}, ccall(:jl_profile_get_data, Ptr{UInt8}, ()))
80✔
666

667
len_data() = convert(Int, ccall(:jl_profile_len_data, Csize_t, ()))
91✔
668

669
maxlen_data() = convert(Int, ccall(:jl_profile_maxlen_data, Csize_t, ()))
81✔
670

671
error_codes = Dict(
672
    -1=>"cannot specify signal action for profiling",
673
    -2=>"cannot create the timer for profiling",
674
    -3=>"cannot start the timer for profiling",
675
    -4=>"cannot unblock SIGUSR1")
676

677

678
"""
679
    fetch(;include_meta = true) -> data
680

681
Return a copy of the buffer of profile backtraces. Note that the
682
values in `data` have meaning only on this machine in the current session, because it
683
depends on the exact memory addresses used in JIT-compiling. This function is primarily for
684
internal use; [`retrieve`](@ref) may be a better choice for most users.
685
By default metadata such as threadid and taskid is included. Set `include_meta` to `false` to strip metadata.
686
"""
687
function fetch(;include_meta = true, limitwarn = true)
162✔
688
    maxlen = maxlen_data()
81✔
689
    if maxlen == 0
81✔
690
        error("The profiling data buffer is not initialized. A profile has not been requested this session.")
1✔
691
    end
692
    len = len_data()
80✔
693
    if limitwarn && is_buffer_full()
80✔
694
        @warn """The profile data buffer is full; profiling probably terminated
1✔
695
                 before your program finished. To profile for longer runs, call
696
                 `Profile.init()` with a larger buffer and/or larger delay."""
697
    end
698
    data = Vector{UInt}(undef, len)
80✔
699
    GC.@preserve data unsafe_copyto!(pointer(data), get_data_pointer(), len)
80✔
700
    if include_meta || isempty(data)
81✔
701
        return data
79✔
702
    end
703
    return strip_meta(data)
1✔
704
end
705

706
function strip_meta(data)
73✔
707
    nblocks = count(Base.Fix1(is_block_end, data), eachindex(data))
146✔
708
    data_stripped = Vector{UInt}(undef, length(data) - (nblocks * (nmeta + 1)))
73✔
709
    j = length(data_stripped)
73✔
710
    i = length(data)
73✔
711
    while i > 0 && j > 0
156,629✔
712
        data_stripped[j] = data[i]
156,556✔
713
        if is_block_end(data, i)
313,027✔
714
            i -= (nmeta + 1) # metadata fields and the extra NULL IP
13,385✔
715
        end
716
        i -= 1
156,556✔
717
        j -= 1
156,556✔
718
    end
156,556✔
719
    @assert i == j == 0 "metadata stripping failed"
73✔
720
    return data_stripped
73✔
721
end
722

723
"""
724
    Profile.add_fake_meta(data; threadid = 1, taskid = 0xf0f0f0f0) -> data_with_meta
725

726
The converse of `Profile.fetch(;include_meta = false)`; this will add fake metadata, and can be used
727
for compatibility and by packages (e.g., FlameGraphs.jl) that would rather not depend on the internal
728
details of the metadata format.
729
"""
730
function add_fake_meta(data; threadid = 1, taskid = 0xf0f0f0f0)
10✔
731
    threadid == 0 && error("Fake threadid cannot be 0")
5✔
732
    taskid == 0 && error("Fake taskid cannot be 0")
5✔
733
    !isempty(data) && has_meta(data) && error("input already has metadata")
5✔
734
    cpu_clock_cycle = UInt64(99)
4✔
735
    data_with_meta = similar(data, 0)
4✔
736
    for i in eachindex(data)
4✔
737
        val = data[i]
1,762✔
738
        if iszero(val)
1,762✔
739
            # META_OFFSET_THREADID, META_OFFSET_TASKID, META_OFFSET_CPUCYCLECLOCK, META_OFFSET_SLEEPSTATE
740
            push!(data_with_meta, threadid, taskid, cpu_clock_cycle+=1, false+1, 0, 0)
143✔
741
        else
742
            push!(data_with_meta, val)
1,619✔
743
        end
744
    end
3,520✔
745
    return data_with_meta
4✔
746
end
747

748
## Print as a flat list
749
# Counts the number of times each line appears, at any nesting level and at the topmost level
750
# Merging multiple equivalent entries and recursive calls
751
function parse_flat(::Type{T}, data::Vector{UInt64}, lidict::Union{LineInfoDict, LineInfoFlatDict}, C::Bool,
31✔
752
                    threads::Union{Int,AbstractVector{Int}}, tasks::Union{UInt,AbstractVector{UInt}}) where {T}
753
    !isempty(data) && !has_meta(data) && error("Profile data is missing required metadata")
31✔
754
    lilist = StackFrame[]
31✔
755
    n = Int[]
31✔
756
    m = Int[]
31✔
757
    lilist_idx = Dict{T, Int}()
31✔
758
    recursive = Set{T}()
31✔
759
    leaf = 0
31✔
760
    totalshots = 0
31✔
761
    startframe = length(data)
31✔
762
    skip = false
31✔
763
    nsleeping = 0
31✔
764
    is_task_profile = false
31✔
765
    for i in startframe:-1:1
62✔
766
        (startframe - 1) >= i >= (startframe - (nmeta + 1)) && continue # skip metadata (its read ahead below) and extra block end NULL IP
70,361✔
767
        ip = data[i]
50,491✔
768
        if is_block_end(data, i)
100,952✔
769
            # read metadata
770
            thread_sleeping_state = data[i - META_OFFSET_SLEEPSTATE] - 1 # subtract 1 as state is incremented to avoid being equal to 0
3,974✔
771
            if thread_sleeping_state == 2
3,974✔
772
                is_task_profile = true
×
773
            end
774
            # cpu_cycle_clock = data[i - META_OFFSET_CPUCYCLECLOCK]
775
            taskid = data[i - META_OFFSET_TASKID]
3,974✔
776
            threadid = data[i - META_OFFSET_THREADID]
3,974✔
777
            if !in(threadid, threads) || !in(taskid, tasks)
7,948✔
778
                skip = true
×
779
                continue
×
780
            end
781
            if thread_sleeping_state == 1
3,974✔
782
                nsleeping += 1
×
783
            end
784
            skip = false
3,974✔
785
            totalshots += 1
3,974✔
786
            empty!(recursive)
3,974✔
787
            if leaf != 0
3,974✔
788
                m[leaf] += 1
1,757✔
789
            end
790
            leaf = 0
3,974✔
791
            startframe = i
3,974✔
792
        elseif !skip
46,517✔
793
            frames = lidict[ip]
46,517✔
794
            nframes = (frames isa Vector ? length(frames) : 1)
46,517✔
795
            # the last lookup is the non-inlined root frame, the first is the inlined leaf frame
796
            for j = nframes:-1:1
59,944✔
797
                frame = (frames isa Vector ? frames[j] : frames)
64,903✔
798
                !C && frame.from_c && continue
64,903✔
799
                key = (T === UInt64 ? ip : frame)
31,989✔
800
                idx = get!(lilist_idx, key, length(lilist) + 1)
31,989✔
801
                if idx > length(lilist)
31,989✔
802
                    push!(recursive, key)
709✔
803
                    push!(lilist, frame)
709✔
804
                    push!(n, 1)
709✔
805
                    push!(m, 0)
709✔
806
                elseif !(key in recursive)
31,280✔
807
                    push!(recursive, key)
30,510✔
808
                    n[idx] += 1
30,510✔
809
                end
810
                leaf = idx
31,989✔
811
            end
64,903✔
812
        end
813
    end
140,692✔
814
    @assert length(lilist) == length(n) == length(m) == length(lilist_idx)
31✔
815
    return (lilist, n, m, totalshots, nsleeping, is_task_profile)
31✔
816
end
817

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

820
function flat(io::IO, data::Vector{UInt64}, lidict::Union{LineInfoDict, LineInfoFlatDict}, cols::Int, fmt::ProfileFormat,
31✔
821
                threads::Union{Int,AbstractVector{Int}}, tasks::Union{UInt,AbstractVector{UInt}}, is_subsection::Bool)
822
    lilist, n, m, totalshots, nsleeping, is_task_profile = parse_flat(fmt.combine ? StackFrame : UInt64, data, lidict, fmt.C, threads, tasks)
62✔
823
    if false # optional: drop the "non-interpretable" ones
31✔
824
        keep = map(frame -> frame != UNKNOWN && frame.line != 0, lilist)
×
825
        lilist = lilist[keep]
×
826
        n = n[keep]
×
827
        m = m[keep]
×
828
    end
829
    util_perc = (1 - (nsleeping / totalshots)) * 100
31✔
830
    filenamemap = FileNameMap()
31✔
831
    if isempty(lilist)
31✔
832
        if is_subsection
2✔
833
            Base.print(io, "Total snapshots: ")
×
834
            printstyled(io, "$(totalshots)", color=Base.warn_color())
×
835
            Base.print(io, ". Utilization: ", round(Int, util_perc), "%\n")
×
836
        else
837
            warning_empty()
2✔
838
        end
839
        return true
2✔
840
    end
841
    is_subsection || print_flat(io, lilist, n, m, cols, filenamemap, fmt)
38✔
842
    if is_task_profile
29✔
843
        Base.print(io, "Total snapshots: ", totalshots, "\n")
×
844
    else
845
        Base.print(io, "Total snapshots: ", totalshots, ". Utilization: ", round(Int, util_perc), "%")
29✔
846
    end
847
    if is_subsection
29✔
848
        println(io)
20✔
849
        print_flat(io, lilist, n, m, cols, filenamemap, fmt)
20✔
850
    elseif !is_task_profile
9✔
851
        Base.print(io, " across all threads and tasks. Use the `groupby` kwarg to break down by thread and/or task.\n")
9✔
852
    end
853
    return false
29✔
854
end
855

856
# make a terminal-clickable link to the file and linenum.
857
# Similar to `define_default_editors` in `Base.Filesystem` but for creating URIs not commands
858
function editor_link(path::String, linenum::Int)
4,329✔
859
    # Note: the editor path can include spaces (if escaped) and flags.
860
    editor = nothing
4,329✔
861
    for var in ["JULIA_EDITOR", "VISUAL", "EDITOR"]
4,329✔
862
        str = get(ENV, var, nothing)
12,987✔
863
        str isa String || continue
12,987✔
864
        editor = str
×
865
        break
×
866
    end
12,987✔
867
    path_encoded = Base.Filesystem.encode_uri_component(path)
4,329✔
868
    if editor !== nothing
4,329✔
869
        if editor == "code"
×
870
            return "vscode://file/$path_encoded:$linenum"
×
871
        elseif editor == "subl" || editor == "sublime_text"
×
872
            return "subl://open?url=file://$path_encoded&line=$linenum"
×
873
        elseif editor == "idea" || occursin("idea", editor)
×
874
            return "idea://open?file=$path_encoded&line=$linenum"
×
875
        elseif editor == "pycharm"
×
876
            return "pycharm://open?file=$path_encoded&line=$linenum"
×
877
        elseif editor == "atom"
×
878
            return "atom://core/open/file?filename=$path_encoded&line=$linenum"
×
879
        elseif editor == "emacsclient" || editor == "emacs"
×
880
            return "emacs://open?file=$path_encoded&line=$linenum"
×
881
        elseif editor == "vim" || editor == "nvim"
×
882
            # Note: Vim/Nvim may not support standard URI schemes without specific plugins
883
            return "vim://open?file=$path_encoded&line=$linenum"
×
884
        end
885
    end
886
    # fallback to generic URI, but line numbers are not supported by generic URI
887
    return Base.Filesystem.uripath(path)
4,329✔
888
end
889

890
function print_flat(io::IO, lilist::Vector{StackFrame},
31✔
891
        n::Vector{Int}, m::Vector{Int},
892
        cols::Int, filenamemap::FileNameMap,
893
        fmt::ProfileFormat)
894
    if fmt.sortedby === :count
31✔
895
        p = sortperm(n)
3✔
896
    elseif fmt.sortedby === :overhead
28✔
897
        p = sortperm(m)
×
898
    else
899
        p = liperm(lilist)
28✔
900
    end
901
    lilist = lilist[p]
31✔
902
    n = n[p]
31✔
903
    m = m[p]
31✔
904
    pkgnames_filenames = Tuple{String,String,String}[short_path(li.file, filenamemap) for li in lilist]
31✔
905
    funcnames = String[string(li.func) for li in lilist]
31✔
906
    wcounts = max(6, ndigits(maximum(n)))
31✔
907
    wself = max(9, ndigits(maximum(m)))
31✔
908
    maxline = 1
31✔
909
    maxfile = 6
31✔
910
    maxfunc = 10
31✔
911
    for i in eachindex(lilist)
31✔
912
        li = lilist[i]
1,083✔
913
        maxline = max(maxline, li.line)
1,083✔
914
        maxfunc = max(maxfunc, textwidth(funcnames[i]))
1,083✔
915
        maxfile = max(maxfile, sum(textwidth, pkgnames_filenames[i][2:3]) + 1)
1,083✔
916
    end
2,135✔
917
    wline = max(5, ndigits(maxline))
31✔
918
    ntext = max(20, cols - wcounts - wself - wline - 3)
31✔
919
    maxfunc += 25 # for type signatures
31✔
920
    if maxfile + maxfunc <= ntext
31✔
921
        wfile = maxfile
2✔
922
        wfunc = ntext - maxfunc # take the full width (for type sig)
2✔
923
    else
924
        wfile = 2*ntext÷5
29✔
925
        wfunc = 3*ntext÷5
29✔
926
    end
927
    println(io, lpad("Count", wcounts, " "), " ", lpad("Overhead", wself, " "), " ",
31✔
928
            rpad("File", wfile, " "), " ", lpad("Line", wline, " "), " Function")
929
    println(io, lpad("=====", wcounts, " "), " ", lpad("========", wself, " "), " ",
31✔
930
            rpad("====", wfile, " "), " ", lpad("====", wline, " "), " ========")
931
    for i in eachindex(n)
31✔
932
        n[i] < fmt.mincount && continue
1,083✔
933
        li = lilist[i]
1,083✔
934
        Base.print(io, lpad(string(n[i]), wcounts, " "), " ")
1,083✔
935
        Base.print(io, lpad(string(m[i]), wself, " "), " ")
1,083✔
936
        if li == UNKNOWN
1,083✔
937
            if !fmt.combine && li.pointer != 0
3✔
938
                Base.print(io, "@0x", string(li.pointer, base=16))
×
939
            else
940
                Base.print(io, "[any unknown stackframes]")
3✔
941
            end
942
        else
943
            path, pkgname, file = pkgnames_filenames[i]
1,080✔
944
            isempty(file) && (file = "[unknown file]")
1,080✔
945
            pkgcolor = get!(() -> popfirst!(Base.STACKTRACE_MODULECOLORS), PACKAGE_FIXEDCOLORS, pkgname)
1,080✔
946
            Base.printstyled(io, pkgname, color=pkgcolor)
2,160✔
947
            file_trunc = ltruncate(file, max(1, wfile))
1,080✔
948
            wpad = wfile - textwidth(pkgname)
1,080✔
949
            if !isempty(pkgname) && !startswith(file_trunc, slash)
1,080✔
950
                Base.print(io, slash)
743✔
951
                wpad -= 1
743✔
952
            end
953
            if isempty(path)
1,080✔
954
                Base.print(io, rpad(file_trunc, wpad, " "))
×
955
            else
956
                link = editor_link(path, li.line)
1,080✔
957
                Base.print(io, rpad(styled"{link=$link:$file_trunc}", wpad, " "))
1,080✔
958
            end
959
            Base.print(io, lpad(li.line > 0 ? string(li.line) : "?", wline, " "), " ")
1,080✔
960
            fname = funcnames[i]
1,080✔
961
            if !li.from_c && li.linfo !== nothing
1,080✔
962
                fname = sprint(show_spec_linfo, li)
521✔
963
            end
964
            isempty(fname) && (fname = "[unknown function]")
1,080✔
965
            Base.print(io, rtruncate(fname, wfunc))
1,080✔
966
        end
967
        println(io)
1,083✔
968
    end
2,135✔
969
    nothing
31✔
970
end
971

972
## A tree representation
973

974
# Representation of a prefix trie of backtrace counts
975
mutable struct StackFrameTree{T} # where T <: Union{UInt64, StackFrame}
976
    # content fields:
977
    frame::StackFrame
978
    count::Int          # number of frames this appeared in
979
    overhead::Int       # number frames where this was the code being executed
980
    flat_count::Int     # number of times this frame was in the flattened representation (unlike count, this'll sum to 100% of parent)
981
    max_recur::Int      # maximum number of times this frame was the *top* of the recursion in the stack
982
    count_recur::Int    # sum of the number of times this frame was the *top* of the recursion in a stack (divide by count to get an average)
983
    sleeping::Bool      # whether this frame was in a sleeping state
984
    down::Dict{T, StackFrameTree{T}}
985
    # construction workers:
986
    recur::Int
987
    builder_key::Vector{UInt64}
988
    builder_value::Vector{StackFrameTree{T}}
989
    up::StackFrameTree{T}
990
    StackFrameTree{T}() where {T} = new(UNKNOWN, 0, 0, 0, 0, 0, true, Dict{T, StackFrameTree{T}}(), 0, UInt64[], StackFrameTree{T}[])
3,724✔
991
end
992

993

994
const indent_s = "    ╎"^10
995
const indent_z = collect(eachindex(indent_s))
996
function indent(depth::Int)
3,204✔
997
    depth < 1 && return ""
3,204✔
998
    depth <= length(indent_z) && return indent_s[1:indent_z[depth]]
3,163✔
999
    div, rem = divrem(depth, length(indent_z))
873✔
1000
    indent = indent_s^div
873✔
1001
    rem != 0 && (indent *= SubString(indent_s, 1, indent_z[rem]))
873✔
1002
    return indent
873✔
1003
end
1004

1005
# mimics Stacktraces
1006
const PACKAGE_FIXEDCOLORS = Dict{String, Any}("@Base" => :gray, "@Core" => :gray)
1007

1008
function tree_format(frames::Vector{<:StackFrameTree}, level::Int, cols::Int, maxes, filenamemap::FileNameMap, showpointer::Bool)
2,813✔
1009
    nindent = min(cols>>1, level)
2,813✔
1010
    ndigoverhead = ndigits(maxes.overhead)
2,813✔
1011
    ndigcounts = ndigits(maxes.count)
2,813✔
1012
    ndigline = ndigits(maximum(frame.frame.line for frame in frames)) + 6
5,626✔
1013
    ntext = max(30, cols - ndigoverhead - nindent - ndigcounts - ndigline - 6)
2,813✔
1014
    widthfile = 2*ntext÷5 # min 12
2,813✔
1015
    strs = Vector{AnnotatedString{String}}(undef, length(frames))
2,813✔
1016
    showextra = false
2,813✔
1017
    if level > nindent
2,813✔
1018
        nextra = level - nindent
649✔
1019
        nindent -= ndigits(nextra) + 2
649✔
1020
        showextra = true
649✔
1021
    end
1022
    for i in eachindex(frames)
2,881✔
1023
        frame = frames[i]
3,271✔
1024
        li = frame.frame
3,271✔
1025
        stroverhead = lpad(frame.overhead > 0 ? string(frame.overhead) : "", ndigoverhead, " ")
3,271✔
1026
        base = nindent == 0 ? "" : indent(nindent - 1) * " "
6,475✔
1027
        if showextra
3,271✔
1028
            base = string(base, "+", nextra, " ")
745✔
1029
        end
1030
        strcount = rpad(string(frame.count), ndigcounts, " ")
3,271✔
1031
        if frame.sleeping
3,271✔
1032
            stroverhead = styled"{gray:$(stroverhead)}"
2,714✔
1033
            strcount = styled"{gray:$(strcount)}"
2,714✔
1034
        end
1035
        if li != UNKNOWN
3,271✔
1036
            if li.line == li.pointer
3,250✔
1037
                strs[i] = string(stroverhead, "╎", base, strcount, " ",
×
1038
                    "[unknown function] (pointer: 0x",
1039
                    string(li.pointer, base = 16, pad = 2*sizeof(Ptr{Cvoid})),
1040
                    ")")
1041
            else
1042
                if !li.from_c && li.linfo !== nothing
3,250✔
1043
                    fname = sprint(show_spec_linfo, li)
1,079✔
1044
                else
1045
                    fname = string(li.func)
2,171✔
1046
                end
1047
                frame.sleeping && (fname = styled"{gray:$(fname)}")
4,597✔
1048
                path, pkgname, filename = short_path(li.file, filenamemap)
3,250✔
1049
                if showpointer
3,250✔
1050
                    fname = string(
×
1051
                        "0x",
1052
                        string(li.pointer, base = 16, pad = 2*sizeof(Ptr{Cvoid})),
1053
                        " ",
1054
                        fname)
1055
                end
1056
                pkgcolor = get!(() -> popfirst!(Base.STACKTRACE_MODULECOLORS), PACKAGE_FIXEDCOLORS, pkgname)
3,260✔
1057
                remaining_path = ltruncate(filename, max(1, widthfile - textwidth(pkgname) - 1))
3,250✔
1058
                linenum = li.line == -1 ? "?" : string(li.line)
5,965✔
1059
                _slash = (!isempty(pkgname) && !startswith(remaining_path, slash)) ? slash : ""
3,250✔
1060
                styled_path = styled"{$pkgcolor:$pkgname}$(_slash)$remaining_path:$linenum"
6,234✔
1061
                rich_file = if isempty(path)
3,250✔
1062
                    styled_path
1✔
1063
                else
1064
                    link = editor_link(path, li.line)
3,249✔
1065
                    styled"{link=$link:$styled_path}"
6,499✔
1066
                end
1067
                strs[i] = Base.annotatedstring(stroverhead, "╎", base, strcount, " ", rich_file, "  ", fname)
3,250✔
1068
                if frame.overhead > 0
3,250✔
1069
                    strs[i] = styled"{bold:$(strs[i])}"
549✔
1070
                end
1071
            end
1072
        else
1073
            strs[i] = string(stroverhead, "╎", base, strcount, " [unknown stackframe]")
21✔
1074
        end
1075
        strs[i] = rtruncate(strs[i], cols)
3,271✔
1076
    end
3,729✔
1077
    return strs
2,813✔
1078
end
1079

1080
# turn a list of backtraces into a tree (implicitly separated by NULL markers)
1081
function tree!(root::StackFrameTree{T}, all::Vector{UInt64}, lidict::Union{LineInfoFlatDict, LineInfoDict}, C::Bool, recur::Symbol,
41✔
1082
                threads::Union{Int,AbstractVector{Int},Nothing}=nothing, tasks::Union{UInt,AbstractVector{UInt},Nothing}=nothing) where {T}
1083
    !isempty(all) && !has_meta(all) && error("Profile data is missing required metadata")
41✔
1084
    parent = root
40✔
1085
    tops = Vector{StackFrameTree{T}}()
40✔
1086
    build = Vector{StackFrameTree{T}}()
40✔
1087
    startframe = length(all)
40✔
1088
    skip = false
40✔
1089
    nsleeping = 0
40✔
1090
    is_task_profile = false
40✔
1091
    is_sleeping = true
40✔
1092
    for i in startframe:-1:1
80✔
1093
        (startframe - 1) >= i >= (startframe - (nmeta + 1)) && continue # skip metadata (it's read ahead below) and extra block end NULL IP
99,766✔
1094
        ip = all[i]
69,341✔
1095
        if is_block_end(all, i)
138,634✔
1096
            # read metadata
1097
            thread_sleeping_state = all[i - META_OFFSET_SLEEPSTATE] - 1 # subtract 1 as state is incremented to avoid being equal to 0
6,198✔
1098
            is_sleeping = thread_sleeping_state == 1
6,198✔
1099
            is_task_profile = thread_sleeping_state == 2
6,198✔
1100
            # cpu_cycle_clock = all[i - META_OFFSET_CPUCYCLECLOCK]
1101
            taskid = all[i - META_OFFSET_TASKID]
6,198✔
1102
            threadid = all[i - META_OFFSET_THREADID]
6,198✔
1103
            if (threads !== nothing && !in(threadid, threads)) ||
12,392✔
1104
               (tasks !== nothing && !in(taskid, tasks))
1105
                skip = true
114✔
1106
                continue
114✔
1107
            end
1108
            if thread_sleeping_state == 1
6,084✔
1109
                nsleeping += 1
213✔
1110
            end
1111
            skip = false
6,084✔
1112
            # sentinel value indicates the start of a new backtrace
1113
            empty!(build)
9,018✔
1114
            root.recur = 0
6,084✔
1115
            if recur !== :off
6,084✔
1116
                # We mark all visited nodes to so we'll only count those branches
1117
                # once for each backtrace. Reset that now for the next backtrace.
1118
                push!(tops, parent)
274✔
1119
                for top in tops
274✔
1120
                    while top.recur != 0
2,312✔
1121
                        top.max_recur < top.recur && (top.max_recur = top.recur)
2,038✔
1122
                        top.recur = 0
2,038✔
1123
                        top = top.up
2,038✔
1124
                    end
2,038✔
1125
                end
274✔
1126
                empty!(tops)
274✔
1127
            end
1128
            let this = parent
6,084✔
1129
                while this !== root
53,340✔
1130
                    this.flat_count += 1
47,256✔
1131
                    this = this.up
47,256✔
1132
                end
47,256✔
1133
            end
1134
            parent.overhead += 1
6,084✔
1135
            parent = root
6,084✔
1136
            root.count += 1
6,084✔
1137
            startframe = i
6,084✔
1138
        elseif !skip
63,143✔
1139
            if recur === :flat || recur === :flatc
121,036✔
1140
                pushfirst!(build, parent)
3,300✔
1141
                # Rewind the `parent` tree back, if this exact ip was already present *higher* in the current tree
1142
                found = false
3,208✔
1143
                for j in 1:(startframe - i)
3,208✔
1144
                    if ip == all[i + j]
58,702✔
1145
                        if recur === :flat # if not flattening C frames, check that now
110✔
1146
                            frames = lidict[ip]
110✔
1147
                            frame = (frames isa Vector ? frames[1] : frames)
110✔
1148
                            frame.from_c && break # not flattening this frame
110✔
1149
                        end
1150
                        push!(tops, parent)
×
1151
                        parent = build[j]
×
1152
                        parent.recur += 1
×
1153
                        parent.count_recur += 1
×
1154
                        parent.sleeping &= is_sleeping
×
1155
                        found = true
×
1156
                        break
×
1157
                    end
1158
                end
58,592✔
1159
                found && continue
3,208✔
1160
            end
1161
            builder_key = parent.builder_key
62,122✔
1162
            builder_value = parent.builder_value
62,122✔
1163
            fastkey = searchsortedfirst(builder_key, ip)
62,122✔
1164
            if fastkey < length(builder_key) && builder_key[fastkey] === ip
62,122✔
1165
                # jump forward to the end of the inlining chain
1166
                # avoiding an extra (slow) lookup of `ip` in `lidict`
1167
                # and an extra chain of them in `down`
1168
                # note that we may even have this === parent (if we're ignoring this frame ip)
1169
                this = builder_value[fastkey]
58,484✔
1170
                let this = this
58,484✔
1171
                    while this !== parent && (recur === :off || this.recur == 0)
104,338✔
1172
                        this.count += 1
43,912✔
1173
                        this.recur = 1
43,912✔
1174
                        this.sleeping &= is_sleeping
43,912✔
1175
                        this = this.up
43,912✔
1176
                    end
43,912✔
1177
                end
1178
                parent = this
58,484✔
1179
                continue
58,484✔
1180
            end
1181

1182
            frames = lidict[ip]
3,638✔
1183
            nframes = (frames isa Vector ? length(frames) : 1)
3,638✔
1184
            this = parent
3,638✔
1185
            # add all the inlining frames
1186
            for i = nframes:-1:1
4,517✔
1187
                frame = (frames isa Vector ? frames[i] : frames)
4,809✔
1188
                !C && frame.from_c && continue
4,809✔
1189
                key = (T === UInt64 ? ip : frame)
3,381✔
1190
                this = get!(StackFrameTree{T}, parent.down, key)
3,381✔
1191
                if recur === :off || this.recur == 0
3,477✔
1192
                    this.frame = frame
3,381✔
1193
                    this.up = parent
3,381✔
1194
                    this.count += 1
3,381✔
1195
                    this.recur = 1
3,381✔
1196
                    this.sleeping &= is_sleeping
3,381✔
1197
                end
1198
                parent = this
3,381✔
1199
            end
5,980✔
1200
            # record where the end of this chain is for this ip
1201
            insert!(builder_key, fastkey, ip)
3,638✔
1202
            insert!(builder_value, fastkey, this)
3,638✔
1203
        end
1204
    end
199,493✔
1205
    function cleanup!(node::StackFrameTree)
80✔
1206
        stack = [node]
40✔
1207
        while !isempty(stack)
2,042✔
1208
            node = pop!(stack)
2,002✔
1209
            node.recur = 0
2,002✔
1210
            empty!(node.builder_key)
4,365✔
1211
            empty!(node.builder_value)
4,365✔
1212
            append!(stack, values(node.down))
2,002✔
1213
        end
2,002✔
1214
        nothing
40✔
1215
    end
1216
    cleanup!(root)
40✔
1217
    return root, nsleeping, is_task_profile
40✔
1218
end
1219

1220
function maxstats(root::StackFrameTree)
43✔
1221
    maxcount = Ref(0)
43✔
1222
    maxflatcount = Ref(0)
43✔
1223
    maxoverhead = Ref(0)
43✔
1224
    maxmaxrecur = Ref(0)
43✔
1225
    stack = [root]
43✔
1226
    while !isempty(stack)
3,757✔
1227
        node = pop!(stack)
3,714✔
1228
        maxcount[] = max(maxcount[], node.count)
3,714✔
1229
        maxoverhead[] = max(maxoverhead[], node.overhead)
3,714✔
1230
        maxflatcount[] = max(maxflatcount[], node.flat_count)
3,714✔
1231
        maxmaxrecur[] = max(maxmaxrecur[], node.max_recur)
3,714✔
1232
        append!(stack, values(node.down))
3,714✔
1233
    end
3,714✔
1234
    return (count=maxcount[], count_flat=maxflatcount[], overhead=maxoverhead[], max_recur=maxmaxrecur[])
43✔
1235
end
1236

1237
# Print the stack frame tree starting at a particular root. Uses a worklist to
1238
# avoid stack overflows.
1239
function print_tree(io::IO, bt::StackFrameTree{T}, cols::Int, fmt::ProfileFormat, is_subsection::Bool) where T
43✔
1240
    maxes = maxstats(bt)
43✔
1241
    filenamemap = FileNameMap()
43✔
1242
    worklist = [(bt, 0, 0, AnnotatedString(""))]
43✔
1243
    if !is_subsection
43✔
1244
        Base.print(io, "Overhead ╎ [+additional indent] Count File:Line  Function\n")
21✔
1245
        Base.print(io, "=========================================================\n")
21✔
1246
    end
1247
    while !isempty(worklist)
3,357✔
1248
        (bt, level, noisefloor, str) = popfirst!(worklist)
3,314✔
1249
        isempty(str) || println(io, str)
6,585✔
1250
        level > fmt.maxdepth && continue
3,314✔
1251
        isempty(bt.down) && continue
3,311✔
1252
        # Order the line information
1253
        nexts = collect(values(bt.down))
2,813✔
1254
        # Generate the string for each line
1255
        strs = tree_format(nexts, level, cols, maxes, filenamemap, T === UInt64)
2,813✔
1256
        # Recurse to the next level
1257
        if fmt.sortedby === :count
2,813✔
1258
            counts = collect(frame.count for frame in nexts)
×
1259
            p = sortperm(counts)
×
1260
        elseif fmt.sortedby === :overhead
2,813✔
1261
            m = collect(frame.overhead for frame in nexts)
×
1262
            p = sortperm(m)
×
1263
        elseif fmt.sortedby === :flat_count
2,813✔
1264
            m = collect(frame.flat_count for frame in nexts)
×
1265
            p = sortperm(m)
×
1266
        else
1267
            lilist = collect(frame.frame for frame in nexts)
5,626✔
1268
            p = liperm(lilist)
2,813✔
1269
        end
1270
        for i in reverse(p)
2,813✔
1271
            down = nexts[i]
3,271✔
1272
            count = down.count
3,271✔
1273
            count < fmt.mincount && continue
3,271✔
1274
            count < noisefloor && continue
3,271✔
1275
            str = strs[i]::AnnotatedString
3,271✔
1276
            noisefloor_down = fmt.noisefloor > 0 ? floor(Int, fmt.noisefloor * sqrt(count)) : 0
3,271✔
1277
            pushfirst!(worklist, (down, level + 1, noisefloor_down, str))
3,271✔
1278
        end
3,271✔
1279
    end
3,314✔
1280
    return
43✔
1281
end
1282

1283
function tree(io::IO, data::Vector{UInt64}, lidict::Union{LineInfoFlatDict, LineInfoDict}, cols::Int, fmt::ProfileFormat,
39✔
1284
                threads::Union{Int,AbstractVector{Int}}, tasks::Union{UInt,AbstractVector{UInt}}, is_subsection::Bool)
1285
    if fmt.combine
39✔
1286
        root, nsleeping, is_task_profile = tree!(StackFrameTree{StackFrame}(), data, lidict, fmt.C, fmt.recur, threads, tasks)
39✔
1287
    else
1288
        root, nsleeping, is_task_profile = tree!(StackFrameTree{UInt64}(), data, lidict, fmt.C, fmt.recur, threads, tasks)
×
1289
    end
1290
    util_perc = (1 - (nsleeping / root.count)) * 100
78✔
1291
    is_subsection || print_tree(io, root, cols, fmt, is_subsection)
55✔
1292
    if isempty(root.down)
78✔
1293
        if is_subsection
3✔
1294
            Base.print(io, "Total snapshots: ")
1✔
1295
            printstyled(io, "$(root.count)", color=Base.warn_color())
2✔
1296
            Base.print(io, ". Utilization: ", round(Int, util_perc), "%\n")
1✔
1297
        else
1298
            warning_empty()
2✔
1299
        end
1300
        return true
3✔
1301
    end
1302
    if is_task_profile
36✔
1303
        Base.print(io, "Total snapshots: ", root.count, "\n")
×
1304
    else
1305
        Base.print(io, "Total snapshots: ", root.count, ". Utilization: ", round(Int, util_perc), "%")
36✔
1306
    end
1307
    if is_subsection
36✔
1308
        Base.println(io)
22✔
1309
        print_tree(io, root, cols, fmt, is_subsection)
44✔
1310
    elseif !is_task_profile
14✔
1311
        Base.print(io, " across all threads and tasks. Use the `groupby` kwarg to break down by thread and/or task.\n")
14✔
1312
    end
1313
    return false
36✔
1314
end
1315

1316
function callersf(matchfunc::Function, bt::Vector, lidict::LineInfoFlatDict)
2✔
1317
    counts = Dict{StackFrame, Int}()
2✔
1318
    lastmatched = false
2✔
1319
    for id in bt
2✔
1320
        if id == 0
×
1321
            lastmatched = false
×
1322
            continue
×
1323
        end
1324
        li = lidict[id]
×
1325
        if lastmatched
×
1326
            if haskey(counts, li)
×
1327
                counts[li] += 1
×
1328
            else
1329
                counts[li] = 1
×
1330
            end
1331
        end
1332
        lastmatched = matchfunc(li)
×
1333
    end
×
1334
    k = collect(keys(counts))
2✔
1335
    v = collect(values(counts))
2✔
1336
    p = sortperm(v, rev=true)
2✔
1337
    return [(v[i], k[i]) for i in p]
2✔
1338
end
1339

1340
## Utilities
1341

1342
# Order alphabetically (file, function) and then by line number
1343
function liperm(lilist::Vector{StackFrame})
1344
    function lt(a::StackFrame, b::StackFrame)
18,281✔
1345
        a == UNKNOWN && return false
15,440✔
1346
        b == UNKNOWN && return true
15,386✔
1347
        fcmp = cmp(a.file, b.file)
15,332✔
1348
        fcmp < 0 && return true
15,332✔
1349
        fcmp > 0 && return false
9,481✔
1350
        fcmp = cmp(a.func, b.func)
3,630✔
1351
        fcmp < 0 && return true
3,630✔
1352
        fcmp > 0 && return false
2,342✔
1353
        fcmp = cmp(a.line, b.line)
1,054✔
1354
        fcmp < 0 && return true
1,054✔
1355
        return false
558✔
1356
    end
1357
    return sortperm(lilist, lt = lt)
2,841✔
1358
end
1359

1360
function warning_empty(;summary = false)
46✔
1361
    if summary
46✔
1362
        @warn """
42✔
1363
        There were no samples collected in one or more groups.
1364
        This may be due to idle threads, or you may need to run your
1365
        program longer (perhaps by running it multiple times),
1366
        or adjust the delay between samples with `Profile.init()`."""
1367
    else
1368
        @warn """
4✔
1369
        There were no samples collected.
1370
        Run your program longer (perhaps by running it multiple times),
1371
        or adjust the delay between samples with `Profile.init()`."""
1372
    end
1373
end
1374

1375

1376
"""
1377
    Profile.take_heap_snapshot(filepath::String, all_one::Bool=false;
1378
                               redact_data::Bool=true, streaming::Bool=false)
1379
    Profile.take_heap_snapshot(all_one::Bool=false; redact_data:Bool=true,
1380
                               dir::String=nothing, streaming::Bool=false)
1381

1382
Write a snapshot of the heap, in the JSON format expected by the Chrome
1383
Devtools Heap Snapshot viewer (.heapsnapshot extension) to a file
1384
(`\$pid_\$timestamp.heapsnapshot`) in the current directory by default (or tempdir if
1385
the current directory is unwritable), or in `dir` if given, or the given
1386
full file path, or IO stream.
1387

1388
If `all_one` is true, then report the size of every object as one so they can be easily
1389
counted. Otherwise, report the actual size.
1390

1391
If `redact_data` is true (default), then do not emit the contents of any object.
1392

1393
If `streaming` is true, we will stream the snapshot data out into four files, using filepath
1394
as the prefix, to avoid having to hold the entire snapshot in memory. This option should be
1395
used for any setting where your memory is constrained. These files can then be reassembled
1396
by calling Profile.HeapSnapshot.assemble_snapshot(), which can
1397
be done offline.
1398

1399
NOTE: We strongly recommend setting streaming=true for performance reasons. Reconstructing
1400
the snapshot from the parts requires holding the entire snapshot in memory, so if the
1401
snapshot is large, you can run out of memory while processing it. Streaming allows you to
1402
reconstruct the snapshot offline, after your workload is done running.
1403
If you do attempt to collect a snapshot with streaming=false (the default, for
1404
backwards-compatibility) and your process is killed, note that this will always save the
1405
parts in the same directory as your provided filepath, so you can still reconstruct the
1406
snapshot after the fact, via `assemble_snapshot()`.
1407
"""
1408
function take_heap_snapshot(filepath::AbstractString, all_one::Bool=false; redact_data::Bool=true, streaming::Bool=false)
4✔
1409
    if streaming
2✔
1410
        _stream_heap_snapshot(filepath, all_one, redact_data)
×
1411
    else
1412
        # Support the legacy, non-streaming mode, by first streaming the parts, then
1413
        # reassembling it after we're done.
1414
        prefix = filepath
2✔
1415
        _stream_heap_snapshot(prefix, all_one, redact_data)
2✔
1416
        Profile.HeapSnapshot.assemble_snapshot(prefix, filepath)
2✔
1417
        Profile.HeapSnapshot.cleanup_streamed_files(prefix)
2✔
1418
    end
1419
    return filepath
2✔
1420
end
1421
function take_heap_snapshot(io::IO, all_one::Bool=false; redact_data::Bool=true)
×
1422
    # Support the legacy, non-streaming mode, by first streaming the parts to a tempdir,
1423
    # then reassembling it after we're done.
1424
    dir = tempdir()
×
1425
    prefix = joinpath(dir, "snapshot")
×
1426
    _stream_heap_snapshot(prefix, all_one, redact_data)
×
1427
    Profile.HeapSnapshot.assemble_snapshot(prefix, io)
×
1428
end
1429
function _stream_heap_snapshot(prefix::AbstractString, all_one::Bool, redact_data::Bool)
1430
    # Nodes and edges are binary files
1431
    open("$prefix.nodes", "w") do nodes
2✔
1432
        open("$prefix.edges", "w") do edges
2✔
1433
            open("$prefix.strings", "w") do strings
2✔
1434
                # The following file is json data
1435
                open("$prefix.metadata.json", "w") do json
2✔
1436
                    Base.@_lock_ios(nodes,
2✔
1437
                    Base.@_lock_ios(edges,
1438
                    Base.@_lock_ios(strings,
1439
                    Base.@_lock_ios(json,
1440
                        ccall(:jl_gc_take_heap_snapshot,
1441
                            Cvoid,
1442
                            (Ptr{Cvoid},Ptr{Cvoid},Ptr{Cvoid},Ptr{Cvoid}, Cchar, Cchar),
1443
                            nodes.handle, edges.handle, strings.handle, json.handle,
1444
                            Cchar(all_one), Cchar(redact_data))
1445
                    )
1446
                    )
1447
                    )
1448
                    )
1449
                end
1450
            end
1451
        end
1452
    end
1453
end
1454
function take_heap_snapshot(all_one::Bool=false; dir::Union{Nothing,S}=nothing, kwargs...) where {S <: AbstractString}
6✔
1455
    fname = "$(getpid())_$(time_ns()).heapsnapshot"
2✔
1456
    if isnothing(dir)
2✔
1457
        wd = pwd()
2✔
1458
        fpath = joinpath(wd, fname)
2✔
1459
        try
2✔
1460
            touch(fpath)
2✔
1461
            rm(fpath; force=true)
2✔
1462
        catch
1463
            @warn "Cannot write to current directory `$(pwd())` so saving heap snapshot to `$(tempdir())`" maxlog=1 _id=Symbol(wd)
×
1464
            fpath = joinpath(tempdir(), fname)
×
1465
        end
1466
    else
1467
        fpath = joinpath(expanduser(dir), fname)
×
1468
    end
1469
    return take_heap_snapshot(fpath, all_one; kwargs...)
2✔
1470
end
1471

1472
"""
1473
    Profile.take_page_profile(io::IOStream)
1474
    Profile.take_page_profile(filepath::String)
1475

1476
Write a JSON snapshot of the pages from Julia's pool allocator, printing for every pool allocated object, whether it's garbage, or its type.
1477
"""
1478
function take_page_profile(io::IOStream)
1✔
1479
    Base.@_lock_ios(io, ccall(:jl_gc_take_page_profile, Cvoid, (Ptr{Cvoid},), io.handle))
1✔
1480
end
1481
function take_page_profile(filepath::String)
1✔
1482
    open(filepath, "w") do io
1✔
1483
        take_page_profile(io)
1✔
1484
    end
1485
    return filepath
1✔
1486
end
1487

1488
include("Allocs.jl")
1489
include("heapsnapshot_reassemble.jl")
1490
include("precompile.jl")
1491

1492
end # module
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc