Flame graph chart
A profile as stacked frames, callers below callees, each frame as wide as its total time. Frames colour by name, the tooltip splits self from total time, and a search highlights matching frames.
2 frames match “json”
| Frame | Total | Self |
|---|---|---|
| handle_request | 412 ms | 23 ms |
| handle_request > authenticate | 38 ms | 5 ms |
| handle_request > authenticate > verify_jwt | 21 ms | 21 ms |
| handle_request > authenticate > load_session | 12 ms | 12 ms |
| handle_request > load_cart | 146 ms | 12 ms |
| handle_request > load_cart > db_query | 98 ms | 53 ms |
| handle_request > load_cart > db_query > pool_acquire | 14 ms | 14 ms |
| handle_request > load_cart > db_query > parse_rows | 31 ms | 31 ms |
| handle_request > load_cart > price_items | 36 ms | 36 ms |
| handle_request > render | 188 ms | 22 ms |
| handle_request > render > serialize_json | 72 ms | 53 ms |
| handle_request > render > serialize_json > escape_strings | 19 ms | 19 ms |
| handle_request > render > template | 94 ms | 21 ms |
| handle_request > render > template > parse_json | 22 ms | 22 ms |
| handle_request > render > template > layout | 51 ms | 51 ms |
| handle_request > log_access | 17 ms | 17 ms |
Usage
use hydroxy_leptos::flame_graph_chart::*;
use hydroxy_leptos::search_field::SearchField;
use leptos::prelude::*;
// A frame's value is its total time; whatever its callees don't cover is self time.
fn frame(name: &str, total: f64, calls: Vec<Tree>) -> Tree {
Tree { name: name.into(), value: total, children: calls }
}
#[component]
pub fn Demo() -> impl IntoView {
// CPU time for one checkout request, in ms.
let profile = frame(
"handle_request",
412.0,
vec![
frame("authenticate", 38.0, vec![Tree::leaf("verify_jwt", 21.0), Tree::leaf("load_session", 12.0)]),
frame(
"load_cart",
146.0,
vec![
frame("db_query", 98.0, vec![Tree::leaf("pool_acquire", 14.0), Tree::leaf("parse_rows", 31.0)]),
Tree::leaf("price_items", 36.0),
],
),
frame(
"render",
188.0,
vec![
frame("serialize_json", 72.0, vec![Tree::leaf("escape_strings", 19.0)]),
frame("template", 94.0, vec![Tree::leaf("parse_json", 22.0), Tree::leaf("layout", 51.0)]),
],
),
Tree::leaf("log_access", 17.0),
],
);
let (query, set_query) = signal("json".to_string());
view! {
<div style="display: grid; gap: var(--space-5);">
<div style="max-width: 280px;">
<SearchField label="Highlight frames" placeholder="Highlight frames" value=query on_input=move |v| set_query.set(v) />
</div>
<FlameGraphChart root=profile label="CPU time for a checkout request" highlight=query format=|v: f64| format!("{v:.0} ms") />
</div>
}
}use dioxus::prelude::*;
use hydroxy_dioxus::flame_graph_chart::{FlameGraphChart, Tree};
use hydroxy_dioxus::search_field::SearchField;
// A frame's value is its total time; whatever its callees don't cover is self time.
fn frame(name: &str, total: f64, calls: Vec<Tree>) -> Tree {
Tree { name: name.into(), value: total, children: calls }
}
#[component]
pub fn FlameGraphChartDemo() -> Element {
// CPU time for one checkout request, in ms.
let profile = frame(
"handle_request",
412.0,
vec![
frame("authenticate", 38.0, vec![Tree::leaf("verify_jwt", 21.0), Tree::leaf("load_session", 12.0)]),
frame(
"load_cart",
146.0,
vec![
frame("db_query", 98.0, vec![Tree::leaf("pool_acquire", 14.0), Tree::leaf("parse_rows", 31.0)]),
Tree::leaf("price_items", 36.0),
],
),
frame(
"render",
188.0,
vec![
frame("serialize_json", 72.0, vec![Tree::leaf("escape_strings", 19.0)]),
frame("template", 94.0, vec![Tree::leaf("parse_json", 22.0), Tree::leaf("layout", 51.0)]),
],
),
Tree::leaf("log_access", 17.0),
],
);
let mut query = use_signal(|| "json".to_string());
rsx! {
div { style: "display: grid; gap: var(--space-5);",
div { style: "max-width: 280px;",
SearchField {
label: "Highlight frames",
placeholder: "Highlight frames",
value: Some(query()),
on_input: move |v| query.set(v),
}
}
FlameGraphChart {
root: profile,
label: "CPU time for a checkout request",
highlight: query,
format: |v: f64| format!("{v:.0} ms"),
}
}
}
}
Anatomy
| Component | data-part | Renders |
|---|---|---|
FlameGraphChart | root | figure; `data-searching` while a highlight is set |
| breadcrumb | Path from the root frame to the current zoom |
| marks | Focusable SVG; arrow keys move between frames |
| cell | One frame; `data-match` when it matches the highlight, `data-active` |
| tooltip | Total time, self time and share of the zoomed frame |
| sr-table | Every frame with its total and self time |
Keyboard
| Key | Does |
|---|---|
Arrow keys | Move between frames in call order |
Home / End | Jump to the first or last frame |
Enter / Space | Zoom into the focused frame, or out from the bottom row |
Escape | Clear the focused frame, then zoom out |
Props
| Prop | Type | Notes |
|---|---|---|
root | Tree | Required. A branch's value is its total time; the rest after its callees is self time |
label | String | Required. What the chart shows; the accessible name |
highlight | String | Marks frames whose name contains it, ignoring case; the rest dim |
height | u16 | Plot height in px at an 800px drawing width. Default 320 |
format | Fn(f64) -> String | Formats times in the tooltip and table |
Events
| Prop | Type | Notes |
|---|---|---|
on_hover | Callback<Option<Datum>> | The mark under the pointer or keyboard focus changes, or is left. Fires only on a change |
on_select | Callback<Datum> | Click, tap or Enter on a mark |
view | Signal<View> | Controlled focused node. Uncontrolled when left off |
on_view_change | Callback<View> | Called when the focused node changes; not for changes that came from `view` |