Skip to main content

puffin/
profile_view.rs

1use itertools::Itertools;
2use std::{
3    cmp::Ordering,
4    collections::{BTreeSet, VecDeque},
5    sync::Arc,
6};
7
8use crate::{FrameData, FrameSinkId, ScopeCollection};
9
10/// A view of recent and slowest frames, used by GUIs.
11#[derive(Clone)]
12pub struct FrameView {
13    /// newest first
14    recent: VecDeque<OrderedByIndex>,
15    max_recent: usize,
16
17    slowest_by_index: BTreeSet<OrderedByIndex>,
18    slowest_by_duration: BTreeSet<OrderedByDuration>,
19    max_slow: usize,
20
21    /// Minimizes memory usage at the expense of CPU time.
22    ///
23    /// Only recommended if you set a large max_recent size.
24    pack_frames: bool,
25
26    /// Maintain stats as we add/remove frames
27    stats: FrameStats,
28
29    /// Collect all scope infos(id/name) from the start of the profiling.
30    scope_collection: ScopeCollection,
31}
32
33impl Default for FrameView {
34    fn default() -> Self {
35        let max_recent = 1_000;
36        let max_slow = 256;
37
38        Self {
39            recent: VecDeque::with_capacity(max_recent),
40            max_recent,
41            slowest_by_index: BTreeSet::new(),
42            slowest_by_duration: BTreeSet::new(),
43            max_slow,
44            pack_frames: true,
45            stats: Default::default(),
46            scope_collection: Default::default(),
47        }
48    }
49}
50
51impl FrameView {
52    /// Returns `true` if there are no recent or slowest frames.
53    pub fn is_empty(&self) -> bool {
54        self.recent.is_empty() && self.slowest_by_duration.is_empty()
55    }
56
57    /// Returns the collection of scope details.
58    /// This can be used to fetch more information about a specific scope.
59    pub fn scope_collection(&self) -> &ScopeCollection {
60        &self.scope_collection
61    }
62
63    /// Adds a new frame to the view.
64    pub fn add_frame(&mut self, new_frame: Arc<FrameData>) {
65        // Register all scopes from the new frame into the scope collection.
66        for new_scope in &new_frame.scope_delta {
67            self.scope_collection.insert(new_scope.clone());
68        }
69
70        if let Some(last) = self.recent.iter().last()
71            && new_frame.frame_index() <= last.0.frame_index()
72        {
73            // A frame from the past!?
74            // Likely we are `puffin_viewer`, and the server restarted.
75            // The safe choice is to clear everything:
76            self.stats.clear();
77            self.recent.clear();
78            self.slowest_by_index.clear();
79            self.slowest_by_duration.clear();
80        }
81
82        if let Some(last) = self.recent.iter().last() {
83            // Assume there is a viewer viewing the newest frame,
84            // and compress the previously newest frame to save RAM:
85            if self.pack_frames {
86                last.0.pack();
87            }
88
89            self.stats.add(&last.0);
90        }
91
92        let add_to_slowest = if self.slowest_by_duration.len() < self.max_slow {
93            true
94        } else if let Some(fastest_of_the_slow) = self.slowest_by_duration.iter().last() {
95            new_frame.duration_ns() > fastest_of_the_slow.0.duration_ns()
96        } else {
97            false
98        };
99
100        if add_to_slowest {
101            self.add_slow_frame(&new_frame);
102        }
103
104        self.add_recent_frame(&new_frame);
105    }
106
107    fn add_slow_frame(&mut self, new_frame: &Arc<FrameData>) {
108        assert_eq!(self.slowest_by_duration.len(), self.slowest_by_index.len());
109
110        self.slowest_by_duration
111            .insert(OrderedByDuration(new_frame.clone()));
112        self.slowest_by_index
113            .insert(OrderedByIndex(new_frame.clone()));
114
115        while self.slowest_by_duration.len() > self.max_slow {
116            if let Some(removed_frame) = self.slowest_by_duration.pop_last() {
117                let removed_by_index = OrderedByIndex(removed_frame.0.clone());
118                self.slowest_by_index.remove(&removed_by_index);
119
120                // Only remove from stats if the frame is not present in recent
121                if self.recent.binary_search(&removed_by_index).is_err() {
122                    self.stats.remove(&removed_frame.0);
123                }
124            }
125        }
126    }
127
128    fn add_recent_frame(&mut self, new_frame: &Arc<FrameData>) {
129        self.recent.push_back(OrderedByIndex(new_frame.clone()));
130
131        while self.recent.len() > self.max_recent {
132            if let Some(removed_frame) = self.recent.pop_front() {
133                // Only remove from stats if the frame is not present in slowest
134                if !self.slowest_by_index.contains(&removed_frame) {
135                    self.stats.remove(&removed_frame.0);
136                }
137            }
138        }
139    }
140
141    /// The latest fully captured frame of data.
142    pub fn latest_frame(&self) -> Option<Arc<FrameData>> {
143        self.recent.back().map(|f| f.0.clone())
144    }
145
146    /// Returns up to `n` latest fully captured frames of data.
147    pub fn latest_frames(&self, n: usize) -> impl Iterator<Item = &Arc<FrameData>> {
148        // Probably not the best way to do this, but since
149        // [`self.recent`] is immutable in this context and
150        // working with deque slices is complicated, we'll do
151        // it this way for now.
152        self.recent.iter().rev().take(n).rev().map(|f| &f.0)
153    }
154
155    /// Oldest first
156    pub fn recent_frames(&self) -> impl Iterator<Item = &Arc<FrameData>> {
157        self.recent.iter().map(|f| &f.0)
158    }
159
160    /// The slowest frames so far (or since last call to [`Self::clear_slowest()`])
161    /// in chronological order.
162    pub fn slowest_frames_chronological(&self) -> impl Iterator<Item = &Arc<FrameData>> {
163        self.slowest_by_index.iter().map(|f| &f.0)
164    }
165
166    /// All frames sorted chronologically.
167    pub fn all_uniq(&self) -> impl Iterator<Item = &Arc<FrameData>> {
168        Itertools::merge(self.recent.iter(), self.slowest_by_index.iter())
169            .dedup()
170            .map(|f| &f.0)
171    }
172
173    /// Clean history of the slowest frames.
174    pub fn clear_slowest(&mut self) {
175        for frame in self.slowest_by_index.iter() {
176            self.stats.remove(&frame.0);
177        }
178
179        self.slowest_by_duration.clear();
180        self.slowest_by_index.clear();
181    }
182
183    /// How many frames of recent history to store.
184    pub fn max_recent(&self) -> usize {
185        self.max_recent
186    }
187
188    /// How many frames of recent history to store.
189    pub fn set_max_recent(&mut self, max_recent: usize) {
190        self.max_recent = max_recent;
191    }
192
193    /// How many slow "spike" frames to store.
194    pub fn max_slow(&self) -> usize {
195        self.max_slow
196    }
197
198    /// How many slow "spike" frames to store.
199    pub fn set_max_slow(&mut self, max_slow: usize) {
200        self.max_slow = max_slow;
201    }
202
203    /// Returns if frames are packed (compressed).
204    pub fn pack_frames(&self) -> bool {
205        self.pack_frames
206    }
207
208    /// Sets whether frames should be packed (compressed).
209    /// Packing frames will increase CPU time and decrease memory usage.
210    pub fn set_pack_frames(&mut self, pack_frames: bool) {
211        self.pack_frames = pack_frames;
212    }
213
214    /// Retrieve statistics for added frames. This operation is efficient and suitable when
215    /// frames have not been manipulated outside of `ProfileView`, such as being unpacked. For
216    /// comprehensive statistics, refer to [`Self::stats_full()`]
217    pub fn stats(&self) -> FrameStats {
218        self.stats
219    }
220
221    /// Retrieve detailed statistics by performing a full computation on all the added frames.
222    pub fn stats_full(&self) -> FrameStats {
223        FrameStats::from_frames(self.all_uniq().map(Arc::as_ref))
224    }
225
226    /// Export profile data as a `.puffin` file/stream.
227    #[cfg(feature = "serialization")]
228    #[cfg(not(target_arch = "wasm32"))] // compression not supported on wasm
229    pub fn write(&self, write: &mut impl std::io::Write) -> anyhow::Result<()> {
230        write.write_all(b"PUF0")?;
231
232        for frame in self.all_uniq() {
233            frame.write_into(None, write)?;
234        }
235        Ok(())
236    }
237
238    /// Import profile data from a `.puffin` file/stream.
239    #[cfg(feature = "serialization")]
240    pub fn read(read: &mut impl std::io::Read) -> anyhow::Result<Self> {
241        let mut magic = [0_u8; 4];
242        read.read_exact(&mut magic)?;
243        if &magic != b"PUF0" {
244            anyhow::bail!("Expected .puffin magic header of 'PUF0', found {magic:?}");
245        }
246
247        let mut slf = Self {
248            max_recent: usize::MAX,
249            ..Default::default()
250        };
251        while let Some(frame) = FrameData::read_next(read)? {
252            slf.add_frame(frame.into());
253        }
254
255        Ok(slf)
256    }
257}
258
259// ----------------------------------------------------------------------------
260
261/// Select the slowest frames, up to a certain count.
262pub fn select_slowest(frames: &[Arc<FrameData>], max: usize) -> Vec<Arc<FrameData>> {
263    let mut slowest: std::collections::BinaryHeap<OrderedByDuration> = Default::default();
264    for frame in frames {
265        slowest.push(OrderedByDuration(frame.clone()));
266        while slowest.len() > max {
267            slowest.pop();
268        }
269    }
270    let mut slowest: Vec<_> = slowest.drain().map(|x| x.0).collect();
271    slowest.sort_by_key(|frame| frame.frame_index());
272    slowest
273}
274
275// ----------------------------------------------------------------------------
276
277#[derive(Clone)]
278struct OrderedByDuration(Arc<FrameData>);
279
280impl Ord for OrderedByDuration {
281    fn cmp(&self, other: &Self) -> Ordering {
282        match self.0.duration_ns().cmp(&other.0.duration_ns()).reverse() {
283            Ordering::Equal => self.0.frame_index().cmp(&other.0.frame_index()),
284            res => res,
285        }
286    }
287}
288
289impl PartialOrd for OrderedByDuration {
290    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
291        Some(self.cmp(other))
292    }
293}
294
295impl Eq for OrderedByDuration {}
296
297impl PartialEq for OrderedByDuration {
298    fn eq(&self, other: &Self) -> bool {
299        self.0.duration_ns() == other.0.duration_ns()
300            && self.0.frame_index() == other.0.frame_index()
301    }
302}
303
304// ----------------------------------------------------------------------------
305#[derive(Clone)]
306struct OrderedByIndex(Arc<FrameData>);
307
308impl Ord for OrderedByIndex {
309    fn cmp(&self, other: &Self) -> Ordering {
310        match self.0.frame_index().cmp(&other.0.frame_index()) {
311            Ordering::Equal => self.0.duration_ns().cmp(&other.0.duration_ns()),
312            res => res,
313        }
314    }
315}
316
317impl PartialOrd for OrderedByIndex {
318    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
319        Some(self.cmp(other))
320    }
321}
322
323impl Eq for OrderedByIndex {}
324
325impl PartialEq for OrderedByIndex {
326    fn eq(&self, other: &Self) -> bool {
327        self.0.frame_index() == other.0.frame_index()
328            && self.0.duration_ns() == other.0.duration_ns()
329    }
330}
331
332// ----------------------------------------------------------------------------
333
334/// Automatically connects to [`crate::GlobalProfiler`].
335pub struct GlobalFrameView {
336    sink_id: FrameSinkId,
337    view: Arc<parking_lot::Mutex<FrameView>>,
338}
339
340impl Default for GlobalFrameView {
341    fn default() -> Self {
342        let view = Arc::new(parking_lot::Mutex::new(FrameView::default()));
343        let view_clone = view.clone();
344        let mut profiler = crate::GlobalProfiler::lock();
345        let sink_id = profiler.add_sink(Box::new(move |frame| {
346            view_clone.lock().add_frame(frame);
347        }));
348        // GlobalFrameView might be created after scope scopes were already created
349        // and our registered sink won't see them without prior propagation.
350        profiler.emit_scope_snapshot();
351
352        Self { sink_id, view }
353    }
354}
355
356impl Drop for GlobalFrameView {
357    fn drop(&mut self) {
358        crate::GlobalProfiler::lock().remove_sink(self.sink_id);
359    }
360}
361
362impl GlobalFrameView {
363    /// Sink ID
364    pub fn sink_id(&self) -> FrameSinkId {
365        self.sink_id
366    }
367
368    /// View the latest profiling data.
369    pub fn lock(&self) -> parking_lot::MutexGuard<'_, FrameView> {
370        self.view.lock()
371    }
372}
373
374// ----------------------------------------------------------------------------
375
376fn stats_entry(frame: &FrameData) -> (usize, usize) {
377    let info = frame.packing_info();
378    (
379        info.packed_size.unwrap_or(0) + info.unpacked_size.unwrap_or(0),
380        info.unpacked_size.is_some() as usize,
381    )
382}
383
384/// Collect statistics for maintained frames
385#[derive(Clone, Copy, Debug, Default)]
386pub struct FrameStats {
387    unique_frames: usize,
388    total_ram_used: usize,
389    unpacked_frames: usize,
390}
391
392impl FrameStats {
393    /// Creates a `FrameStats` instance from an iterator of frames.
394    pub fn from_frames<'a>(frames: impl Iterator<Item = &'a FrameData>) -> Self {
395        let mut stats = FrameStats::default();
396
397        for frame in frames {
398            stats.add(frame);
399        }
400
401        stats
402    }
403
404    /// Adds a frame's statistics to the `FrameStats`.
405    fn add(&mut self, frame: &FrameData) {
406        let (total, unpacked) = stats_entry(frame);
407
408        self.total_ram_used = self.total_ram_used.saturating_add(total);
409        self.unpacked_frames = self.unpacked_frames.saturating_add(unpacked);
410        self.unique_frames = self.unique_frames.saturating_add(1);
411    }
412
413    /// Removes a frame's statistics from the `FrameStats`.
414    fn remove(&mut self, frame: &FrameData) {
415        let (total, unpacked) = stats_entry(frame);
416
417        self.total_ram_used = self.total_ram_used.saturating_sub(total);
418        self.unpacked_frames = self.unpacked_frames.saturating_sub(unpacked);
419        self.unique_frames = self.unique_frames.saturating_sub(1);
420    }
421
422    /// Returns the number of unique frames.
423    pub fn frames(&self) -> usize {
424        self.unique_frames
425    }
426
427    /// Returns the number of unpacked frames.
428    pub fn unpacked_frames(&self) -> usize {
429        self.unpacked_frames
430    }
431
432    /// Returns the total bytes of RAM used.
433    pub fn bytes_of_ram_used(&self) -> usize {
434        self.total_ram_used
435    }
436
437    /// Clears all statistics in `FrameStats`.
438    pub fn clear(&mut self) {
439        self.unique_frames = 0;
440        self.unpacked_frames = 0;
441        self.total_ram_used = 0;
442    }
443}
444
445#[cfg(all(test, feature = "serialization"))]
446mod tests {
447    use super::FrameView;
448
449    #[test]
450    fn read_pfd4_file() -> anyhow::Result<()> {
451        let mut file = std::fs::File::open("tests/data/capture_PFD4.puffin")?;
452        let _ = FrameView::read(&mut file)?;
453        Ok(())
454    }
455
456    #[test]
457    fn read_pfd3_file() -> anyhow::Result<()> {
458        let mut file = std::fs::File::open("tests/data/capture_PFD3.puffin")?;
459        let _ = FrameView::read(&mut file)?;
460        Ok(())
461    }
462
463    #[test]
464    fn read_pfd2_file() -> anyhow::Result<()> {
465        let mut file = std::fs::File::open("tests/data/capture_PFD2.puffin")?;
466        let _ = FrameView::read(&mut file)?;
467        Ok(())
468    }
469
470    #[test]
471    fn read_pfd1_file() -> anyhow::Result<()> {
472        let mut file = std::fs::File::open("tests/data/capture_PFD1.puffin")?;
473        let _ = FrameView::read(&mut file)?;
474        Ok(())
475    }
476}