#![deny(unsafe_code)]
#![warn(
clippy::all,
clippy::await_holding_lock,
clippy::char_lit_as_u8,
clippy::checked_conversions,
clippy::dbg_macro,
clippy::debug_assert_with_mut_call,
clippy::disallowed_methods,
clippy::disallowed_types,
clippy::doc_markdown,
clippy::empty_enum,
clippy::enum_glob_use,
clippy::exit,
clippy::expl_impl_clone_on_copy,
clippy::explicit_deref_methods,
clippy::explicit_into_iter_loop,
clippy::fallible_impl_from,
clippy::filter_map_next,
clippy::flat_map_option,
clippy::float_cmp_const,
clippy::fn_params_excessive_bools,
clippy::from_iter_instead_of_collect,
clippy::if_let_mutex,
clippy::implicit_clone,
clippy::imprecise_flops,
clippy::inefficient_to_string,
clippy::invalid_upcast_comparisons,
clippy::large_digit_groups,
clippy::large_stack_arrays,
clippy::large_types_passed_by_value,
clippy::let_unit_value,
clippy::linkedlist,
clippy::lossy_float_literal,
clippy::macro_use_imports,
clippy::manual_ok_or,
clippy::map_err_ignore,
clippy::map_flatten,
clippy::map_unwrap_or,
clippy::match_on_vec_items,
clippy::match_same_arms,
clippy::match_wild_err_arm,
clippy::match_wildcard_for_single_variants,
clippy::mem_forget,
clippy::mismatched_target_os,
clippy::missing_enforced_import_renames,
clippy::mut_mut,
clippy::mutex_integer,
clippy::needless_borrow,
clippy::needless_continue,
clippy::needless_for_each,
clippy::option_option,
clippy::path_buf_push_overwrite,
clippy::ptr_as_ptr,
clippy::rc_mutex,
clippy::ref_option_ref,
clippy::rest_pat_in_fully_bound_structs,
clippy::same_functions_in_if_condition,
clippy::semicolon_if_nothing_returned,
clippy::single_match_else,
clippy::string_add_assign,
clippy::string_add,
clippy::string_lit_as_bytes,
clippy::string_to_string,
clippy::todo,
clippy::trait_duplication_in_bounds,
clippy::unimplemented,
clippy::unnested_or_patterns,
clippy::unused_self,
clippy::useless_transmute,
clippy::verbose_file_reads,
clippy::zero_sized_map_values,
future_incompatible,
nonstandard_style,
rust_2018_idioms
)]
#![allow(clippy::float_cmp, clippy::manual_range_contains)]
mod filter;
mod flamegraph;
mod maybe_mut_ref;
mod stats;
pub use {egui, maybe_mut_ref::MaybeMutRef, puffin};
use egui::*;
use puffin::*;
use std::{
collections::{BTreeMap, BTreeSet},
fmt::Write as _,
sync::{Arc, Mutex},
};
use time::OffsetDateTime;
const ERROR_COLOR: Color32 = Color32::RED;
const HOVER_COLOR: Rgba = Rgba::from_rgb(0.8, 0.8, 0.8);
pub fn profiler_window(ctx: &egui::Context) -> bool {
puffin::profile_function!();
let mut open = true;
egui::Window::new("Profiler")
.default_size([1024.0, 600.0])
.open(&mut open)
.show(ctx, profiler_ui);
open
}
static PROFILE_UI: once_cell::sync::Lazy<Mutex<GlobalProfilerUi>> =
once_cell::sync::Lazy::new(Default::default);
pub fn profiler_ui(ui: &mut egui::Ui) {
let mut profile_ui = PROFILE_UI.lock().unwrap();
profile_ui.ui(ui);
}
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct GlobalProfilerUi {
#[cfg_attr(feature = "serde", serde(skip))]
global_frame_view: GlobalFrameView,
pub profiler_ui: ProfilerUi,
}
impl GlobalProfilerUi {
pub fn window(&mut self, ctx: &egui::Context) -> bool {
let mut frame_view = self.global_frame_view.lock();
self.profiler_ui
.window(ctx, &mut MaybeMutRef::MutRef(&mut frame_view))
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
let mut frame_view = self.global_frame_view.lock();
self.profiler_ui
.ui(ui, &mut MaybeMutRef::MutRef(&mut frame_view));
}
pub fn global_frame_view(&self) -> &GlobalFrameView {
&self.global_frame_view
}
}
#[derive(Clone)]
pub struct AvailableFrames {
pub recent: Vec<Arc<FrameData>>,
pub slowest: Vec<Arc<FrameData>>,
}
impl AvailableFrames {
fn latest(frame_view: &FrameView) -> Self {
Self {
recent: frame_view.recent_frames().cloned().collect(),
slowest: frame_view.slowest_frames_chronological(),
}
}
fn all_uniq(&self) -> Vec<Arc<FrameData>> {
let mut all = self.slowest.clone();
all.extend(self.recent.iter().cloned());
all.sort_by_key(|frame| frame.frame_index());
all.dedup_by_key(|frame| frame.frame_index());
all
}
}
#[derive(Clone)]
pub struct Streams {
streams: Vec<Arc<StreamInfo>>,
merged_scopes: Vec<MergeScope<'static>>,
max_depth: usize,
}
impl Streams {
fn new(frames: &[Arc<UnpackedFrameData>], thread_info: &ThreadInfo) -> Self {
crate::profile_function!();
let mut streams = vec![];
for frame in frames {
if let Some(stream_info) = frame.thread_streams.get(thread_info) {
streams.push(stream_info.clone());
}
}
let merges = {
puffin::profile_scope!("merge_scopes_for_thread");
puffin::merge_scopes_for_thread(frames, thread_info).unwrap()
};
let merges = merges.into_iter().map(|ms| ms.into_owned()).collect();
let mut max_depth = 0;
for stream_info in &streams {
max_depth = stream_info.depth.max(max_depth);
}
Self {
streams,
merged_scopes: merges,
max_depth,
}
}
}
#[derive(Clone)]
pub struct SelectedFrames {
pub frames: vec1::Vec1<Arc<UnpackedFrameData>>,
pub raw_range_ns: (NanoSecond, NanoSecond),
pub merged_range_ns: (NanoSecond, NanoSecond),
pub threads: BTreeMap<ThreadInfo, Streams>,
}
impl SelectedFrames {
fn try_from_vec(frames: Vec<Arc<UnpackedFrameData>>) -> Option<Self> {
let frames = vec1::Vec1::try_from_vec(frames).ok()?;
Some(Self::from_vec1(frames))
}
fn from_vec1(mut frames: vec1::Vec1<Arc<UnpackedFrameData>>) -> Self {
puffin::profile_function!();
frames.sort_by_key(|f| f.frame_index());
frames.dedup_by_key(|f| f.frame_index());
let mut threads: BTreeSet<ThreadInfo> = BTreeSet::new();
for frame in &frames {
for ti in frame.thread_streams.keys() {
threads.insert(ti.clone());
}
}
let threads: BTreeMap<ThreadInfo, Streams> = threads
.iter()
.map(|ti| (ti.clone(), Streams::new(&frames, ti)))
.collect();
let mut merged_min_ns = NanoSecond::MAX;
let mut merged_max_ns = NanoSecond::MIN;
for stream in threads.values() {
for scope in &stream.merged_scopes {
let scope_start = scope.relative_start_ns;
let scope_end = scope_start + scope.duration_per_frame_ns;
merged_min_ns = merged_min_ns.min(scope_start);
merged_max_ns = merged_max_ns.max(scope_end);
}
}
let raw_range_ns = (frames.first().range_ns().0, frames.last().range_ns().1);
Self {
frames,
raw_range_ns,
merged_range_ns: (merged_min_ns, merged_max_ns),
threads,
}
}
pub fn contains(&self, frame_index: u64) -> bool {
self.frames.iter().any(|f| f.frame_index() == frame_index)
}
}
#[derive(Clone)]
pub struct Paused {
selected: SelectedFrames,
frames: AvailableFrames,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum View {
Flamegraph,
Stats,
}
impl Default for View {
fn default() -> Self {
Self::Flamegraph
}
}
#[derive(Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct ProfilerUi {
#[cfg_attr(feature = "serde", serde(alias = "options"))]
pub flamegraph_options: flamegraph::Options,
#[cfg_attr(feature = "serde", serde(skip))]
pub stats_options: stats::Options,
pub view: View,
#[cfg_attr(feature = "serde", serde(skip))]
paused: Option<Paused>,
slowest_frame: f32,
#[cfg_attr(feature = "serde", serde(skip))]
last_pack_pass: Option<instant::Instant>,
}
impl Default for ProfilerUi {
fn default() -> Self {
Self {
flamegraph_options: Default::default(),
stats_options: Default::default(),
view: Default::default(),
paused: None,
slowest_frame: 0.16,
last_pack_pass: None,
}
}
}
impl ProfilerUi {
pub fn reset(&mut self) {
self.paused = None;
}
pub fn window(
&mut self,
ctx: &egui::Context,
frame_view: &mut MaybeMutRef<'_, FrameView>,
) -> bool {
puffin::profile_function!();
let mut open = true;
egui::Window::new("Profiler")
.default_size([1024.0, 600.0])
.open(&mut open)
.show(ctx, |ui| self.ui(ui, frame_view));
open
}
fn frames(&self, frame_view: &FrameView) -> AvailableFrames {
self.paused.as_ref().map_or_else(
|| AvailableFrames::latest(frame_view),
|paused| paused.frames.clone(),
)
}
fn pause_and_select(&mut self, frame_view: &FrameView, selected: SelectedFrames) {
if let Some(paused) = &mut self.paused {
paused.selected = selected;
} else {
self.paused = Some(Paused {
selected,
frames: self.frames(frame_view),
});
}
}
fn is_selected(&self, frame_view: &FrameView, frame_index: u64) -> bool {
if let Some(paused) = &self.paused {
paused.selected.contains(frame_index)
} else if let Some(latest_frame) = frame_view.latest_frame() {
latest_frame.frame_index() == frame_index
} else {
false
}
}
fn all_known_frames(&self, frame_view: &FrameView) -> Vec<Arc<FrameData>> {
let mut all = frame_view.all_uniq();
if let Some(paused) = &self.paused {
all.append(&mut paused.frames.all_uniq());
}
all.sort_by_key(|frame| frame.frame_index());
all.dedup_by_key(|frame| frame.frame_index());
all
}
fn run_pack_pass_if_needed(&mut self, frame_view: &FrameView) {
if !frame_view.pack_frames() {
return;
}
let last_pack_pass = self
.last_pack_pass
.get_or_insert_with(instant::Instant::now);
let time_since_last_pack = last_pack_pass.elapsed();
if time_since_last_pack > instant::Duration::from_secs(1) {
puffin::profile_scope!("pack_pass");
for frame in self.all_known_frames(frame_view) {
if !self.is_selected(frame_view, frame.frame_index()) {
frame.pack();
}
}
self.last_pack_pass = Some(instant::Instant::now());
}
}
pub fn ui(&mut self, ui: &mut egui::Ui, frame_view: &mut MaybeMutRef<'_, FrameView>) {
#![allow(clippy::collapsible_else_if)]
puffin::profile_function!();
self.run_pack_pass_if_needed(frame_view);
if !puffin::are_scopes_on() {
ui.colored_label(ERROR_COLOR, "The puffin profiler is OFF!")
.on_hover_text("Turn it on with puffin::set_scopes_on(true)");
}
if frame_view.is_empty() {
ui.label("No profiling data");
return;
};
let mut hovered_frame = None;
egui::CollapsingHeader::new("Frames")
.default_open(true)
.show(ui, |ui| {
hovered_frame = self.show_frames(ui, frame_view);
});
let frames = if let Some(frame) = hovered_frame {
match frame.unpacked() {
Ok(frame) => SelectedFrames::try_from_vec(vec![frame]),
Err(err) => {
ui.colored_label(ERROR_COLOR, format!("Failed to load hovered frame: {err}"));
return;
}
}
} else if let Some(paused) = &self.paused {
Some(paused.selected.clone())
} else if let Some(frame) = frame_view.latest_frame() {
match frame.unpacked() {
Ok(frame) => SelectedFrames::try_from_vec(vec![frame]),
Err(err) => {
ui.colored_label(ERROR_COLOR, format!("Failed to load latest frame: {err}"));
return;
}
}
} else {
None
};
let frames = if let Some(frames) = frames {
frames
} else {
ui.label("No profiling data");
return;
};
ui.horizontal(|ui| {
let play_pause_button_size = Vec2::splat(24.0);
if self.paused.is_some() {
if ui
.add_sized(play_pause_button_size, egui::Button::new("▶"))
.on_hover_text("Show latest data. Toggle with space.")
.clicked()
|| ui.input().key_pressed(egui::Key::Space)
{
self.paused = None;
}
} else {
ui.horizontal(|ui| {
if ui
.add_sized(play_pause_button_size, egui::Button::new("⏸"))
.on_hover_text("Pause on this frame. Toggle with space.")
.clicked()
|| ui.input().key_pressed(egui::Key::Space)
{
let latest = frame_view.latest_frame();
if let Some(latest) = latest {
if let Ok(latest) = latest.unpacked() {
self.pause_and_select(
frame_view,
SelectedFrames::from_vec1(vec1::vec1![latest]),
);
}
}
}
});
}
ui.separator();
frames_info_ui(ui, &frames);
});
if self.paused.is_none() {
ui.ctx().request_repaint(); }
ui.separator();
ui.horizontal(|ui| {
ui.selectable_value(&mut self.view, View::Flamegraph, "Flamegraph");
ui.selectable_value(&mut self.view, View::Stats, "Stats");
});
ui.separator();
match self.view {
View::Flamegraph => flamegraph::ui(ui, &mut self.flamegraph_options, &frames),
View::Stats => stats::ui(ui, &mut self.stats_options, &frames.frames),
}
}
fn show_frames(
&mut self,
ui: &mut egui::Ui,
frame_view: &mut MaybeMutRef<'_, FrameView>,
) -> Option<Arc<FrameData>> {
puffin::profile_function!();
let frames = self.frames(frame_view);
let mut hovered_frame = None;
egui::Grid::new("frame_grid").num_columns(2).show(ui, |ui| {
ui.label("");
ui.label("Click to select a frame, or drag to select multiple frames.");
ui.end_row();
ui.label("Recent:");
Frame::dark_canvas(ui.style()).show(ui, |ui| {
egui::ScrollArea::horizontal()
.stick_to_right(true)
.show(ui, |ui| {
let slowest_visible = self.show_frame_list(
ui,
frame_view,
&frames.recent,
false,
&mut hovered_frame,
self.slowest_frame,
);
self.slowest_frame = lerp(self.slowest_frame..=slowest_visible as f32, 0.2);
});
});
ui.end_row();
ui.vertical(|ui| {
ui.style_mut().wrap = Some(false);
ui.add_space(16.0); ui.label("Slowest:");
if let Some(frame_view) = frame_view.as_mut() {
if ui.button("Clear").clicked() {
frame_view.clear_slowest();
}
}
});
Frame::dark_canvas(ui.style()).show(ui, |ui| {
let num_fit = (ui.available_size_before_wrap().x
/ self.flamegraph_options.frame_width)
.floor();
let num_fit = (num_fit as usize).at_least(1).at_most(frames.slowest.len());
let slowest_of_the_slow = puffin::select_slowest(&frames.slowest, num_fit);
let mut slowest_frame = 0;
for frame in &slowest_of_the_slow {
slowest_frame = frame.duration_ns().max(slowest_frame);
}
self.show_frame_list(
ui,
frame_view,
&slowest_of_the_slow,
true,
&mut hovered_frame,
slowest_frame as f32,
);
});
});
{
let uniq = frames.all_uniq();
let mut bytes = 0;
let mut unpacked = 0;
for frame in &uniq {
bytes += frame.bytes_of_ram_used();
unpacked += frame.has_unpacked() as usize;
}
ui.label(format!(
"{} frames ({} unpacked) using approximately {:.1} MB.",
uniq.len(),
unpacked,
bytes as f64 * 1e-6
));
if let Some(frame_view) = frame_view.as_mut() {
max_frames_ui(ui, frame_view);
}
}
hovered_frame
}
fn show_frame_list(
&mut self,
ui: &mut egui::Ui,
frame_view: &FrameView,
frames: &[Arc<FrameData>],
tight: bool,
hovered_frame: &mut Option<Arc<FrameData>>,
slowest_frame: f32,
) -> NanoSecond {
let frame_width_including_spacing = self.flamegraph_options.frame_width;
let desired_width = if tight {
frames.len() as f32 * frame_width_including_spacing
} else {
let num_frames = frames[frames.len() - 1].frame_index() + 1 - frames[0].frame_index();
num_frames as f32 * frame_width_including_spacing
};
let desired_size = Vec2::new(desired_width, self.flamegraph_options.frame_list_height);
let (response, painter) = ui.allocate_painter(desired_size, Sense::click_and_drag());
let rect = response.rect;
let frame_spacing = 2.0;
let frame_width = frame_width_including_spacing - frame_spacing;
let viewing_multiple_frames = if let Some(paused) = &self.paused {
paused.selected.frames.len() > 1 && !self.flamegraph_options.merge_scopes
} else {
false
};
let mut new_selection = vec![];
let mut slowest_visible_frame = 0;
for (i, frame) in frames.iter().enumerate() {
let x = if tight {
rect.right() - (frames.len() as f32 - i as f32) * frame_width_including_spacing
} else {
let latest_frame_index = frames[frames.len() - 1].frame_index();
rect.right()
- (latest_frame_index + 1 - frame.frame_index()) as f32
* frame_width_including_spacing
};
let frame_rect = Rect::from_min_max(
Pos2::new(x, rect.top()),
Pos2::new(x + frame_width, rect.bottom()),
);
if ui.clip_rect().intersects(frame_rect) {
let duration = frame.duration_ns();
slowest_visible_frame = duration.max(slowest_visible_frame);
let is_selected = self.is_selected(frame_view, frame.frame_index());
let is_hovered = if let Some(mouse_pos) = response.hover_pos() {
response.hovered()
&& !response.dragged()
&& frame_rect
.expand2(vec2(0.5 * frame_spacing, 0.0))
.contains(mouse_pos)
} else {
false
};
if is_hovered && !is_selected && !viewing_multiple_frames {
*hovered_frame = Some(frame.clone());
egui::show_tooltip_at_pointer(
ui.ctx(),
Id::new("puffin_frame_tooltip"),
|ui| {
ui.label(format!("{:.1} ms", frame.duration_ns() as f64 * 1e-6));
},
);
}
if response.dragged() {
if let (Some(start), Some(curr)) = (
ui.input().pointer.press_origin(),
ui.input().pointer.interact_pos(),
) {
let min_x = start.x.min(curr.x);
let max_x = start.x.max(curr.x);
let intersects = min_x <= frame_rect.right() && frame_rect.left() <= max_x;
if intersects {
if let Ok(frame) = frame.unpacked() {
new_selection.push(frame);
}
}
}
}
let color = if is_selected {
Rgba::WHITE
} else if is_hovered {
HOVER_COLOR
} else {
Rgba::from_rgb(0.6, 0.6, 0.4)
};
let alpha = if is_selected || is_hovered { 0.6 } else { 0.25 };
painter.rect_filled(frame_rect, 0.0, color * alpha);
let mut short_rect = frame_rect;
short_rect.min.y = lerp(
frame_rect.bottom_up_range(),
duration as f32 / slowest_frame,
);
painter.rect_filled(short_rect, 0.0, color);
}
}
if let Some(new_selection) = SelectedFrames::try_from_vec(new_selection) {
self.pause_and_select(frame_view, new_selection);
}
slowest_visible_frame
}
}
fn frames_info_ui(ui: &mut egui::Ui, selection: &SelectedFrames) {
let mut sum_ns = 0;
let mut sum_scopes = 0;
for frame in &selection.frames {
let (min_ns, max_ns) = frame.range_ns();
sum_ns += max_ns - min_ns;
sum_scopes += frame.meta.num_scopes;
}
let frame_indices = if selection.frames.len() == 1 {
format!("frame #{}", selection.frames[0].frame_index())
} else if selection.frames.len() as u64
== selection.frames.last().frame_index() - selection.frames.first().frame_index() + 1
{
format!(
"{} frames (#{} - #{})",
selection.frames.len(),
selection.frames.first().frame_index(),
selection.frames.last().frame_index()
)
} else {
format!("{} frames", selection.frames.len())
};
let mut info = format!(
"Showing {}, {:.1} ms, {} threads, {} scopes.",
frame_indices,
sum_ns as f64 * 1e-6,
selection.threads.len(),
sum_scopes,
);
if let Some(time) = format_time(selection.raw_range_ns.0) {
let _ = write!(&mut info, " Recorded {time}.");
}
ui.label(info);
}
fn format_time(nanos: NanoSecond) -> Option<String> {
let years_since_epoch = nanos / 1_000_000_000 / 60 / 60 / 24 / 365;
if 50 <= years_since_epoch && years_since_epoch <= 150 {
let offset = OffsetDateTime::from_unix_timestamp_nanos(nanos as i128).ok()?;
let format_desc = time::macros::format_description!(
"[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]"
);
let datetime = offset.format(&format_desc).ok()?;
Some(datetime)
} else {
None }
}
fn max_frames_ui(ui: &mut egui::Ui, frame_view: &mut FrameView) {
let uniq = frame_view.all_uniq();
let mut bytes = 0;
for frame in &uniq {
bytes += frame.bytes_of_ram_used();
}
let frames_per_second = if let (Some(first), Some(last)) = (uniq.first(), uniq.last()) {
let nanos = last.range_ns().1 - first.range_ns().0;
let seconds = nanos as f64 * 1e-9;
let frames = last.frame_index() - first.frame_index() + 1;
frames as f64 / seconds
} else {
60.0
};
ui.horizontal(|ui| {
ui.label("Max recent frames to store:");
let mut memory_length = frame_view.max_recent();
ui.add(egui::Slider::new(&mut memory_length, 10..=100_000).logarithmic(true));
frame_view.set_max_recent(memory_length);
ui.label(format!(
"(≈ {:.1} minutes, ≈ {:.0} MB)",
memory_length as f64 / 60.0 / frames_per_second,
memory_length as f64 * bytes as f64 / uniq.len() as f64 * 1e-6,
));
});
}