1use wowlab_types::sim::{FastMap, FastSet};
2
3use super::{
4 symbols::{clean_name, parse_module, pct, round1, round2, symbol_to_source},
5 types::{CallEdge, ContextEntry, Function, HotspotContext},
6};
7use crate::profile::{categories, samply::RawProfile};
8
9const MAX_CAT_FUNCS: usize = 10;
10const MIN_BOTTLENECK_PCT: f64 = 5.0;
11const MAX_BOTTLENECKS: usize = 15;
12const MIN_EDGE_PCT: f64 = 0.3;
13const MAX_CALL_EDGES: usize = 80;
14const MIN_CATEGORY_PCT: f64 = 0.1;
15const MAX_CONTEXT_ENTRIES: usize = 5;
16const MAX_HOTSPOT_CONTEXT: usize = 15;
17const BOTTLENECK_SELF_PCT_THRESHOLD: f64 = 1.0;
18const BOTTLENECK_RATIO_THRESHOLD: f64 = 3.0;
19const BOTTLENECK_RATIO_EPSILON: f64 = 0.01;
20
21pub(super) struct SampleCounts {
22 self_counts: FastMap<String, u64>,
23 total_counts: FastMap<String, u64>,
24 edge_counts: FastMap<(String, String), u64>,
25 call_sources: FastMap<String, FastMap<String, u64>>,
26 call_destinations: FastMap<String, FastMap<String, u64>>,
27}
28
29pub(super) struct FunctionListResult {
30 pub(super) all_functions: Vec<Function>,
31 pub(super) category_self: FastMap<String, f64>,
32 pub(super) cat_funcs: FastMap<String, Vec<Function>>,
33 pub(super) source_self: FastMap<String, f64>,
34}
35
36pub(super) fn count_samples<'a>(
38 profile: &'a RawProfile,
39 sym: &dyn Fn(usize) -> &'a str,
40) -> SampleCounts {
41 let sample_capacity = profile.samples.len();
42 let mut self_counts = FastMap::default();
43 let mut total_counts = FastMap::default();
44 let mut edge_counts = FastMap::default();
45 let mut call_sources: FastMap<String, FastMap<String, u64>> = FastMap::default();
46 let mut call_destinations: FastMap<String, FastMap<String, u64>> = FastMap::default();
47
48 self_counts.reserve(sample_capacity);
49 total_counts.reserve(sample_capacity);
50 edge_counts.reserve(sample_capacity);
51 call_sources.reserve(sample_capacity);
52 call_destinations.reserve(sample_capacity);
53 let mut seen = FastSet::default();
54
55 for sample in &profile.samples {
56 if sample.frames.is_empty() {
57 continue;
58 }
59
60 let weight = sample.weight;
61
62 let leaf = sym(sample.frames[0]).to_string();
64
65 *self_counts.entry(leaf.clone()).or_default() += weight;
66
67 seen.clear();
68 let mut prev: Option<String> = None;
69
70 for &frame_idx in &sample.frames {
71 let func = sym(frame_idx).to_string();
72
73 if seen.insert(func.clone()) {
74 *total_counts.entry(func.clone()).or_default() += weight;
75 }
76
77 if let Some(ref p) = prev {
78 if p != &func {
79 *edge_counts.entry((func.clone(), p.clone())).or_default() += weight;
80 *call_sources
81 .entry(p.clone())
82 .or_default()
83 .entry(func.clone())
84 .or_default() += weight;
85 *call_destinations
86 .entry(func.clone())
87 .or_default()
88 .entry(p.clone())
89 .or_default() += weight;
90 }
91 }
92
93 prev = Some(func);
94 }
95 }
96
97 SampleCounts {
98 self_counts,
99 total_counts,
100 edge_counts,
101 call_sources,
102 call_destinations,
103 }
104}
105
106pub(super) fn build_function_list(counts: &SampleCounts, total_weight: u64) -> FunctionListResult {
108 let mut all_functions = Vec::with_capacity(counts.self_counts.len());
109 let mut category_self = FastMap::default();
110 let mut cat_funcs: FastMap<String, Vec<Function>> = FastMap::default();
111 let mut source_self = FastMap::default();
112
113 let mut by_self: Vec<(&String, &u64)> = counts.self_counts.iter().collect();
114
115 by_self.sort_by(|a, b| b.1.cmp(a.1));
116
117 for &(name, self_cnt) in &by_self {
118 let self_cnt = *self_cnt;
119 let module = parse_module(name);
120 let category = categories::classify(name).to_string();
121 let self_p = pct(self_cnt, total_weight);
122 let total_p = pct(
123 *counts.total_counts.get(name.as_str()).unwrap_or(&0),
124 total_weight,
125 );
126
127 *category_self.entry(category.clone()).or_default() += self_p;
128
129 if let Some(src) = symbol_to_source(name) {
130 *source_self.entry(src).or_default() += self_p;
131 }
132
133 if category == "stdlib" {
134 continue;
135 }
136
137 let func = Function {
138 name: name.clone(),
139 module,
140 category: category.clone(),
141 self_pct: round2(self_p),
142 total_pct: round2(total_p),
143 self_samples: self_cnt,
144 total_samples: *counts.total_counts.get(name.as_str()).unwrap_or(&0),
145 };
146
147 let cat_list = cat_funcs.entry(category).or_default();
148
149 if cat_list.len() < MAX_CAT_FUNCS {
150 cat_list.push(Function {
151 name: clean_name(name),
152 ..func.clone()
153 });
154 }
155
156 all_functions.push(func);
157 }
158
159 FunctionListResult {
160 all_functions,
161 category_self,
162 cat_funcs,
163 source_self,
164 }
165}
166
167pub(super) fn find_bottlenecks(counts: &SampleCounts, total_weight: u64) -> Vec<Function> {
169 let mut bottlenecks = Vec::with_capacity(MAX_BOTTLENECKS);
170 let mut by_total: Vec<(&String, &u64)> = counts.total_counts.iter().collect();
171
172 by_total.sort_by(|a, b| b.1.cmp(a.1));
173
174 for &(name, total_cnt) in &by_total {
175 let total_cnt = *total_cnt;
176 let total_p = pct(total_cnt, total_weight);
177
178 if total_p < MIN_BOTTLENECK_PCT {
179 break;
180 }
181
182 let cat = categories::classify(name);
183
184 if cat == "stdlib" {
185 continue;
186 }
187
188 let self_p = pct(
189 *counts.self_counts.get(name.as_str()).unwrap_or(&0),
190 total_weight,
191 );
192
193 if self_p < BOTTLENECK_SELF_PCT_THRESHOLD
194 || total_p / self_p.max(BOTTLENECK_RATIO_EPSILON) >= BOTTLENECK_RATIO_THRESHOLD
195 {
196 bottlenecks.push(Function {
197 name: name.clone(),
198 module: parse_module(name),
199 category: cat.to_string(),
200 self_pct: round2(self_p),
201 total_pct: round2(total_p),
202 self_samples: *counts.self_counts.get(name.as_str()).unwrap_or(&0),
203 total_samples: total_cnt,
204 });
205 }
206
207 if bottlenecks.len() >= MAX_BOTTLENECKS {
208 break;
209 }
210 }
211
212 bottlenecks
213}
214
215pub(super) fn build_call_edges(
217 counts: &SampleCounts,
218 all_functions: &[Function],
219 top_n: usize,
220 total_weight: u64,
221) -> Vec<CallEdge> {
222 let top_names: FastSet<&str> = all_functions
223 .iter()
224 .take(top_n)
225 .map(|f| f.name.as_str())
226 .collect();
227
228 let mut call_edges = Vec::with_capacity(MAX_CALL_EDGES);
229 let mut edges_sorted: Vec<(&(String, String), &u64)> = counts.edge_counts.iter().collect();
230
231 edges_sorted.sort_by(|a, b| b.1.cmp(a.1));
232
233 for &(edge, cnt) in &edges_sorted {
234 let (source, destination) = edge;
235 let cnt = *cnt;
236
237 if top_names.contains(source.as_str()) || top_names.contains(destination.as_str()) {
238 let p = pct(cnt, total_weight);
239
240 if p >= MIN_EDGE_PCT {
241 call_edges.push(CallEdge {
242 caller: source.clone(),
243 callee: destination.clone(),
244 samples: cnt,
245 pct: round2(p),
246 });
247 }
248 }
249
250 if call_edges.len() >= MAX_CALL_EDGES {
251 break;
252 }
253 }
254
255 call_edges
256}
257
258pub(super) fn build_hotspot_context(
259 counts: &SampleCounts,
260 all_functions: &[Function],
261 total_weight: u64,
262) -> Vec<HotspotContext> {
263 all_functions
264 .iter()
265 .take(MAX_HOTSPOT_CONTEXT)
266 .map(|f| {
267 let incoming = top_entries(&counts.call_sources, &f.name, total_weight);
268 let outgoing = top_entries(&counts.call_destinations, &f.name, total_weight);
269
270 HotspotContext {
271 name: clean_name(&f.name),
272 self_pct: f.self_pct,
273 total_pct: f.total_pct,
274 category: f.category.clone(),
275 callers: incoming,
276 callees: outgoing,
277 }
278 })
279 .collect()
280}
281
282pub(super) fn build_categories(category_self: FastMap<String, f64>) -> Vec<(String, f64)> {
283 let mut categories: Vec<(String, f64)> = category_self
284 .into_iter()
285 .filter(|(_, p)| *p >= MIN_CATEGORY_PCT)
286 .map(|(k, v)| (k, round1(v)))
287 .collect();
288
289 categories.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
290
291 categories
292}
293
294fn top_entries(
295 map: &FastMap<String, FastMap<String, u64>>,
296 key: &str,
297 total_weight: u64,
298) -> Vec<ContextEntry> {
299 let Some(inner) = map.get(key) else {
300 return Vec::new();
301 };
302 let mut entries: Vec<(&String, &u64)> = inner.iter().collect();
303
304 entries.sort_by(|a, b| b.1.cmp(a.1));
305
306 entries
307 .into_iter()
308 .take(MAX_CONTEXT_ENTRIES)
309 .filter(|(_, cnt)| pct(**cnt, total_weight) >= MIN_EDGE_PCT)
310 .map(|(name, cnt)| {
311 let cnt = *cnt;
312
313 ContextEntry {
314 name: clean_name(name),
315 pct: round2(pct(cnt, total_weight)),
316 samples: cnt,
317 }
318 })
319 .collect()
320}