1use super::Camera;
6use crate::gui::Theme;
7use crate::models::GpuPartialOrderTrace;
8use eframe::egui::{self, Color32, Pos2, Rect, Rounding, Stroke, Vec2};
9use std::collections::HashMap;
10
11pub struct TimelineCanvas {
13 pub camera: Camera,
15 activity_colors: HashMap<u32, Color32>,
17 activity_names: HashMap<u32, String>,
19 selected_trace: Option<u64>,
21 pixels_per_sec: f32,
23}
24
25impl Default for TimelineCanvas {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl TimelineCanvas {
32 pub fn new() -> Self {
34 Self {
35 camera: Camera::default(),
36 activity_colors: HashMap::new(),
37 activity_names: HashMap::new(),
38 selected_trace: None,
39 pixels_per_sec: 0.01, }
41 }
42
43 pub fn set_activity_names(&mut self, names: HashMap<u32, String>) {
45 self.activity_names = names;
46 }
47
48 fn ensure_colors(&mut self, activities: &[u32]) {
50 let palette = [
51 Color32::from_rgb(59, 130, 246), Color32::from_rgb(34, 197, 94), Color32::from_rgb(239, 68, 68), Color32::from_rgb(168, 85, 247), Color32::from_rgb(234, 179, 8), Color32::from_rgb(236, 72, 153), Color32::from_rgb(20, 184, 166), Color32::from_rgb(249, 115, 22), ];
60
61 for (i, &activity_id) in activities.iter().enumerate() {
62 self.activity_colors
63 .entry(activity_id)
64 .or_insert_with(|| palette[i % palette.len()]);
65 }
66 }
67
68 fn format_time(seconds: f32) -> String {
70 if seconds >= 3600.0 {
71 let hours = seconds / 3600.0;
72 format!("{:.1}h", hours)
73 } else if seconds >= 60.0 {
74 let mins = seconds / 60.0;
75 format!("{:.1}m", mins)
76 } else {
77 format!("{:.0}s", seconds)
78 }
79 }
80
81 pub fn render(
83 &mut self,
84 ui: &mut egui::Ui,
85 theme: &Theme,
86 traces: &[GpuPartialOrderTrace],
87 rect: Rect,
88 ) {
89 let painter = ui.painter_at(rect);
90
91 self.handle_input(ui, rect);
93
94 self.camera.update(ui.ctx().input(|i| i.stable_dt));
96
97 self.draw_grid(&painter, theme, rect);
99
100 painter.text(
102 Pos2::new(rect.left() + 10.0, rect.top() + 15.0),
103 egui::Align2::LEFT_CENTER,
104 format!("Partial Order Timeline - {} traces", traces.len()),
105 egui::FontId::proportional(14.0),
106 theme.text,
107 );
108
109 let trace_height = 80.0;
111 let header_height = 40.0;
112 let mut y_offset = header_height;
113
114 if traces.is_empty() {
115 painter.text(
116 rect.center(),
117 egui::Align2::CENTER_CENTER,
118 "No partial order traces yet.\nStart the pipeline to see execution patterns.",
119 egui::FontId::proportional(14.0),
120 theme.text_muted,
121 );
122 } else {
123 for trace in traces.iter().take(6) {
124 if trace.activity_count > 0 {
126 self.draw_trace(&painter, theme, trace, rect, y_offset, trace_height);
127 y_offset += trace_height + 10.0;
128 }
129 }
130 }
131
132 self.draw_time_axis(&painter, theme, rect);
134 }
135
136 fn handle_input(&mut self, ui: &mut egui::Ui, rect: Rect) {
138 let response = ui.interact(
139 rect,
140 ui.id().with("timeline_canvas"),
141 egui::Sense::click_and_drag(),
142 );
143
144 if response.dragged() {
146 self.camera.pan_by(response.drag_delta());
147 }
148
149 let scroll = ui.ctx().input(|i| i.raw_scroll_delta.y);
151 if scroll != 0.0 {
152 if let Some(pointer) = ui.ctx().input(|i| i.pointer.hover_pos()) {
153 if rect.contains(pointer) {
154 let zoom_factor = 1.0 + scroll * 0.002;
156 let new_zoom = (self.camera.zoom * zoom_factor).clamp(0.1, 10.0);
157
158 let mouse_rel = pointer.x - rect.left();
160 let scale_change = new_zoom / self.camera.zoom;
161 self.camera.offset.x =
162 mouse_rel - (mouse_rel - self.camera.offset.x) * scale_change;
163
164 self.camera.zoom = new_zoom;
165 }
166 }
167 }
168
169 if response.double_clicked() {
171 self.camera.reset();
172 self.pixels_per_sec = 0.01;
173 }
174 }
175
176 fn draw_grid(&self, painter: &egui::Painter, theme: &Theme, rect: Rect) {
178 let grid_color = theme.panel_bg.linear_multiply(1.5);
179
180 let effective_scale = self.pixels_per_sec * self.camera.zoom;
182
183 let target_pixels = 100.0;
186 let raw_step = target_pixels / effective_scale;
187
188 let grid_step_secs = if raw_step < 60.0 {
190 60.0 } else if raw_step < 300.0 {
192 300.0 } else if raw_step < 600.0 {
194 600.0 } else if raw_step < 1800.0 {
196 1800.0 } else if raw_step < 3600.0 {
198 3600.0 } else if raw_step < 7200.0 {
200 7200.0 } else if raw_step < 21600.0 {
202 21600.0 } else if raw_step < 43200.0 {
204 43200.0 } else {
206 86400.0 };
208
209 let start_time_secs = (-self.camera.offset.x / effective_scale).max(0.0);
210 let end_time_secs = start_time_secs + rect.width() / effective_scale;
211
212 let first_line = (start_time_secs / grid_step_secs).floor() as i64 * grid_step_secs as i64;
214 let mut t = first_line as f32;
215
216 while t <= end_time_secs {
217 let x = rect.left() + self.camera.offset.x + t * effective_scale;
218 if x >= rect.left() && x <= rect.right() {
219 painter.line_segment(
220 [Pos2::new(x, rect.top() + 30.0), Pos2::new(x, rect.bottom())],
221 Stroke::new(1.0, grid_color),
222 );
223 }
224 t += grid_step_secs;
225 }
226
227 for i in 0..8 {
229 let y = rect.top() + 40.0 + i as f32 * 90.0;
230 if y < rect.bottom() {
231 painter.line_segment(
232 [Pos2::new(rect.left(), y), Pos2::new(rect.right(), y)],
233 Stroke::new(1.0, grid_color),
234 );
235 }
236 }
237 }
238
239 fn draw_trace(
241 &mut self,
242 painter: &egui::Painter,
243 theme: &Theme,
244 trace: &GpuPartialOrderTrace,
245 rect: Rect,
246 y_offset: f32,
247 height: f32,
248 ) {
249 let activities: Vec<u32> = trace.activity_ids[..trace.activity_count as usize].to_vec();
251 if activities.is_empty() {
252 return;
253 }
254 self.ensure_colors(&activities);
255
256 let total_duration_ms = trace
258 .end_time
259 .physical_ms
260 .saturating_sub(trace.start_time.physical_ms);
261 let total_duration_secs = (total_duration_ms as f32 / 1000.0).max(1.0);
262
263 let label_width = 90.0;
265 let row_height = (height / activities.len() as f32).min(14.0);
266 let effective_scale = self.pixels_per_sec * self.camera.zoom;
267 let timeline_x_start = rect.left() + label_width;
268
269 painter.text(
271 Pos2::new(rect.left() + 5.0, rect.top() + y_offset),
272 egui::Align2::LEFT_CENTER,
273 format!("Case {}", trace.case_id % 10000),
274 egui::FontId::proportional(10.0),
275 theme.text,
276 );
277
278 painter.text(
280 Pos2::new(rect.left() + 5.0, rect.top() + y_offset + 12.0),
281 egui::Align2::LEFT_CENTER,
282 format!("{} acts", activities.len()),
283 egui::FontId::proportional(9.0),
284 theme.text_muted,
285 );
286
287 if trace.max_width > 1 {
289 painter.text(
290 Pos2::new(rect.left() + 5.0, rect.top() + y_offset + 24.0),
291 egui::Align2::LEFT_CENTER,
292 format!("⇉{}", trace.max_width),
293 egui::FontId::proportional(9.0),
294 Color32::YELLOW,
295 );
296 }
297
298 painter.text(
300 Pos2::new(rect.left() + label_width - 5.0, rect.top() + y_offset),
301 egui::Align2::RIGHT_CENTER,
302 Self::format_time(total_duration_secs),
303 egui::FontId::proportional(9.0),
304 theme.text_muted,
305 );
306
307 let bars_y_start = rect.top() + y_offset + 5.0;
308
309 let timeline_rect = Rect::from_min_size(
311 Pos2::new(timeline_x_start, bars_y_start),
312 Vec2::new(
313 rect.width() - label_width - 10.0,
314 activities.len() as f32 * row_height,
315 ),
316 );
317 painter.rect_filled(
318 timeline_rect,
319 Rounding::same(2.0),
320 theme.panel_bg.linear_multiply(0.6),
321 );
322
323 for (i, &activity_id) in activities.iter().enumerate() {
325 let color = self
326 .activity_colors
327 .get(&activity_id)
328 .copied()
329 .unwrap_or(theme.accent);
330 let row_y = bars_y_start + i as f32 * row_height;
331
332 let start_secs = trace.activity_start_secs[i] as f32;
334 let duration_secs = trace.activity_duration_secs[i] as f32;
335
336 let bar_x = timeline_x_start + self.camera.offset.x + start_secs * effective_scale;
338 let bar_w = (duration_secs * effective_scale).max(8.0); if bar_x + bar_w < rect.left() || bar_x > rect.right() {
342 continue;
343 }
344
345 let clipped_x = bar_x.max(timeline_x_start);
347 let clipped_w = (bar_x + bar_w).min(rect.right() - 5.0) - clipped_x;
348
349 if clipped_w <= 0.0 {
350 continue;
351 }
352
353 let bar_rect = Rect::from_min_size(
354 Pos2::new(clipped_x, row_y + 1.0),
355 Vec2::new(clipped_w, row_height - 2.0),
356 );
357
358 let is_concurrent = self.has_concurrent_activities(trace, i);
360 let fill_color = if is_concurrent {
361 color
362 } else {
363 color.linear_multiply(0.7)
364 };
365
366 painter.rect_filled(bar_rect, Rounding::same(2.0), fill_color);
367
368 if is_concurrent {
370 painter.rect_stroke(
371 bar_rect,
372 Rounding::same(2.0),
373 Stroke::new(1.5, Color32::YELLOW),
374 );
375 }
376
377 let name = self
379 .activity_names
380 .get(&activity_id)
381 .map(|n| if n.len() > 8 { &n[..8] } else { n })
382 .unwrap_or("?");
383
384 if clipped_w > 40.0 && bar_rect.left() >= timeline_x_start {
385 painter.text(
386 Pos2::new(bar_rect.left() + 3.0, bar_rect.center().y),
387 egui::Align2::LEFT_CENTER,
388 name,
389 egui::FontId::proportional(8.0),
390 Color32::WHITE,
391 );
392 }
393
394 if clipped_w > 60.0 {
396 painter.text(
397 Pos2::new(bar_rect.right() - 2.0, bar_rect.center().y),
398 egui::Align2::RIGHT_CENTER,
399 Self::format_time(duration_secs),
400 egui::FontId::proportional(7.0),
401 Color32::WHITE.linear_multiply(0.8),
402 );
403 }
404 }
405
406 for i in 0..activities.len() {
408 for j in 0..activities.len() {
409 if i != j && trace.precedes(i, j) {
410 let is_direct = !(0..activities.len())
412 .any(|k| k != i && k != j && trace.precedes(i, k) && trace.precedes(k, j));
413
414 if is_direct {
415 let start_i = trace.activity_start_secs[i] as f32;
416 let dur_i = trace.activity_duration_secs[i] as f32;
417 let start_j = trace.activity_start_secs[j] as f32;
418
419 let x1 = timeline_x_start
420 + self.camera.offset.x
421 + (start_i + dur_i) * effective_scale;
422 let x2 =
423 timeline_x_start + self.camera.offset.x + start_j * effective_scale;
424 let y1 = bars_y_start + i as f32 * row_height + row_height / 2.0;
425 let y2 = bars_y_start + j as f32 * row_height + row_height / 2.0;
426
427 if x1 >= timeline_x_start && x2 <= rect.right() {
429 painter.line_segment(
430 [Pos2::new(x1, y1), Pos2::new(x2, y2)],
431 Stroke::new(1.0, theme.accent.linear_multiply(0.3)),
432 );
433 }
434 }
435 }
436 }
437 }
438 }
439
440 #[allow(dead_code)]
443 fn compute_parallel_layers(&self, trace: &GpuPartialOrderTrace) -> Vec<usize> {
444 let n = trace.activity_count as usize;
445 let mut layers = vec![0usize; n];
446
447 for _ in 0..n {
450 for i in 0..n {
451 for j in 0..n {
452 if i != j && trace.precedes(j, i) {
453 layers[i] = layers[i].max(layers[j] + 1);
454 }
455 }
456 }
457 }
458
459 layers
460 }
461
462 fn has_concurrent_activities(&self, trace: &GpuPartialOrderTrace, idx: usize) -> bool {
464 for j in 0..trace.activity_count as usize {
465 if idx != j && trace.is_concurrent(idx, j) {
466 return true;
467 }
468 }
469 false
470 }
471
472 fn draw_time_axis(&self, painter: &egui::Painter, theme: &Theme, rect: Rect) {
474 let axis_y = rect.top() + 32.0;
475 let label_width = 90.0;
476 let timeline_x_start = rect.left() + label_width;
477
478 let effective_scale = self.pixels_per_sec * self.camera.zoom;
480
481 let target_pixels = 100.0;
484 let raw_step = target_pixels / effective_scale;
485
486 let time_step_secs = if raw_step < 60.0 {
488 60.0 } else if raw_step < 300.0 {
490 300.0 } else if raw_step < 600.0 {
492 600.0 } else if raw_step < 1800.0 {
494 1800.0 } else if raw_step < 3600.0 {
496 3600.0 } else if raw_step < 7200.0 {
498 7200.0 } else if raw_step < 21600.0 {
500 21600.0 } else if raw_step < 43200.0 {
502 43200.0 } else {
504 86400.0 };
506
507 let start_time_secs = (-self.camera.offset.x / effective_scale).max(0.0);
508 let end_time_secs = start_time_secs + (rect.width() - label_width) / effective_scale;
509
510 painter.line_segment(
512 [
513 Pos2::new(timeline_x_start, axis_y),
514 Pos2::new(rect.right(), axis_y),
515 ],
516 Stroke::new(1.0, theme.text_muted),
517 );
518
519 let first_label = (start_time_secs / time_step_secs).floor() as i64 * time_step_secs as i64;
521 let mut t = first_label as f32;
522
523 while t <= end_time_secs {
524 let x = timeline_x_start + self.camera.offset.x + t * effective_scale;
525 if x >= timeline_x_start && x <= rect.right() - 20.0 {
526 painter.line_segment(
528 [Pos2::new(x, axis_y - 3.0), Pos2::new(x, axis_y + 3.0)],
529 Stroke::new(1.0, theme.text_muted),
530 );
531
532 painter.text(
534 Pos2::new(x, axis_y - 8.0),
535 egui::Align2::CENTER_BOTTOM,
536 Self::format_time(t),
537 egui::FontId::proportional(9.0),
538 theme.text_muted,
539 );
540 }
541 t += time_step_secs;
542 }
543 }
544
545 pub fn reset(&mut self) {
547 self.camera.reset();
548 self.activity_colors.clear();
549 self.selected_trace = None;
550 self.pixels_per_sec = 0.01;
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557
558 #[test]
559 fn test_timeline_creation() {
560 let canvas = TimelineCanvas::new();
561 assert!(canvas.activity_colors.is_empty());
562 }
563
564 #[test]
565 fn test_format_time() {
566 assert_eq!(TimelineCanvas::format_time(30.0), "30s");
567 assert_eq!(TimelineCanvas::format_time(90.0), "1.5m");
568 assert_eq!(TimelineCanvas::format_time(3600.0), "1.0h");
569 assert_eq!(TimelineCanvas::format_time(7200.0), "2.0h");
570 }
571}