From 06b8cac896f2a701bc54c12613537ae611f112fb Mon Sep 17 00:00:00 2001 From: Mark Kalsbeek Date: Thu, 11 Dec 2025 06:40:15 +0100 Subject: [PATCH] better already --- examples/bouncing_ball.rs | 6 +- examples/demo.rs | 4 + teleprof/src/lib.rs | 418 ++++++++++++++++++++++++++++++++------ 3 files changed, 362 insertions(+), 66 deletions(-) diff --git a/examples/bouncing_ball.rs b/examples/bouncing_ball.rs index 6235234..fa04b85 100644 --- a/examples/bouncing_ball.rs +++ b/examples/bouncing_ball.rs @@ -2,6 +2,7 @@ use minifb::{Key, Window, WindowOptions}; use std::thread; use std::time::{Duration, Instant}; use std::sync::{Arc, Mutex}; +use std::sync::atomic::{AtomicUsize, Ordering}; use rand::Rng; use teleprof::instrument; @@ -9,6 +10,8 @@ const WIDTH: usize = 800; const HEIGHT: usize = 600; const BALL_RADIUS: usize = 20; +static COLOR_PICKER_COUNTER: AtomicUsize = AtomicUsize::new(0); + struct Ball { x: f32, y: f32, @@ -94,8 +97,9 @@ fn main_frame( // If we hit a wall, spawn a thread to pick a new color if hit_wall { let ball_clone = Arc::clone(ball); + let id = COLOR_PICKER_COUNTER.fetch_add(1, Ordering::Relaxed); thread::spawn(move || { - teleprof::set_thread_name("ColorPicker"); + teleprof::set_thread_name(format!("ColorPicker-{}", id)); pick_new_color(ball_clone); }); } diff --git a/examples/demo.rs b/examples/demo.rs index a5e8d58..c9a9c3b 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -5,6 +5,9 @@ use teleprof::instrument; fn main() { // Start the telemetry window teleprof::start(); + + // Name the main thread + teleprof::set_thread_name("Main"); println!("Teleprof demo running..."); println!("Press Space in the profiler window to pause/unpause"); @@ -44,6 +47,7 @@ fn physics_update() { // Spawn some worker threads let handles: Vec<_> = (0..3).map(|i| { thread::spawn(move || { + teleprof::set_thread_name(format!("Physics-{}", i)); physics_worker(i); }) }).collect(); diff --git a/teleprof/src/lib.rs b/teleprof/src/lib.rs index 50d0587..b749761 100644 --- a/teleprof/src/lib.rs +++ b/teleprof/src/lib.rs @@ -144,15 +144,14 @@ mod window { const MAX_EVENTS: usize = 1_000_000; // Monokai palette - const COLORS: [u32; 8] = [ + const COLORS: [u32; 7] = [ + 0x75715E, // Gray 0xF92672, // Pink 0xA6E22E, // Green 0xFD971F, // Orange 0x66D9EF, // Cyan 0xAE81FF, // Purple 0xE6DB74, // Yellow - 0xF8F8F2, // White - 0x75715E, // Gray ]; const BG_COLOR: u32 = 0x272822; @@ -230,6 +229,10 @@ mod window { timeline_time_offset: f64, timeline_time_scale: f64, + // Vertical scroll + icicle_scroll_y: f32, + timeline_scroll_y: f32, + // Mouse state mouse_down: bool, last_mouse_x: f32, @@ -238,10 +241,25 @@ mod window { mouse_x: f32, mouse_y: f32, + // Box selection for zoom + selecting: bool, + selection_start_x: f32, + selection_end_x: f32, + + // Hover state + hovered_span: Option, + // Pause state paused: bool, pause_guard: Option>, } + + struct HoveredSpan { + name: &'static str, + duration_us: f64, + thread_id: u64, + start_time: f64, + } impl ViewState { fn new() -> Self { @@ -250,12 +268,18 @@ mod window { icicle_time_scale: 100.0, timeline_time_offset: 0.0, timeline_time_scale: 100.0, + icicle_scroll_y: 0.0, + timeline_scroll_y: 0.0, mouse_down: false, last_mouse_x: 0.0, last_mouse_y: 0.0, mouse_visible: false, mouse_x: 0.0, mouse_y: 0.0, + selecting: false, + selection_start_x: 0.0, + selection_end_x: 0.0, + hovered_span: None, paused: false, pause_guard: None, } @@ -291,8 +315,16 @@ mod window { let (width, height) = window.get_size(); + // Collect spans for this frame + let spans: Vec<_> = buffer.iter().collect(); + let earliest = if !spans.is_empty() { + spans.iter().map(|s| s.start).min().unwrap() + } else { + Instant::now() + }; + // Handle input - handle_input(&mut window, &mut view, width, height); + handle_input(&mut window, &mut view, &buffer, earliest, width, height); // Resize framebuffer if needed if framebuffer.len() != width * height { @@ -310,7 +342,7 @@ mod window { Ok(()) } - fn handle_input(window: &mut Window, view: &mut ViewState, _width: usize, height: usize) { + fn handle_input(window: &mut Window, view: &mut ViewState, buffer: &RingBuffer, earliest: Instant, _width: usize, height: usize) { // Pause/unpause if window.is_key_pressed(Key::Space, minifb::KeyRepeat::No) { view.paused = !view.paused; @@ -328,47 +360,128 @@ mod window { view.mouse_y = my; let icicle_height = height / 2; - let _is_icicle = my < icicle_height as f32; + let is_icicle = my < icicle_height as f32; - // Handle mouse wheel (zoom) + // Check if shift is pressed (for timeline zoom) + let shift_pressed = window.is_key_down(Key::LeftShift) || window.is_key_down(Key::RightShift); + + // Handle mouse wheel if let Some((_, scroll_y)) = window.get_scroll_wheel() { if scroll_y != 0.0 { - let zoom_factor = if scroll_y > 0.0 { 1.2 } else { 1.0 / 1.2 }; - - // Synchronize both views - zoom changes scale for both - let old_scale = view.icicle_time_scale; - let new_scale = old_scale * zoom_factor; - - // Calculate mouse time in world space (same for both views since synchronized) - let mouse_time = view.icicle_time_offset + (mx as f64 / old_scale); - - // Update scale and adjust offset to keep mouse position stable - view.icicle_time_scale = new_scale; - view.timeline_time_scale = new_scale; - - let new_offset = mouse_time - (mx as f64 / new_scale); - view.icicle_time_offset = new_offset; - view.timeline_time_offset = new_offset; + if shift_pressed && !is_icicle { + // Shift + scroll in timeline = zoom timeline only + let zoom_factor = if scroll_y > 0.0 { 1.2 } else { 1.0 / 1.2 }; + let old_scale = view.timeline_time_scale; + let new_scale = old_scale * zoom_factor; + let mouse_time = view.timeline_time_offset + (mx as f64 / old_scale); + + view.timeline_time_scale = new_scale; + view.timeline_time_offset = mouse_time - (mx as f64 / new_scale); + + // Also update icicle to match + view.icicle_time_scale = new_scale; + view.icicle_time_offset = view.timeline_time_offset; + } else if !shift_pressed && scroll_y.abs() > 0.5 { + // Regular scroll = zoom both views (horizontal) + let zoom_factor = if scroll_y > 0.0 { 1.2 } else { 1.0 / 1.2 }; + let old_scale = view.icicle_time_scale; + let new_scale = old_scale * zoom_factor; + let mouse_time = view.icicle_time_offset + (mx as f64 / old_scale); + + view.icicle_time_scale = new_scale; + view.timeline_time_scale = new_scale; + + let new_offset = mouse_time - (mx as f64 / new_scale); + view.icicle_time_offset = new_offset; + view.timeline_time_offset = new_offset; + } else { + // Vertical scroll + let scroll_amount = scroll_y * 20.0; + if is_icicle { + view.icicle_scroll_y = (view.icicle_scroll_y - scroll_amount).max(0.0); + } else { + view.timeline_scroll_y = (view.timeline_scroll_y - scroll_amount).max(0.0); + } + } } } - // Handle mouse drag (pan) - let mouse_down = window.get_mouse_down(MouseButton::Left); + // Handle mouse buttons + let left_down = window.get_mouse_down(MouseButton::Left); + let right_down = window.get_mouse_down(MouseButton::Right); - if mouse_down && view.mouse_down { + // Right click = pan + if right_down && view.mouse_down { let dx = mx - view.last_mouse_x; - - // Pan both views together (synchronized) let delta = dx as f64 / view.icicle_time_scale; view.icicle_time_offset -= delta; view.timeline_time_offset -= delta; } - view.mouse_down = mouse_down; + // Left click = box selection for zoom + if left_down && !view.selecting && !view.mouse_down { + view.selecting = true; + view.selection_start_x = mx; + view.selection_end_x = mx; + } else if left_down && view.selecting { + view.selection_end_x = mx; + } else if !left_down && view.selecting { + // Complete selection - zoom to selected region + let x1 = view.selection_start_x.min(view.selection_end_x); + let x2 = view.selection_start_x.max(view.selection_end_x); + + if (x2 - x1) > 5.0 { // Minimum selection size + let time1 = view.icicle_time_offset + (x1 as f64 / view.icicle_time_scale); + let time2 = view.icicle_time_offset + (x2 as f64 / view.icicle_time_scale); + let time_range = time2 - time1; + + // Calculate new scale to fit selection in view + let new_scale = window.get_size().0 as f64 / time_range; + view.icicle_time_scale = new_scale; + view.timeline_time_scale = new_scale; + view.icicle_time_offset = time1; + view.timeline_time_offset = time1; + } + + view.selecting = false; + } + + // Update hover detection + update_hover(view, buffer, earliest, mx, my, icicle_height); + + view.mouse_down = left_down || right_down; view.last_mouse_x = mx; view.last_mouse_y = my; } else { view.mouse_visible = false; + view.hovered_span = None; + } + } + + fn update_hover(view: &mut ViewState, buffer: &RingBuffer, earliest: Instant, mx: f32, my: f32, icicle_height: usize) { + view.hovered_span = None; + + if my >= icicle_height as f32 { + return; // Only hover in icicle view for now + } + + let mouse_time = view.icicle_time_offset + (mx as f64 / view.icicle_time_scale); + + // Find span under cursor + for span in buffer.iter() { + let start_time = (span.start - earliest).as_secs_f64(); + let end_time = (span.end - earliest).as_secs_f64(); + + if mouse_time >= start_time && mouse_time <= end_time { + let duration_us = (end_time - start_time) * 1_000_000.0; + view.hovered_span = Some(HoveredSpan { + name: span.name, + duration_us, + thread_id: span.thread_id, + start_time, + }); + break; // Take first match (could be improved to find best match by y-position) + } } } @@ -412,31 +525,189 @@ mod window { font, ); - // Draw cursor - if view.mouse_visible { - let cursor_x = view.mouse_x as usize; + // Draw timestamp axis at the bottom of icicle view + draw_timestamp_axis(framebuffer, width, icicle_height - 20, view, earliest, font); + + // Draw selection box if selecting + if view.selecting { + let x1 = view.selection_start_x.min(view.selection_end_x) as usize; + let x2 = view.selection_start_x.max(view.selection_end_x) as usize; + let selection_color = 0x4080FF; // Semi-transparent blue - // Draw vertical line for y in 0..height { - if cursor_x < width { - let idx = y * width + cursor_x; + for x in x1..x2.min(width) { + let idx = y * width + x; if idx < framebuffer.len() { - // Make cursor semi-transparent by blending let bg = framebuffer[idx]; - let bg_r = ((bg >> 16) & 0xFF) as u32; - let bg_g = ((bg >> 8) & 0xFF) as u32; - let bg_b = (bg & 0xFF) as u32; + let bg_r = ((bg >> 16) & 0xFF) as f32; + let bg_g = ((bg >> 8) & 0xFF) as f32; + let bg_b = (bg & 0xFF) as f32; - let alpha = 0.5; - let r = (bg_r as f32 * (1.0 - alpha) + 255.0 * alpha) as u32; - let g = (bg_g as f32 * (1.0 - alpha) + 255.0 * alpha) as u32; - let b = (bg_b as f32 * (1.0 - alpha) + 255.0 * alpha) as u32; + let sel_r = ((selection_color >> 16) & 0xFF) as f32; + let sel_g = ((selection_color >> 8) & 0xFF) as f32; + let sel_b = (selection_color & 0xFF) as f32; + + let alpha = 0.3; + let r = (bg_r * (1.0 - alpha) + sel_r * alpha) as u32; + let g = (bg_g * (1.0 - alpha) + sel_g * alpha) as u32; + let b = (bg_b * (1.0 - alpha) + sel_b * alpha) as u32; framebuffer[idx] = (r << 16) | (g << 8) | b; } } } } + + // Draw crosshair cursor + if view.mouse_visible && !view.selecting { + let cursor_x = view.mouse_x as usize; + let cursor_y = view.mouse_y as usize; + + // Vertical line + for y in 0..height { + if cursor_x < width { + let idx = y * width + cursor_x; + if idx < framebuffer.len() { + framebuffer[idx] = blend_cursor(framebuffer[idx]); + } + } + } + + // Horizontal line + if cursor_y < height { + for x in 0..width { + let idx = cursor_y * width + x; + if idx < framebuffer.len() { + framebuffer[idx] = blend_cursor(framebuffer[idx]); + } + } + } + } + + // Draw hover tooltip + if let Some(ref hover) = view.hovered_span { + draw_tooltip(framebuffer, width, height, view, hover, font); + } + } + + fn blend_cursor(bg: u32) -> u32 { + let bg_r = ((bg >> 16) & 0xFF) as f32; + let bg_g = ((bg >> 8) & 0xFF) as f32; + let bg_b = (bg & 0xFF) as f32; + + let alpha = 0.6; + let r = (bg_r * (1.0 - alpha) + 255.0 * alpha) as u32; + let g = (bg_g * (1.0 - alpha) + 255.0 * alpha) as u32; + let b = (bg_b * (1.0 - alpha) + 255.0 * alpha) as u32; + + (r << 16) | (g << 8) | b + } + + fn draw_tooltip( + framebuffer: &mut [u32], + width: usize, + height: usize, + view: &ViewState, + hover: &HoveredSpan, + font: &fontdue::Font, + ) { + let tooltip_w = 280; + let tooltip_h = 80; + let padding = 8; + + // Position tooltip near cursor but keep it on screen + let tooltip_x = (view.mouse_x as usize + 20).min(width.saturating_sub(tooltip_w + 10)); + let tooltip_y = (view.mouse_y as usize + 20).min(height.saturating_sub(tooltip_h + 10)); + + // Draw tooltip background + for dy in 0..tooltip_h { + for dx in 0..tooltip_w { + let x = tooltip_x + dx; + let y = tooltip_y + dy; + if x < width && y < height { + let idx = y * width + x; + if idx < framebuffer.len() { + // Dark background with border + let color = if dx == 0 || dy == 0 || dx == tooltip_w - 1 || dy == tooltip_h - 1 { + 0x808080 // Border + } else { + 0x1E1E1E // Background + }; + framebuffer[idx] = color; + } + } + } + } + + // Draw text + let text_x = tooltip_x + padding; + let text_y = tooltip_y + padding; + + draw_text(framebuffer, width, text_x, text_y, hover.name, font, 14.0, 0xFFFFFF); + draw_text(framebuffer, width, text_x, text_y + 20, + &format!("Duration: {:.2} μs", hover.duration_us), font, 12.0, 0xCCCCCC); + draw_text(framebuffer, width, text_x, text_y + 38, + &format!("Thread: {}", hover.thread_id), font, 12.0, 0xCCCCCC); + draw_text(framebuffer, width, text_x, text_y + 56, + &format!("Start: {:.6} s", hover.start_time), font, 12.0, 0xCCCCCC); + } + + fn draw_timestamp_axis( + framebuffer: &mut [u32], + width: usize, + y: usize, + view: &ViewState, + _earliest: Instant, + font: &fontdue::Font, + ) { + // Draw background bar + for x in 0..width { + let idx = y * width + x; + if idx < framebuffer.len() { + framebuffer[idx] = 0x1E1E1E; + } + } + + // Calculate time markers + let visible_duration = width as f64 / view.icicle_time_scale; + let time_step = calculate_time_step(visible_duration); + + let start_time = view.icicle_time_offset; + let first_marker = (start_time / time_step).ceil() * time_step; + + let mut time = first_marker; + while time < start_time + visible_duration { + let x = ((time - view.icicle_time_offset) * view.icicle_time_scale) as i32; + + if x >= 0 && x < width as i32 { + // Draw tick mark + for dy in 0..6 { + let idx = (y + dy) * width + x as usize; + if idx < framebuffer.len() { + framebuffer[idx] = 0x808080; + } + } + + // Draw time label + let label = format!("{:.3}s", time); + draw_text(framebuffer, width, (x + 2).max(0) as usize, y + 6, &label, font, 10.0, 0xCCCCCC); + } + + time += time_step; + } + } + + fn calculate_time_step(visible_duration: f64) -> f64 { + // Choose appropriate time step based on visible duration + let steps = [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0]; + + for &step in &steps { + if visible_duration / step < 20.0 { + return step; + } + } + + 10.0 } fn render_icicle( @@ -460,23 +731,32 @@ mod window { } } + // Sort roots by start time + roots.sort_by_key(|s| s.start); + // Render each root and its children recursively let row_height = 24; - let mut y_offset = 0; + let mut y_offset = view.icicle_scroll_y as usize; for root in roots { - y_offset = render_icicle_span( - framebuffer, - width, - height, - root, - &children, - earliest, - y_offset, - row_height, - view, - font, - ); + // Only render if in view + if y_offset < height { + y_offset = render_icicle_span( + framebuffer, + width, + height, + root, + &children, + earliest, + y_offset, + row_height, + view, + font, + 0, // depth + ); + } else { + break; + } } } @@ -491,6 +771,7 @@ mod window { row_height: usize, view: &ViewState, font: &fontdue::Font, + depth: usize, ) -> usize { if y + row_height > height { return y; @@ -506,36 +787,43 @@ mod window { // Only render if visible if x2 > 0 && x1 < width as i32 { let color = get_color_for_name(span.name); + let bar_width = (x2 - x1).max(1) as usize; - fill_rect(framebuffer, width, x1.max(0) as usize, y, (x2 - x1).max(1) as usize, row_height - 2, color); + fill_rect(framebuffer, width, x1.max(0) as usize, y, bar_width, row_height - 2, color); - // Render text if there's enough space - let text_width = (x2 - x1) as usize; - if text_width > 40 { + // Render text if there's enough space, otherwise render to the right + if bar_width > 40 { draw_text(framebuffer, width, x1.max(0) as usize + 4, y + 4, span.name, font, 14.0, TEXT_COLOR); + } else if x2 >= 0 && x2 < width as i32 { + // Draw text to the right of the bar + draw_text(framebuffer, width, x2.max(0) as usize + 4, y + 4, span.name, font, 14.0, TEXT_COLOR); } } - // Render children - let mut next_y = y + row_height; + // Render children at next depth level + let mut child_y = y + row_height; if let Some(child_spans) = children.get(&span.span_id) { for child in child_spans { - next_y = render_icicle_span( + if child_y >= height { + break; + } + child_y = render_icicle_span( framebuffer, width, height, child, children, earliest, - next_y, + child_y, row_height, view, font, + depth + 1, ); } } - next_y + child_y } fn render_timeline(