Skip to main content

forge/profile/analyze/
mod.rs

1mod aggregate;
2mod observations;
3mod symbols;
4mod types;
5
6use aggregate::{
7    build_call_edges, build_categories, build_function_list, build_hotspot_context, count_samples,
8    find_bottlenecks,
9};
10use observations::generate_observations;
11use symbols::{round1, round2};
12use wowlab_types::sim::FastMap;
13
14use crate::profile::samply::RawProfile;
15
16#[rustfmt::skip]
17pub(super) use symbols::clean_name;
18
19#[rustfmt::skip]
20#[expect(
21    unused_imports,
22    reason = "report modes consume different analysis projections"
23)]
24pub(super) use types::{CallEdge, ContextEntry, Function, HotspotContext, ProfileAnalysis};
25
26const MAX_SOURCE_FILES: usize = 20;
27const MIN_SOURCE_FILE_PCT: f64 = 0.3;
28
29pub(super) fn analyze(
30    profile: &RawProfile,
31    symbols: &FastMap<String, String>,
32    spec: &str,
33    iterations: u32,
34    duration_sec: f64,
35    top_n: usize,
36) -> ProfileAnalysis {
37    let total_weight = profile.total_weight.max(1);
38
39    let sym = |idx: usize| -> &str {
40        // BOUNDS: idx comes from profile frame indices; .get() returns fallback
41        let raw = profile.strings.get(idx).map_or("?", String::as_str);
42
43        symbols.get(raw).map_or(raw, String::as_str)
44    };
45
46    let counts = count_samples(profile, &sym);
47    let func_result = build_function_list(&counts, total_weight);
48    let bottlenecks = find_bottlenecks(&counts, total_weight);
49    let call_edges = build_call_edges(&counts, &func_result.all_functions, top_n, total_weight);
50    let hotspot_context = build_hotspot_context(&counts, &func_result.all_functions, total_weight);
51    let categories = build_categories(func_result.category_self);
52
53    let mut source_files: Vec<(String, f64)> = func_result
54        .source_self
55        .into_iter()
56        .filter(|(_, p)| *p >= MIN_SOURCE_FILE_PCT)
57        .map(|(k, v)| (k, round2(v)))
58        .collect();
59
60    source_files.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
61    source_files.truncate(MAX_SOURCE_FILES);
62
63    let observations = generate_observations(&categories, &func_result.all_functions, &bottlenecks);
64
65    let unresolved =
66        u32::try_from(symbols.values().filter(|v| v.starts_with("0x")).count()).unwrap_or(u32::MAX);
67
68    let throughput = if duration_sec > 0.0 {
69        f64::from(iterations) / duration_sec
70    } else {
71        0.0
72    };
73
74    ProfileAnalysis {
75        spec: spec.to_string(),
76        iterations,
77        duration_sec: round2(duration_sec),
78        throughput: round1(throughput),
79        total_samples: profile.samples.len() as u64,
80        unresolved_symbols: unresolved,
81        functions: func_result.all_functions.into_iter().take(top_n).collect(),
82        bottlenecks,
83        categories,
84        category_functions: func_result.cat_funcs,
85        call_edges,
86        hotspot_context,
87        source_files,
88        observations,
89    }
90}