Skip to main content

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}