use imgui::*;
use puffin::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug)]
struct Vec2 {
pub x: f32,
pub y: f32,
}
impl Vec2 {
pub fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
pub fn min(self, other: Self) -> Self {
Self {
x: self.x.min(other.x),
y: self.y.min(other.y),
}
}
pub fn max(self, other: Self) -> Self {
Self {
x: self.x.max(other.x),
y: self.y.max(other.y),
}
}
}
impl From<[f32; 2]> for Vec2 {
fn from(v: [f32; 2]) -> Self {
Self::new(v[0], v[1])
}
}
impl From<Vec2> for [f32; 2] {
fn from(v: Vec2) -> Self {
[v.x, v.y]
}
}
impl std::ops::Add<Vec2> for Vec2 {
type Output = Vec2;
fn add(self, rhs: Vec2) -> Self::Output {
Self {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
const ERROR_COLOR: [f32; 4] = [1.0, 0.0, 0.0, 1.0];
#[derive(Clone, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct ProfilerUi {
options: Options,
#[serde(skip)]
is_panning: bool,
#[serde(skip)]
paused_data: Option<FullProfileData>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
enum View {
Latest,
Spike,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
struct Options {
start_ns: NanoSecond,
pixels_per_ns: f32,
sideways_pan_in_pixels: f32,
view: View,
scroll_speed: f32,
scroll_zoom_speed: f32,
cull_width: f32,
rect_height: f32,
spacing: f32,
rounding: f32,
merge_scopes: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
start_ns: 0,
pixels_per_ns: 0.0,
sideways_pan_in_pixels: 0.0,
view: View::Latest,
scroll_speed: 10.0,
scroll_zoom_speed: 0.05,
cull_width: 0.5,
rect_height: 16.0,
spacing: 4.0,
rounding: 4.0,
merge_scopes: true,
}
}
}
struct Painter<'ui> {
canvas_min: Vec2,
canvas_max: Vec2,
mouse_pos: Vec2,
ui: &'ui Ui<'ui>,
draw_list: DrawListMut<'ui>,
font_size: f32,
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum PaintResult {
Culled,
Hovered,
Normal,
}
impl<'ui> Painter<'ui> {
fn canvas_width(&self) -> f32 {
self.canvas_max.x - self.canvas_min.x
}
fn pixel_from_ns(&self, options: &Options, ns: NanoSecond) -> f32 {
self.canvas_min.x
+ options.sideways_pan_in_pixels
+ ((ns - options.start_ns) as f32) * options.pixels_per_ns
}
}
impl ProfilerUi {
pub fn window(&mut self, ui: &Ui<'_>) -> bool {
let mut open = true;
imgui::Window::new(im_str!("Profiler"))
.position([10.0, 25.0], Condition::FirstUseEver)
.size([600.0, 250.0], Condition::FirstUseEver)
.bg_alpha(0.99)
.always_auto_resize(false)
.opened(&mut open)
.build(ui, || self.ui(ui));
open
}
fn get_latest_data(&self) -> FullProfileData {
let profiler = GlobalProfiler::lock();
match self.options.view {
View::Latest => profiler.past_frame().clone(),
View::Spike => profiler.spike_frame().clone(),
}
}
pub fn ui(&mut self, ui: &Ui<'_>) {
#![allow(clippy::collapsible_if)]
if !puffin::are_scopes_on() {
ui.text_colored(ERROR_COLOR, im_str!("The puffin profiler is OFF!"));
}
{
let mut view = self.options.view;
ui.text("Show:");
ui.same_line(0.0);
ui.radio_button(im_str!("Latest frame"), &mut view, View::Latest);
ui.same_line(0.0);
ui.radio_button(im_str!("Frame spike"), &mut view, View::Spike);
if view != self.options.view {
self.options.view = view;
self.paused_data = None;
}
}
ui.same_line(0.0);
if self.paused_data.is_none() {
if ui.button(im_str!("Pause"), Default::default()) {
self.paused_data = Some(self.get_latest_data());
}
} else {
if ui.button(im_str!("Resume"), Default::default()) {
self.paused_data = None;
}
}
ui.same_line(0.0);
ui.checkbox(
im_str!("Merge children with same ID"),
&mut self.options.merge_scopes,
);
if self.options.view == View::Spike {
ui.same_line(0.0);
if ui.button(im_str!("Clear current spike"), Default::default()) {
GlobalProfiler::lock().clear_spike_frame();
self.paused_data = None;
}
}
let profile_data = self
.paused_data
.clone()
.unwrap_or_else(|| self.get_latest_data());
let (min_ns, max_ns) = match profile_data.range_ns() {
Err(err) => {
ui.text_colored(ERROR_COLOR, im_str!("Profile data error: {:?}", err));
return;
}
Ok(Some(bounds)) => bounds,
Ok(None) => {
ui.text("No profiling data");
return;
}
};
ui.text("Pan using left mouse button. Drag up/down to zoom, or use scroll.");
ui.same_line(0.0);
if ui.button(im_str!("Reset view"), Default::default()) {
self.options.pixels_per_ns = 0.0;
self.options.sideways_pan_in_pixels = 0.0;
}
let content_min: Vec2 = ui.cursor_screen_pos().into();
let content_region_avail: Vec2 = ui.content_region_avail().into();
let content_max = content_min + content_region_avail;
let mut painter = Painter {
canvas_min: content_min,
canvas_max: content_max,
mouse_pos: ui.io().mouse_pos.into(),
ui,
draw_list: ui.get_window_draw_list(),
font_size: ui.current_font_size(),
};
ui.invisible_button(im_str!("canvas"), content_region_avail.into());
self.interact(&painter);
self.options.start_ns = min_ns;
if self.options.pixels_per_ns <= 0.0 {
self.options.pixels_per_ns = painter.canvas_width() / ((max_ns - min_ns) as f32);
}
let options = &self.options;
paint_timeline(&painter, options, min_ns, max_ns);
let mut cursor_y = painter.canvas_max.y;
cursor_y -= painter.font_size;
for (info, stream) in &profile_data.0 {
painter
.draw_list
.add_line(
[painter.canvas_min.x, cursor_y],
[painter.canvas_max.x, cursor_y],
[1.0, 1.0, 1.0, 0.5],
)
.build();
cursor_y -= painter.font_size;
let text_pos = [content_min.x, cursor_y];
paint_thread_info(&painter, info, stream, text_pos);
painter.canvas_max.y = cursor_y;
let mut paint_stream = || -> Result<()> {
let top_scopes = Reader::from_start(stream).read_top_scopes()?;
if options.merge_scopes {
let merges = puffin::merge_top_scopes(&top_scopes);
for merge in merges {
paint_merge_scope(&painter, options, stream, &merge, 0, &mut cursor_y)?;
}
} else {
for scope in top_scopes {
paint_scope(&painter, options, stream, &scope, 0, &mut cursor_y)?;
}
}
Ok(())
};
if let Err(err) = paint_stream() {
let text = format!("Profiler stream error: {:?}", err);
painter
.draw_list
.add_text([painter.canvas_min.x, cursor_y], ERROR_COLOR, &text);
}
cursor_y -= painter.font_size;
}
}
fn interact(&mut self, painter: &Painter<'_>) {
let ui = &painter.ui;
let pan_button = MouseButton::Left;
self.is_panning |= ui.is_item_hovered() && ui.is_mouse_clicked(pan_button);
self.is_panning &= !ui.is_mouse_released(pan_button);
let pan_delta = if self.is_panning {
let pan_delta = ui.mouse_drag_delta(pan_button);
ui.reset_mouse_drag_delta(pan_button);
pan_delta
} else {
[0.0, 0.0]
};
if self.is_panning {
self.options.sideways_pan_in_pixels += pan_delta[0];
}
if ui.is_item_hovered() {
self.options.sideways_pan_in_pixels +=
ui.io().mouse_wheel_h * self.options.scroll_speed;
let zoom_factor = if self.is_panning {
(-pan_delta[1] * 0.01).exp()
} else {
(-ui.io().mouse_wheel * self.options.scroll_zoom_speed).exp()
};
self.options.pixels_per_ns *= zoom_factor;
let zoom_center = painter.mouse_pos.x - painter.canvas_min.x;
self.options.sideways_pan_in_pixels =
(self.options.sideways_pan_in_pixels - zoom_center) * zoom_factor + zoom_center;
}
}
}
fn paint_timeline(
painter: &Painter<'_>,
options: &Options,
start_ns: NanoSecond,
stop_ns: NanoSecond,
) {
if options.pixels_per_ns <= 0.0 {
return;
}
let canvas_width_ns = painter.canvas_width() / options.pixels_per_ns;
let max_lines = 300.0;
let mut grid_spacing_ns = 1_000;
while canvas_width_ns / (grid_spacing_ns as f32) > max_lines {
grid_spacing_ns *= 10;
}
let num_tiny_lines = canvas_width_ns / (grid_spacing_ns as f32);
let zoom_factor = remap_clamp(num_tiny_lines, (0.1 * max_lines)..=max_lines, 1.0..=0.0);
let zoom_factor = zoom_factor.powi(2);
let big_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.5..=1.0);
let medium_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.1..=0.5);
let tiny_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.0..=0.1);
let mut grid_ns = 0;
loop {
if start_ns + grid_ns > stop_ns {
break;
}
let line_x = painter.pixel_from_ns(options, start_ns + grid_ns);
if line_x > painter.canvas_max.x {
break;
}
if painter.canvas_min.x <= line_x {
let big_line = grid_ns % (grid_spacing_ns * 100) == 0;
let medium_line = grid_ns % (grid_spacing_ns * 10) == 0;
let line_alpha = if big_line {
big_alpha
} else if medium_line {
medium_alpha
} else {
tiny_alpha
};
let line_color = [1.0, 1.0, 1.0, line_alpha];
painter
.draw_list
.add_line(
[line_x, painter.canvas_min.y],
[line_x, painter.canvas_max.y],
line_color,
)
.build();
let text_alpha = if big_line {
medium_alpha
} else if medium_line {
tiny_alpha
} else {
0.0
};
if text_alpha > 0.0 {
let text = grid_text(grid_ns);
let text_x = line_x + 4.0;
let text_color = [1.0, 1.0, 1.0, (text_alpha * 2.0).min(1.0)];
painter
.draw_list
.add_text([text_x, painter.canvas_min.y], text_color, &text);
painter.draw_list.add_text(
[text_x, painter.canvas_max.y - painter.font_size],
text_color,
&text,
);
}
}
grid_ns += grid_spacing_ns;
}
}
fn grid_text(grid_ns: NanoSecond) -> String {
let grid_ms = to_ms(grid_ns);
if grid_ns % 1_000_000 == 0 {
format!("{:.0} ms", grid_ms)
} else if grid_ns % 100_000 == 0 {
format!("{:.1} ms", grid_ms)
} else if grid_ns % 10_000 == 0 {
format!("{:.2} ms", grid_ms)
} else {
format!("{:.3} ms", grid_ms)
}
}
fn paint_record(
painter: &Painter<'_>,
options: &Options,
prefix: &str,
record: &Record<'_>,
top_y: f32,
) -> PaintResult {
let start_x = painter.pixel_from_ns(options, record.start_ns);
let stop_x = painter.pixel_from_ns(options, record.stop_ns());
let width = stop_x - start_x;
if painter.canvas_max.x < start_x || stop_x < painter.canvas_min.x || width < options.cull_width
{
return PaintResult::Culled;
}
let bottom_y = top_y + options.rect_height;
let is_hovered = start_x <= painter.mouse_pos.x
&& painter.mouse_pos.x <= stop_x
&& top_y <= painter.mouse_pos.y
&& painter.mouse_pos.y <= bottom_y;
let rect_min = Vec2::new(start_x, top_y);
let rect_max = Vec2::new(stop_x, bottom_y);
let rect_color = if is_hovered {
[1.0, 0.5, 0.5, 1.0]
} else {
color_from_duration(record.duration_ns)
};
let text_color = [0.1, 0.1, 0.1, 1.0];
painter
.draw_list
.add_rect(rect_min.into(), rect_max.into(), rect_color)
.filled(true)
.rounding(options.rounding)
.build();
let wide_enough_for_text = width > 32.0;
if wide_enough_for_text {
let rect_min = rect_min.max(painter.canvas_min);
let rect_max = rect_max.min(painter.canvas_max);
painter
.draw_list
.with_clip_rect_intersect(rect_min.into(), rect_max.into(), || {
let duration_ms = to_ms(record.duration_ns);
let text = if record.data.is_empty() {
format!("{}{} {:6.3} ms", prefix, record.id, duration_ms)
} else {
format!(
"{}{} {:?} {:6.3} ms",
prefix, record.id, record.data, duration_ms
)
};
painter.draw_list.add_text(
[
start_x + 4.0,
top_y + 0.5 * (options.rect_height - painter.font_size),
],
text_color,
text,
);
});
}
if is_hovered {
PaintResult::Hovered
} else {
PaintResult::Normal
}
}
fn color_from_duration(ns: NanoSecond) -> [f32; 4] {
let ms = to_ms(ns) as f32;
let b = remap_clamp(ms, 0.0..=5.0, 1.0..=0.0);
let r = remap_clamp(ms, 0.0..=10.0, 0.5..=0.8);
let g = remap_clamp(ms, 10.0..=20.0, 0.0..=0.8);
let a = 0.8;
[r, g, b, a]
}
fn to_ms(ns: NanoSecond) -> f64 {
ns as f64 * 1e-6
}
use std::ops::{Add, Mul, RangeInclusive};
fn lerp<T>(range: RangeInclusive<T>, t: f32) -> T
where
f32: Mul<T, Output = T>,
T: Add<T, Output = T> + Copy,
{
(1.0 - t) * *range.start() + t * *range.end()
}
fn remap_clamp(x: f32, from: RangeInclusive<f32>, to: RangeInclusive<f32>) -> f32 {
let t = if x <= *from.start() {
0.0
} else if x >= *from.end() {
1.0
} else {
(x - from.start()) / (from.end() - from.start())
};
lerp(to, t)
}
fn paint_scope(
painter: &Painter<'_>,
options: &Options,
stream: &Stream,
scope: &Scope<'_>,
depth: usize,
min_y: &mut f32,
) -> Result<PaintResult> {
let top_y =
painter.canvas_max.y - (1.0 + depth as f32) * (options.rect_height + options.spacing);
*min_y = min_y.min(top_y);
let result = paint_record(painter, options, "", &scope.record, top_y);
if result != PaintResult::Culled {
let mut num_children = 0;
for child_scope in Reader::with_offset(stream, scope.child_begin_position)? {
paint_scope(painter, options, stream, &child_scope?, depth + 1, min_y)?;
num_children += 1;
}
if result == PaintResult::Hovered {
let ui = painter.ui;
ui.tooltip(|| {
ui.text(&format!("id: {}", scope.record.id));
if !scope.record.location.is_empty() {
ui.text(&format!("location: {}", scope.record.location));
}
if !scope.record.data.is_empty() {
ui.text(&format!("data: {}", scope.record.data));
}
ui.text(&format!(
"duration: {:6.3} ms",
to_ms(scope.record.duration_ns)
));
ui.text(&format!("children: {}", num_children));
});
}
}
Ok(result)
}
fn paint_merge_scope(
painter: &Painter<'_>,
options: &Options,
stream: &Stream,
merge: &MergeScope<'_>,
depth: usize,
min_y: &mut f32,
) -> Result<PaintResult> {
let top_y =
painter.canvas_max.y - (1.0 + depth as f32) * (options.rect_height + options.spacing);
*min_y = min_y.min(top_y);
let prefix = if merge.pieces.len() <= 1 {
String::default()
} else {
format!("{}x ", merge.pieces.len())
};
let result = paint_record(painter, options, &prefix, &merge.record, top_y);
if result != PaintResult::Culled {
for merged_child in merge_children_of_pieces(stream, merge)? {
paint_merge_scope(painter, options, stream, &merged_child, depth + 1, min_y)?;
}
if result == PaintResult::Hovered {
let ui = painter.ui;
ui.tooltip(|| merge_scope_tooltip(ui, merge));
}
}
Ok(result)
}
fn merge_scope_tooltip(ui: &Ui<'_>, merge: &MergeScope<'_>) {
ui.text(&format!("id: {}", merge.record.id));
if !merge.record.location.is_empty() {
ui.text(&format!("location: {}", merge.record.location));
}
if !merge.record.data.is_empty() {
ui.text(&format!("data: {}", merge.record.data));
}
if merge.pieces.len() <= 1 {
ui.text(&format!(
"duration: {:6.3} ms",
to_ms(merge.record.duration_ns)
));
} else {
ui.text(&format!("sum of: {} scopes", merge.pieces.len()));
ui.text(&format!(
"total: {:6.3} ms",
to_ms(merge.record.duration_ns)
));
ui.text(&format!(
"mean: {:6.3} ms",
to_ms(merge.record.duration_ns) / (merge.pieces.len() as f64),
));
let max_duration_ns = merge
.pieces
.iter()
.map(|piece| piece.scope.record.duration_ns)
.max()
.unwrap();
ui.text(&format!("max: {:6.3} ms", to_ms(max_duration_ns)));
}
}
fn paint_thread_info(painter: &Painter<'_>, info: &ThreadInfo, stream: &Stream, pos: [f32; 2]) {
let text = format!(
"{} ({:.1} kiB profiler data)",
info.name,
stream.len() as f32 / 1024.0
);
let text_size = painter.ui.calc_text_size(&ImString::new(&text), false, 0.0);
painter
.draw_list
.add_rect(
pos,
[pos[0] + text_size[0], pos[1] + text_size[1]],
[0.0, 0.0, 0.0, 0.5],
)
.filled(true)
.rounding(0.0)
.build();
painter.draw_list.add_text(pos, [0.9, 0.9, 0.9, 1.0], &text);
}