|
| 1 | +# anyplot.ai |
| 2 | +# flamegraph-basic: Flame Graph for Performance Profiling |
| 3 | +# Library: makie 0.22.10 | Julia 1.11.9 |
| 4 | +# Quality: 88/100 | Created: 2026-06-08 |
| 5 | + |
| 6 | +using CairoMakie |
| 7 | +using Colors |
| 8 | +using Random |
| 9 | + |
| 10 | +Random.seed!(42) |
| 11 | + |
| 12 | +# Theme tokens ---------------------------------------------------------------- |
| 13 | +const THEME = get(ENV, "ANYPLOT_THEME", "light") |
| 14 | +const PAGE_BG = THEME == "light" ? colorant"#FAF8F1" : colorant"#1A1A17" |
| 15 | +const INK = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8" |
| 16 | +const INK_SOFT = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0" |
| 17 | + |
| 18 | +# Imprint warm subset — semantic exception (the conventional flame-graph |
| 19 | +# aesthetic is yellow → orange → red, so brand green sits out for this spec). |
| 20 | +const FLAME_COLORS = [ |
| 21 | + colorant"#DDCC77", # amber (Imprint anchor — warning / heat) |
| 22 | + colorant"#BD8233", # ochre (Imprint #4) |
| 23 | + colorant"#AE3030", # matte red (Imprint #5) |
| 24 | +] |
| 25 | + |
| 26 | +# In-bar label ink chosen per fill by relative luminance — dark ink on the |
| 27 | +# light amber / ochre bars, light ink on the matte-red bars where dark text |
| 28 | +# would lose contrast. |
| 29 | +function contrast_ink(c) |
| 30 | + r, g, b = red(c), green(c), blue(c) |
| 31 | + 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.5 ? |
| 32 | + colorant"#1A1A17" : colorant"#FAF8F1" |
| 33 | +end |
| 34 | +const FLAME_LABEL_INK = [contrast_ink(c) for c in FLAME_COLORS] |
| 35 | + |
| 36 | +# Theme() hoists chrome tokens into a single declarative block — the per-Axis |
| 37 | +# kwargs below only need to override plot-specific knobs (title, limits, etc). |
| 38 | +set_theme!(Theme( |
| 39 | + fontsize = 14, |
| 40 | + backgroundcolor = PAGE_BG, |
| 41 | + Axis = ( |
| 42 | + backgroundcolor = PAGE_BG, |
| 43 | + titlecolor = INK, |
| 44 | + xlabelcolor = INK, |
| 45 | + ylabelcolor = INK_SOFT, |
| 46 | + xticklabelcolor = INK_SOFT, |
| 47 | + bottomspinecolor = INK_SOFT, |
| 48 | + xtickcolor = INK_SOFT, |
| 49 | + topspinevisible = false, |
| 50 | + rightspinevisible = false, |
| 51 | + leftspinevisible = false, |
| 52 | + yticksvisible = false, |
| 53 | + yticklabelsvisible = false, |
| 54 | + xgridvisible = false, |
| 55 | + ygridvisible = false, |
| 56 | + ), |
| 57 | +)) |
| 58 | + |
| 59 | +# Simulated CPU profile of a web request handler. |
| 60 | +# Each entry: (semicolon-delimited stack from root to leaf, sample count). |
| 61 | +profile = [ |
| 62 | + ("main;server.handle_request;parse_request;read_headers", 18), |
| 63 | + ("main;server.handle_request;parse_request;parse_body", 12), |
| 64 | + ("main;server.handle_request;app.route;auth.verify;jwt.decode", 22), |
| 65 | + ("main;server.handle_request;app.route;auth.verify;cache.get", 9), |
| 66 | + ("main;server.handle_request;app.route;user_handler;db.query;db.connect", 14), |
| 67 | + ("main;server.handle_request;app.route;user_handler;db.query;db.execute;db.fetch_rows", 86), |
| 68 | + ("main;server.handle_request;app.route;user_handler;db.query;db.execute;db.parse_result", 32), |
| 69 | + ("main;server.handle_request;app.route;user_handler;serializer.to_json", 27), |
| 70 | + ("main;server.handle_request;app.route;user_handler;serializer.escape_html", 11), |
| 71 | + ("main;server.handle_request;app.route;product_handler;db.query;db.execute;db.fetch_rows", 41), |
| 72 | + ("main;server.handle_request;app.route;product_handler;serializer.to_json", 15), |
| 73 | + ("main;server.handle_request;app.route;product_handler;recommend;model.predict;matmul", 48), |
| 74 | + ("main;server.handle_request;app.route;product_handler;recommend;model.predict;softmax", 9), |
| 75 | + ("main;server.handle_request;app.route;product_handler;recommend;feature_lookup;cache.get", 7), |
| 76 | + ("main;server.handle_request;send_response;write_headers", 5), |
| 77 | + ("main;server.handle_request;send_response;write_body;gzip.compress", 19), |
| 78 | + ("main;server.handle_request;send_response;write_body;tcp.send", 8), |
| 79 | + ("main;server.poll_events;epoll_wait", 24), |
| 80 | + ("main;runtime.gc;mark_phase;walk_heap", 31), |
| 81 | + ("main;runtime.gc;sweep_phase", 12), |
| 82 | +] |
| 83 | + |
| 84 | +total_samples = sum(samples for (_, samples) in profile) |
| 85 | + |
| 86 | +# Aggregate each (depth, prefix) into total samples; record children sets. |
| 87 | +counts = Dict{Tuple{Int,String},Int}() |
| 88 | +children = Dict{Tuple{Int,String},Set{String}}() |
| 89 | +for (stack, samples) in profile |
| 90 | + parts = String.(split(stack, ';')) |
| 91 | + for i in 1:length(parts) |
| 92 | + prefix = join(parts[1:i], ';') |
| 93 | + key = (i - 1, prefix) |
| 94 | + counts[key] = get(counts, key, 0) + samples |
| 95 | + if i > 1 |
| 96 | + pkey = (i - 2, join(parts[1:i-1], ';')) |
| 97 | + push!(get!(children, pkey, Set{String}()), prefix) |
| 98 | + end |
| 99 | + end |
| 100 | +end |
| 101 | + |
| 102 | +# Lay out rectangles top-down from the root, children sorted alphabetically. |
| 103 | +# Iterative DFS keeps the implementation top-level — no recursive function. |
| 104 | +NodeT = NamedTuple{ |
| 105 | + (:depth, :x0, :w, :name, :prefix), |
| 106 | + Tuple{Int,Float64,Float64,String,String}, |
| 107 | +} |
| 108 | +nodes = NodeT[] |
| 109 | +queue = [("main", 0, 0.0)] |
| 110 | +while !isempty(queue) |
| 111 | + prefix, depth, x0 = pop!(queue) |
| 112 | + width = counts[(depth, prefix)] / total_samples |
| 113 | + name = String(split(prefix, ';')[end]) |
| 114 | + push!(nodes, (depth = depth, x0 = x0, w = width, name = name, prefix = prefix)) |
| 115 | + |
| 116 | + kids = sort!(collect(get(children, (depth, prefix), Set{String}()))) |
| 117 | + child_starts = Float64[] |
| 118 | + cursor = x0 |
| 119 | + for c in kids |
| 120 | + push!(child_starts, cursor) |
| 121 | + cursor += counts[(depth + 1, c)] / total_samples |
| 122 | + end |
| 123 | + for i in length(kids):-1:1 |
| 124 | + push!(queue, (kids[i], depth + 1, child_starts[i])) |
| 125 | + end |
| 126 | +end |
| 127 | + |
| 128 | +max_depth = maximum(n.depth for n in nodes) |
| 129 | + |
| 130 | +# Widest leaf = dominant CPU hot path; gets a focal-point accent below. |
| 131 | +leaves = filter(n -> !haskey(children, (n.depth, n.prefix)), nodes) |
| 132 | +hot = leaves[argmax([l.w for l in leaves])] |
| 133 | + |
| 134 | +# Title scaled to fit when prefixed with a descriptive subtitle. |
| 135 | +title_text = "CPU Profile of a Web Request Handler · flamegraph-basic · julia · makie · anyplot.ai" |
| 136 | +title_default = 20 |
| 137 | +title_size = length(title_text) > 67 ? |
| 138 | + max(round(Int, title_default * 67 / length(title_text)), 13) : |
| 139 | + title_default |
| 140 | + |
| 141 | +fig = Figure(resolution = (1600, 900)) |
| 142 | + |
| 143 | +ax = Axis( |
| 144 | + fig[1, 1]; |
| 145 | + title = title_text, |
| 146 | + titlesize = title_size, |
| 147 | + xlabel = "Proportion of CPU samples", |
| 148 | + ylabel = "Stack depth (caller → callee)", |
| 149 | + xlabelsize = 14, |
| 150 | + ylabelsize = 13, |
| 151 | + xticklabelsize = 12, |
| 152 | + limits = ((-0.002, 1.002), (-0.15, max_depth + 1.75)), |
| 153 | + xticks = (0:0.2:1.0, ["0%", "20%", "40%", "60%", "80%", "100%"]), |
| 154 | +) |
| 155 | + |
| 156 | +# Draw flame bars: one rectangle per node, hairline page-bg stroke between |
| 157 | +# adjacent siblings keeps same-color neighbours visually distinct. |
| 158 | +bar_height = 0.93 |
| 159 | +rects = [Rect2f(n.x0, n.depth, n.w, bar_height) for n in nodes] |
| 160 | +flame_idx = [(abs(hash(n.name)) % length(FLAME_COLORS)) + 1 for n in nodes] |
| 161 | +fill_colors = [FLAME_COLORS[i] for i in flame_idx] |
| 162 | +poly!(ax, rects; |
| 163 | + color = fill_colors, |
| 164 | + strokecolor = PAGE_BG, |
| 165 | + strokewidth = 1.5, |
| 166 | +) |
| 167 | + |
| 168 | +# Focal-point cue: a thicker INK outline on the dominant hot-path leaf, plus |
| 169 | +# a short label above it stating the share of CPU samples. Subtle enough to |
| 170 | +# preserve the flame aesthetic, explicit enough to direct the eye. |
| 171 | +poly!(ax, Rect2f(hot.x0, hot.depth, hot.w, bar_height); |
| 172 | + color = (:white, 0.0), |
| 173 | + strokecolor = INK, |
| 174 | + strokewidth = 2.5, |
| 175 | +) |
| 176 | +hot_pct = round(Int, hot.w * 100) |
| 177 | +text!(ax, hot.x0 + hot.w / 2, hot.depth + bar_height + 0.18; |
| 178 | + text = "▼ hot path · $(hot_pct)% of CPU samples", |
| 179 | + align = (:center, :bottom), |
| 180 | + color = INK_SOFT, |
| 181 | + fontsize = 12, |
| 182 | +) |
| 183 | + |
| 184 | +# Function-name labels, only where the bar is wide enough to fit the text. |
| 185 | +# Label ink is chosen per fill color: dark on amber/ochre, light on red. |
| 186 | +label_fontsize = 12 |
| 187 | +for (n, fc_idx) in zip(nodes, flame_idx) |
| 188 | + needed = length(n.name) * 0.0058 + 0.012 |
| 189 | + if n.w >= needed |
| 190 | + text!(ax, n.x0 + 0.005, n.depth + bar_height / 2; |
| 191 | + text = n.name, |
| 192 | + align = (:left, :center), |
| 193 | + color = FLAME_LABEL_INK[fc_idx], |
| 194 | + fontsize = label_fontsize, |
| 195 | + ) |
| 196 | + end |
| 197 | +end |
| 198 | + |
| 199 | +save("plot-$(THEME).png", fig; px_per_unit = 2) |
0 commit comments