use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use std::collections::BTreeMap;
use std::time::Duration;
use std::cell::RefCell;
use once_cell::sync::Lazy;
use quanta::Clock;
use petgraph::graph::{Graph, NodeIndex};
use crate::{CallSite, CallSiteId};
static CLOCK: Lazy<Clock> = Lazy::new(Clock::new);
static CALL_GRAPH: Lazy<Mutex<LightCallGraph>> = Lazy::new(|| {
Mutex::new(LightCallGraph::new())
});
static COLLECTION_ENABLED: AtomicBool = AtomicBool::new(false);
thread_local! {
pub static LOCAL_CURRENT_SPAN: RefCell<Option<CallSiteId>> = RefCell::new(None);
}
pub struct Span {
callsite: &'static CallSite,
}
impl Span {
pub fn new(callsite: &'static CallSite) -> Span {
Span {
callsite: callsite,
}
}
#[must_use]
pub fn enter(&self) -> SpanGuard<'_> {
if !COLLECTION_ENABLED.load(Ordering::Acquire) {
return SpanGuard {
span: self,
parent: None,
start: 0,
};
}
let id = self.callsite.id();
let parent = LOCAL_CURRENT_SPAN.with(|parent| {
let mut parent = parent.borrow_mut();
let previous = *parent;
*parent = Some(id);
return previous;
});
SpanGuard {
span: self,
parent: parent,
start: CLOCK.start(),
}
}
}
pub struct SpanGuard<'a> {
span: &'a Span,
parent: Option<CallSiteId>,
start: u64,
}
impl<'a> Drop for SpanGuard<'a> {
fn drop(&mut self) {
if !COLLECTION_ENABLED.load(Ordering::Acquire) {
return;
}
let elapsed = CLOCK.delta(self.start, CLOCK.end());
LOCAL_CURRENT_SPAN.with(|parent| {
let mut parent = parent.borrow_mut();
*parent = self.parent;
});
let mut graph = CALL_GRAPH.lock().expect("poisoned mutex");
let callsite = self.span.callsite.id();
graph.add_node(callsite);
graph.increase_timing(callsite, elapsed);
if let Some(parent) = self.parent {
graph.add_node(parent);
graph.increase_call_count(parent, callsite);
}
}
}
struct LightGraphNode {
callsite: CallSiteId,
elapsed: Duration,
called: u32,
}
impl LightGraphNode {
fn new(callsite: CallSiteId) -> LightGraphNode {
LightGraphNode {
callsite: callsite,
elapsed: Duration::new(0, 0),
called: 0,
}
}
}
struct LightCallGraph {
graph: Graph<LightGraphNode, usize>
}
impl LightCallGraph {
fn new() -> LightCallGraph {
LightCallGraph {
graph: Graph::new(),
}
}
pub fn clear(&mut self) {
self.graph.clear()
}
fn find(&mut self, callsite: CallSiteId) -> Option<NodeIndex> {
for id in self.graph.node_indices() {
if self.graph[id].callsite == callsite {
return Some(id);
}
}
return None;
}
pub fn add_node(&mut self, callsite: CallSiteId) {
if self.find(callsite).is_none() {
self.graph.add_node(LightGraphNode::new(callsite));
}
}
pub fn increase_call_count(&mut self, parent: CallSiteId, child: CallSiteId) {
let parent = self.find(parent).expect("missing node for parent");
let child = self.find(child).expect("missing node for child");
if let Some(edge) = self.graph.find_edge(parent, child) {
let count = self
.graph
.edge_weight_mut(edge)
.expect("failed to get edge weights");
*count += 1;
} else {
self.graph.add_edge(parent, child, 1);
}
}
pub fn increase_timing(&mut self, span: CallSiteId, time: Duration) {
let id = self.find(span).expect("missing node");
self.graph[id].elapsed += time;
self.graph[id].called += 1;
}
}
pub fn clear_collected_data() {
CALL_GRAPH.lock().expect("poisoned mutex").clear();
}
pub fn enable_data_collection(enabled: bool) {
COLLECTION_ENABLED.store(enabled, Ordering::Release);
}
pub fn get_full_graph() -> FullCallGraph {
let graph = CALL_GRAPH.lock().expect("poisoned mutex");
let mut all_callsites = BTreeMap::new();
crate::traverse_registered_callsite(|callsite| {
all_callsites.insert(callsite.id(), callsite);
});
let graph = graph.graph.map(|index, node| {
TimedSpan::new(node, index.index(), all_callsites[&node.callsite])
}, |_, &edge| edge);
return FullCallGraph {
graph: graph
};
}
pub struct TimedSpan {
pub id: usize,
pub callsite: &'static CallSite,
pub elapsed: Duration,
pub called: u32,
}
impl TimedSpan {
fn new(node: &LightGraphNode, id: usize, callsite: &'static CallSite) -> TimedSpan {
TimedSpan {
id: id,
callsite: callsite,
elapsed: node.elapsed,
called: node.called,
}
}
}
impl std::fmt::Display for TimedSpan {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} ran for {:?}, called {} times",
self.callsite.full_name(), self.elapsed, self.called
)
}
}
pub struct FullCallGraph {
graph: Graph<TimedSpan, usize>
}
pub struct Calls {
pub caller: usize,
pub callee: usize,
pub count: usize,
}
impl FullCallGraph {
pub fn spans(&self) -> impl Iterator<Item = &TimedSpan> {
self.graph.raw_nodes().iter().map(|node| &node.weight)
}
pub fn calls(&self) -> impl Iterator<Item = Calls> + '_ {
self.graph.raw_edges().iter().map(|edge| Calls {
caller: edge.target().index(),
callee: edge.source().index(),
count: edge.weight,
})
}
pub fn as_dot(&self) -> String {
petgraph::dot::Dot::new(&self.graph).to_string()
}
#[cfg(feature = "table")]
pub fn as_table(&self) -> String {
self.as_table_impl(false)
}
#[cfg(feature = "table")]
pub fn as_short_table(&self) -> String {
self.as_table_impl(true)
}
#[cfg(feature = "table")]
fn as_table_impl(&self, short_names: bool) -> String {
use petgraph::Direction;
use term_table::row::Row;
use term_table::table_cell::{Alignment, TableCell};
let mut names = BTreeMap::new();
for node in self.graph.node_weights() {
if short_names {
names.insert(node.id, node.callsite.name().to_string());
} else {
names.insert(node.id, node.callsite.full_name());
}
}
let mut table = term_table::Table::new();
table.style = term_table::TableStyle::extended();
table.add_row(Row::new(vec![
"id",
"span name ",
"call count",
"called by",
"total",
"mean",
]));
for &node_id in petgraph::algo::kosaraju_scc(&self.graph)
.iter()
.rev()
.flatten()
{
let node = &self.graph[node_id];
let mut called_by = vec![];
for other in self.graph.neighbors_directed(node_id, Direction::Incoming) {
called_by.push(self.graph[other].id.to_string());
}
let called_by = if !called_by.is_empty() {
called_by.join(", ")
} else {
"—".into()
};
let mean = node.elapsed / node.called;
let warn = if mean < Duration::from_nanos(1500) { " ⚠️ " } else { "" };
table.add_row(Row::new(vec![
TableCell::new_with_alignment(node.id, 1, Alignment::Right),
TableCell::new(&names[&node.id]),
TableCell::new_with_alignment(node.called, 1, Alignment::Right),
TableCell::new_with_alignment(called_by, 1, Alignment::Right),
TableCell::new_with_alignment(
&format!("{:.2?}", node.elapsed),
1,
Alignment::Right,
),
TableCell::new_with_alignment(
&format!("{:.2?}{}", mean, warn),
1,
Alignment::Right,
),
]));
}
return table.render();
}
#[cfg(feature = "json")]
pub fn as_json(&self) -> String {
let mut spans = json::JsonValue::new_object();
for span in self.spans() {
spans[&span.callsite.full_name()] = json::object! {
"id" => span.id,
"elapsed" => format!("{:?}", span.elapsed),
"called" => span.called,
};
}
let mut all_calls = json::JsonValue::new_array();
for call in self.calls() {
all_calls.push(json::object! {
"caller" => call.caller,
"callee" => call.callee,
"count" => call.count,
}).expect("failed to add edge information to JSON");
}
return json::stringify(json::object! {
"timings" => spans,
"calls" => all_calls,
});
}
}