Skip to main content

polars_io/
metrics.rs

1use std::sync::Arc;
2
3use polars_utils::live_timer::{LiveTimer, LiveTimerSession};
4use polars_utils::relaxed_cell::RelaxedCell;
5
6#[derive(Debug, Default, Clone)]
7pub struct IOMetrics {
8    pub io_timer: LiveTimer,
9    pub query_io_timers: Option<QueryIOTimers>,
10    pub bytes_requested: RelaxedCell<u64>,
11    pub bytes_received: RelaxedCell<u64>,
12    pub bytes_sent: RelaxedCell<u64>,
13}
14
15#[derive(Debug, Default, Clone)]
16pub struct QueryIOTimers {
17    pub total: LiveTimer,
18    pub rx: LiveTimer,
19    pub tx: LiveTimer,
20}
21
22pub struct IOSession {
23    _node: LiveTimerSession,
24    _query: Option<QueryIOSession>,
25}
26
27struct QueryIOSession {
28    _direction: LiveTimerSession,
29    _total: LiveTimerSession,
30}
31
32#[derive(Clone, Copy)]
33enum IODirection {
34    Rx,
35    Tx,
36}
37
38impl IOMetrics {
39    fn start_session(&self, direction: IODirection) -> IOSession {
40        let query = self.query_io_timers.as_ref().map(|timers| {
41            let total = timers.total.start_session();
42            let direction_timer = match direction {
43                IODirection::Rx => &timers.rx,
44                IODirection::Tx => &timers.tx,
45            };
46            QueryIOSession {
47                _direction: direction_timer.start_session(),
48                _total: total,
49            }
50        });
51
52        IOSession {
53            _node: self.io_timer.start_session(),
54            _query: query,
55        }
56    }
57}
58
59#[derive(Debug, Clone)]
60pub struct OptIOMetrics(pub Option<Arc<IOMetrics>>);
61
62impl OptIOMetrics {
63    pub fn start_rx_session(&self) -> Option<IOSession> {
64        self.0.as_ref().map(|x| x.start_session(IODirection::Rx))
65    }
66
67    pub fn start_tx_session(&self) -> Option<IOSession> {
68        self.0.as_ref().map(|x| x.start_session(IODirection::Tx))
69    }
70
71    pub fn add_bytes_requested(&self, bytes_requested: u64) {
72        self.0
73            .as_ref()
74            .map(|x| x.bytes_requested.fetch_add(bytes_requested));
75    }
76
77    pub fn add_bytes_received(&self, bytes_received: u64) {
78        self.0
79            .as_ref()
80            .map(|x| x.bytes_received.fetch_add(bytes_received));
81    }
82
83    pub fn add_bytes_sent(&self, bytes_sent: u64) {
84        self.0.as_ref().map(|x| x.bytes_sent.fetch_add(bytes_sent));
85    }
86
87    pub async fn record_io_read<F, O>(&self, num_bytes: u64, fut: F) -> O
88    where
89        F: Future<Output = O>,
90    {
91        self.add_bytes_requested(num_bytes);
92
93        let io_session = self.start_rx_session();
94
95        let out = fut.await;
96
97        drop(io_session);
98
99        self.add_bytes_received(num_bytes);
100
101        out
102    }
103
104    /// [`Self::record_io_read`] for a read done on the calling thread.
105    pub fn record_io_read_blocking<F, O>(&self, num_bytes: u64, f: F) -> O
106    where
107        F: FnOnce() -> O,
108    {
109        self.add_bytes_requested(num_bytes);
110
111        let io_session = self.start_rx_session();
112
113        let out = f();
114
115        drop(io_session);
116
117        self.add_bytes_received(num_bytes);
118
119        out
120    }
121
122    pub async fn record_bytes_tx<F, O>(&self, num_bytes: u64, fut: F) -> O
123    where
124        F: Future<Output = O>,
125    {
126        let io_session = self.start_tx_session();
127
128        let out = fut.await;
129
130        drop(io_session);
131
132        self.add_bytes_sent(num_bytes);
133
134        out
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use std::pin::pin;
141    use std::task::{Context, Poll, Waker};
142    use std::time::Duration;
143
144    use super::*;
145
146    const IO_TIME: Duration = Duration::from_millis(10);
147
148    fn tracked(timers: &QueryIOTimers) -> OptIOMetrics {
149        OptIOMetrics(Some(Arc::new(IOMetrics {
150            query_io_timers: Some(timers.clone()),
151            ..Default::default()
152        })))
153    }
154
155    fn node_ns(metrics: &OptIOMetrics) -> u64 {
156        metrics.0.as_ref().unwrap().io_timer.total_time_live_ns()
157    }
158
159    /// Polls a future that completes without waiting.
160    fn run_ready<F: Future>(fut: F) -> F::Output {
161        match pin!(fut).poll(&mut Context::from_waker(Waker::noop())) {
162            Poll::Ready(out) => out,
163            Poll::Pending => unreachable!(),
164        }
165    }
166
167    #[test]
168    fn an_rx_session_ticks_the_node_rx_and_total_timers() {
169        let timers = QueryIOTimers::default();
170        let metrics = tracked(&timers);
171
172        let session = metrics.start_rx_session();
173        std::thread::sleep(IO_TIME);
174        drop(session);
175
176        let node = node_ns(&metrics);
177        assert!(node > 0);
178        assert!(timers.rx.total_time_live_ns() >= node);
179        assert!(timers.total.total_time_live_ns() >= timers.rx.total_time_live_ns());
180        assert_eq!(timers.tx.total_time_live_ns(), 0);
181    }
182
183    #[test]
184    fn a_tx_session_ticks_the_node_tx_and_total_timers() {
185        let timers = QueryIOTimers::default();
186        let metrics = tracked(&timers);
187
188        let session = metrics.start_tx_session();
189        std::thread::sleep(IO_TIME);
190        drop(session);
191
192        let node = node_ns(&metrics);
193        assert!(node > 0);
194        assert!(timers.tx.total_time_live_ns() >= node);
195        assert!(timers.total.total_time_live_ns() >= timers.tx.total_time_live_ns());
196        assert_eq!(timers.rx.total_time_live_ns(), 0);
197    }
198
199    #[test]
200    fn reads_are_timed_as_rx() {
201        let timers = QueryIOTimers::default();
202        let metrics = tracked(&timers);
203
204        metrics.record_io_read_blocking(1, || std::thread::sleep(IO_TIME));
205        run_ready(metrics.record_io_read(1, async { std::thread::sleep(IO_TIME) }));
206
207        assert!(timers.rx.total_time_live_ns() >= 2 * IO_TIME.as_nanos() as u64);
208        assert_eq!(timers.tx.total_time_live_ns(), 0);
209    }
210
211    #[test]
212    fn sends_are_timed_as_tx() {
213        let timers = QueryIOTimers::default();
214        let metrics = tracked(&timers);
215
216        run_ready(metrics.record_bytes_tx(1, async { std::thread::sleep(IO_TIME) }));
217
218        assert!(timers.tx.total_time_live_ns() >= IO_TIME.as_nanos() as u64);
219        assert_eq!(timers.rx.total_time_live_ns(), 0);
220    }
221
222    #[test]
223    fn without_query_timers_only_the_node_timer_ticks() {
224        let metrics = OptIOMetrics(Some(Arc::default()));
225
226        let session = metrics.start_rx_session();
227        std::thread::sleep(IO_TIME);
228        drop(session);
229
230        assert!(node_ns(&metrics) > 0);
231    }
232
233    #[test]
234    fn untracked_io_starts_no_session() {
235        let metrics = OptIOMetrics(None);
236
237        assert!(metrics.start_rx_session().is_none());
238        assert!(metrics.start_tx_session().is_none());
239    }
240}