|
| 1 | +using Printf |
| 2 | + |
| 3 | +""" |
| 4 | + AllocFrameSummary(name, count, bytes) |
| 5 | +
|
| 6 | +One row of an aggregated `AllocProfileResult` view — a frame, leaf call site, |
| 7 | +or allocated type with its (optionally scaled) sample count and byte total. |
| 8 | +""" |
| 9 | +struct AllocFrameSummary |
| 10 | + name::String |
| 11 | + count::Int |
| 12 | + bytes::Int |
| 13 | +end |
| 14 | + |
| 15 | +# Frames that come from the Julia runtime, the allocation profiler itself, |
| 16 | +# or the loader. Carry no information about user-code hotpaths. |
| 17 | +const _ALLOC_NOISE_FRAME_PATTERNS = [ |
| 18 | + "Profile.Allocs", |
| 19 | + "gc-alloc-profiler", |
| 20 | + "gc-stock.c", |
| 21 | + "gc.c:", |
| 22 | + "jl_apply", |
| 23 | + "jl_toplevel_", |
| 24 | + "ijl_toplevel_", |
| 25 | + "jl_interpret_toplevel_thunk", |
| 26 | + "jl_repl_entrypoint", |
| 27 | + "interpreter.c", |
| 28 | + "_include(", |
| 29 | + "include_string(", |
| 30 | + "loading.jl", |
| 31 | + "client.jl", |
| 32 | + "_start() at sys.so", |
| 33 | + "ip:0x", |
| 34 | + "_start at ", |
| 35 | + " at Base.jl:", |
| 36 | + "true_main at jlapi.c", |
| 37 | + "__libc_start_main", |
| 38 | + "loader_exe.c", |
| 39 | + "jl_system_image_data", |
| 40 | + "macro expansion at Allocs.jl", |
| 41 | + "boot.jl:", |
| 42 | + "jl_f__call_latest", |
| 43 | +] |
| 44 | + |
| 45 | +# Wrapper frames inside HarmoniqsBenchmarks itself — when the caller wants the |
| 46 | +# first frame in *their* code, the harness call stack is noise. |
| 47 | +const _ALLOC_HBJ_WRAPPER_FRAME_PATTERNS = [ |
| 48 | + "benchmark_memory!", |
| 49 | + "HarmoniqsBenchmarks", |
| 50 | +] |
| 51 | + |
| 52 | +const _ALLOC_NOISE_TYPE_PATTERNS = [ |
| 53 | + "Profile.Allocs", |
| 54 | +] |
| 55 | + |
| 56 | +_is_noise_frame(f, extra) = any(p -> occursin(p, f), _ALLOC_NOISE_FRAME_PATTERNS) || |
| 57 | + any(p -> occursin(p, f), extra) |
| 58 | +_is_wrapper_frame(f, extra) = any(p -> occursin(p, f), _ALLOC_HBJ_WRAPPER_FRAME_PATTERNS) || |
| 59 | + any(p -> occursin(p, f), extra) |
| 60 | +_is_noise_type(t) = any(p -> occursin(p, t), _ALLOC_NOISE_TYPE_PATTERNS) |
| 61 | + |
| 62 | +function _first_user_frame(stack, extra_noise, extra_wrappers) |
| 63 | + for f in stack |
| 64 | + _is_noise_frame(f, extra_noise) && continue |
| 65 | + _is_wrapper_frame(f, extra_wrappers) && continue |
| 66 | + return f |
| 67 | + end |
| 68 | + return isempty(stack) ? "<empty>" : stack[end] |
| 69 | +end |
| 70 | + |
| 71 | +_alloc_scale(profile::AllocProfileResult, scale::Bool) = |
| 72 | + scale ? max(1.0, 1 / profile.sample_rate) : 1.0 |
| 73 | + |
| 74 | +function _rank(by_key::Dict{String,Tuple{Int,Int}}, k::Int, scale::Real) |
| 75 | + rows = Vector{AllocFrameSummary}() |
| 76 | + sizehint!(rows, length(by_key)) |
| 77 | + for (name, (cnt, bytes)) in by_key |
| 78 | + push!(rows, AllocFrameSummary(name, round(Int, cnt * scale), round(Int, bytes * scale))) |
| 79 | + end |
| 80 | + sort!(rows; by = r -> -r.bytes) |
| 81 | + return rows[1:min(k, length(rows))] |
| 82 | +end |
| 83 | + |
| 84 | +""" |
| 85 | + top_alloc_types(profile; k=15, scale=true) -> Vector{AllocFrameSummary} |
| 86 | +
|
| 87 | +Aggregate samples in `profile` by allocated type, sorted by bytes descending. |
| 88 | +With `scale=true` (default), counts and bytes are extrapolated by |
| 89 | +`1 / profile.sample_rate` so totals match the actual run rather than the |
| 90 | +recorded sample. |
| 91 | +""" |
| 92 | +function top_alloc_types(profile::AllocProfileResult; k::Int = 15, scale::Bool = true) |
| 93 | + by_type = Dict{String,Tuple{Int,Int}}() |
| 94 | + for s in profile.samples |
| 95 | + _is_noise_type(s.type_name) && continue |
| 96 | + cnt, bytes = get(by_type, s.type_name, (0, 0)) |
| 97 | + by_type[s.type_name] = (cnt + 1, bytes + s.size_bytes) |
| 98 | + end |
| 99 | + return _rank(by_type, k, _alloc_scale(profile, scale)) |
| 100 | +end |
| 101 | + |
| 102 | +""" |
| 103 | + top_alloc_frames(profile; k=25, scale=true, drop_wrappers=true, extra_noise=String[], extra_wrappers=String[]) |
| 104 | + -> Vector{AllocFrameSummary} |
| 105 | +
|
| 106 | +Aggregate samples by every frame in their stacktraces (a single allocation |
| 107 | +contributes once per frame on its stack). Useful for finding hot regions of |
| 108 | +code that participate in many allocations even if they are not the leaf call. |
| 109 | +
|
| 110 | +Pass `extra_noise` / `extra_wrappers` to drop additional caller-specific |
| 111 | +patterns alongside the built-in Julia runtime / HBJ wrapper filters. |
| 112 | +""" |
| 113 | +function top_alloc_frames( |
| 114 | + profile::AllocProfileResult; |
| 115 | + k::Int = 25, |
| 116 | + scale::Bool = true, |
| 117 | + drop_wrappers::Bool = true, |
| 118 | + extra_noise::Vector{String} = String[], |
| 119 | + extra_wrappers::Vector{String} = String[], |
| 120 | +) |
| 121 | + by_frame = Dict{String,Tuple{Int,Int}}() |
| 122 | + for s in profile.samples |
| 123 | + _is_noise_type(s.type_name) && continue |
| 124 | + for frame in s.stacktrace |
| 125 | + _is_noise_frame(frame, extra_noise) && continue |
| 126 | + drop_wrappers && _is_wrapper_frame(frame, extra_wrappers) && continue |
| 127 | + cnt, bytes = get(by_frame, frame, (0, 0)) |
| 128 | + by_frame[frame] = (cnt + 1, bytes + s.size_bytes) |
| 129 | + end |
| 130 | + end |
| 131 | + return _rank(by_frame, k, _alloc_scale(profile, scale)) |
| 132 | +end |
| 133 | + |
| 134 | +""" |
| 135 | + top_alloc_leaves(profile; k=25, scale=true, extra_noise=String[], extra_wrappers=String[]) |
| 136 | + -> Vector{AllocFrameSummary} |
| 137 | +
|
| 138 | +Aggregate samples by their *first non-noise, non-wrapper frame* — i.e. the |
| 139 | +call site in user code that directly produced the allocation. Most useful |
| 140 | +view for triaging allocation hotspots. |
| 141 | +""" |
| 142 | +function top_alloc_leaves( |
| 143 | + profile::AllocProfileResult; |
| 144 | + k::Int = 25, |
| 145 | + scale::Bool = true, |
| 146 | + extra_noise::Vector{String} = String[], |
| 147 | + extra_wrappers::Vector{String} = String[], |
| 148 | +) |
| 149 | + by_leaf = Dict{String,Tuple{Int,Int}}() |
| 150 | + for s in profile.samples |
| 151 | + _is_noise_type(s.type_name) && continue |
| 152 | + leaf = _first_user_frame(s.stacktrace, extra_noise, extra_wrappers) |
| 153 | + cnt, bytes = get(by_leaf, leaf, (0, 0)) |
| 154 | + by_leaf[leaf] = (cnt + 1, bytes + s.size_bytes) |
| 155 | + end |
| 156 | + return _rank(by_leaf, k, _alloc_scale(profile, scale)) |
| 157 | +end |
| 158 | + |
| 159 | +function _fmt_alloc_bytes(b::Real) |
| 160 | + b >= 1 << 30 && return @sprintf("%.2f GB", b / (1 << 30)) |
| 161 | + b >= 1 << 20 && return @sprintf("%.2f MB", b / (1 << 20)) |
| 162 | + b >= 1 << 10 && return @sprintf("%.2f KB", b / (1 << 10)) |
| 163 | + return @sprintf("%d B", Int(round(b))) |
| 164 | +end |
| 165 | + |
| 166 | +_truncate_name(s, n) = length(s) <= n ? s : string(first(s, n - 1), "…") |
| 167 | + |
| 168 | +function _print_summary_table(io::IO, header::AbstractString, rows::Vector{AllocFrameSummary}, name_width::Int) |
| 169 | + println(io, "\n", header) |
| 170 | + println(io, rpad(" bytes", 14), rpad("samples", 10), "name") |
| 171 | + for r in rows |
| 172 | + @printf(io, " %-12s %-8d %s\n", _fmt_alloc_bytes(r.bytes), r.count, _truncate_name(r.name, name_width)) |
| 173 | + end |
| 174 | +end |
| 175 | + |
| 176 | +""" |
| 177 | + report_alloc_profile(profile; io=stdout, k_types=10, k_leaves=20, k_frames=20, |
| 178 | + scale=true, extra_noise=String[], extra_wrappers=String[]) |
| 179 | +
|
| 180 | +Pretty-print a three-section human-readable report of `profile`: top allocated |
| 181 | +types, top leaf call sites, and top all-frames. Sections are sorted by |
| 182 | +allocated bytes; counts and totals are scaled by `1 / sample_rate` unless |
| 183 | +`scale=false`. |
| 184 | +""" |
| 185 | +function report_alloc_profile( |
| 186 | + profile::AllocProfileResult; |
| 187 | + io::IO = stdout, |
| 188 | + k_types::Int = 10, |
| 189 | + k_leaves::Int = 20, |
| 190 | + k_frames::Int = 20, |
| 191 | + scale::Bool = true, |
| 192 | + extra_noise::Vector{String} = String[], |
| 193 | + extra_wrappers::Vector{String} = String[], |
| 194 | +) |
| 195 | + scale_factor = _alloc_scale(profile, scale) |
| 196 | + @printf( |
| 197 | + io, |
| 198 | + "=== Alloc profile: %s / %s (N=%d, sample_rate=%g, samples=%d, total≈%s) ===\n", |
| 199 | + profile.solver, profile.benchmark_name, profile.N, profile.sample_rate, |
| 200 | + profile.total_count, _fmt_alloc_bytes(profile.total_bytes * scale_factor), |
| 201 | + ) |
| 202 | + _print_summary_table( |
| 203 | + io, |
| 204 | + "Top $k_types allocated types (scaled ×$(Int(round(scale_factor)))):", |
| 205 | + top_alloc_types(profile; k = k_types, scale = scale), |
| 206 | + 120, |
| 207 | + ) |
| 208 | + _print_summary_table( |
| 209 | + io, |
| 210 | + "Top $k_leaves leaf call sites (scaled ×$(Int(round(scale_factor)))):", |
| 211 | + top_alloc_leaves( |
| 212 | + profile; |
| 213 | + k = k_leaves, |
| 214 | + scale = scale, |
| 215 | + extra_noise = extra_noise, |
| 216 | + extra_wrappers = extra_wrappers, |
| 217 | + ), |
| 218 | + 140, |
| 219 | + ) |
| 220 | + _print_summary_table( |
| 221 | + io, |
| 222 | + "Top $k_frames frames (scaled ×$(Int(round(scale_factor)))):", |
| 223 | + top_alloc_frames( |
| 224 | + profile; |
| 225 | + k = k_frames, |
| 226 | + scale = scale, |
| 227 | + extra_noise = extra_noise, |
| 228 | + extra_wrappers = extra_wrappers, |
| 229 | + ), |
| 230 | + 140, |
| 231 | + ) |
| 232 | + return nothing |
| 233 | +end |
0 commit comments