Skip to main content

micromegas_analytics/
call_tree.rs

1use crate::metadata::{StreamMetadata, get_thread_name_from_stream_metadata};
2use crate::scope::BorrowedScopeDesc;
3use crate::scope::ScopeDesc;
4use crate::scope::ScopeHashMap;
5use crate::thread_block_processor::ThreadBlockProcessor;
6use crate::thread_block_processor::parse_thread_block;
7use crate::time::ConvertTicks;
8use anyhow::Result;
9use micromegas_telemetry::blob_storage::BlobStorage;
10use micromegas_telemetry::types::block::BlockMetadata;
11use micromegas_tracing::prelude::*;
12use std::sync::Arc;
13
14/// A node in a call tree, representing a single scope instance.
15#[derive(Debug)]
16pub struct CallTreeNode {
17    /// The unique identifier of the scope instance.
18    pub id: Option<i64>,
19    /// The hash of the scope description.
20    pub hash: u32,
21    /// The start time of the scope instance in nanoseconds.
22    pub begin: i64, //absolute nanoseconds
23    /// The end time of the scope instance in nanoseconds.
24    pub end: i64,
25    /// The children of this node in the call tree.
26    pub children: Vec<CallTreeNode>,
27}
28
29/// A call tree, representing the execution of a single thread.
30#[derive(Debug)]
31pub struct CallTree {
32    /// A map from scope hash to scope description.
33    pub scopes: ScopeHashMap,
34    /// The root node of the call tree.
35    // the root node corresponds to the thread and has a span equal to the query range
36    pub call_tree_root: Option<CallTreeNode>,
37}
38
39/// A builder for creating a `CallTree` from a stream of thread events.
40pub struct CallTreeBuilder {
41    begin_range_ns: i64,
42    end_range_ns: i64,
43    limit: Option<i64>,
44    nb_spans: i64,
45    stack: Vec<CallTreeNode>,
46    scopes: ScopeHashMap,
47    convert_ticks: ConvertTicks,
48    root_hash: u32,
49}
50
51impl CallTreeBuilder {
52    pub fn new(
53        begin_range_ns: i64,
54        end_range_ns: i64,
55        limit: Option<i64>,
56        convert_ticks: ConvertTicks,
57        thread_name: String,
58    ) -> Self {
59        let thread_scope_desc = ScopeDesc::new(
60            Arc::new(thread_name),
61            Arc::new("".to_owned()),
62            Arc::new("".to_owned()),
63            0,
64        );
65        let mut scopes = ScopeHashMap::new();
66        let root_hash = thread_scope_desc.hash;
67        scopes.insert(root_hash, thread_scope_desc);
68        Self {
69            begin_range_ns,
70            end_range_ns,
71            limit,
72            nb_spans: 0,
73            stack: Vec::new(),
74            scopes,
75            convert_ticks,
76            root_hash,
77        }
78    }
79
80    #[span_fn]
81    pub fn finish(mut self) -> CallTree {
82        if self.stack.is_empty() {
83            return CallTree {
84                scopes: self.scopes,
85                call_tree_root: None,
86            };
87        }
88        while self.stack.len() > 1 {
89            let top = self.stack.pop().unwrap();
90            let last_index = self.stack.len() - 1;
91            let parent = &mut self.stack[last_index];
92            parent.children.push(top);
93        }
94        assert_eq!(1, self.stack.len());
95        CallTree {
96            scopes: self.scopes,
97            call_tree_root: self.stack.pop(),
98        }
99    }
100
101    fn add_child_to_top(&mut self, node: CallTreeNode) {
102        if let Some(mut top) = self.stack.pop() {
103            top.children.push(node);
104            self.stack.push(top);
105        } else {
106            let new_root = CallTreeNode {
107                id: None,
108                hash: self.root_hash,
109                begin: self.begin_range_ns,
110                end: self.end_range_ns,
111                children: vec![node],
112            };
113            self.stack.push(new_root);
114            self.nb_spans += 1;
115        }
116    }
117
118    fn record_scope_desc(&mut self, scope_desc: BorrowedScopeDesc<'_>) {
119        // Own the scope strings only on first encounter (once per distinct scope),
120        // not per event — the borrowed scope comes from the per-block arena.
121        self.scopes
122            .entry(scope_desc.hash)
123            .or_insert_with(|| scope_desc.to_owned());
124    }
125}
126
127impl ThreadBlockProcessor for CallTreeBuilder {
128    fn on_begin_thread_scope(
129        &mut self,
130        _block_id: &str,
131        event_id: i64,
132        scope: BorrowedScopeDesc<'_>,
133        ts: i64,
134    ) -> Result<bool> {
135        if self.limit.is_some() && self.nb_spans >= self.limit.unwrap() {
136            return Ok(false);
137        }
138        let time = self.convert_ticks.ticks_to_nanoseconds(ts);
139        if time < self.begin_range_ns {
140            return Ok(true);
141        }
142        if time > self.end_range_ns {
143            return Ok(false);
144        }
145        let hash = scope.hash;
146        self.record_scope_desc(scope);
147        let node = CallTreeNode {
148            id: Some(event_id),
149            hash,
150            begin: time,
151            end: self.end_range_ns,
152            children: Vec::new(),
153        };
154        self.stack.push(node);
155        self.nb_spans += 1;
156        Ok(true) // continue even if we reached the limit to allow the opportunity to close than span
157    }
158
159    fn on_end_thread_scope(
160        &mut self,
161        _block_id: &str,
162        event_id: i64,
163        scope: BorrowedScopeDesc<'_>,
164        ts: i64,
165    ) -> Result<bool> {
166        let time = self.convert_ticks.ticks_to_nanoseconds(ts);
167        if time < self.begin_range_ns {
168            return Ok(true);
169        }
170        if time > self.end_range_ns {
171            return Ok(false);
172        }
173        let hash = scope.hash;
174        self.record_scope_desc(scope);
175        if let Some(mut old_top) = self.stack.pop() {
176            if old_top.hash == hash {
177                old_top.end = time;
178                self.add_child_to_top(old_top);
179            } else if old_top.hash == self.root_hash {
180                old_top.id = Some(event_id);
181                old_top.hash = hash;
182                old_top.end = time;
183                self.add_child_to_top(old_top);
184            } else {
185                let ending_name = self.scopes.get(&hash).map_or("?", |s| s.name.as_str());
186                let open_name = self
187                    .scopes
188                    .get(&old_top.hash)
189                    .map_or("?", |s| s.name.as_str());
190                anyhow::bail!(
191                    "top scope mismatch in block {_block_id}: closing '{ending_name}' but '{open_name}' is open"
192                );
193            }
194        } else {
195            if self.limit.is_some() && self.nb_spans >= self.limit.unwrap() {
196                return Ok(false);
197            }
198            let node = CallTreeNode {
199                id: Some(event_id),
200                hash,
201                begin: self.begin_range_ns,
202                end: time,
203                children: Vec::new(),
204            };
205            self.add_child_to_top(node);
206        }
207        Ok(true)
208    }
209}
210
211/// Creates a call tree from a set of thread event blocks.
212#[allow(clippy::cast_precision_loss)]
213#[span_fn]
214pub async fn make_call_tree(
215    blocks: &[BlockMetadata],
216    begin_range_ns: i64,
217    end_range_ns: i64,
218    limit: Option<i64>,
219    blob_storage: Arc<BlobStorage>,
220    convert_ticks: ConvertTicks,
221    stream: &StreamMetadata,
222) -> Result<CallTree> {
223    let mut builder = CallTreeBuilder::new(
224        begin_range_ns,
225        end_range_ns,
226        limit,
227        convert_ticks,
228        get_thread_name_from_stream_metadata(stream)?,
229    );
230    for block in blocks {
231        parse_thread_block(
232            blob_storage.clone(),
233            stream,
234            block.block_id,
235            block.object_offset,
236            &mut builder,
237        )
238        .await?;
239    }
240    Ok(builder.finish())
241}