1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::time::Duration;
4
5#[derive(Debug, Clone, Serialize, Deserialize, Default)]
10pub struct ExecutionTiming {
11 #[serde(rename = "iopub.status.busy", skip_serializing_if = "Option::is_none")]
13 pub status_busy: Option<DateTime<Utc>>,
14
15 #[serde(
17 rename = "iopub.execute_input",
18 skip_serializing_if = "Option::is_none"
19 )]
20 pub execute_input: Option<DateTime<Utc>>,
21
22 #[serde(
24 rename = "shell.execute_reply.started",
25 skip_serializing_if = "Option::is_none"
26 )]
27 pub reply_started: Option<DateTime<Utc>>,
28
29 #[serde(
31 rename = "shell.execute_reply",
32 skip_serializing_if = "Option::is_none"
33 )]
34 pub reply: Option<DateTime<Utc>>,
35
36 #[serde(rename = "iopub.status.idle", skip_serializing_if = "Option::is_none")]
38 pub status_idle: Option<DateTime<Utc>>,
39}
40
41impl ExecutionTiming {
42 pub fn total_duration(&self) -> Option<Duration> {
47 match (self.status_busy, self.status_idle) {
48 (Some(busy), Some(idle)) => {
49 let duration = idle.signed_duration_since(busy);
50 duration.to_std().ok()
51 }
52 _ => None,
53 }
54 }
55
56 pub fn execution_duration(&self) -> Option<Duration> {
60 match (self.reply_started, self.reply) {
61 (Some(started), Some(finished)) => {
62 let duration = finished.signed_duration_since(started);
63 duration.to_std().ok()
64 }
65 _ => None,
66 }
67 }
68
69 pub fn output_overhead(&self) -> Option<Duration> {
73 match (self.reply, self.status_idle) {
74 (Some(reply), Some(idle)) => {
75 let duration = idle.signed_duration_since(reply);
76 duration.to_std().ok()
77 }
78 _ => None,
79 }
80 }
81
82 pub fn from_duration(start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
87 Self {
88 status_busy: Some(start),
89 execute_input: Some(start),
90 reply_started: Some(start),
91 reply: Some(end),
92 status_idle: Some(end),
93 }
94 }
95
96 pub fn has_timing(&self) -> bool {
98 self.status_busy.is_some() && self.status_idle.is_some()
99 }
100}
101
102#[derive(Debug, Clone, Serialize)]
104pub struct CellTiming {
105 pub cell_index: usize,
107 pub execution_count: Option<i32>,
109 pub timing: ExecutionTiming,
111 pub duration: Option<Duration>,
113}
114
115impl CellTiming {
116 pub fn new(cell_index: usize, execution_count: Option<i32>, timing: ExecutionTiming) -> Self {
118 let duration = timing.total_duration();
119 Self {
120 cell_index,
121 execution_count,
122 timing,
123 duration,
124 }
125 }
126
127 pub fn duration_seconds(&self) -> Option<f64> {
129 self.duration.map(|d| d.as_secs_f64())
130 }
131}
132
133#[derive(Debug, Clone, Serialize)]
135pub struct TimingSummary {
136 pub total_duration: Duration,
138 pub cells_executed: usize,
140 pub average_duration: Duration,
142 pub max_duration: Duration,
144 pub max_duration_cell: usize,
146 pub min_duration: Duration,
148 pub min_duration_cell: usize,
150}
151
152impl TimingSummary {
153 pub fn from_cells(cells: &[CellTiming]) -> Option<Self> {
155 if cells.is_empty() {
156 return None;
157 }
158
159 let cells_with_duration: Vec<_> = cells.iter().filter(|c| c.duration.is_some()).collect();
160
161 if cells_with_duration.is_empty() {
162 return None;
163 }
164
165 let total_duration: Duration = cells_with_duration.iter().filter_map(|c| c.duration).sum();
166
167 let average_duration = total_duration
168 .checked_div(cells_with_duration.len() as u32)
169 .unwrap_or(Duration::ZERO);
170
171 let first_cell = cells_with_duration.first()?;
174
175 let (max_cell, min_cell) =
176 cells_with_duration
177 .iter()
178 .fold((first_cell, first_cell), |(max, min), cell| {
179 let new_max = if cell.duration > max.duration {
180 cell
181 } else {
182 max
183 };
184 let new_min = if cell.duration < min.duration {
185 cell
186 } else {
187 min
188 };
189 (new_max, new_min)
190 });
191
192 let max_duration = max_cell.duration?;
194 let max_duration_cell = max_cell.cell_index;
195
196 let min_duration = min_cell.duration?;
197 let min_duration_cell = min_cell.cell_index;
198
199 Some(Self {
200 total_duration,
201 cells_executed: cells_with_duration.len(),
202 average_duration,
203 max_duration,
204 max_duration_cell,
205 min_duration,
206 min_duration_cell,
207 })
208 }
209}
210
211#[derive(Debug, Clone)]
213pub enum TimingFormat {
214 Seconds,
216 Milliseconds,
218 Human,
220}
221
222pub fn format_duration(duration: Duration, format: &TimingFormat) -> String {
224 match format {
225 TimingFormat::Seconds => {
226 format!("{:.2}s", duration.as_secs_f64())
227 }
228 TimingFormat::Milliseconds => {
229 format!("{}ms", duration.as_millis())
230 }
231 TimingFormat::Human => {
232 let secs = duration.as_secs();
233 let millis = duration.subsec_millis();
234
235 if secs >= 60 {
236 let mins = secs / 60;
237 let remaining_secs = secs % 60;
238 if millis > 0 {
239 format!("{mins}m {remaining_secs}s {millis}ms")
240 } else {
241 format!("{mins}m {remaining_secs}s")
242 }
243 } else if secs > 0 {
244 if millis > 0 {
245 format!("{secs}s {millis}ms")
246 } else {
247 format!("{secs}s")
248 }
249 } else {
250 format!("{millis}ms")
251 }
252 }
253 }
254}
255
256pub fn parse_duration(s: &str) -> Result<Duration, String> {
258 let s = s.trim();
259
260 if let Some(stripped) = s.strip_suffix("ms") {
261 let num = stripped
262 .trim()
263 .parse::<u64>()
264 .map_err(|_| format!("Invalid duration: {s}"))?;
265 Ok(Duration::from_millis(num))
266 } else if let Some(stripped) = s.strip_suffix('s') {
267 let num = stripped
268 .trim()
269 .parse::<f64>()
270 .map_err(|_| format!("Invalid duration: {s}"))?;
271 Ok(Duration::from_secs_f64(num))
272 } else if let Some(stripped) = s.strip_suffix('m') {
273 let num = stripped
274 .trim()
275 .parse::<u64>()
276 .map_err(|_| format!("Invalid duration: {s}"))?;
277 Ok(Duration::from_secs(num * 60))
278 } else {
279 Err(format!(
280 "Invalid duration format: {s}. Use '5s', '500ms', or '2m'"
281 ))
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn test_timing_total_duration() {
291 let timing = ExecutionTiming {
292 status_busy: "2020-01-01T00:00:00Z".parse().ok(),
293 status_idle: "2020-01-01T00:00:05Z".parse().ok(),
294 ..Default::default()
295 };
296
297 assert_eq!(timing.total_duration().map(|d| d.as_secs()), Some(5));
298 }
299
300 #[test]
301 fn test_timing_execution_duration() {
302 let timing = ExecutionTiming {
303 reply_started: "2020-01-01T00:00:00Z".parse().ok(),
304 reply: "2020-01-01T00:00:03Z".parse().ok(),
305 ..Default::default()
306 };
307
308 assert_eq!(timing.execution_duration().map(|d| d.as_secs()), Some(3));
309 }
310
311 #[test]
312 fn test_format_duration_seconds() {
313 let duration = Duration::from_millis(1234);
314 assert_eq!(format_duration(duration, &TimingFormat::Seconds), "1.23s");
315 }
316
317 #[test]
318 fn test_format_duration_milliseconds() {
319 let duration = Duration::from_millis(1234);
320 assert_eq!(
321 format_duration(duration, &TimingFormat::Milliseconds),
322 "1234ms"
323 );
324 }
325
326 #[test]
327 fn test_format_duration_human() {
328 let duration = Duration::from_secs(125);
329 assert_eq!(format_duration(duration, &TimingFormat::Human), "2m 5s");
330
331 let duration = Duration::from_millis(1500);
332 assert_eq!(format_duration(duration, &TimingFormat::Human), "1s 500ms");
333
334 let duration = Duration::from_millis(500);
335 assert_eq!(format_duration(duration, &TimingFormat::Human), "500ms");
336 }
337
338 #[test]
339 fn test_parse_duration() {
340 assert_eq!(parse_duration("5s"), Ok(Duration::from_secs(5)));
341 assert_eq!(parse_duration("500ms"), Ok(Duration::from_millis(500)));
342 assert_eq!(parse_duration("2m"), Ok(Duration::from_secs(120)));
343 assert_eq!(parse_duration("1.5s"), Ok(Duration::from_millis(1500)));
344
345 assert!(parse_duration("invalid").is_err());
346 assert!(parse_duration("5x").is_err());
347 }
348
349 #[test]
350 fn test_timing_summary() {
351 let cells = vec![
352 CellTiming::new(
353 0,
354 Some(1),
355 ExecutionTiming {
356 status_busy: "2020-01-01T00:00:00Z".parse().ok(),
357 status_idle: "2020-01-01T00:00:01Z".parse().ok(),
358 ..Default::default()
359 },
360 ),
361 CellTiming::new(
362 1,
363 Some(2),
364 ExecutionTiming {
365 status_busy: "2020-01-01T00:00:00Z".parse().ok(),
366 status_idle: "2020-01-01T00:00:05Z".parse().ok(),
367 ..Default::default()
368 },
369 ),
370 ];
371
372 let summary = TimingSummary::from_cells(&cells);
373 assert!(summary.is_some(), "Failed to create summary: {summary:?}");
374 if let Some(summary) = summary {
375 assert_eq!(summary.total_duration.as_secs(), 6);
376 assert_eq!(summary.cells_executed, 2);
377 assert_eq!(summary.average_duration.as_secs(), 3);
378 assert_eq!(summary.max_duration.as_secs(), 5);
379 assert_eq!(summary.max_duration_cell, 1);
380 }
381 }
382}