1use std::sync::Mutex;
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::collections::BTreeMap;
4use std::time::Duration;
5use std::cell::RefCell;
6
7use once_cell::sync::Lazy;
8use quanta::Clock;
9use petgraph::graph::{Graph, NodeIndex};
10
11use crate::{CallSite, CallSiteId};
12
13static CLOCK: Lazy<Clock> = Lazy::new(Clock::new);
15
16static CALL_GRAPH: Lazy<Mutex<LightCallGraph>> = Lazy::new(|| {
18 Mutex::new(LightCallGraph::new())
19});
20
21static COLLECTION_ENABLED: AtomicBool = AtomicBool::new(false);
23
24thread_local! {
25 pub static LOCAL_CURRENT_SPAN: RefCell<Option<CallSiteId>> = const { RefCell::new(None) };
28}
29
30pub struct Span {
36 callsite: &'static CallSite,
37}
38
39impl Span {
40 pub fn new(callsite: &'static CallSite) -> Span {
42 Span {
43 callsite: callsite,
44 }
45 }
46
47 #[must_use]
50 pub fn enter(&self) -> SpanGuard<'_> {
51 if !COLLECTION_ENABLED.load(Ordering::Acquire) {
52 return SpanGuard {
53 span: self,
54 parent: None,
55 start: 0,
56 };
57 }
58
59 let id = self.callsite.id();
60 let parent = LOCAL_CURRENT_SPAN.with(|parent| {
61 let mut parent = parent.borrow_mut();
62
63 let previous = *parent;
64 *parent = Some(id);
65 return previous;
66 });
67
68 SpanGuard {
69 span: self,
70 parent: parent,
71 start: CLOCK.raw(),
72 }
73 }
74}
75
76pub struct SpanGuard<'a> {
79 span: &'a Span,
80 parent: Option<CallSiteId>,
81 start: u64,
82}
83
84impl<'a> Drop for SpanGuard<'a> {
85 fn drop(&mut self) {
86 if !COLLECTION_ENABLED.load(Ordering::Acquire) {
87 return;
88 }
89 let elapsed = CLOCK.delta(self.start, CLOCK.raw());
90
91 LOCAL_CURRENT_SPAN.with(|parent| {
92 let mut parent = parent.borrow_mut();
93 *parent = self.parent;
94 });
95
96
97 let mut graph = CALL_GRAPH.lock().expect("poisoned mutex");
98 let callsite = self.span.callsite.id();
99 graph.add_node(callsite);
100 graph.increase_timing(callsite, elapsed);
101
102 if let Some(parent) = self.parent {
103 graph.add_node(parent);
104 graph.increase_call_count(parent, callsite);
105 }
106 }
107}
108
109struct LightGraphNode {
111 callsite: CallSiteId,
112 elapsed: Duration,
113 called: u32,
114}
115
116impl LightGraphNode {
117 fn new(callsite: CallSiteId) -> LightGraphNode {
118 LightGraphNode {
119 callsite: callsite,
120 elapsed: Duration::new(0, 0),
121 called: 0,
122 }
123 }
124}
125
126struct LightCallGraph {
131 graph: Graph<LightGraphNode, usize>
132}
133
134impl LightCallGraph {
135 fn new() -> LightCallGraph {
136 LightCallGraph {
137 graph: Graph::new(),
138 }
139 }
140
141 pub fn clear(&mut self) {
142 self.graph.clear()
143 }
144
145 fn find(&mut self, callsite: CallSiteId) -> Option<NodeIndex> {
147 self.graph.node_indices().find(|&id| self.graph[id].callsite == callsite)
148 }
149
150 pub fn add_node(&mut self, callsite: CallSiteId) {
153 if self.find(callsite).is_none() {
154 self.graph.add_node(LightGraphNode::new(callsite));
155 }
156 }
157
158 pub fn increase_call_count(&mut self, parent: CallSiteId, child: CallSiteId) {
161 let parent = self.find(parent).expect("missing node for parent");
162 let child = self.find(child).expect("missing node for child");
163 if let Some(edge) = self.graph.find_edge(parent, child) {
164 let count = self
165 .graph
166 .edge_weight_mut(edge)
167 .expect("failed to get edge weights");
168 *count += 1;
169 } else {
170 self.graph.add_edge(parent, child, 1);
172 }
173 }
174
175 pub fn increase_timing(&mut self, span: CallSiteId, time: Duration) {
178 let id = self.find(span).expect("missing node");
179 self.graph[id].elapsed += time;
180 self.graph[id].called += 1;
181 }
182}
183
184pub fn clear_collected_data() {
186 CALL_GRAPH.lock().expect("poisoned mutex").clear();
187}
188
189pub fn enable_data_collection(enabled: bool) {
191 COLLECTION_ENABLED.store(enabled, Ordering::Release);
192}
193
194pub fn get_full_graph() -> FullCallGraph {
196 let graph = CALL_GRAPH.lock().expect("poisoned mutex");
197
198 let mut all_callsites = BTreeMap::new();
199 crate::traverse_registered_callsite(|callsite| {
200 all_callsites.insert(callsite.id(), callsite);
201 });
202
203 let graph = graph.graph.map(|index, node| {
204 TimedSpan::new(node, index.index(), all_callsites[&node.callsite])
205 }, |_, &edge| edge);
206
207 return FullCallGraph {
208 graph: graph
209 };
210}
211
212pub struct TimedSpan {
215 pub id: usize,
217 pub callsite: &'static CallSite,
219 pub elapsed: Duration,
221 pub called: u32,
223}
224
225impl TimedSpan {
226 fn new(node: &LightGraphNode, id: usize, callsite: &'static CallSite) -> TimedSpan {
227 TimedSpan {
228 id: id,
229 callsite: callsite,
230 elapsed: node.elapsed,
231 called: node.called,
232 }
233 }
234}
235
236impl std::fmt::Display for TimedSpan {
237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 write!(
239 f,
240 "{} ran for {:?}, called {} times",
241 self.callsite.full_name(), self.elapsed, self.called
242 )
243 }
244}
245
246pub struct FullCallGraph {
288 graph: Graph<TimedSpan, usize>
289}
290
291pub struct Calls {
293 pub caller: usize,
295 pub callee: usize,
297 pub count: usize,
299}
300
301impl FullCallGraph {
302 pub fn spans(&self) -> impl Iterator<Item = &TimedSpan> {
304 self.graph.raw_nodes().iter().map(|node| &node.weight)
305 }
306
307 pub fn calls(&self) -> impl Iterator<Item = Calls> + '_ {
309 self.graph.raw_edges().iter().map(|edge| Calls {
310 caller: edge.target().index(),
311 callee: edge.source().index(),
312 count: edge.weight,
313 })
314 }
315
316 pub fn as_dot(&self) -> String {
320 petgraph::dot::Dot::new(&self.graph).to_string()
321 }
322
323 #[cfg(feature = "table")]
334 pub fn as_table(&self) -> String {
335 self.as_table_impl(false)
336 }
337
338 #[cfg(feature = "table")]
341 pub fn as_short_table(&self) -> String {
342 self.as_table_impl(true)
343 }
344
345 #[cfg(feature = "table")]
346 fn as_table_impl(&self, short_names: bool) -> String {
347 use petgraph::Direction;
348
349 use crate::table::Row;
350 use crate::table::TableCell;
351
352 let mut names = BTreeMap::new();
353 for node in self.graph.node_weights() {
354 if short_names {
355 names.insert(node.id, node.callsite.name().to_string());
356 } else {
357 names.insert(node.id, node.callsite.full_name());
358 }
359 }
360
361 let mut table = crate::table::Table::new();
362 table.style = crate::table::TableStyle::extended();
363
364 table.add_row(Row::new(vec![
365 "id",
366 "span name ",
368 "call count",
369 "called by",
370 "total",
371 "mean",
372 ]));
373
374 for &node_id in petgraph::algo::kosaraju_scc(&self.graph)
375 .iter()
376 .rev()
377 .flatten()
378 {
379 let node = &self.graph[node_id];
380
381 let mut called_by = vec![];
382 for other in self.graph.neighbors_directed(node_id, Direction::Incoming) {
383 called_by.push(self.graph[other].id.to_string());
384 }
385 let called_by = if !called_by.is_empty() {
386 called_by.join(", ")
387 } else {
388 "—".into()
389 };
390
391 let mean = node.elapsed / node.called;
392 let warn = if mean < Duration::from_nanos(1500) { " ⚠️ " } else { "" };
393
394 table.add_row(Row::new(vec![
395 TableCell::new_right_aligned(node.id),
396 TableCell::new(&names[&node.id]),
397 TableCell::new_right_aligned(node.called),
398 TableCell::new_right_aligned(called_by),
399 TableCell::new_right_aligned(format!("{:.2?}", node.elapsed)),
400 TableCell::new_right_aligned(format!("{:.2?}{}", mean, warn)),
401 ]));
402 }
403
404 return table.render();
405 }
406
407 #[cfg(feature = "json")]
413 pub fn as_json(&self) -> String {
414 let mut spans = json::JsonValue::new_object();
415 for span in self.spans() {
416 spans[&span.callsite.full_name()] = json::object! {
417 "id" => span.id,
418 "elapsed" => format!("{:?}", span.elapsed),
419 "called" => span.called,
420 };
421 }
422
423 let mut all_calls = json::JsonValue::new_array();
424 for call in self.calls() {
425 all_calls.push(json::object! {
426 "caller" => call.caller,
427 "callee" => call.callee,
428 "count" => call.count,
429 }).expect("failed to add edge information to JSON");
430 }
431
432 return json::stringify(json::object! {
433 "timings" => spans,
434 "calls" => all_calls,
435 });
436 }
437}