use std::fmt::Write;
use std::sync::Arc;
use grafeo_common::types::{LogicalType, Value};
use grafeo_core::execution::profile::{ProfileStats, SharedProfileStats};
use parking_lot::Mutex;
use super::plan::LogicalOperator;
use crate::database::QueryResult;
#[derive(Debug)]
pub struct ProfileNode {
pub name: String,
pub label: String,
pub stats: SharedProfileStats,
pub children: Vec<ProfileNode>,
}
pub struct ProfileEntry {
pub name: String,
pub label: String,
pub stats: SharedProfileStats,
}
impl ProfileEntry {
pub fn new(name: &str, label: String) -> (Self, SharedProfileStats) {
let stats = Arc::new(Mutex::new(ProfileStats::default()));
let entry = Self {
name: name.to_string(),
label,
stats: Arc::clone(&stats),
};
(entry, stats)
}
}
pub fn build_profile_tree(
logical: &LogicalOperator,
entries: &mut impl Iterator<Item = ProfileEntry>,
) -> ProfileNode {
let children: Vec<ProfileNode> = logical
.children()
.into_iter()
.map(|child| build_profile_tree(child, entries))
.collect();
let entry = entries
.next()
.expect("profile entry count must match logical operator count");
ProfileNode {
name: entry.name,
label: entry.label,
stats: entry.stats,
children,
}
}
pub fn profile_result(root: &ProfileNode, total_time_ms: f64) -> QueryResult {
let mut output = String::new();
format_node(&mut output, root, 0);
let _ = writeln!(output);
let _ = write!(output, "Total time: {total_time_ms:.2}ms");
QueryResult {
columns: vec!["profile".to_string()],
column_types: vec![LogicalType::String],
rows: vec![vec![Value::String(output.into())]],
execution_time_ms: Some(total_time_ms),
rows_scanned: None,
status_message: None,
gql_status: grafeo_common::utils::GqlStatus::SUCCESS,
}
}
fn format_node(out: &mut String, node: &ProfileNode, depth: usize) {
let indent = " ".repeat(depth);
let self_time_ns = self_time_ns(node);
let self_time_ms = self_time_ns as f64 / 1_000_000.0;
let rows_out = node.stats.lock().rows_out;
let _ = writeln!(
out,
"{indent}{name} ({label}) rows={rows} time={time:.2}ms",
name = node.name,
label = node.label,
rows = rows_out,
time = self_time_ms,
);
for child in &node.children {
format_node(out, child, depth + 1);
}
}
fn self_time_ns(node: &ProfileNode) -> u64 {
let own_time = node.stats.lock().time_ns;
let child_time: u64 = node.children.iter().map(|c| c.stats.lock().time_ns).sum();
own_time.saturating_sub(child_time)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::query::plan::{
FilterOp, LogicalExpression, LogicalOperator, NodeScanOp, ReturnItem, ReturnOp,
};
fn three_level_plan() -> LogicalOperator {
LogicalOperator::Return(ReturnOp {
items: vec![ReturnItem {
expression: LogicalExpression::Variable("n".to_string()),
alias: None,
}],
distinct: false,
input: Box::new(LogicalOperator::Filter(FilterOp {
predicate: LogicalExpression::Literal(grafeo_common::types::Value::Bool(true)),
input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
variable: "n".to_string(),
label: Some("Person".to_string()),
input: None,
})),
pushdown_hint: None,
})),
})
}
#[test]
fn test_profile_tree_post_order() {
let plan = three_level_plan();
let (scan_entry, scan_stats) = ProfileEntry::new("NodeScan", "n:Person".to_string());
let (filter_entry, filter_stats) = ProfileEntry::new("Filter", "true".to_string());
let (return_entry, return_stats) = ProfileEntry::new("Return", "n".to_string());
scan_stats.lock().time_ns = 100;
scan_stats.lock().rows_out = 10;
filter_stats.lock().time_ns = 250; filter_stats.lock().rows_out = 10;
return_stats.lock().time_ns = 400; return_stats.lock().rows_out = 10;
let mut iter = vec![scan_entry, filter_entry, return_entry].into_iter();
let root = build_profile_tree(&plan, &mut iter);
assert_eq!(root.name, "Return");
assert_eq!(root.children.len(), 1);
let filter = &root.children[0];
assert_eq!(filter.name, "Filter");
assert_eq!(filter.children.len(), 1);
let scan = &filter.children[0];
assert_eq!(scan.name, "NodeScan");
assert!(scan.children.is_empty());
assert_eq!(self_time_ns(&root), 150);
assert_eq!(self_time_ns(filter), 150);
assert_eq!(self_time_ns(scan), 100);
}
#[test]
fn test_profile_result_formatting_and_saturating_self_time() {
let plan = LogicalOperator::Filter(FilterOp {
predicate: LogicalExpression::Literal(grafeo_common::types::Value::Bool(true)),
input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
variable: "n".to_string(),
label: None,
input: None,
})),
pushdown_hint: None,
});
let (scan_entry, scan_stats) = ProfileEntry::new("NodeScan", "n:*".to_string());
let (filter_entry, filter_stats) = ProfileEntry::new("Filter", "true".to_string());
scan_stats.lock().time_ns = 500;
filter_stats.lock().time_ns = 100;
filter_stats.lock().rows_out = 3;
let mut iter = vec![scan_entry, filter_entry].into_iter();
let root = build_profile_tree(&plan, &mut iter);
assert_eq!(self_time_ns(&root), 0);
let result = profile_result(&root, 1.23);
assert_eq!(result.columns, vec!["profile".to_string()]);
let text = match &result.rows[0][0] {
grafeo_common::types::Value::String(s) => s.to_string(),
other => panic!("expected String, got {other:?}"),
};
assert!(text.contains("Filter"));
assert!(text.contains("NodeScan"));
assert!(text.contains("Total time: 1.23ms"));
}
}