duckdb/profiling.rs
1//! Query profiling support for DuckDB.
2//!
3//! This module provides access to DuckDB's query profiling infrastructure,
4//! allowing you to inspect per-operator metrics for executed queries.
5//!
6//! # Usage
7//!
8//! Profiling must be explicitly enabled on a connection before metrics are
9//! collected. Use the `enable_profiling` and `profiling_mode` PRAGMAs to
10//! configure it:
11//!
12//! ```rust,no_run
13//! # use duckdb::Connection;
14//! let conn = Connection::open_in_memory().unwrap();
15//!
16//! // Prevents DuckDB from printing profiling output to stdout after each query,
17//! // which is the default behavior when profiling is enabled.
18//! conn.execute("PRAGMA enable_profiling = 'no_output'", []).unwrap();
19//!
20//! // Enables standard profiling mode, which collects a comprehensive set of metrics for each operator in the query plan.
21//! // There is also `detailed` mode for even more fine-grained metrics. See DuckDB's documentation for more details.
22//! conn.execute("PRAGMA profiling_mode = 'standard'", []).unwrap();
23//!
24//! // Now we can execute a query and retrieve its profiling info.
25//! conn.execute("SELECT 42", []).unwrap();
26//!
27//! let info = conn.get_profiling_info().expect("profiling should be enabled");
28//! println!("rows returned: {}", info.metrics["ROWS_RETURNED"]);
29//! ```
30//!
31//! # Structure
32//!
33//! [`ProfilingInfo`] is a tree that mirrors DuckDB's query plan. The root node
34//! (`QUERY_ROOT`) holds metrics for the entire query (e.g. total `LATENCY` and
35//! `ROWS_RETURNED`). Each child node represents a plan operator and carries its
36//! own metrics such as `OPERATOR_NAME` and `OPERATOR_CARDINALITY`.
37
38use std::collections::HashMap;
39
40use crate::{Connection, inner_connection::InnerConnection};
41
42/// [`ProfilingInfo`] is a recursive type containing metrics for each node in DuckDB's query plan.
43/// There are two types of nodes: the "QUERY_ROOT" and "OPERATOR" nodes.
44/// The "QUERY_ROOT" refers exclusively to the top-level node; its metrics are measured over the entire query.
45/// The "OPERATOR" nodes refer to the individual operators in the query plan.
46#[derive(Debug, Clone)]
47pub struct ProfilingInfo {
48 /// The metrics for the node, represented as a map from metric name to metric value.
49 /// The actual format and units of the metric varies between metric kinds. For example,
50 /// - "ROWS_RETURNED" is an integer, formatted as a string
51 /// - "LATENCY" is a double-floating point number measuring the total query time in seconds, formatted as a string
52 /// - "OPERATOR_NAME" is a string
53 /// - "EXTRA_INFO" is a map of its own encoded as a string
54 pub metrics: HashMap<String, String>,
55 /// The children of the node and their respective metrics.
56 pub children: Vec<ProfilingInfo>,
57}
58
59impl ProfilingInfo {
60 /// # Safety
61 /// `info` must be a valid (or NULL) pointer obtained from [`libduckdb_sys::duckdb_get_profiling_info`].
62 fn from_raw(info: libduckdb_sys::duckdb_profiling_info) -> Option<Self> {
63 if info.is_null() {
64 return None;
65 }
66
67 // Extract metrics
68 let mut map = unsafe { libduckdb_sys::duckdb_profiling_info_get_metrics(info) };
69 let map_size = unsafe { libduckdb_sys::duckdb_get_map_size(map) };
70
71 let mut metrics = HashMap::<String, String>::with_capacity(map_size as usize);
72
73 for i in 0..map_size {
74 let mut key = unsafe { libduckdb_sys::duckdb_get_map_key(map, i) };
75 let mut val = unsafe { libduckdb_sys::duckdb_get_map_value(map, i) };
76
77 let key_mem = unsafe { libduckdb_sys::duckdb_get_varchar(key) };
78 let val_mem = unsafe { libduckdb_sys::duckdb_get_varchar(val) };
79
80 let key_str = unsafe { std::ffi::CStr::from_ptr(key_mem) }
81 .to_string_lossy()
82 .to_string();
83 let val_str = unsafe { std::ffi::CStr::from_ptr(val_mem) }
84 .to_string_lossy()
85 .to_string();
86
87 metrics.insert(key_str, val_str);
88
89 unsafe { libduckdb_sys::duckdb_free(key_mem as *mut std::ffi::c_void) };
90 unsafe { libduckdb_sys::duckdb_free(val_mem as *mut std::ffi::c_void) };
91
92 unsafe { libduckdb_sys::duckdb_destroy_value(&mut key) };
93 unsafe { libduckdb_sys::duckdb_destroy_value(&mut val) };
94 }
95
96 unsafe { libduckdb_sys::duckdb_destroy_value(&mut map) };
97
98 // Extract children
99 let child_count = unsafe { libduckdb_sys::duckdb_profiling_info_get_child_count(info) };
100 let mut children = Vec::with_capacity(child_count as usize);
101 for i in 0..child_count {
102 let child_info = unsafe { Self::from_raw(libduckdb_sys::duckdb_profiling_info_get_child(info, i)) };
103 if let Some(info) = child_info {
104 children.push(info);
105 }
106 }
107
108 Some(ProfilingInfo { metrics, children })
109 }
110}
111
112impl InnerConnection {
113 /// Retrieves the [`ProfilingInfo`] for the last executed query, if profiling is enabled.
114 pub fn get_profiling_info(&self) -> Option<ProfilingInfo> {
115 let info = unsafe { libduckdb_sys::duckdb_get_profiling_info(self.con) };
116 ProfilingInfo::from_raw(info)
117 }
118}
119
120impl Connection {
121 /// Retrieves the [`ProfilingInfo`] for the last executed query, if profiling is enabled.
122 pub fn get_profiling_info(&self) -> Option<ProfilingInfo> {
123 self.db.borrow().get_profiling_info()
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use crate::Connection;
130
131 #[test]
132 fn test_profiling_info() {
133 let conn = Connection::open_in_memory().unwrap();
134
135 conn.execute("SELECT 1", []).unwrap();
136 let info = conn.get_profiling_info();
137
138 assert!(
139 info.is_none(),
140 "Profiling info should be None when profiling is not enabled"
141 );
142
143 conn.execute("PRAGMA enable_profiling = 'no_output'", []).unwrap();
144 conn.execute("PRAGMA profiling_mode = 'standard'", []).unwrap();
145 conn.execute("SELECT 1", []).unwrap();
146 let info = conn.get_profiling_info();
147
148 assert!(info.is_some(), "Metrics should be present when profiling is enabled");
149 let info = info.unwrap();
150
151 assert!(!info.metrics.is_empty(), "Metrics should not be empty");
152 assert!(
153 !info.children.is_empty(),
154 "There should be at least one child for a simple query"
155 );
156
157 assert!(
158 info.metrics.contains_key("ROWS_RETURNED"),
159 "Metrics should contain ROWS_RETURNED"
160 );
161 assert!(
162 info.metrics.get("ROWS_RETURNED").unwrap() == "1",
163 "ROWS_RETURNED should be 1"
164 );
165 }
166}