use {
super::trace::FlamegraphReport,
inferno::flamegraph::{from_lines, Options},
};
pub fn render(report: &FlamegraphReport) -> String {
let lines: Vec<String> = report
.stacks
.iter()
.map(|(stack, count)| format!("{} {}", stack.join(";"), count))
.collect();
let mut opts = Options::default();
opts.title = format!("{} flamegraph", report.program_name);
opts.subtitle = Some(format!(
"Approximate CU: {} (BPF insns × 1 + ComputeBudget syscall base costs; variable syscall \
costs underestimated)",
report.total_cu
));
opts.count_name = "CU".to_string();
opts.font_type = "monospace".to_string();
opts.hash = true;
let mut out: Vec<u8> = Vec::new();
if let Err(err) = from_lines(&mut opts, lines.iter().map(String::as_str), &mut out) {
return error_svg(&format!("flamegraph render failed: {err}"));
}
String::from_utf8(out)
.unwrap_or_else(|err| error_svg(&format!("invalid UTF-8 from inferno: {err}")))
}
fn error_svg(msg: &str) -> String {
format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"800\" height=\"60\">\n<text x=\"8\" \
y=\"30\" font-family=\"monospace\" fill=\"#b00\">{}</text>\n</svg>\n",
msg.replace('<', "<").replace('&', "&")
)
}
#[cfg(test)]
mod tests {
use {super::*, std::collections::BTreeMap};
#[test]
fn render_outputs_svg_with_title_subtitle_and_cu_label() {
let report = FlamegraphReport {
program_name: "demo · tx1".to_string(),
total_cu: 7,
stacks: BTreeMap::from([(
vec![
"[program demo]".to_string(),
"entry @ 0x0".to_string(),
"handler @ 0x4".to_string(),
],
7,
)]),
};
let svg = render(&report);
assert!(svg.contains("<svg"));
assert!(svg.contains("demo · tx1 flamegraph"));
assert!(svg.contains("Approximate CU: 7"));
assert!(svg.contains("entry @ 0x0"));
assert!(svg.contains("CU"));
}
}