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 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 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}