1use std::fmt::Write;
8use std::sync::Arc;
9
10use grafeo_common::types::{LogicalType, Value};
11use grafeo_core::execution::profile::{ProfileStats, SharedProfileStats};
12use parking_lot::Mutex;
13
14use super::plan::LogicalOperator;
15use crate::database::QueryResult;
16
17#[derive(Debug)]
19pub struct ProfileNode {
20 pub name: String,
22 pub label: String,
24 pub stats: SharedProfileStats,
26 pub children: Vec<ProfileNode>,
28}
29
30pub struct ProfileEntry {
32 pub name: String,
34 pub label: String,
36 pub stats: SharedProfileStats,
38}
39
40impl ProfileEntry {
41 pub fn new(name: &str, label: String) -> (Self, SharedProfileStats) {
43 let stats = Arc::new(Mutex::new(ProfileStats::default()));
44 let entry = Self {
45 name: name.to_string(),
46 label,
47 stats: Arc::clone(&stats),
48 };
49 (entry, stats)
50 }
51}
52
53pub fn build_profile_tree(
63 logical: &LogicalOperator,
64 entries: &mut impl Iterator<Item = ProfileEntry>,
65) -> ProfileNode {
66 let children: Vec<ProfileNode> = logical
68 .children()
69 .into_iter()
70 .map(|child| build_profile_tree(child, entries))
71 .collect();
72
73 let entry = entries.next().unwrap_or_else(|| {
75 panic!(
76 "profile entry count must match logical operator count: \
77 ran out of entries while building tree node for logical \
78 operator: {:?} (label='{}')",
79 std::mem::discriminant(logical),
80 logical.display_label(),
81 )
82 });
83
84 ProfileNode {
85 name: entry.name,
86 label: entry.label,
87 stats: entry.stats,
88 children,
89 }
90}
91
92pub fn profile_result(root: &ProfileNode, total_time_ms: f64) -> QueryResult {
95 let mut output = String::new();
96 format_node(&mut output, root, 0);
97 let _ = writeln!(output);
98 let _ = write!(output, "Total time: {total_time_ms:.2}ms");
99
100 QueryResult {
101 columns: vec!["profile".to_string()],
102 column_types: vec![LogicalType::String],
103 rows: vec![vec![Value::String(output.into())]],
104 execution_time_ms: Some(total_time_ms),
105 rows_scanned: None,
106 status_message: None,
107 gql_status: grafeo_common::utils::GqlStatus::SUCCESS,
108 }
109}
110
111fn format_node(out: &mut String, node: &ProfileNode, depth: usize) {
113 let indent = " ".repeat(depth);
114
115 let self_time_ns = self_time_ns(node);
117 let self_time_ms = self_time_ns as f64 / 1_000_000.0;
118
119 let rows_out = node.stats.lock().rows_out;
120
121 let _ = writeln!(
122 out,
123 "{indent}{name} ({label}) rows={rows} time={time:.2}ms",
124 name = node.name,
125 label = node.label,
126 rows = rows_out,
127 time = self_time_ms,
128 );
129
130 for child in &node.children {
131 format_node(out, child, depth + 1);
132 }
133}
134
135fn self_time_ns(node: &ProfileNode) -> u64 {
137 let own_time = node.stats.lock().time_ns;
138 let child_time: u64 = node.children.iter().map(|c| c.stats.lock().time_ns).sum();
139 own_time.saturating_sub(child_time)
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::query::plan::{
146 FilterOp, LogicalExpression, LogicalOperator, NodeScanOp, ReturnItem, ReturnOp,
147 };
148
149 fn three_level_plan() -> LogicalOperator {
151 LogicalOperator::Return(ReturnOp {
152 items: vec![ReturnItem {
153 expression: LogicalExpression::Variable("n".to_string()),
154 alias: None,
155 }],
156 distinct: false,
157 input: Box::new(LogicalOperator::Filter(FilterOp {
158 predicate: LogicalExpression::Literal(grafeo_common::types::Value::Bool(true)),
159 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
160 variable: "n".to_string(),
161 label: Some("Person".to_string()),
162 input: None,
163 })),
164 pushdown_hint: None,
165 })),
166 })
167 }
168
169 #[test]
172 fn test_profile_tree_post_order() {
173 let plan = three_level_plan();
174
175 let (scan_entry, scan_stats) = ProfileEntry::new("NodeScan", "n:Person".to_string());
178 let (filter_entry, filter_stats) = ProfileEntry::new("Filter", "true".to_string());
179 let (return_entry, return_stats) = ProfileEntry::new("Return", "n".to_string());
180
181 scan_stats.lock().time_ns = 100;
182 scan_stats.lock().rows_out = 10;
183 filter_stats.lock().time_ns = 250; filter_stats.lock().rows_out = 10;
185 return_stats.lock().time_ns = 400; return_stats.lock().rows_out = 10;
187
188 let mut iter = vec![scan_entry, filter_entry, return_entry].into_iter();
189 let root = build_profile_tree(&plan, &mut iter);
190
191 assert_eq!(root.name, "Return");
193 assert_eq!(root.children.len(), 1);
194 let filter = &root.children[0];
195 assert_eq!(filter.name, "Filter");
196 assert_eq!(filter.children.len(), 1);
197 let scan = &filter.children[0];
198 assert_eq!(scan.name, "NodeScan");
199 assert!(scan.children.is_empty());
200
201 assert_eq!(self_time_ns(&root), 150);
203 assert_eq!(self_time_ns(filter), 150);
204 assert_eq!(self_time_ns(scan), 100);
206 }
207
208 #[test]
210 fn test_profile_result_formatting_and_saturating_self_time() {
211 let plan = LogicalOperator::Filter(FilterOp {
214 predicate: LogicalExpression::Literal(grafeo_common::types::Value::Bool(true)),
215 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
216 variable: "n".to_string(),
217 label: None,
218 input: None,
219 })),
220 pushdown_hint: None,
221 });
222 let (scan_entry, scan_stats) = ProfileEntry::new("NodeScan", "n:*".to_string());
223 let (filter_entry, filter_stats) = ProfileEntry::new("Filter", "true".to_string());
224 scan_stats.lock().time_ns = 500;
226 filter_stats.lock().time_ns = 100;
227 filter_stats.lock().rows_out = 3;
228
229 let mut iter = vec![scan_entry, filter_entry].into_iter();
230 let root = build_profile_tree(&plan, &mut iter);
231 assert_eq!(self_time_ns(&root), 0);
232
233 let result = profile_result(&root, 1.23);
234 assert_eq!(result.columns, vec!["profile".to_string()]);
235 let text = match &result.rows[0][0] {
236 grafeo_common::types::Value::String(s) => s.to_string(),
237 other => panic!("expected String, got {other:?}"),
238 };
239 assert!(text.contains("Filter"));
240 assert!(text.contains("NodeScan"));
241 assert!(text.contains("Total time: 1.23ms"));
242 }
243}