1#![forbid(unsafe_code)]
11#![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
34pub 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 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 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
72pub 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
90pub fn profiler_ui(ui: &mut egui::Ui) {
94 let mut profile_ui = PROFILE_UI.lock();
95
96 profile_ui.ui(ui);
97}
98
99#[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 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 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 pub fn global_frame_view(&self) -> &GlobalFrameView {
135 &self.global_frame_view
136 }
137}
138
139#[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#[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#[derive(Clone)]
206pub struct SelectedFrames {
207 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 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#[derive(Clone)]
291#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
292#[cfg_attr(feature = "serde", serde(default))]
293pub struct ProfilerUi {
294 #[cfg_attr(feature = "serde", serde(alias = "options"))]
296 pub flamegraph_options: flamegraph::Options,
297 #[cfg_attr(feature = "serde", serde(skip))]
299 pub stats_options: stats::Options,
300
301 pub view: View,
303
304 #[cfg_attr(feature = "serde", serde(skip))]
306 paused: Option<Paused>,
307
308 max_num_latest: usize,
310
311 slowest_frame: f32,
313
314 #[cfg_attr(feature = "serde", serde(skip))]
316 last_pack_pass: Option<web_time::Instant>,
317
318 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 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 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 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 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; 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(); }
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 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 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); 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 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 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 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 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 let visual_rect = frame_rect.expand2(vec2(-0.5 * frame_spacing, 0.0));
783
784 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 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 }
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}