use crate::inspector::Inspector;
use crate::timeline::EventKind;
use std::collections::HashMap;
use std::fs::File;
use std::io::{self, Write};
use std::path::Path;
pub struct FlamegraphExporter;
impl FlamegraphExporter {
pub fn export_to_file<P: AsRef<Path>>(inspector: &Inspector, path: P) -> io::Result<()> {
let folded = Self::generate_folded_stacks(inspector);
let mut file = File::create(path)?;
file.write_all(folded.as_bytes())?;
Ok(())
}
#[must_use]
pub fn export_to_string(inspector: &Inspector) -> String {
Self::generate_folded_stacks(inspector)
}
fn generate_folded_stacks(inspector: &Inspector) -> String {
let mut output = String::new();
let events = inspector.get_events();
let mut task_stacks: HashMap<u64, Vec<String>> = HashMap::new();
let mut stack_samples: HashMap<String, u64> = HashMap::new();
for event in events {
let task_id = event.task_id.as_u64();
match &event.kind {
EventKind::TaskSpawned { name, parent, .. } => {
let mut stack = Vec::new();
if let Some(parent_id) = parent {
if let Some(parent_stack) = task_stacks.get(&parent_id.as_u64()) {
stack.extend(parent_stack.clone());
}
}
stack.push(Self::sanitize_frame_name(name));
task_stacks.insert(task_id, stack);
}
EventKind::AwaitEnded {
await_point,
duration,
} => {
if let Some(stack) = task_stacks.get_mut(&task_id) {
stack.push(Self::sanitize_frame_name(await_point));
let stack_str = stack.join(";");
let duration_ms = duration.as_millis() as u64;
*stack_samples.entry(stack_str).or_insert(0) += duration_ms;
stack.pop();
}
}
EventKind::PollEnded { duration } => {
if let Some(stack) = task_stacks.get(&task_id) {
let mut poll_stack = stack.clone();
poll_stack.push("poll".to_string());
let stack_str = poll_stack.join(";");
let duration_ms = duration.as_millis() as u64;
*stack_samples.entry(stack_str).or_insert(0) += duration_ms;
}
}
EventKind::TaskCompleted { duration } => {
if let Some(stack) = task_stacks.get(&task_id) {
let stack_str = stack.join(";");
let duration_ms = duration.as_millis() as u64;
*stack_samples.entry(stack_str).or_insert(0) += duration_ms;
}
}
_ => {}
}
}
let mut sorted_stacks: Vec<_> = stack_samples.into_iter().collect();
sorted_stacks.sort_by(|a, b| b.1.cmp(&a.1));
for (stack, count) in sorted_stacks {
if count > 0 {
output.push_str(&format!("{stack} {count}\n"));
}
}
output
}
fn sanitize_frame_name(name: &str) -> String {
name.replace(';', ":")
.replace('\n', " ")
.replace('\r', "")
.trim()
.to_string()
}
#[cfg(feature = "flamegraph")]
pub fn generate_svg<P: AsRef<Path>>(inspector: &Inspector, path: P) -> io::Result<()> {
use inferno::flamegraph::{self, Options};
let folded = Self::generate_folded_stacks(inspector);
let folded_bytes = folded.as_bytes();
let mut options = Options::default();
options.title = "async-inspect Flamegraph".to_string();
options.subtitle = Some("Async task execution time".to_string());
let mut svg_output = File::create(path)?;
flamegraph::from_reader(&mut options, folded_bytes, &mut svg_output)
.map_err(io::Error::other)?;
Ok(())
}
}
pub struct FlamegraphBuilder {
include_polls: bool,
include_awaits: bool,
min_duration_ms: u64,
}
impl Default for FlamegraphBuilder {
fn default() -> Self {
Self {
include_polls: true,
include_awaits: true,
min_duration_ms: 0,
}
}
}
impl FlamegraphBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn include_polls(mut self, include: bool) -> Self {
self.include_polls = include;
self
}
#[must_use]
pub fn include_awaits(mut self, include: bool) -> Self {
self.include_awaits = include;
self
}
#[must_use]
pub fn min_duration_ms(mut self, ms: u64) -> Self {
self.min_duration_ms = ms;
self
}
pub fn export_to_file<P: AsRef<Path>>(self, inspector: &Inspector, path: P) -> io::Result<()> {
FlamegraphExporter::export_to_file(inspector, path)
}
#[must_use]
pub fn export_to_string(self, inspector: &Inspector) -> String {
FlamegraphExporter::export_to_string(inspector)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitize_frame_name() {
assert_eq!(FlamegraphExporter::sanitize_frame_name("simple"), "simple");
assert_eq!(
FlamegraphExporter::sanitize_frame_name("with;semicolon"),
"with:semicolon"
);
assert_eq!(
FlamegraphExporter::sanitize_frame_name("with\nnewline"),
"with newline"
);
assert_eq!(
FlamegraphExporter::sanitize_frame_name(" trimmed "),
"trimmed"
);
}
#[test]
fn test_flamegraph_builder() {
let builder = FlamegraphBuilder::new()
.include_polls(false)
.include_awaits(true)
.min_duration_ms(10);
assert!(!builder.include_polls);
assert!(builder.include_awaits);
assert_eq!(builder.min_duration_ms, 10);
}
}