Skip to main content

puffin_egui/
lib.rs

1//! Bindings for showing [`puffin`] profile scopes in [`egui`].
2//!
3//! Usage:
4//! ```
5//! # let mut egui_ctx = egui::Context::default();
6//! # egui_ctx.begin_frame(Default::default());
7//! puffin_egui::profiler_window(&egui_ctx);
8//! ```
9
10#![forbid(unsafe_code)]
11// crate-specific exceptions:
12#![allow(clippy::float_cmp, clippy::manual_range_contains)]
13
14mod filter;
15mod flamegraph;
16mod maybe_mut_ref;
17mod stats;
18
19pub use {egui, maybe_mut_ref::MaybeMutRef, puffin};
20
21use egui::{scroll_area::ScrollSource, *};
22use puffin::*;
23use std::{
24    collections::{BTreeMap, BTreeSet},
25    fmt::Write as _,
26    iter,
27    sync::Arc,
28};
29use time::OffsetDateTime;
30
31const ERROR_COLOR: Color32 = Color32::RED;
32const HOVER_COLOR: Rgba = Rgba::from_rgb(0.8, 0.8, 0.8);
33
34// ----------------------------------------------------------------------------
35
36/// Show the puffin profiler if [`puffin::are_scopes_on`] is true,
37/// i.e. if profiling is enabled for your app.
38///
39/// The profiler will be shown in its own viewport (native window)
40/// if the egui backend supports it (e.g. when using `eframe`);
41/// else it will be shown in a floating [`egui::Window`].
42///
43/// Closing the viewport or window will call `puffin::set_scopes_on(false)`.
44pub fn show_viewport_if_enabled(ctx: &egui::Context) {
45    if !puffin::are_scopes_on() {
46        return;
47    }
48
49    ctx.show_viewport_deferred(
50        egui::ViewportId::from_hash_of("puffin_profiler"),
51        egui::ViewportBuilder::default().with_title("Puffin Profiler"),
52        move |ctx, class| {
53            if class == egui::ViewportClass::Embedded {
54                // Viewports not supported. Show it as a floating egui window instead.
55                let mut open = true;
56                egui::Window::new("Puffin Profiler")
57                    .default_size([1024.0, 600.0])
58                    .open(&mut open)
59                    .show(ctx, profiler_ui);
60                puffin::set_scopes_on(open);
61            } else {
62                // A proper viewport!
63                egui::CentralPanel::default().show(ctx, profiler_ui);
64                if ctx.input(|i| i.viewport().close_requested()) {
65                    puffin::set_scopes_on(false);
66                }
67            }
68        },
69    );
70}
71
72/// Show an [`egui::Window`] with the profiler contents.
73///
74/// If you want to control the window yourself, use [`profiler_ui`] instead.
75///
76/// Returns `false` if the user closed the profile window.
77pub fn profiler_window(ctx: &egui::Context) -> bool {
78    puffin::profile_function!();
79    let mut open = true;
80    egui::Window::new("Profiler")
81        .default_size([1024.0, 600.0])
82        .open(&mut open)
83        .show(ctx, profiler_ui);
84    open
85}
86
87static PROFILE_UI: std::sync::LazyLock<parking_lot::Mutex<GlobalProfilerUi>> =
88    std::sync::LazyLock::new(Default::default);
89
90/// Show the profiler.
91///
92/// Call this from within an [`egui::Window`], or use [`profiler_window`] instead.
93pub fn profiler_ui(ui: &mut egui::Ui) {
94    let mut profile_ui = PROFILE_UI.lock();
95
96    profile_ui.ui(ui);
97}
98
99// ----------------------------------------------------------------------------
100
101/// Show [`puffin::GlobalProfiler`], i.e. profile the app we are running in.
102#[derive(Default)]
103#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
104#[cfg_attr(feature = "serde", serde(default))]
105pub struct GlobalProfilerUi {
106    #[cfg_attr(feature = "serde", serde(skip))]
107    global_frame_view: GlobalFrameView,
108
109    pub profiler_ui: ProfilerUi,
110}
111
112impl GlobalProfilerUi {
113    /// Show an [`egui::Window`] with the profiler contents.
114    ///
115    /// If you want to control the window yourself, use [`Self::ui`] instead.
116    ///
117    /// Returns `false` if the user closed the profile window.
118    pub fn window(&mut self, ctx: &egui::Context) -> bool {
119        let mut frame_view = self.global_frame_view.lock();
120        self.profiler_ui
121            .window(ctx, &mut MaybeMutRef::MutRef(&mut frame_view))
122    }
123
124    /// Show the profiler.
125    ///
126    /// Call this from within an [`egui::Window`], or use [`Self::window`] instead.
127    pub fn ui(&mut self, ui: &mut egui::Ui) {
128        let mut frame_view = self.global_frame_view.lock();
129        self.profiler_ui
130            .ui(ui, &mut MaybeMutRef::MutRef(&mut frame_view));
131    }
132
133    /// The frames we are looking at.
134    pub fn global_frame_view(&self) -> &GlobalFrameView {
135        &self.global_frame_view
136    }
137}
138
139// ----------------------------------------------------------------------------
140
141/// The frames we can chose between when selecting what frame(s) to view.
142#[derive(Clone)]
143pub struct AvailableFrames {
144    pub recent: Vec<Arc<FrameData>>,
145    pub slowest: Vec<Arc<FrameData>>,
146    pub uniq: Vec<Arc<FrameData>>,
147    pub stats: FrameStats,
148}
149
150impl AvailableFrames {
151    fn latest(frame_view: &FrameView) -> Self {
152        Self {
153            recent: frame_view.recent_frames().cloned().collect(),
154            slowest: frame_view.slowest_frames_chronological().cloned().collect(),
155            uniq: frame_view.all_uniq().cloned().collect(),
156            stats: Default::default(),
157        }
158    }
159}
160
161/// Multiple streams for one thread.
162#[derive(Clone)]
163pub struct Streams {
164    streams: Vec<Arc<StreamInfo>>,
165    merged_scopes: Vec<MergeScope<'static>>,
166    max_depth: usize,
167}
168
169impl Streams {
170    fn new(
171        scope_collection: &ScopeCollection,
172        frames: &[Arc<UnpackedFrameData>],
173        thread_info: &ThreadInfo,
174    ) -> Self {
175        crate::profile_function!();
176
177        let mut streams = vec![];
178        for frame in frames {
179            if let Some(stream_info) = frame.thread_streams.get(thread_info) {
180                streams.push(stream_info.clone());
181            }
182        }
183
184        let merges = {
185            puffin::profile_scope!("merge_scopes_for_thread");
186            puffin::merge_scopes_for_thread(scope_collection, frames, thread_info).unwrap()
187        };
188        let merges = merges.into_iter().map(|ms| ms.into_owned()).collect();
189
190        let mut max_depth = 0;
191        for stream_info in &streams {
192            max_depth = stream_info.depth.max(max_depth);
193        }
194
195        Self {
196            streams,
197            merged_scopes: merges,
198            max_depth,
199        }
200    }
201}
202
203/// Selected frames ready to be viewed.
204/// Never empty.
205#[derive(Clone)]
206pub struct SelectedFrames {
207    /// ordered, but not necessarily in sequence
208    pub frames: vec1::Vec1<Arc<UnpackedFrameData>>,
209    pub raw_range_ns: (NanoSecond, NanoSecond),
210    pub merged_range_ns: (NanoSecond, NanoSecond),
211    pub threads: BTreeMap<ThreadInfo, Streams>,
212}
213
214impl SelectedFrames {
215    fn try_from_iter(
216        scope_collection: &ScopeCollection,
217        frames: impl Iterator<Item = Arc<UnpackedFrameData>>,
218    ) -> Option<Self> {
219        let mut it = frames;
220        let first = it.next()?;
221        let mut frames = vec1::Vec1::new(first);
222        frames.extend(it);
223
224        Some(Self::from_vec1(scope_collection, frames))
225    }
226
227    fn from_vec1(
228        scope_collection: &ScopeCollection,
229        mut frames: vec1::Vec1<Arc<UnpackedFrameData>>,
230    ) -> Self {
231        puffin::profile_function!();
232        frames.sort_by_key(|f| f.frame_index());
233        frames.dedup_by_key(|f| f.frame_index());
234
235        let mut threads: BTreeSet<ThreadInfo> = BTreeSet::new();
236        for frame in &frames {
237            for ti in frame.thread_streams.keys() {
238                threads.insert(ti.clone());
239            }
240        }
241
242        let threads: BTreeMap<ThreadInfo, Streams> = threads
243            .iter()
244            .map(|ti| (ti.clone(), Streams::new(scope_collection, &frames, ti)))
245            .collect();
246
247        let mut merged_min_ns = NanoSecond::MAX;
248        let mut merged_max_ns = NanoSecond::MIN;
249        for stream in threads.values() {
250            for scope in &stream.merged_scopes {
251                let scope_start = scope.relative_start_ns;
252                let scope_end = scope_start + scope.duration_per_frame_ns;
253                merged_min_ns = merged_min_ns.min(scope_start);
254                merged_max_ns = merged_max_ns.max(scope_end);
255            }
256        }
257
258        let raw_range_ns = (frames.first().range_ns().0, frames.last().range_ns().1);
259
260        Self {
261            frames,
262            raw_range_ns,
263            merged_range_ns: (merged_min_ns, merged_max_ns),
264            threads,
265        }
266    }
267
268    pub fn contains(&self, frame_index: u64) -> bool {
269        self.frames.iter().any(|f| f.frame_index() == frame_index)
270    }
271}
272
273#[derive(Clone)]
274pub struct Paused {
275    /// What we are viewing
276    selected: SelectedFrames,
277    frames: AvailableFrames,
278}
279
280#[derive(Copy, Clone, Debug, PartialEq, Eq)]
281#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
282#[derive(Default)]
283pub enum View {
284    #[default]
285    Flamegraph,
286    Stats,
287}
288
289/// Contains settings for the profiler.
290#[derive(Clone)]
291#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
292#[cfg_attr(feature = "serde", serde(default))]
293pub struct ProfilerUi {
294    /// Options for configuring how the flamegraph is displayed.
295    #[cfg_attr(feature = "serde", serde(alias = "options"))]
296    pub flamegraph_options: flamegraph::Options,
297    /// Options for configuring how the stats page is displayed.
298    #[cfg_attr(feature = "serde", serde(skip))]
299    pub stats_options: stats::Options,
300
301    /// What view is active.
302    pub view: View,
303
304    /// If `None`, we show the latest frames.
305    #[cfg_attr(feature = "serde", serde(skip))]
306    paused: Option<Paused>,
307
308    /// How many frames should be used for latest view
309    max_num_latest: usize,
310
311    /// Used to normalize frame height in frame view
312    slowest_frame: f32,
313
314    /// When did we last run a pass to pack all the frames?
315    #[cfg_attr(feature = "serde", serde(skip))]
316    last_pack_pass: Option<web_time::Instant>,
317
318    /// Order to sort scopes in table view
319    sort_order: stats::SortOrder,
320}
321
322impl Default for ProfilerUi {
323    fn default() -> Self {
324        Self {
325            flamegraph_options: Default::default(),
326            stats_options: Default::default(),
327            view: Default::default(),
328            paused: None,
329            max_num_latest: 1,
330            slowest_frame: 0.16,
331            last_pack_pass: None,
332            sort_order: stats::SortOrder {
333                key: stats::SortKey::Count,
334                rev: true,
335            },
336        }
337    }
338}
339
340impl ProfilerUi {
341    pub fn reset(&mut self) {
342        self.paused = None;
343    }
344
345    /// Show an [`egui::Window`] with the profiler contents.
346    ///
347    /// If you want to control the window yourself, use [`Self::ui`] instead.
348    ///
349    /// Returns `false` if the user closed the profile window.
350    pub fn window(
351        &mut self,
352        ctx: &egui::Context,
353        frame_view: &mut MaybeMutRef<'_, FrameView>,
354    ) -> bool {
355        puffin::profile_function!();
356        let mut open = true;
357        egui::Window::new("Profiler")
358            .default_size([1024.0, 600.0])
359            .open(&mut open)
360            .show(ctx, |ui| self.ui(ui, frame_view));
361        open
362    }
363
364    /// The frames we can select between
365    fn frames(&self, frame_view: &FrameView) -> AvailableFrames {
366        self.paused.as_ref().map_or_else(
367            || {
368                let mut frames = AvailableFrames::latest(frame_view);
369                frames.stats = frame_view.stats();
370                frames
371            },
372            |paused| {
373                let mut frames = paused.frames.clone();
374                frames.stats = FrameStats::from_frames(paused.frames.uniq.iter().map(Arc::as_ref));
375                frames
376            },
377        )
378    }
379
380    /// Pause on the specific frame
381    fn pause_and_select(&mut self, frame_view: &FrameView, selected: SelectedFrames) {
382        if let Some(paused) = &mut self.paused {
383            paused.selected = selected;
384        } else {
385            self.paused = Some(Paused {
386                selected,
387                frames: self.frames(frame_view),
388            });
389        }
390    }
391
392    fn is_selected(&self, frame_view: &FrameView, frame_index: u64) -> bool {
393        if let Some(paused) = &self.paused {
394            paused.selected.contains(frame_index)
395        } else if let Some(latest_frame) = frame_view.latest_frame() {
396            latest_frame.frame_index() == frame_index
397        } else {
398            false
399        }
400    }
401
402    fn all_known_frames<'a>(
403        &'a self,
404        frame_view: &'a FrameView,
405    ) -> Box<dyn Iterator<Item = &'a Arc<FrameData>> + 'a> {
406        match &self.paused {
407            Some(paused) => Box::new(frame_view.all_uniq().chain(paused.frames.uniq.iter())),
408            None => Box::new(frame_view.all_uniq()),
409        }
410    }
411
412    fn run_pack_pass_if_needed(&mut self, frame_view: &FrameView) {
413        if !frame_view.pack_frames() {
414            return;
415        }
416        let last_pack_pass = self
417            .last_pack_pass
418            .get_or_insert_with(web_time::Instant::now);
419        let time_since_last_pack = last_pack_pass.elapsed();
420        if time_since_last_pack > web_time::Duration::from_secs(1) {
421            puffin::profile_scope!("pack_pass");
422            for frame in self.all_known_frames(frame_view) {
423                if !self.is_selected(frame_view, frame.frame_index()) {
424                    frame.pack();
425                }
426            }
427            self.last_pack_pass = Some(web_time::Instant::now());
428        }
429    }
430
431    /// Show the profiler.
432    ///
433    /// Call this from within an [`egui::Window`], or use [`Self::window`] instead.
434    pub fn ui(&mut self, ui: &mut egui::Ui, frame_view: &mut MaybeMutRef<'_, FrameView>) {
435        #![allow(clippy::collapsible_else_if)]
436        puffin::profile_function!();
437
438        self.run_pack_pass_if_needed(frame_view);
439
440        if !puffin::are_scopes_on() {
441            ui.colored_label(ERROR_COLOR, "The puffin profiler is OFF!")
442                .on_hover_text("Turn it on with puffin::set_scopes_on(true)");
443        }
444
445        if frame_view.is_empty() {
446            ui.label("No profiling data");
447            return;
448        };
449
450        ui.scope(|ui| {
451            ui.spacing_mut().item_spacing.y = 6.0;
452            self.ui_impl(ui, frame_view);
453        });
454    }
455
456    fn ui_impl(&mut self, ui: &mut egui::Ui, frame_view: &mut MaybeMutRef<'_, FrameView>) {
457        let mut hovered_frame = None;
458
459        egui::CollapsingHeader::new("Frame history")
460            .default_open(false)
461            .show(ui, |ui| {
462                hovered_frame = self.show_frames(ui, frame_view);
463            });
464
465        let frames = if let Some(frame) = hovered_frame {
466            match frame.unpacked() {
467                Ok(frame) => {
468                    SelectedFrames::try_from_iter(frame_view.scope_collection(), iter::once(frame))
469                }
470                Err(err) => {
471                    ui.colored_label(ERROR_COLOR, format!("Failed to load hovered frame: {err}"));
472                    return;
473                }
474            }
475        } else if let Some(paused) = &self.paused {
476            Some(paused.selected.clone())
477        } else {
478            puffin::profile_scope!("select_latest_frames");
479            let latest = frame_view
480                .latest_frames(self.max_num_latest)
481                .map(|frame| frame.unpacked())
482                .filter_map(|unpacked| unpacked.ok());
483
484            SelectedFrames::try_from_iter(frame_view.scope_collection(), latest)
485        };
486
487        let frames = if let Some(frames) = frames {
488            frames
489        } else {
490            ui.label("No profiling data");
491            return;
492        };
493
494        ui.horizontal(|ui| {
495            let play_pause_button_size = Vec2::splat(24.0);
496            let space_pressed = ui.input(|i| i.key_pressed(egui::Key::Space))
497                && ui.memory(|m| m.focused().is_none());
498
499            if self.paused.is_some() {
500                if ui
501                    .add_sized(play_pause_button_size, egui::Button::new("▶"))
502                    .on_hover_text("Show latest data. Toggle with space.")
503                    .clicked()
504                    || space_pressed
505                {
506                    self.paused = None;
507                }
508            } else {
509                ui.horizontal(|ui| {
510                    if ui
511                        .add_sized(play_pause_button_size, egui::Button::new("⏸"))
512                        .on_hover_text("Pause on this frame. Toggle with space.")
513                        .clicked()
514                        || space_pressed
515                    {
516                        let latest = frame_view.latest_frame();
517                        if let Some(latest) = latest
518                            && let Ok(latest) = latest.unpacked()
519                        {
520                            self.pause_and_select(
521                                frame_view,
522                                SelectedFrames::from_vec1(
523                                    frame_view.scope_collection(),
524                                    vec1::vec1![latest],
525                                ),
526                            );
527                        }
528                    }
529                });
530            }
531
532            frames_info_ui(ui, &frames);
533        });
534
535        if frames.frames.len() == 1 {
536            let frame = frames.frames.first();
537
538            let num_scopes = frame.meta.num_scopes;
539            let realistic_ns_overhead = 200.0; // Micro-benchmarks puts it at 50ns, but real-life tests show it's much higher.
540            let overhead_ms = num_scopes as f64 * 1.0e-6 * realistic_ns_overhead;
541            if overhead_ms > 1.0 {
542                let overhead = if overhead_ms < 2.0 {
543                    format!("{overhead_ms:.1} ms")
544                } else {
545                    format!("{overhead_ms:.0} ms")
546                };
547
548                let text = format!(
549                    "There are {num_scopes} scopes in this frame, which adds around ~{overhead} of overhead.\n\
550                    Use the Table view to find which scopes are triggered often, and either remove them or replace them with profile_function_if!()"
551                );
552
553                ui.label(egui::RichText::new(text).color(ui.visuals().warn_fg_color));
554            }
555        }
556
557        if self.paused.is_none() {
558            ui.ctx().request_repaint(); // keep refreshing to see latest data
559        }
560
561        ui.horizontal(|ui| {
562            ui.label("View:");
563            ui.selectable_value(&mut self.view, View::Flamegraph, "Flamegraph");
564            ui.selectable_value(&mut self.view, View::Stats, "Table");
565        });
566
567        match self.view {
568            View::Flamegraph => flamegraph::ui(
569                ui,
570                &mut self.flamegraph_options,
571                frame_view.scope_collection(),
572                &frames,
573            ),
574            View::Stats => stats::ui(
575                ui,
576                &mut self.stats_options,
577                frame_view.scope_collection(),
578                &frames.frames,
579                &mut self.sort_order,
580            ),
581        }
582    }
583
584    /// Returns hovered, if any
585    fn show_frames(
586        &mut self,
587        ui: &mut egui::Ui,
588        frame_view: &mut MaybeMutRef<'_, FrameView>,
589    ) -> Option<Arc<FrameData>> {
590        puffin::profile_function!();
591
592        let frames = self.frames(frame_view);
593
594        let mut hovered_frame = None;
595
596        egui::Grid::new("frame_grid").num_columns(2).show(ui, |ui| {
597            ui.label("");
598            ui.horizontal(|ui| {
599                ui.label("Click to select a frame, or drag to select multiple frames.");
600
601                ui.menu_button("🔧 Settings", |ui| {
602                    let uniq = &frames.uniq;
603                    let stats = &frames.stats;
604
605                    ui.label(format!(
606                        "{} frames ({} unpacked) using approximately {:.1} MB.",
607                        stats.frames(),
608                        stats.unpacked_frames(),
609                        stats.bytes_of_ram_used() as f64 * 1e-6
610                    ));
611
612                    if let Some(frame_view) = frame_view.as_mut() {
613                        max_frames_ui(ui, frame_view, uniq);
614                        if self.paused.is_none() {
615                            max_num_latest_ui(ui, &mut self.max_num_latest);
616                        }
617                    }
618                });
619            });
620            ui.end_row();
621
622            ui.label("Recent:");
623
624            Frame::dark_canvas(ui.style()).show(ui, |ui| {
625                egui::ScrollArea::horizontal()
626                    .stick_to_right(true)
627                    .scroll_source(ScrollSource::SCROLL_BAR | ScrollSource::MOUSE_WHEEL)
628                    .show(ui, |ui| {
629                        let slowest_visible = self.show_frame_list(
630                            ui,
631                            frame_view,
632                            &frames.recent,
633                            false,
634                            &mut hovered_frame,
635                            self.slowest_frame,
636                        );
637                        // quickly, but smoothly, normalize frame height:
638                        self.slowest_frame = lerp(self.slowest_frame..=slowest_visible as f32, 0.2);
639                    });
640            });
641
642            ui.end_row();
643
644            ui.vertical(|ui| {
645                ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
646                ui.add_space(16.0); // make it a bit more centered
647                ui.label("Slowest:");
648                if let Some(frame_view) = frame_view.as_mut()
649                    && ui.button("Clear").clicked()
650                {
651                    frame_view.clear_slowest();
652                }
653            });
654
655            // Show as many slow frames as we fit in the view:
656            Frame::dark_canvas(ui.style()).show(ui, |ui| {
657                let num_fit = (ui.available_size_before_wrap().x
658                    / self.flamegraph_options.frame_width)
659                    .floor();
660                let num_fit = (num_fit as usize).at_least(1).at_most(frames.slowest.len());
661                let slowest_of_the_slow = puffin::select_slowest(&frames.slowest, num_fit);
662
663                let mut slowest_frame = 0;
664                for frame in &slowest_of_the_slow {
665                    slowest_frame = frame.duration_ns().max(slowest_frame);
666                }
667
668                self.show_frame_list(
669                    ui,
670                    frame_view,
671                    &slowest_of_the_slow,
672                    true,
673                    &mut hovered_frame,
674                    slowest_frame as f32,
675                );
676            });
677        });
678
679        hovered_frame
680    }
681
682    /// Returns the slowest visible frame
683    fn show_frame_list(
684        &mut self,
685        ui: &mut egui::Ui,
686        frame_view: &FrameView,
687        frames: &[Arc<FrameData>],
688        tight: bool,
689        hovered_frame: &mut Option<Arc<FrameData>>,
690        slowest_frame: f32,
691    ) -> NanoSecond {
692        let frame_width_including_spacing = self.flamegraph_options.frame_width;
693
694        let desired_width = if tight {
695            frames.len() as f32 * frame_width_including_spacing
696        } else {
697            // leave gaps in the view for the missing frames
698            let num_frames = frames[frames.len() - 1].frame_index() + 1 - frames[0].frame_index();
699            num_frames as f32 * frame_width_including_spacing
700        };
701
702        let desired_size = Vec2::new(desired_width, self.flamegraph_options.frame_list_height);
703        let (response, painter) = ui.allocate_painter(desired_size, Sense::drag());
704        let rect = response.rect;
705
706        let frame_spacing = 2.0;
707        let frame_width = frame_width_including_spacing - frame_spacing;
708
709        let viewing_multiple_frames = if let Some(paused) = &self.paused {
710            paused.selected.frames.len() > 1 && !self.flamegraph_options.merge_scopes
711        } else {
712            false
713        };
714
715        let mut new_selection = vec![];
716        let mut slowest_visible_frame = 0;
717
718        for (i, frame) in frames.iter().enumerate() {
719            let x = if tight {
720                rect.right() - (frames.len() as f32 - i as f32) * frame_width_including_spacing
721            } else {
722                let latest_frame_index = frames[frames.len() - 1].frame_index();
723                rect.right()
724                    - (latest_frame_index + 1 - frame.frame_index()) as f32
725                        * frame_width_including_spacing
726            };
727
728            let frame_rect = Rect::from_min_max(
729                Pos2::new(x, rect.top()),
730                Pos2::new(x + frame_width, rect.bottom()),
731            )
732            .expand2(vec2(0.5 * frame_spacing, 0.0));
733
734            if ui.clip_rect().intersects(frame_rect) {
735                let duration = frame.duration_ns();
736                slowest_visible_frame = duration.max(slowest_visible_frame);
737
738                let is_selected = self.is_selected(frame_view, frame.frame_index());
739
740                let is_hovered = if let Some(mouse_pos) = response.hover_pos() {
741                    !response.dragged() && frame_rect.contains(mouse_pos)
742                } else {
743                    false
744                };
745
746                // preview when hovering is really annoying when viewing multiple frames
747                if is_hovered && !is_selected && !viewing_multiple_frames {
748                    *hovered_frame = Some(frame.clone());
749                    Tooltip::always_open(
750                        ui.ctx().clone(),
751                        ui.layer_id(),
752                        Id::new("puffin_frame_tooltip"),
753                        PopupAnchor::Pointer,
754                    )
755                    .show(|ui| {
756                        ui.label(format!("{:.1} ms", frame.duration_ns() as f64 * 1e-6));
757                    });
758                }
759
760                if response.dragged()
761                    && let (Some(start), Some(curr)) =
762                        ui.input(|i| (i.pointer.press_origin(), i.pointer.interact_pos()))
763                {
764                    let min_x = start.x.min(curr.x);
765                    let max_x = start.x.max(curr.x);
766                    let intersects = min_x <= frame_rect.right() && frame_rect.left() <= max_x;
767                    if intersects && let Ok(frame) = frame.unpacked() {
768                        new_selection.push(frame);
769                    }
770                }
771
772                let color = if is_selected {
773                    Rgba::WHITE
774                } else if is_hovered {
775                    HOVER_COLOR
776                } else {
777                    Rgba::from_rgb(0.6, 0.6, 0.4)
778                };
779
780                // Shrink the rect as the visual representation of the frame rect includes empty
781                // space between each bar
782                let visual_rect = frame_rect.expand2(vec2(-0.5 * frame_spacing, 0.0));
783
784                // Transparent, full height:
785                let alpha: f32 = if is_selected || is_hovered { 0.6 } else { 0.25 };
786                painter.rect_filled(visual_rect, 0.0, color * alpha);
787
788                // Opaque, height based on duration:
789                let mut short_rect = visual_rect;
790                short_rect.min.y = lerp(
791                    visual_rect.bottom_up_range(),
792                    duration as f32 / slowest_frame,
793                );
794                painter.rect_filled(short_rect, 0.0, color);
795            }
796        }
797
798        if let Some(new_selection) =
799            SelectedFrames::try_from_iter(frame_view.scope_collection(), new_selection.into_iter())
800        {
801            self.pause_and_select(frame_view, new_selection);
802        }
803
804        slowest_visible_frame
805    }
806}
807
808fn frames_info_ui(ui: &mut egui::Ui, selection: &SelectedFrames) {
809    let mut sum_ns = 0;
810    let mut sum_scopes = 0;
811
812    for frame in &selection.frames {
813        let (min_ns, max_ns) = frame.range_ns();
814        sum_ns += max_ns - min_ns;
815        sum_scopes += frame.meta.num_scopes;
816    }
817
818    let frame_indices = if selection.frames.len() == 1 {
819        format!("frame #{}", selection.frames[0].frame_index())
820    } else if selection.frames.len() as u64
821        == selection.frames.last().frame_index() - selection.frames.first().frame_index() + 1
822    {
823        format!(
824            "{} frames (#{} - #{})",
825            selection.frames.len(),
826            selection.frames.first().frame_index(),
827            selection.frames.last().frame_index()
828        )
829    } else {
830        format!("{} frames", selection.frames.len())
831    };
832
833    let mut info = format!(
834        "Showing {frame_indices}, {:.1} ms, {} threads, {sum_scopes} scopes.",
835        sum_ns as f64 * 1e-6,
836        selection.threads.len(),
837    );
838    if let Some(time) = format_time(selection.raw_range_ns.0) {
839        let _ = write!(&mut info, " Recorded {time}.");
840    }
841
842    ui.label(info);
843}
844
845fn format_time(nanos: NanoSecond) -> Option<String> {
846    let years_since_epoch = nanos / 1_000_000_000 / 60 / 60 / 24 / 365;
847    if 50 <= years_since_epoch && years_since_epoch <= 150 {
848        let offset = OffsetDateTime::from_unix_timestamp_nanos(nanos as i128).ok()?;
849
850        let format_desc = time::macros::format_description!(
851            "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]"
852        );
853        let datetime = offset.format(&format_desc).ok()?;
854
855        Some(datetime)
856    } else {
857        None // `nanos` is likely not counting from epoch.
858    }
859}
860
861fn max_frames_ui(ui: &mut egui::Ui, frame_view: &mut FrameView, uniq: &[Arc<FrameData>]) {
862    let stats = frame_view.stats();
863    let bytes = stats.bytes_of_ram_used();
864
865    let frames_per_second = if let (Some(first), Some(last)) = (uniq.first(), uniq.last()) {
866        let nanos = last.range_ns().1 - first.range_ns().0;
867        let seconds = nanos as f64 * 1e-9;
868        let frames = last.frame_index() - first.frame_index() + 1;
869        frames as f64 / seconds
870    } else {
871        60.0
872    };
873
874    ui.horizontal(|ui| {
875        ui.label("Max recent frames to store:");
876
877        let mut memory_length = frame_view.max_recent();
878        ui.add(egui::Slider::new(&mut memory_length, 10..=100_000).logarithmic(true));
879        frame_view.set_max_recent(memory_length);
880
881        ui.label(format!(
882            "(≈ {:.1} minutes, ≈ {:.0} MB)",
883            memory_length as f64 / 60.0 / frames_per_second,
884            memory_length as f64 * bytes as f64 / uniq.len() as f64 * 1e-6,
885        ));
886    });
887}
888
889fn max_num_latest_ui(ui: &mut egui::Ui, max_num_latest: &mut usize) {
890    ui.horizontal(|ui| {
891        ui.label("Max latest frames to show:");
892        ui.add(egui::Slider::new(max_num_latest, 1..=100).logarithmic(true));
893    });
894}