use super::{Component, EventContext, RenderContext};
use crate::input::{Event, Key};
mod node;
mod render;
pub use node::FlameNode;
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum FlameGraphMessage {
SetRoot(FlameNode),
Clear,
ZoomIn,
ZoomOut,
ResetZoom,
SelectUp,
SelectDown,
SelectLeft,
SelectRight,
SetSearch(String),
ClearSearch,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum FlameGraphOutput {
FrameSelected {
label: String,
value: u64,
self_value: u64,
},
ZoomedIn(String),
ZoomedOut,
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct FlameGraphState {
root: Option<FlameNode>,
zoom_stack: Vec<String>,
selected_depth: usize,
selected_index: usize,
search_query: String,
title: Option<String>,
}
impl FlameGraphState {
pub fn new() -> Self {
Self::default()
}
pub fn with_root(root: FlameNode) -> Self {
Self {
root: Some(root),
..Self::default()
}
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn root(&self) -> Option<&FlameNode> {
self.root.as_ref()
}
pub fn root_mut(&mut self) -> Option<&mut FlameNode> {
self.root.as_mut()
}
pub fn set_root(&mut self, root: FlameNode) {
self.root = Some(root);
self.zoom_stack.clear();
self.selected_depth = 0;
self.selected_index = 0;
}
pub fn clear(&mut self) {
self.root = None;
self.zoom_stack.clear();
self.selected_depth = 0;
self.selected_index = 0;
self.search_query.clear();
}
pub fn current_view_root(&self) -> Option<&FlameNode> {
let mut current = self.root.as_ref()?;
for label in &self.zoom_stack {
let found = current.children.iter().find(|c| &c.label == label);
match found {
Some(child) => current = child,
None => return Some(current),
}
}
Some(current)
}
pub fn selected_frame(&self) -> Option<&FlameNode> {
let view_root = self.current_view_root()?;
Self::find_at_depth(view_root, self.selected_depth, self.selected_index)
}
pub fn zoom_stack(&self) -> &[String] {
&self.zoom_stack
}
pub fn selected_depth(&self) -> usize {
self.selected_depth
}
pub fn selected_index(&self) -> usize {
self.selected_index
}
pub fn search_query(&self) -> &str {
&self.search_query
}
pub fn search(&self) -> Option<&str> {
if self.search_query.is_empty() {
None
} else {
Some(&self.search_query)
}
}
pub fn set_search(&mut self, query: String) {
self.search_query = query;
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn set_title(&mut self, title: impl Into<String>) {
self.title = Some(title.into());
}
pub fn zoom_in(&mut self) -> bool {
if let Some(frame) = self.selected_frame() {
if !frame.children.is_empty() {
let label = frame.label.clone();
self.zoom_stack.push(label);
self.selected_depth = 0;
self.selected_index = 0;
return true;
}
}
false
}
pub fn zoom_out(&mut self) -> bool {
if self.zoom_stack.pop().is_some() {
self.selected_depth = 0;
self.selected_index = 0;
true
} else {
false
}
}
pub fn reset_zoom(&mut self) {
self.zoom_stack.clear();
self.selected_depth = 0;
self.selected_index = 0;
}
pub fn select_up(&mut self) -> bool {
if self.root.is_none() {
return false;
}
if self.selected_depth > 0 {
let parent_index = self.find_parent_index();
self.selected_depth -= 1;
self.selected_index = parent_index;
true
} else {
false
}
}
pub fn select_down(&mut self) -> bool {
let view_root = match self.current_view_root() {
Some(r) => r,
None => return false,
};
let frames_at_next = Self::frames_at_depth(view_root, self.selected_depth + 1);
if frames_at_next.is_empty() {
return false;
}
if let Some(selected) = self.selected_frame() {
if !selected.children.is_empty() {
let first_child_label = &selected.children[0].label;
let child_idx = frames_at_next
.iter()
.position(|f| &f.label == first_child_label)
.unwrap_or(0);
self.selected_depth += 1;
self.selected_index = child_idx;
return true;
}
}
self.selected_depth += 1;
self.selected_index = 0;
true
}
pub fn select_left(&mut self) -> bool {
if self.root.is_none() {
return false;
}
if self.selected_index > 0 {
self.selected_index -= 1;
true
} else {
false
}
}
pub fn select_right(&mut self) -> bool {
let view_root = match self.current_view_root() {
Some(r) => r,
None => return false,
};
let frames_at_depth = Self::frames_at_depth(view_root, self.selected_depth);
if self.selected_index + 1 < frames_at_depth.len() {
self.selected_index += 1;
true
} else {
false
}
}
pub fn update(&mut self, msg: FlameGraphMessage) -> Option<FlameGraphOutput> {
FlameGraph::update(self, msg)
}
fn find_parent_index(&self) -> usize {
if self.selected_depth == 0 {
return 0;
}
let view_root = match self.current_view_root() {
Some(r) => r,
None => return 0,
};
let parent_frames = Self::frames_at_depth(view_root, self.selected_depth - 1);
let child_frames = Self::frames_at_depth(view_root, self.selected_depth);
if let Some(selected_child) = child_frames.get(self.selected_index) {
for (pi, parent) in parent_frames.iter().enumerate() {
if parent
.children
.iter()
.any(|c| c.label == selected_child.label && c.value == selected_child.value)
{
return pi;
}
}
}
0
}
fn frames_at_depth(root: &FlameNode, depth: usize) -> Vec<&FlameNode> {
let mut result = Vec::new();
Self::collect_at_depth(root, 0, depth, &mut result);
result
}
fn collect_at_depth<'a>(
node: &'a FlameNode,
current_depth: usize,
target_depth: usize,
result: &mut Vec<&'a FlameNode>,
) {
if current_depth == target_depth {
result.push(node);
return;
}
for child in &node.children {
Self::collect_at_depth(child, current_depth + 1, target_depth, result);
}
}
fn find_at_depth(root: &FlameNode, depth: usize, index: usize) -> Option<&FlameNode> {
let frames = Self::frames_at_depth(root, depth);
frames.into_iter().nth(index)
}
pub(crate) fn max_depth(&self) -> usize {
match self.current_view_root() {
Some(root) => Self::compute_max_depth(root, 0),
None => 0,
}
}
fn compute_max_depth(node: &FlameNode, current: usize) -> usize {
if node.children.is_empty() {
return current;
}
node.children
.iter()
.map(|c| Self::compute_max_depth(c, current + 1))
.max()
.unwrap_or(current)
}
}
pub struct FlameGraph;
impl Component for FlameGraph {
type State = FlameGraphState;
type Message = FlameGraphMessage;
type Output = FlameGraphOutput;
fn init() -> Self::State {
FlameGraphState::default()
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
FlameGraphMessage::SetRoot(root) => {
state.set_root(root);
None
}
FlameGraphMessage::Clear => {
state.clear();
None
}
FlameGraphMessage::SetSearch(query) => {
state.search_query = query;
None
}
FlameGraphMessage::ClearSearch => {
state.search_query.clear();
None
}
FlameGraphMessage::ResetZoom => {
state.reset_zoom();
None
}
_ => {
state.root.as_ref()?;
match msg {
FlameGraphMessage::SelectUp => {
if state.select_up() {
make_selected_output(state)
} else {
None
}
}
FlameGraphMessage::SelectDown => {
if state.select_down() {
make_selected_output(state)
} else {
None
}
}
FlameGraphMessage::SelectLeft => {
if state.select_left() {
make_selected_output(state)
} else {
None
}
}
FlameGraphMessage::SelectRight => {
if state.select_right() {
make_selected_output(state)
} else {
None
}
}
FlameGraphMessage::ZoomIn => {
if let Some(frame) = state.selected_frame() {
let label = frame.label.clone();
if state.zoom_in() {
Some(FlameGraphOutput::ZoomedIn(label))
} else {
None
}
} else {
None
}
}
FlameGraphMessage::ZoomOut => {
if state.zoom_out() {
Some(FlameGraphOutput::ZoomedOut)
} else {
None
}
}
FlameGraphMessage::SetRoot(_)
| FlameGraphMessage::Clear
| FlameGraphMessage::SetSearch(_)
| FlameGraphMessage::ClearSearch
| FlameGraphMessage::ResetZoom => unreachable!(),
}
}
}
}
fn handle_event(
_state: &Self::State,
event: &Event,
ctx: &EventContext,
) -> Option<Self::Message> {
if !ctx.focused || ctx.disabled {
return None;
}
if let Some(key) = event.as_key() {
match key.code {
Key::Up | Key::Char('k') => Some(FlameGraphMessage::SelectUp),
Key::Down | Key::Char('j') => Some(FlameGraphMessage::SelectDown),
Key::Left | Key::Char('h') => Some(FlameGraphMessage::SelectLeft),
Key::Right | Key::Char('l') => Some(FlameGraphMessage::SelectRight),
Key::Enter => Some(FlameGraphMessage::ZoomIn),
Key::Esc | Key::Backspace => Some(FlameGraphMessage::ZoomOut),
Key::Home => Some(FlameGraphMessage::ResetZoom),
Key::Char('/') => Some(FlameGraphMessage::SetSearch(String::new())),
_ => None,
}
} else {
None
}
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
render::render_flame_graph(
state,
ctx.frame,
ctx.area,
ctx.theme,
ctx.focused,
ctx.disabled,
);
}
}
fn make_selected_output(state: &FlameGraphState) -> Option<FlameGraphOutput> {
state
.selected_frame()
.map(|frame| FlameGraphOutput::FrameSelected {
label: frame.label.clone(),
value: frame.total_value(),
self_value: frame.self_value(),
})
}
#[cfg(test)]
mod snapshot_tests;
#[cfg(test)]
mod tests;