Skip to main content

puffin/
merge.rs

1use crate::{
2    NanoSecond, Reader, Result, Scope, ScopeCollection, ScopeId, Stream, ThreadInfo,
3    UnpackedFrameData,
4};
5use std::{collections::BTreeMap, hash::Hash};
6
7/// Temporary structure while building a [`MergeScope`].
8#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
9struct MergeId<'s> {
10    id: ScopeId,
11    data: &'s str,
12}
13
14/// Temporary structure while building a [`MergeScope`].
15#[derive(Default)]
16struct MergeNode<'s> {
17    /// These are the raw scopes that got merged into us.
18    /// All these scopes have the same `id`.
19    pieces: Vec<MergePiece<'s>>,
20
21    /// indexed by their id+data
22    children: BTreeMap<MergeId<'s>, MergeNode<'s>>,
23}
24#[derive(Clone, Copy, Debug, PartialEq)]
25struct MergePiece<'s> {
26    /// The start of the scope relative to its *parent* [`Scope`].
27    pub relative_start_ns: NanoSecond,
28    /// The raw scope, just like it is found in the input stream
29    pub scope: Scope<'s>,
30}
31
32/// A scope that has been merged from many different sources
33#[derive(Clone, Debug, PartialEq)]
34pub struct MergeScope<'s> {
35    /// Relative to parent.
36    pub relative_start_ns: NanoSecond,
37    /// Sum of all durations over all frames
38    pub total_duration_ns: NanoSecond,
39    /// [`Self::total_duration_ns`] divided by number of frames.
40    pub duration_per_frame_ns: NanoSecond,
41    /// The slowest individual piece.
42    pub max_duration_ns: NanoSecond,
43    /// Number of pieces that got merged together to us.
44    pub num_pieces: usize,
45    /// The common identifier that we merged using.
46    pub id: ScopeId,
47    /// only set if all children had the same
48    pub data: std::borrow::Cow<'s, str>,
49    /// The merged children of this merged scope.
50    pub children: Vec<MergeScope<'s>>,
51}
52
53impl MergeScope<'_> {
54    /// Clones the merge scope.
55    pub fn into_owned(self) -> MergeScope<'static> {
56        MergeScope::<'static> {
57            relative_start_ns: self.relative_start_ns,
58            total_duration_ns: self.total_duration_ns,
59            duration_per_frame_ns: self.duration_per_frame_ns,
60            max_duration_ns: self.max_duration_ns,
61            num_pieces: self.num_pieces,
62            id: self.id,
63            data: std::borrow::Cow::Owned(self.data.into_owned()),
64            children: self.children.into_iter().map(Self::into_owned).collect(),
65        }
66    }
67}
68
69impl<'s> MergeNode<'s> {
70    fn add<'slf>(&'slf mut self, stream: &'s Stream, piece: MergePiece<'s>) -> Result<()> {
71        self.pieces.push(piece);
72
73        for child in Reader::with_offset(stream, piece.scope.child_begin_position)? {
74            let child = child?;
75
76            self.children
77                .entry(MergeId {
78                    id: child.id,
79                    data: child.record.data,
80                })
81                .or_default()
82                .add(
83                    stream,
84                    MergePiece {
85                        relative_start_ns: child.record.start_ns - piece.scope.record.start_ns,
86                        scope: child,
87                    },
88                )?;
89        }
90
91        Ok(())
92    }
93
94    fn build(self, scope_collection: &ScopeCollection, num_frames: i64) -> MergeScope<'s> {
95        assert!(!self.pieces.is_empty());
96        let mut relative_start_ns = self.pieces[0].relative_start_ns;
97        let mut total_duration_ns = 0;
98        let mut slowest_ns = 0;
99        let num_pieces = self.pieces.len();
100        let id = self.pieces[0].scope.id;
101        let mut data = self.pieces[0].scope.record.data;
102
103        for piece in &self.pieces {
104            // Merged scope should start at the earliest piece:
105            relative_start_ns = relative_start_ns.min(piece.relative_start_ns);
106            total_duration_ns += piece.scope.record.duration_ns;
107            slowest_ns = slowest_ns.max(piece.scope.record.duration_ns);
108
109            assert_eq!(id, piece.scope.id);
110            if data != piece.scope.record.data {
111                data = ""; // different in different pieces
112            }
113        }
114
115        MergeScope {
116            relative_start_ns,
117            total_duration_ns,
118            duration_per_frame_ns: total_duration_ns / num_frames,
119            max_duration_ns: slowest_ns,
120            num_pieces,
121            id,
122            data: data.into(),
123            children: build(scope_collection, self.children, num_frames),
124        }
125    }
126}
127
128fn build<'s>(
129    scope_collection: &ScopeCollection,
130    nodes: BTreeMap<MergeId<'s>, MergeNode<'s>>,
131    num_frames: i64,
132) -> Vec<MergeScope<'s>> {
133    let mut scopes: Vec<_> = nodes
134        .into_values()
135        .map(|node| node.build(scope_collection, num_frames))
136        .collect();
137
138    // Earliest first:
139    scopes.sort_by_key(|scope| scope.relative_start_ns);
140
141    // Make sure sibling scopes do not overlap:
142    let mut relative_ns = 0;
143    for scope in &mut scopes {
144        scope.relative_start_ns = scope.relative_start_ns.max(relative_ns);
145        relative_ns = scope.relative_start_ns + scope.duration_per_frame_ns;
146    }
147
148    scopes
149}
150
151/// For the given thread, merge all scopes with the same id+data path.
152pub fn merge_scopes_for_thread<'s>(
153    scope_collection: &ScopeCollection,
154    frames: &'s [std::sync::Arc<UnpackedFrameData>],
155    thread_info: &ThreadInfo,
156) -> Result<Vec<MergeScope<'s>>> {
157    let mut top_nodes: BTreeMap<MergeId<'s>, MergeNode<'s>> = Default::default();
158
159    for frame in frames {
160        if let Some(stream_info) = frame.thread_streams.get(thread_info) {
161            let offset_ns = frame.meta.range_ns.0 - frames[0].meta.range_ns.0; // make everything relative to first frame
162
163            let top_scopes = Reader::from_start(&stream_info.stream).read_top_scopes()?;
164            for scope in top_scopes {
165                top_nodes
166                    .entry(MergeId {
167                        id: scope.id,
168                        data: scope.record.data,
169                    })
170                    .or_default()
171                    .add(
172                        &stream_info.stream,
173                        MergePiece {
174                            relative_start_ns: scope.record.start_ns - offset_ns,
175                            scope,
176                        },
177                    )?;
178            }
179        }
180    }
181
182    Ok(build(scope_collection, top_nodes, frames.len() as _))
183}
184
185#[cfg(test)]
186mod tests {
187    use std::{collections::BTreeMap, sync::Arc};
188
189    #[test]
190    fn test_merge() {
191        use crate::*;
192
193        let mut scope_collection = ScopeCollection::default();
194        // top scopes
195        scope_collection.insert(Arc::new(
196            ScopeDetails::from_scope_id(ScopeId::new(1)).with_function_name("a"),
197        ));
198        scope_collection.insert(Arc::new(
199            ScopeDetails::from_scope_id(ScopeId::new(2)).with_function_name("b"),
200        ));
201
202        // middle scopes
203        scope_collection.insert(Arc::new(
204            ScopeDetails::from_scope_id(ScopeId::new(3)).with_function_name("ba"),
205        ));
206        scope_collection.insert(Arc::new(
207            ScopeDetails::from_scope_id(ScopeId::new(4)).with_function_name("bb"),
208        ));
209        scope_collection.insert(Arc::new(
210            ScopeDetails::from_scope_id(ScopeId::new(5)).with_function_name("bba"),
211        ));
212
213        let stream = {
214            let mut stream = Stream::default();
215
216            for i in 0..2 {
217                let ns = 1000 * i;
218
219                let (a, _) = stream.begin_scope(|| ns + 100, ScopeId::new(1), "");
220                stream.end_scope(a, ns + 200);
221
222                let (b, _) = stream.begin_scope(|| ns + 200, ScopeId::new(2), "");
223
224                let (ba, _) = stream.begin_scope(|| ns + 400, ScopeId::new(3), "");
225                stream.end_scope(ba, ns + 600);
226
227                let (bb, _) = stream.begin_scope(|| ns + 600, ScopeId::new(4), "");
228                let (bba, _) = stream.begin_scope(|| ns + 600, ScopeId::new(5), "");
229                stream.end_scope(bba, ns + 700);
230                stream.end_scope(bb, ns + 800);
231                stream.end_scope(b, ns + 900);
232            }
233
234            stream
235        };
236
237        let stream_info = StreamInfo::parse(stream).unwrap();
238        let mut thread_streams = BTreeMap::new();
239        let thread_info = ThreadInfo {
240            start_time_ns: Some(0),
241            name: "main".to_owned(),
242        };
243        thread_streams.insert(thread_info.clone(), stream_info);
244        let frame = UnpackedFrameData::new(0, thread_streams).unwrap();
245        let frames = [Arc::new(frame)];
246        let merged = merge_scopes_for_thread(&scope_collection, &frames, &thread_info).unwrap();
247
248        let expected = vec![
249            MergeScope {
250                relative_start_ns: 100,
251                total_duration_ns: 2 * 100,
252                duration_per_frame_ns: 2 * 100,
253                max_duration_ns: 100,
254                num_pieces: 2,
255                id: ScopeId::new(1),
256                data: "".into(),
257                children: vec![],
258            },
259            MergeScope {
260                relative_start_ns: 300, // moved forward to make place for "a" (as are all children)
261                total_duration_ns: 2 * 700,
262                duration_per_frame_ns: 2 * 700,
263                max_duration_ns: 700,
264                num_pieces: 2,
265                id: ScopeId::new(2),
266                data: "".into(),
267                children: vec![
268                    MergeScope {
269                        relative_start_ns: 200,
270                        total_duration_ns: 2 * 200,
271                        duration_per_frame_ns: 2 * 200,
272                        max_duration_ns: 200,
273                        num_pieces: 2,
274                        id: ScopeId::new(3),
275                        data: "".into(),
276                        children: vec![],
277                    },
278                    MergeScope {
279                        relative_start_ns: 600,
280                        total_duration_ns: 2 * 200,
281                        duration_per_frame_ns: 2 * 200,
282                        max_duration_ns: 200,
283                        num_pieces: 2,
284                        id: ScopeId::new(4),
285                        data: "".into(),
286                        children: vec![MergeScope {
287                            relative_start_ns: 0,
288                            total_duration_ns: 2 * 100,
289                            duration_per_frame_ns: 2 * 100,
290                            max_duration_ns: 100,
291                            num_pieces: 2,
292                            id: ScopeId::new(5),
293                            data: "".into(),
294                            children: vec![],
295                        }],
296                    },
297                ],
298            },
299        ];
300
301        assert_eq!(
302            merged, expected,
303            "\nGot:\n{merged:#?}\n\n!=\nExpected:\n{expected:#?}",
304        );
305    }
306}