use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::hash::{Hash, Hasher};
use std::io::Cursor;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FocusedFoldedStacks {
pub folded: String,
pub matched_stack_count: usize,
pub excluded_stack_count: usize,
pub included_samples: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct FrameBreakdown {
pub frame: String,
pub samples: u64,
pub percent_total: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct ModeSummary {
pub total_samples: u64,
pub visible_stack_count: usize,
pub matched_stack_count: usize,
pub excluded_stack_count: usize,
pub warning: Option<String>,
pub self_frames: Vec<FrameBreakdown>,
pub inclusive_frames: Vec<FrameBreakdown>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct ArtifactLink {
pub label: String,
pub path: String,
}
impl ArtifactLink {
pub(crate) fn new(label: impl Into<String>, path: impl Into<String>) -> Self {
Self {
label: label.into(),
path: path.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct ViewerMetadataItem {
pub label: String,
pub value: String,
}
#[cfg(test)]
impl ViewerMetadataItem {
pub(crate) fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
Self {
label: label.into(),
value: value.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct ViewerHarnessTimelineSpan {
pub phase: String,
pub start_offset_ns: u64,
pub end_offset_ns: u64,
pub iteration: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ViewerTraceEvent {
pub event_kind: String,
pub start_offset_ns: u64,
pub end_offset_ns: Option<u64>,
pub frames: Vec<String>,
pub phase: Option<String>,
pub iteration: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ViewerTraceLane {
pub id: String,
pub label: String,
pub events: Vec<ViewerTraceEvent>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct FrameSourceLink {
pub frame: String,
pub location: String,
pub href: String,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FlamegraphMode {
Focused,
Full,
Timeline,
}
impl FlamegraphMode {
fn as_str(self) -> &'static str {
match self {
Self::Focused => "focused",
Self::Full => "full",
Self::Timeline => "timeline",
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct FlamegraphViewerDoc {
pub title: String,
pub browser_title: String,
pub full_svg_document: String,
pub focused_svg_document: String,
pub full_summary: ModeSummary,
pub focused_summary: ModeSummary,
pub sampled_duration_secs: Option<f64>,
pub run_metadata: Vec<ViewerMetadataItem>,
pub harness_timeline: Vec<ViewerHarnessTimelineSpan>,
pub timeline_lanes: Vec<ViewerTraceLane>,
pub timeline_total_duration_ns: Option<u64>,
pub timeline_note: Option<String>,
pub default_mode: FlamegraphMode,
pub artifact_links: Vec<ArtifactLink>,
pub source_links: Vec<FrameSourceLink>,
pub source_link_note: Option<String>,
}
pub(crate) fn derive_benchmark_focused_folded_stacks(
folded: &str,
anchors: &[&str],
) -> FocusedFoldedStacks {
let mut lines = Vec::new();
let mut matched_stack_count = 0_usize;
let mut excluded_stack_count = 0_usize;
let mut included_samples = 0_u64;
for line in folded.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let Some((stack, count)) = split_folded_stack_line(trimmed) else {
excluded_stack_count += 1;
continue;
};
let frames: Vec<&str> = stack.split(';').collect();
let Some(trimmed_frames) = trim_stack_to_first_anchor(&frames, anchors) else {
excluded_stack_count += 1;
continue;
};
matched_stack_count += 1;
included_samples += count;
lines.push(format!("{} {}", trimmed_frames.join(";"), count));
}
FocusedFoldedStacks {
folded: lines.join("\n"),
matched_stack_count,
excluded_stack_count,
included_samples,
}
}
pub(crate) fn summarize_folded_stacks(
folded: &str,
matched_stack_count: usize,
excluded_stack_count: usize,
warning: Option<String>,
) -> ModeSummary {
let mut total_samples = 0_u64;
let mut visible_stack_count = 0_usize;
let mut inclusive_samples: BTreeMap<String, u64> = BTreeMap::new();
let mut self_samples: BTreeMap<String, u64> = BTreeMap::new();
for line in folded.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let Some((stack, count)) = split_folded_stack_line(trimmed) else {
continue;
};
let frames: Vec<String> = stack.split(';').map(prettify_frame_label).collect();
visible_stack_count += 1;
total_samples += count;
let mut seen_frames = BTreeSet::new();
for frame in &frames {
if seen_frames.insert(frame.clone()) {
*inclusive_samples.entry(frame.clone()).or_default() += count;
}
}
if let Some(leaf_frame) = frames.last() {
*self_samples.entry(leaf_frame.clone()).or_default() += count;
}
}
ModeSummary {
total_samples,
visible_stack_count,
matched_stack_count,
excluded_stack_count,
warning,
self_frames: build_frame_breakdown_list(self_samples, total_samples),
inclusive_frames: build_frame_breakdown_list(inclusive_samples, total_samples),
}
}
pub(crate) fn count_folded_stack_lines(folded: &str) -> usize {
folded
.lines()
.filter(|line| !line.trim().is_empty())
.count()
}
pub(crate) fn render_standalone_flamegraph_svg(folded_stacks: &str, title: &str) -> Result<String> {
if folded_stacks.trim().is_empty() {
return Ok(
"<!DOCTYPE html><html><body><p>No native frames were symbolized.</p></body></html>"
.into(),
);
}
let mut options = inferno::flamegraph::Options::default();
options.title = title.into();
let mut rendered = Vec::new();
let display_stacks = prettify_folded_stacks_for_display(folded_stacks);
inferno::flamegraph::from_reader(
&mut options,
Cursor::new(display_stacks.as_bytes()),
&mut rendered,
)?;
let rendered = String::from_utf8(rendered).context("inferno produced non-UTF-8 flamegraph")?;
Ok(finalize_standalone_flamegraph_document(rendered))
}
pub(crate) fn render_flamegraph_viewer_html(doc: FlamegraphViewerDoc) -> String {
let template = include_str!("flamegraph_viewer_template.html");
let default_mode = escape_json_for_inline_script(
&serde_json::to_string(doc.default_mode.as_str())
.expect("serialize default flamegraph mode"),
);
let full_svg = escape_json_for_inline_script(
&serde_json::to_string(&doc.full_svg_document).expect("serialize full svg"),
);
let focused_svg = escape_json_for_inline_script(
&serde_json::to_string(&doc.focused_svg_document).expect("serialize focused svg"),
);
let full_summary = escape_json_for_inline_script(
&serde_json::to_string(&doc.full_summary).expect("serialize full mode summary"),
);
let focused_summary = escape_json_for_inline_script(
&serde_json::to_string(&doc.focused_summary).expect("serialize focused mode summary"),
);
let artifact_links = escape_json_for_inline_script(
&serde_json::to_string(&doc.artifact_links).expect("serialize flamegraph artifact links"),
);
let sampled_duration_secs = escape_json_for_inline_script(
&serde_json::to_string(&doc.sampled_duration_secs)
.expect("serialize sampled duration seconds"),
);
let harness_timeline = escape_json_for_inline_script(
&serde_json::to_string(&doc.harness_timeline).expect("serialize harness timeline"),
);
let timeline_lanes = escape_json_for_inline_script(
&serde_json::to_string(&doc.timeline_lanes).expect("serialize timeline lanes"),
);
let timeline_total_duration_ns = escape_json_for_inline_script(
&serde_json::to_string(&doc.timeline_total_duration_ns)
.expect("serialize timeline total duration"),
);
let timeline_note = escape_json_for_inline_script(
&serde_json::to_string(&doc.timeline_note).expect("serialize timeline note"),
);
let source_links = escape_json_for_inline_script(
&serde_json::to_string(&doc.source_links).expect("serialize source links"),
);
let source_link_note = escape_json_for_inline_script(
&serde_json::to_string(&doc.source_link_note).expect("serialize source link note"),
);
template
.replace("__BROWSER_TITLE__", &escape_html(&doc.browser_title))
.replace("__VIEWER_TITLE__", &escape_html(&doc.title))
.replace(
"__RUN_METADATA_MARKUP__",
&render_run_metadata_markup(&doc.run_metadata),
)
.replace(
"__HARNESS_TIMELINE_MARKUP__",
&render_harness_timeline_markup(&doc.harness_timeline),
)
.replace("__DEFAULT_MODE__", &default_mode)
.replace("__FOCUSED_SVG__", &focused_svg)
.replace("__FULL_SVG__", &full_svg)
.replace("__FOCUSED_SUMMARY__", &focused_summary)
.replace("__FULL_SUMMARY__", &full_summary)
.replace("__ARTIFACT_LINKS__", &artifact_links)
.replace("__SAMPLED_DURATION_SECS__", &sampled_duration_secs)
.replace("__HARNESS_TIMELINE_JSON__", &harness_timeline)
.replace("__TIMELINE_LANES_JSON__", &timeline_lanes)
.replace(
"__TIMELINE_TOTAL_DURATION_NS__",
&timeline_total_duration_ns,
)
.replace("__TIMELINE_NOTE__", &timeline_note)
.replace("__SOURCE_LINKS__", &source_links)
.replace("__SOURCE_LINK_NOTE__", &source_link_note)
}
fn render_run_metadata_markup(items: &[ViewerMetadataItem]) -> String {
items.iter()
.map(|item| {
format!(
"<div class=\"run-metadata-item\"><span class=\"run-metadata-label\">{}</span><span class=\"run-metadata-value\">{}</span></div>",
escape_html(&item.label),
escape_html(&item.value)
)
})
.collect::<Vec<_>>()
.join("")
}
fn render_harness_timeline_markup(spans: &[ViewerHarnessTimelineSpan]) -> String {
let total_duration_ns = spans
.iter()
.map(|span| span.end_offset_ns)
.max()
.unwrap_or(0);
let mut segments = String::new();
for span in spans {
let left = if total_duration_ns == 0 {
0.0
} else {
(span.start_offset_ns as f64 / total_duration_ns as f64) * 100.0
};
let width = if total_duration_ns == 0 {
100.0
} else {
(((span.end_offset_ns.saturating_sub(span.start_offset_ns)) as f64)
/ total_duration_ns as f64)
* 100.0
};
let label = harness_phase_label(&span.phase, span.iteration);
let duration = format_duration_ns(span.end_offset_ns.saturating_sub(span.start_offset_ns));
let title = format!(
"{} · {} to {}",
label,
format_duration_ns(span.start_offset_ns),
format_duration_ns(span.end_offset_ns)
);
segments.push_str(&format!(
"<span class=\"harness-segment\" data-phase=\"{}\" style=\"left:{:.4}%;width:{:.4}%\" title=\"{}\"><span class=\"harness-segment-name\">{}</span><span class=\"harness-segment-duration\">{}</span></span>",
escape_html(&span.phase),
left,
width.max(0.5),
escape_html(&title),
escape_html(&label),
escape_html(&duration),
));
}
format!(
"<div class=\"harness-timeline-meta\"><span class=\"harness-timeline-title\">Harness Timeline</span><span class=\"harness-timeline-total\" id=\"harness-total\">Exact harness time · {}</span></div><div class=\"harness-track\" id=\"harness-track\">{}</div><div class=\"harness-track-scale\" id=\"harness-scale\"><span>0</span><span>{}</span></div><div class=\"harness-readout\" id=\"harness-readout\">Hover a harness segment to inspect its full label.</div>",
escape_html(&format_duration_ns(total_duration_ns)),
segments,
escape_html(&format_duration_ns(total_duration_ns)),
)
}
fn harness_phase_label(phase: &str, iteration: Option<u32>) -> String {
let base = match phase {
"setup" => "Setup",
"fixture-setup" => "Fixture Setup",
"warmup-benchmark" => "Warmup",
"measured-benchmark" => "Bench Body",
"teardown" => "Teardown",
"fixture-teardown" => "Fixture Teardown",
"harness" => "Harness",
_ => phase,
};
match iteration {
Some(iteration) => format!("{base} #{}", iteration + 1),
None => base.to_string(),
}
}
fn format_duration_ns(ns: u64) -> String {
if ns >= 1_000_000_000 {
format!("{:.2} s", ns as f64 / 1_000_000_000.0)
} else if ns >= 1_000_000 {
format!("{:.2} ms", ns as f64 / 1_000_000.0)
} else if ns >= 1_000 {
format!("{:.2} µs", ns as f64 / 1_000.0)
} else {
format!("{ns} ns")
}
}
fn escape_json_for_inline_script(json: &str) -> String {
json.replace("</", "<\\/")
}
fn build_frame_breakdown_list(
frame_samples: BTreeMap<String, u64>,
total_samples: u64,
) -> Vec<FrameBreakdown> {
let mut frames: Vec<FrameBreakdown> = frame_samples
.into_iter()
.map(|(frame, samples)| FrameBreakdown {
frame,
samples,
percent_total: samples
.saturating_mul(100)
.checked_div(total_samples)
.unwrap_or(0),
})
.collect();
frames.sort_by(|left, right| {
right
.samples
.cmp(&left.samples)
.then_with(|| left.frame.cmp(&right.frame))
});
frames.truncate(12);
frames
}
fn prettify_folded_stacks_for_display(folded_stacks: &str) -> String {
let mut lines = Vec::new();
for line in folded_stacks.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let Some((stack, counts)) = split_folded_stack_counts(trimmed) else {
lines.push(trimmed.to_string());
continue;
};
let pretty_stack = stack
.split(';')
.map(prettify_frame_label)
.collect::<Vec<_>>()
.join(";");
lines.push(format!("{pretty_stack} {counts}"));
}
lines.join("\n")
}
fn prettify_frame_label(frame: &str) -> String {
let mut pretty = frame.to_string();
for (needle, replacement) in [
("_$LT$", "<"),
("$LT$", "<"),
("$GT$", ">"),
("$u20$", " "),
("$u7b$", "{"),
("$u7d$", "}"),
("$u5b$", "["),
("$u5d$", "]"),
("$LP$", "("),
("$RP$", ")"),
("$C$", ","),
("$RF$", "&"),
] {
pretty = pretty.replace(needle, replacement);
}
pretty = pretty.replace("..", "::");
if let Some(hash_idx) = pretty.rfind("::h") {
let hash = &pretty[hash_idx + 3..];
if !hash.is_empty() && hash.chars().all(|ch| ch.is_ascii_hexdigit()) {
pretty.truncate(hash_idx);
}
}
pretty
}
fn trim_stack_to_first_anchor<'a>(
frames: &'a [&'a str],
anchors: &[&str],
) -> Option<&'a [&'a str]> {
frames
.iter()
.position(|frame| anchors.iter().any(|anchor| frame.contains(anchor)))
.map(|idx| &frames[idx..])
}
fn split_folded_stack_line(line: &str) -> Option<(&str, u64)> {
let split = line.rfind(' ')?;
let count = line[split + 1..].parse().ok()?;
Some((&line[..split], count))
}
fn split_folded_stack_counts(line: &str) -> Option<(&str, &str)> {
let (rest, last) = line.rsplit_once(' ')?;
if last.parse::<i64>().is_err() {
return None;
}
if let Some((stack, previous)) = rest.rsplit_once(' ')
&& previous.parse::<i64>().is_ok()
{
return Some((stack, &line[stack.len() + 1..]));
}
Some((rest, last))
}
fn finalize_standalone_flamegraph_document(rendered: String) -> String {
let rendered = rendered.replacen(
"<svg version=\"1.1\"",
"<svg version=\"1.1\" style=\"display:block;width:100vw;min-width:100vw;max-width:100vw;height:auto\"",
1,
);
let rendered = rendered.replacen("var fluiddrawing = true;", "var fluiddrawing = false;", 1);
let rendered = rendered.replacen(
"#unzoom { cursor:pointer; }",
"#unzoom { cursor:pointer; display:none; }\n#search, #matched, #details, #title { display:none; }",
1,
);
let rendered = retint_flamegraph_background(rendered);
let rendered = retint_flamegraph_palette(rendered);
inject_svg_script(rendered, MOBENCH_SVG_HELPER_SCRIPT)
}
fn retint_flamegraph_background(document: String) -> String {
document
.replace(r##"stop-color="#eeeeee""##, r##"stop-color="#ffffff""##)
.replace(r##"stop-color="#eeeeb0""##, r##"stop-color="#ffffff""##)
}
fn retint_flamegraph_palette(document: String) -> String {
const NEEDLE: &str = r#"fill="rgb("#;
if !document.contains(NEEDLE) {
return document;
}
let mut output = String::with_capacity(document.len());
let mut cursor = 0usize;
while let Some(relative) = document[cursor..].find(NEEDLE) {
let start = cursor + relative;
output.push_str(&document[cursor..start]);
let value_start = start + NEEDLE.len();
let Some(value_end) = document[value_start..].find(")\"") else {
output.push_str(&document[start..]);
return output;
};
let value_end = value_start + value_end;
let raw = &document[value_start..value_end];
let replacement = parse_fill_rgb(raw)
.map(|rgb| flamegraph_fill_for_rgb(rgb, frame_title_for_fill(&document, start)))
.unwrap_or_else(|| format!(r#"fill="rgb({raw})""#));
output.push_str(&replacement);
cursor = value_end + 2;
}
output.push_str(&document[cursor..]);
output
}
fn frame_title_for_fill(document: &str, fill_start: usize) -> &str {
document[..fill_start]
.rfind("<title>")
.and_then(|title_start| {
let title_body_start = title_start + "<title>".len();
document[title_body_start..fill_start]
.find("</title>")
.map(|title_end| &document[title_body_start..title_body_start + title_end])
})
.unwrap_or("neutral-frame")
}
fn parse_fill_rgb(raw: &str) -> Option<(u8, u8, u8)> {
let mut parts = raw.split(',');
let r = parts.next()?.trim().parse().ok()?;
let g = parts.next()?.trim().parse().ok()?;
let b = parts.next()?.trim().parse().ok()?;
if parts.next().is_some() {
return None;
}
Some((r, g, b))
}
fn flamegraph_fill_for_rgb(rgb: (u8, u8, u8), title: &str) -> String {
let (r, g, b) = rgb;
if rgb == (0, 0, 0) {
return format!(r#"fill="rgb({r},{g},{b})""#);
}
if is_neutral_fill(rgb) {
return neutral_frame_fill_for_title(title);
}
if r >= g && r >= b && r.saturating_sub(b) > 12 {
let palette = [(255, 107, 116), (255, 123, 122), (255, 139, 127)];
let shade = ((r as usize + g as usize + b as usize) / 48).min(palette.len() - 1);
let (nr, ng, nb) = palette[shade];
return format!(r#"fill="rgb({nr},{ng},{nb})""#);
}
if b >= r && b >= g && b.saturating_sub(r) > 8 {
let palette = [(255, 214, 194), (255, 202, 179), (255, 191, 164)];
let shade = ((r as usize + g as usize + b as usize) / 96).min(palette.len() - 1);
let (nr, ng, nb) = palette[shade];
return format!(r#"fill="rgb({nr},{ng},{nb})""#);
}
format!(r#"fill="rgb({r},{g},{b})""#)
}
fn is_neutral_fill((r, g, b): (u8, u8, u8)) -> bool {
let max = r.max(g).max(b);
let min = r.min(g).min(b);
max >= 242 && max.saturating_sub(min) <= 10
}
fn neutral_frame_fill_for_title(title: &str) -> String {
const PALETTE: [(u8, u8, u8); 8] = [
(255, 107, 116),
(255, 166, 95),
(255, 178, 92),
(255, 194, 99),
(255, 220, 107),
(255, 208, 123),
(255, 156, 120),
(255, 189, 118),
];
let mut hasher = std::collections::hash_map::DefaultHasher::new();
title.hash(&mut hasher);
let (r, g, b) = PALETTE[(hasher.finish() as usize) % PALETTE.len()];
format!(r#"fill="rgb({r},{g},{b})""#)
}
fn inject_svg_script(document: String, script: &str) -> String {
let Some(index) = document.rfind("</svg>") else {
return document;
};
let mut output = String::with_capacity(document.len() + script.len() + 48);
output.push_str(&document[..index]);
output.push_str("<script><![CDATA[");
output.push_str(script);
output.push_str("]]></script>");
output.push_str(&document[index..]);
output
}
fn escape_html(value: &str) -> String {
value
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
const MOBENCH_SVG_HELPER_SCRIPT: &str = r#"
(function () {
function mobenchTotalSamples() {
return typeof total_samples === "number"
? total_samples
: parseInt(frames.attributes.total_samples.value || "0", 10);
}
function mobenchTitleForNode(node) {
try {
var title = find_child(node, "title");
if (!title || !title.textContent) return "selected range";
return title.textContent.replace(/\s+\([^)]*\)$/, "");
} catch (_error) {
return "selected range";
}
}
function mobenchNormalizeView(view) {
var total = mobenchTotalSamples();
var xmin = Math.max(0, Math.floor(view.xmin || 0));
var width = Math.max(1, Math.floor(view.width || total || 1));
if (xmin + width > total && total > 0) {
width = total - xmin;
}
if (width <= 0) {
xmin = 0;
width = Math.max(1, total);
}
return {
xmin: xmin,
width: width,
label: view.label || "selected range"
};
}
var mobenchState = {
history: [],
index: -1,
current: null
};
var mobenchCollapsedTowerState = {
originalViewBox: null,
originalHeight: null,
originalBodyHeight: "",
originalDocumentHeight: ""
};
function mobenchSvgRoot() {
return document.querySelector("svg");
}
function mobenchRememberViewport() {
var svg = mobenchSvgRoot();
if (!svg) return null;
if (mobenchCollapsedTowerState.originalViewBox === null) {
mobenchCollapsedTowerState.originalViewBox =
svg.getAttribute("viewBox")
|| ("0 0 " + (svg.viewBox && svg.viewBox.baseVal ? svg.viewBox.baseVal.width : 1200)
+ " "
+ (svg.viewBox && svg.viewBox.baseVal ? svg.viewBox.baseVal.height : parseFloat(svg.getAttribute("height") || "900")));
mobenchCollapsedTowerState.originalHeight = svg.getAttribute("height");
mobenchCollapsedTowerState.originalBodyHeight = document.body.style.height || "";
mobenchCollapsedTowerState.originalDocumentHeight = document.documentElement.style.height || "";
}
return svg;
}
window.mobenchClearCollapsedTowerPresentation = function () {
var svg = mobenchRememberViewport();
var elements = frames.children;
for (var i = 0; i < elements.length; i++) {
if (elements[i].dataset.mobenchTowerHidden === "1") {
elements[i].style.display = "";
delete elements[i].dataset.mobenchTowerHidden;
}
}
if (svg) {
if (mobenchCollapsedTowerState.originalViewBox !== null) {
svg.setAttribute("viewBox", mobenchCollapsedTowerState.originalViewBox);
}
if (mobenchCollapsedTowerState.originalHeight !== null) {
svg.setAttribute("height", mobenchCollapsedTowerState.originalHeight);
} else {
svg.removeAttribute("height");
}
document.body.style.height = mobenchCollapsedTowerState.originalBodyHeight;
document.documentElement.style.height = mobenchCollapsedTowerState.originalDocumentHeight;
}
};
window.mobenchGetVisibleFrames = function () {
var visible = [];
var elements = frames.children;
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
if (element.classList.contains("hide") || element.dataset.mobenchTowerHidden === "1") {
continue;
}
var rect = find_child(element, "rect");
if (!rect || !rect.attributes["fg:x"] || !rect.attributes["fg:w"]) {
continue;
}
var titleNode = find_child(element, "title");
var titleText = titleNode && titleNode.textContent ? titleNode.textContent : "";
var samplesMatch = titleText.match(/\(([0-9,]+)\s+samples?/);
visible.push({
index: i,
label: mobenchTitleForNode(element),
title: titleText,
xPct: parseFloat(rect.attributes.x.value || "0"),
widthPct: parseFloat(rect.attributes.width.value || "0"),
x: parseInt(rect.attributes["fg:x"].value || "0", 10),
width: parseInt(rect.attributes["fg:w"].value || "0", 10),
y: parseFloat(rect.attributes.y.value || "0"),
samples: samplesMatch ? parseInt(samplesMatch[1].replace(/,/g, ""), 10) : 0
});
}
return visible;
};
window.mobenchSetCollapsedTowerPresentation = function (presentation) {
window.mobenchClearCollapsedTowerPresentation();
if (!presentation || !presentation.hiddenIndexes || !presentation.hiddenIndexes.length) {
return false;
}
var svg = mobenchRememberViewport();
var elements = frames.children;
for (var i = 0; i < presentation.hiddenIndexes.length; i++) {
var index = presentation.hiddenIndexes[i];
if (!elements[index]) continue;
elements[index].style.display = "none";
elements[index].dataset.mobenchTowerHidden = "1";
}
if (svg && presentation.viewport) {
var originalViewBox = (mobenchCollapsedTowerState.originalViewBox || "0 0 1200 900")
.split(/\s+/)
.map(function (value) { return parseFloat(value) || 0; });
var minY = Math.max(0, presentation.viewport.minY || 0);
var height = Math.max(220, presentation.viewport.height || originalViewBox[3] || 900);
svg.setAttribute("viewBox", [originalViewBox[0], minY, originalViewBox[2], height].join(" "));
svg.setAttribute("height", String(Math.round(height)));
document.body.style.height = Math.round(height) + "px";
document.documentElement.style.height = Math.round(height) + "px";
}
return true;
};
function mobenchResetDom() {
window.mobenchClearCollapsedTowerPresentation();
var elements = frames.children;
for (var i = 0; i < elements.length; i++) {
elements[i].classList.remove("parent");
elements[i].classList.remove("hide");
zoom_reset(elements[i]);
}
update_text_for_elements(elements);
}
function mobenchNotifyParent() {
try {
parent.postMessage({
type: "mobench:view-change",
label: mobenchState.current ? mobenchState.current.label : "all",
start: mobenchState.current ? mobenchState.current.xmin : 0,
width: mobenchState.current ? mobenchState.current.width : mobenchTotalSamples(),
total: mobenchTotalSamples()
}, "*");
} catch (_error) {}
}
function mobenchPushHistory(view) {
if (
mobenchState.index >= 0 &&
mobenchState.history[mobenchState.index] &&
mobenchState.history[mobenchState.index].xmin === view.xmin &&
mobenchState.history[mobenchState.index].width === view.width &&
mobenchState.history[mobenchState.index].label === view.label
) {
return;
}
mobenchState.history = mobenchState.history.slice(0, mobenchState.index + 1);
mobenchState.history.push(view);
mobenchState.index = mobenchState.history.length - 1;
}
function mobenchApplyAbsoluteRange(xmin, width, label, pushHistory) {
if (!frames) return false;
var total = mobenchTotalSamples();
var view = mobenchNormalizeView({
xmin: xmin,
width: width,
label: label
});
mobenchResetDom();
var elements = frames.children;
var toUpdate = [];
var xmax = view.xmin + view.width;
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
var rect = find_child(element, "rect");
if (!rect || !rect.attributes["fg:x"] || !rect.attributes["fg:w"]) {
continue;
}
var ex = parseInt(rect.attributes["fg:x"].value, 10);
var ew = parseInt(rect.attributes["fg:w"].value, 10);
var ix0 = Math.max(ex, view.xmin);
var ix1 = Math.min(ex + ew, xmax);
if (!(ix1 > ix0)) {
element.classList.add("hide");
continue;
}
rect.attributes.x.value = format_percent(100 * (ix0 - view.xmin) / view.width);
rect.attributes.width.value = format_percent(100 * (ix1 - ix0) / view.width);
toUpdate.push(element);
}
update_text_for_elements(toUpdate);
mobenchState.current = view;
if (pushHistory !== false) {
mobenchPushHistory(view);
}
mobenchNotifyParent();
return view.width < total;
}
window.mobenchScrollToBase = function () {
function applyScroll() {
var doc = document.documentElement || document.body;
var body = document.body || document.documentElement;
var maxScroll = Math.max(
0,
(doc ? doc.scrollHeight : 0),
(body ? body.scrollHeight : 0)
) - window.innerHeight;
window.scrollTo(0, Math.max(0, maxScroll));
}
applyScroll();
setTimeout(applyScroll, 0);
};
window.mobenchResetView = function () {
var view = {
xmin: 0,
width: Math.max(1, mobenchTotalSamples()),
label: "all"
};
mobenchResetDom();
mobenchState.history = [view];
mobenchState.index = 0;
mobenchState.current = view;
mobenchNotifyParent();
};
window.mobenchCanGoBack = function () {
return mobenchState.index > 0;
};
window.mobenchCanGoForward = function () {
return mobenchState.index >= 0 && mobenchState.index < mobenchState.history.length - 1;
};
window.mobenchHistoryBack = function () {
if (!window.mobenchCanGoBack()) return false;
mobenchState.index -= 1;
var view = mobenchState.history[mobenchState.index];
return mobenchApplyAbsoluteRange(view.xmin, view.width, view.label, false);
};
window.mobenchHistoryForward = function () {
if (!window.mobenchCanGoForward()) return false;
mobenchState.index += 1;
var view = mobenchState.history[mobenchState.index];
return mobenchApplyAbsoluteRange(view.xmin, view.width, view.label, false);
};
window.mobenchZoomToFrame = function (node, pushHistory) {
var rect = find_child(node, "rect");
if (!rect || !rect.attributes["fg:x"] || !rect.attributes["fg:w"]) {
return false;
}
return mobenchApplyAbsoluteRange(
parseInt(rect.attributes["fg:x"].value, 10),
parseInt(rect.attributes["fg:w"].value, 10),
mobenchTitleForNode(node),
pushHistory !== false
);
};
window.mobenchZoomVisibleFraction = function (from, to) {
var total = mobenchTotalSamples();
if (!mobenchState.current) {
mobenchState.current = { xmin: 0, width: total, label: "all" };
}
var start = Math.min(from, to);
var end = Math.max(from, to);
if (!isFinite(start) || !isFinite(end) || (end - start) < 0.015) {
return false;
}
var xmin = Math.floor(mobenchState.current.xmin + mobenchState.current.width * start);
var width = Math.max(1, Math.floor(mobenchState.current.width * (end - start)));
return mobenchApplyAbsoluteRange(xmin, width, "selected range", true);
};
window.mobenchZoomAbsoluteRange = function (xmin, width, label) {
if (!isFinite(xmin) || !isFinite(width) || width <= 0) {
return false;
}
return mobenchApplyAbsoluteRange(xmin, width, label || "selected range", true);
};
window.mobenchSearch = function (term) {
if (!term) {
if (typeof reset_search === "function") {
reset_search();
}
searching = 0;
mobenchNotifyParent();
return;
}
if (typeof search === "function") {
search(term);
mobenchNotifyParent();
}
};
window.mobenchGetViewState = function () {
if (!mobenchState.current) {
return {
label: "all",
start: 0,
width: mobenchTotalSamples(),
total: mobenchTotalSamples()
};
}
return {
label: mobenchState.current.label,
start: mobenchState.current.xmin,
width: mobenchState.current.width,
total: mobenchTotalSamples()
};
};
zoom = function (node) {
return window.mobenchZoomToFrame(node, true);
};
unzoom = function () {
window.mobenchResetView();
};
window.addEventListener("load", function () {
setTimeout(function () {
window.mobenchResetView();
window.mobenchScrollToBase();
}, 0);
});
})();
"#;
#[cfg(test)]
mod tests {
use super::*;
fn sample_doc() -> FlamegraphViewerDoc {
FlamegraphViewerDoc {
title: "iOS Native Profile".into(),
browser_title: "Mobench Flamegraph - mobile-bench-rs".into(),
full_svg_document: "<svg id=\"full\"></svg>".into(),
focused_svg_document: "<svg id=\"focused\"></svg>".into(),
full_summary: ModeSummary {
total_samples: 10,
visible_stack_count: 2,
matched_stack_count: 2,
excluded_stack_count: 0,
warning: None,
self_frames: vec![FrameBreakdown {
frame: "start".into(),
samples: 10,
percent_total: 100,
}],
inclusive_frames: vec![FrameBreakdown {
frame: "start".into(),
samples: 10,
percent_total: 100,
}],
},
focused_summary: ModeSummary {
total_samples: 5,
visible_stack_count: 1,
matched_stack_count: 1,
excluded_stack_count: 1,
warning: Some("focused warning".into()),
self_frames: vec![FrameBreakdown {
frame: "sample_fns::fibonacci".into(),
samples: 5,
percent_total: 100,
}],
inclusive_frames: vec![FrameBreakdown {
frame: "sample_fns::run_benchmark".into(),
samples: 5,
percent_total: 100,
}],
},
sampled_duration_secs: Some(10.0),
run_metadata: vec![
ViewerMetadataItem::new("Target", "ios"),
ViewerMetadataItem::new("Benchmark", "sample_fns::fibonacci"),
ViewerMetadataItem::new("Run ID", "ios-demo"),
],
harness_timeline: vec![
ViewerHarnessTimelineSpan {
phase: "setup".into(),
start_offset_ns: 0,
end_offset_ns: 100,
iteration: None,
},
ViewerHarnessTimelineSpan {
phase: "measured-benchmark".into(),
start_offset_ns: 100,
end_offset_ns: 300,
iteration: Some(0),
},
ViewerHarnessTimelineSpan {
phase: "teardown".into(),
start_offset_ns: 300,
end_offset_ns: 420,
iteration: None,
},
],
timeline_lanes: vec![ViewerTraceLane {
id: "main".into(),
label: "Main Thread".into(),
events: vec![
ViewerTraceEvent {
event_kind: "span".into(),
start_offset_ns: 0,
end_offset_ns: Some(100),
frames: vec!["fixture::setup".into()],
phase: Some("setup".into()),
iteration: None,
},
ViewerTraceEvent {
event_kind: "sample".into(),
start_offset_ns: 140,
end_offset_ns: None,
frames: vec![
"sample_fns::run_benchmark".into(),
"sample_fns::fibonacci".into(),
],
phase: Some("measured-benchmark".into()),
iteration: Some(0),
},
],
}],
timeline_total_duration_ns: Some(420),
timeline_note: Some(
"Timeline shows exact harness chronology and available trace samples.".into(),
),
default_mode: FlamegraphMode::Focused,
artifact_links: vec![ArtifactLink::new("native-report.txt", "native-report.txt")],
source_links: Vec::new(),
source_link_note: Some("Source links are unavailable in this fixture.".into()),
}
}
#[test]
fn focused_stack_derivation_returns_empty_when_no_anchor_matches() {
let folded = "start;UIKitMain 1\n";
let focused =
derive_benchmark_focused_folded_stacks(folded, &["sample_fns::run_benchmark"]);
assert!(focused.folded.is_empty());
assert_eq!(focused.matched_stack_count, 0);
}
#[test]
fn derive_benchmark_focused_folded_stacks_trims_ios_stack_to_benchmark_anchor() {
let folded = concat!(
"start;UIApplicationMain;runBenchmark(spec:);uniffi_sample_fns_fn_func_run_benchmark;",
"sample_fns::run_benchmark;mobench_sdk::timing::profile_phase;",
"sample_fns::fibonacci 5\n"
);
let focused = derive_benchmark_focused_folded_stacks(
folded,
&["runBenchmark(spec:)", "sample_fns::run_benchmark"],
);
assert_eq!(
focused.folded,
"runBenchmark(spec:);uniffi_sample_fns_fn_func_run_benchmark;sample_fns::run_benchmark;mobench_sdk::timing::profile_phase;sample_fns::fibonacci 5"
);
}
#[test]
fn derive_benchmark_focused_folded_stacks_trims_android_stack_to_rust_anchor() {
let folded = concat!(
"all;uniffi.sample_fns.Sample_fnsKt.runBenchmark;",
"libsample_fns.so;sample_fns::run_benchmark;mobench_sdk::timing::run_closure;",
"sample_fns::fibonacci 3\n"
);
let focused = derive_benchmark_focused_folded_stacks(
folded,
&[
"sample_fns::run_benchmark",
"mobench_sdk::timing::run_closure",
],
);
assert_eq!(
focused.folded,
"sample_fns::run_benchmark;mobench_sdk::timing::run_closure;sample_fns::fibonacci 3"
);
}
#[test]
fn standalone_viewer_html_embeds_full_and_focused_modes() {
let html = render_flamegraph_viewer_html(sample_doc());
assert!(html.contains("rel=\"icon\""));
assert!(html.contains("data:image/svg+xml"));
assert!(html.contains("<title>Mobench Flamegraph - mobile-bench-rs</title>"));
assert!(html.contains("iOS Native Profile"));
assert!(html.contains("Benchmark Only"));
assert!(html.contains("Full Process"));
assert!(html.contains("Timeline"));
assert!(html.contains("data-mode=\"focused\""));
assert!(html.contains("data-mode=\"full\""));
assert!(html.contains("data-mode=\"timeline\""));
assert!(html.contains("<svg id=\\\"full\\\"><\\/svg>"));
assert!(html.contains("<svg id=\\\"focused\\\"><\\/svg>"));
}
#[test]
fn viewer_html_embeds_reference_palette_shell_tokens() {
let html = render_flamegraph_viewer_html(sample_doc());
assert!(html.contains("--accent: #ff6b74;"));
assert!(html.contains("--accent-strong: #f25c68;"));
assert!(html.contains("--bg: #fffdfb;"));
assert!(html.contains("font: 14px/1.45 \"Avenir Next\", \"SF Pro Display\""));
}
#[test]
fn viewer_html_includes_history_and_brush_zoom_controls() {
let html = render_flamegraph_viewer_html(sample_doc());
assert!(html.contains("id=\"viewer-back\""));
assert!(html.contains("id=\"viewer-forward\""));
assert!(html.contains("id=\"viewer-fullscreen\""));
assert!(html.contains("id=\"viewer-toggle-timeline\""));
assert!(html.contains("id=\"viewer-legend\""));
assert!(html.contains("id=\"viewer-reset\""));
assert!(html.contains("id=\"viewer-search\""));
assert!(html.contains("id=\"viewer-select-range\" hidden"));
assert!(html.contains("id=\"selection-overlay\""));
assert!(html.contains("Drag across the graph to zoom. Double-click to step back one range. Press T to toggle the timeline. Press R to reset. Press F for fullscreen."));
assert!(html.contains("function stepBackCurrentRange()"));
assert!(html.contains("function requestStepBackCurrentRange()"));
assert!(html.contains("function stepForwardCurrentRange()"));
assert!(html.contains("function toggleFullscreenGraph(force)"));
assert!(html.contains("function toggleTimelineStrip(force)"));
assert!(html.contains("function toggleShortcutLegend(force)"));
assert!(html.contains("function showSelectionHintFor(durationMs)"));
assert!(html.contains("function scrollAggregateFrameToBase(mode)"));
assert!(html.contains("function resetZoomFully()"));
assert!(html.contains("function scrollActiveGraphSurface(deltaX, deltaY)"));
assert!(html.contains("function isRepeatedOverlayTap(event)"));
assert!(html.contains("window.addEventListener(\"keydown\""));
assert!(html.contains("id=\"shortcut-overlay\""));
assert!(
html.contains("Keyboard and graph gestures for moving around the profiler quickly.")
);
assert!(html.contains("shortcut-badge"));
assert!(html.contains("showSelectionHintFor(3000);"));
assert!(html.contains("event.key === \"1\""));
assert!(html.contains("event.key === \"2\""));
assert!(html.contains("event.key === \"3\""));
assert!(html.contains("event.key === \"t\" || event.key === \"T\""));
assert!(html.contains("event.key === \"j\" || event.key === \"J\""));
assert!(html.contains("event.key === \"k\" || event.key === \"K\""));
assert!(html.contains("event.key === \"f\" || event.key === \"F\""));
assert!(html.contains("event.key === \"Escape\" && fullscreenVisible()"));
assert!(html.contains("event.key === \"?\" || (event.key === \"/\" && event.shiftKey)"));
assert!(html.contains("selectionOverlay.addEventListener(\"wheel\""));
assert!(html.contains("selectionOverlay.addEventListener(\"dblclick\""));
assert!(html.contains("id=\"timeline-stage\""));
assert!(html.contains("body.graph-fullscreen .graph-axis"));
assert!(html.contains("body.graph-fullscreen .harness-timeline"));
assert!(!html.contains("target=\"_blank\""));
}
#[test]
fn viewer_html_omits_experimental_tower_controls() {
let html = render_flamegraph_viewer_html(sample_doc());
assert!(!html.contains("id=\"viewer-hide-towers\""));
assert!(!html.contains("Hide Thin Towers"));
assert!(!html.contains("id=\"tower-overlay\""));
assert!(!html.contains("id=\"tower-meta\""));
assert!(!html.contains("applyTowerCollapse"));
}
#[test]
fn viewer_html_renders_hot_frame_summary_for_each_mode() {
let html = render_flamegraph_viewer_html(sample_doc());
assert!(html.contains("Self Time"));
assert!(html.contains("Inclusive Time"));
assert!(html.contains("sample_fns::fibonacci"));
assert!(html.contains("sample_fns::run_benchmark"));
assert!(html.contains("focused warning"));
}
#[test]
fn viewer_html_restores_metadata_harness_and_sampled_time_shell() {
let html = render_flamegraph_viewer_html(sample_doc());
assert!(html.contains("Visible Duration"));
assert!(html.contains("id=\"run-metadata\""));
assert!(html.contains(
"grid-template-columns: repeat(var(--run-metadata-columns, 1), minmax(0, 1fr));"
));
assert!(html.contains("function syncRunMetadataColumns()"));
assert!(html.contains("id=\"harness-timeline\""));
assert!(html.contains("id=\"harness-readout\""));
assert!(html.contains("id=\"graph-axis\""));
assert!(html.contains(".harness-timeline {\n grid-row: 1;"));
assert!(html.contains(".graph-stage {\n grid-row: 2;"));
assert!(html.contains(".graph-axis {\n grid-row: 3;"));
}
#[test]
fn viewer_html_tracks_aggregate_dataset_separately_from_timeline_mode() {
let html = render_flamegraph_viewer_html(sample_doc());
assert!(html.contains("lastAggregateMode"));
assert!(html.contains("getAggregateDatasetMode"));
assert!(html.contains("savedAggregateViews"));
assert!(html.contains("syncAggregateWindowToTimelineView"));
}
#[test]
fn summarize_folded_stacks_caps_inclusive_percent_for_repeated_frames() {
let summary =
summarize_folded_stacks("root;repeat;repeat 4\nroot;repeat;leaf 1\n", 2, 0, None);
let repeat = summary
.inclusive_frames
.iter()
.find(|frame| frame.frame == "repeat")
.expect("repeat frame");
let leaf = summary
.self_frames
.iter()
.find(|frame| frame.frame == "leaf")
.expect("leaf frame");
assert_eq!(repeat.samples, 5);
assert_eq!(repeat.percent_total, 100);
assert_eq!(leaf.samples, 1);
assert_eq!(leaf.percent_total, 20);
}
#[test]
fn summarize_folded_stacks_prettifies_rust_symbol_noise() {
let summary = summarize_folded_stacks(
"root;sample_fns::fibonacci::ha1ebbae54edac99d 3\nroot;_$LT$u32$u20$as$u20$core..iter..range..Step$GT$::forward_unchecked::h2f57f430431a1dbe 2\n",
2,
0,
None,
);
assert!(
summary
.self_frames
.iter()
.any(|frame| frame.frame == "sample_fns::fibonacci")
);
assert!(
summary
.self_frames
.iter()
.any(|frame| frame.frame == "<u32 as core::iter::range::Step>::forward_unchecked")
);
}
#[test]
fn prettify_folded_stacks_preserves_differential_counts() {
let pretty =
prettify_folded_stacks_for_display("root;sample_fns::fibonacci::ha1ebbae54edac99d 3 7");
assert_eq!(pretty, "root;sample_fns::fibonacci 3 7");
}
#[test]
fn viewer_html_escapes_embedded_svg_script_terminators() {
let mut doc = sample_doc();
doc.focused_svg_document = "<svg><script>alert('focused')</script></svg>".into();
doc.full_svg_document = "<svg><script>alert('full')</script></svg>".into();
let html = render_flamegraph_viewer_html(doc);
assert!(html.contains("<\\/script>"));
assert!(!html.contains("alert('focused')</script></svg>,\n full:"));
}
#[test]
fn standalone_svg_defaults_to_viewport_width_and_custom_helpers() {
let svg =
render_standalone_flamegraph_svg("root;sample_fns::fibonacci 1", "Test Flamegraph")
.expect("render svg");
assert!(svg.contains("var fluiddrawing = false;"));
assert!(svg.contains("width:100vw"));
assert!(svg.contains("mobenchZoomVisibleFraction"));
assert!(svg.contains("mobenchScrollToBase"));
}
#[test]
fn standalone_svg_includes_tower_expand_hooks() {
let svg =
render_standalone_flamegraph_svg("root;sample_fns::fibonacci 1", "Test Flamegraph")
.expect("render svg");
assert!(svg.contains("mobenchGetVisibleFrames"));
assert!(svg.contains("mobenchSetCollapsedTowerPresentation"));
assert!(svg.contains("mobenchClearCollapsedTowerPresentation"));
assert!(svg.contains("mobenchZoomAbsoluteRange"));
}
#[test]
fn standalone_svg_retints_neutral_differential_frames() {
let svg = render_standalone_flamegraph_svg(
"root;sample_fns::fibonacci 1 1\nroot;sample_fns::checksum 1 1",
"Diff Flamegraph",
)
.expect("render differential svg");
assert!(!svg.contains("fill=\"rgb(250,250,250)\""));
assert!(!svg.contains("rgb(217, 215, 255)"));
assert!(!svg.contains("rgb(232, 228, 255)"));
assert!(
svg.contains("rgb(255,107,116)")
|| svg.contains("rgb(255,178,92)")
|| svg.contains("rgb(255,220,107)")
|| svg.contains("rgb(255,207,177)")
);
}
#[test]
fn standalone_svg_replaces_default_gray_background_gradient() {
let svg =
render_standalone_flamegraph_svg("root;sample_fns::fibonacci 1", "Test Flamegraph")
.expect("render svg");
assert!(!svg.contains("stop-color=\"#eeeeee\""));
assert!(!svg.contains("stop-color=\"#eeeeb0\""));
assert!(svg.contains("stop-color=\"#ffffff\""));
}
}