Skip to content

Commit cfbe2d2

Browse files
feat(makie): implement flamegraph-basic (#8511)
## Implementation: `flamegraph-basic` - julia/makie Implements the **julia/makie** version of `flamegraph-basic`. **File:** `plots/flamegraph-basic/implementations/julia/makie.jl` **Parent Issue:** #4665 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/27165613659)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 8d6258e commit cfbe2d2

2 files changed

Lines changed: 464 additions & 0 deletions

File tree

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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

Comments
 (0)