diff --git a/teleprof/src/lib.rs b/teleprof/src/lib.rs index 5e925b7..7052f22 100644 --- a/teleprof/src/lib.rs +++ b/teleprof/src/lib.rs @@ -37,41 +37,39 @@ pub fn set_thread_name(name: impl Into) { pub struct SpanGuard { span_id: u64, previous_parent: Option, - previous_parent_name: Option<&'static str>, - name: &'static str, is_duplicate: bool, } impl SpanGuard { pub fn new(name: &'static str) -> Self { - // Handle pause mechanism BEFORE creating the span + // Handle pause mechanism if let Err(_guard) = PAUSE.try_lock() { let _pause_lock = PAUSE.lock().unwrap(); } let thread_id = std::thread::current().id(); let parent_id = PARENT_SPAN.with(|p| p.get()); - let parent_name = PARENT_SPAN_NAME.with(|p| p.get()); - // Check if this is a duplicate span - let is_duplicate = parent_name == Some(name); + // Check if this is a duplicate span (recursion detection) + let is_duplicate = parent_id.is_some() && { + let cache = SPAN_CACHE.lock().unwrap(); + parent_id.and_then(|id| cache.spans.get(&id)) + .map(|s| s.name == name) + .unwrap_or(false) + }; if is_duplicate { return Self { span_id: 0, previous_parent: parent_id, - previous_parent_name: parent_name, - name, is_duplicate: true, }; } let span_id = next_span_id(); let previous_parent = parent_id; - let previous_parent_name = parent_name; PARENT_SPAN.with(|p| p.set(Some(span_id))); - PARENT_SPAN_NAME.with(|p| p.set(Some(name))); EVENT_SENDER.send(Event::SpanStart { span_id, @@ -84,8 +82,6 @@ impl SpanGuard { Self { span_id, previous_parent, - previous_parent_name, - name, is_duplicate: false, } } @@ -103,7 +99,6 @@ impl Drop for SpanGuard { }).ok(); PARENT_SPAN.with(|p| p.set(self.previous_parent)); - PARENT_SPAN_NAME.with(|p| p.set(self.previous_parent_name)); } } @@ -140,7 +135,6 @@ pub(crate) enum Event { thread_local! { static PARENT_SPAN: Cell> = Cell::new(None); - static PARENT_SPAN_NAME: Cell> = Cell::new(None); } static EVENT_SENDER: Lazy> = Lazy::new(|| { @@ -154,6 +148,9 @@ static EVENT_RECEIVER: Lazy>>>> = static SPAN_ID_COUNTER: Lazy>> = Lazy::new(|| Arc::new(Mutex::new(0))); +// Global span cache for duplicate detection +static SPAN_CACHE: Lazy>> = Lazy::new(|| Arc::new(Mutex::new(SpanCache::new()))); + fn next_span_id() -> u64 { let mut counter = SPAN_ID_COUNTER.lock().unwrap(); let id = *counter; @@ -169,6 +166,105 @@ fn thread_id_to_u64(id: std::thread::ThreadId) -> u64 { .unwrap_or(0) } +// ============================================================================ +// Simplified Span Cache +// ============================================================================ + +struct CachedSpan { + span_id: u64, + parent_id: Option, + thread_id: u64, + name: &'static str, + start: Instant, + end: Option, +} + +struct SpanCache { + spans: std::collections::HashMap, + thread_roots: std::collections::HashMap>, + children: std::collections::HashMap>, + thread_names: std::collections::HashMap, + oldest_span_id: Option, +} + +impl SpanCache { + fn new() -> Self { + Self { + spans: std::collections::HashMap::new(), + thread_roots: std::collections::HashMap::new(), + children: std::collections::HashMap::new(), + thread_names: std::collections::HashMap::new(), + oldest_span_id: None, + } + } + + fn add_span_start(&mut self, span_id: u64, parent_id: Option, thread_id: u64, name: &'static str, timestamp: Instant) { + // Track oldest span for ring buffer eviction + if self.oldest_span_id.is_none() { + self.oldest_span_id = Some(span_id); + } + + let span = CachedSpan { + span_id, + parent_id, + thread_id, + name, + start: timestamp, + end: None, + }; + + self.spans.insert(span_id, span); + + if let Some(parent_id) = parent_id { + self.children.entry(parent_id).or_default().push(span_id); + } else { + self.thread_roots.entry(thread_id).or_default().push(span_id); + } + } + + fn complete_span(&mut self, span_id: u64, timestamp: Instant) { + if let Some(span) = self.spans.get_mut(&span_id) { + span.end = Some(timestamp); + } + } + + fn set_thread_name(&mut self, thread_id: u64, name: String) { + self.thread_names.insert(thread_id, name); + } + + fn evict_oldest(&mut self, max_spans: usize) { + if self.spans.len() <= max_spans { + return; + } + + if let Some(oldest_id) = self.oldest_span_id { + let oldest = &self.spans[&oldest_id]; + let thread_id = oldest.thread_id; + let parent_id = oldest.parent_id; + + // Remove from parent's children or thread roots + if let Some(parent_id) = parent_id { + if let Some(children) = self.children.get_mut(&parent_id) { + children.retain(|&id| id != oldest_id); + } + } else { + if let Some(roots) = self.thread_roots.get_mut(&thread_id) { + roots.retain(|&id| id != oldest_id); + } + } + + // Remove children map entry + self.children.remove(&oldest_id); + + // Remove the span itself + self.spans.remove(&oldest_id); + + // Find next oldest + self.oldest_span_id = self.spans.keys().min().copied(); + } + } +} + // ============================================================================ // Window rendering // ============================================================================ @@ -176,11 +272,10 @@ fn thread_id_to_u64(id: std::thread::ThreadId) -> u64 { mod window { use super::*; use minifb::{Key, MouseButton, MouseMode, Window, WindowOptions}; - use std::collections::HashMap; const INITIAL_WIDTH: usize = 1280; const INITIAL_HEIGHT: usize = 720; - const MAX_EVENTS: usize = 1_000_000; + const MAX_SPANS: usize = 1_000_000; const TRACK_HEADER_HEIGHT: usize = 24; const COLLAPSED_TRACK_HEIGHT: usize = 24; const ROW_HEIGHT: usize = 20; @@ -201,177 +296,24 @@ mod window { const HEADER_BG: u32 = 0x1E1E1E; const ACTIVE_COLOR: u32 = 0x4080FF; - // Tree node representing a span in the call stack - #[derive(Clone)] - struct SpanNode { - span_id: u64, - parent_id: Option, // Need this for reparenting - name: &'static str, - start: Instant, - end: Option, // None = ongoing - children: Vec, - } - - impl SpanNode { - fn max_depth(&self) -> usize { - if self.children.is_empty() { - 1 - } else { - 1 + self.children.iter().map(|c| c.max_depth()).max().unwrap_or(0) - } - } - } - - // A track represents one thread's timeline - struct ThreadTrack { - thread_id: u64, - name: Option, - root_spans: Vec, - expanded: bool, - active_intervals: Vec<(Instant, Instant)>, // When thread was active - } - - impl ThreadTrack { - fn max_depth(&self) -> usize { - self.root_spans.iter().map(|n| n.max_depth()).max().unwrap_or(0) - } - - fn height(&self) -> usize { - if self.expanded { - TRACK_HEADER_HEIGHT + self.max_depth() * ROW_HEIGHT - } else { - TRACK_HEADER_HEIGHT + COLLAPSED_TRACK_HEIGHT - } - } - } - - // Parsed frame data - rebuilt each frame - struct FrameData { - tracks: Vec, - earliest: Instant, - latest: Instant, - } - - // Persistent state for incremental tree building - struct TreeCache { - // Per-thread trees - thread_trees: HashMap>, - // Track which span_ids we've already processed - processed_spans: std::collections::HashSet, - // Track last known event count to detect new events - last_event_count: usize, - } - - impl TreeCache { - fn new() -> Self { - Self { - thread_trees: HashMap::new(), - processed_spans: std::collections::HashSet::new(), - last_event_count: 0, - } - } - } - - struct RingBuffer { - spans: Vec, - head: usize, - pending_starts: HashMap, u64, &'static str, Instant)>, - thread_names: HashMap, - } - - struct CompletedSpan { - span_id: u64, - parent_id: Option, - thread_id: u64, - name: &'static str, - start: Instant, - end: Instant, - } - - impl RingBuffer { - fn new() -> Self { - Self { - spans: Vec::with_capacity(MAX_EVENTS), - head: 0, - pending_starts: HashMap::new(), - thread_names: HashMap::new(), - } - } - - fn push_event(&mut self, event: Event) { - match event { - Event::SpanStart { span_id, parent_id, thread_id, name, timestamp } => { - self.pending_starts.insert(span_id, (span_id, parent_id, thread_id, name, timestamp)); - } - Event::SpanEnd { span_id, timestamp } => { - if let Some((span_id, parent_id, thread_id, name, start)) = self.pending_starts.remove(&span_id) { - let span = CompletedSpan { - span_id, - parent_id, - thread_id, - name, - start, - end: timestamp, - }; - - if self.spans.len() < MAX_EVENTS { - self.spans.push(span); - } else { - self.spans[self.head] = span; - self.head = (self.head + 1) % MAX_EVENTS; - } - } - } - Event::ThreadName { thread_id, name } => { - self.thread_names.insert(thread_id, name); - } - } - } - - fn iter(&self) -> Box + '_> { - if self.spans.len() < MAX_EVENTS { - Box::new(self.spans.iter()) - } else { - Box::new(self.spans[self.head..].iter().chain(self.spans[..self.head].iter())) - } - } - - fn get_ongoing_spans(&self) -> Vec<(u64, Option, u64, &'static str, Instant)> { - self.pending_starts.values().cloned().collect() - } - } - struct ViewState { - // Pan/zoom (unified) time_offset: f64, time_scale: f64, - - // Vertical scroll scroll_y: f32, - - // Mouse state mouse_down: bool, last_mouse_x: f32, last_mouse_y: f32, mouse_visible: bool, 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>, - pause_timestamp: Option, // Time when pause was activated - - // Track expansion state (per thread_id) - track_expanded: HashMap, + pause_timestamp: Option, + track_expanded: std::collections::HashMap, } struct HoveredSpan { @@ -401,7 +343,7 @@ mod window { paused: false, pause_guard: None, pause_timestamp: None, - track_expanded: HashMap::new(), + track_expanded: std::collections::HashMap::new(), } } @@ -432,28 +374,37 @@ mod window { let receiver = EVENT_RECEIVER.lock().unwrap().take() .ok_or("Event receiver not initialized")?; - let mut buffer = RingBuffer::new(); let mut view = ViewState::new(); let mut framebuffer = vec![BG_COLOR; INITIAL_WIDTH * INITIAL_HEIGHT]; - let mut tree_cache = TreeCache::new(); // Load font let font_data = include_bytes!("../../fonts/DejaVuSansMono.ttf"); let font = fontdue::Font::from_bytes(font_data as &[u8], fontdue::FontSettings::default())?; while window.is_open() && !window.is_key_down(Key::Escape) { - // Drain events from channel - while let Ok(event) = receiver.try_recv() { - buffer.push_event(event); + // Process events into cache + { + let mut cache = SPAN_CACHE.lock().unwrap(); + while let Ok(event) = receiver.try_recv() { + match event { + Event::SpanStart { span_id, parent_id, thread_id, name, timestamp } => { + cache.add_span_start(span_id, parent_id, thread_id, name, timestamp); + } + Event::SpanEnd { span_id, timestamp } => { + cache.complete_span(span_id, timestamp); + } + Event::ThreadName { thread_id, name } => { + cache.set_thread_name(thread_id, name); + } + } + } + cache.evict_oldest(MAX_SPANS); } let (width, height) = window.get_size(); - // Parse ringbuffer into frame data (incrementally) - let frame_data = parse_frame_data(&buffer, &view, &mut tree_cache); - // Handle input - handle_input(&mut window, &mut view, &frame_data, width, height); + handle_input(&mut window, &mut view, width, height); // Resize framebuffer if needed if framebuffer.len() != width * height { @@ -463,315 +414,17 @@ mod window { framebuffer.fill(BG_COLOR); // Render - render_frame(&mut framebuffer, width, height, &frame_data, &view, &font); + render_frame(&mut framebuffer, width, height, &view, &font); window.update_with_buffer(&framebuffer, width, height)?; } Ok(()) } - - // Parse ringbuffer into per-thread trees (incrementally) - fn parse_frame_data(buffer: &RingBuffer, view: &ViewState, cache: &mut TreeCache) -> FrameData { - let completed_spans: Vec<_> = buffer.iter().collect(); - let ongoing_spans = buffer.get_ongoing_spans(); - - if completed_spans.is_empty() && ongoing_spans.is_empty() { - return FrameData { - tracks: Vec::new(), - earliest: Instant::now(), - latest: Instant::now(), - }; - } - - // Check if we have new events to process - let current_event_count = completed_spans.len(); - let has_new_events = current_event_count != cache.last_event_count; - - if has_new_events { - // Process only new spans - for span in &completed_spans { - if !cache.processed_spans.contains(&span.span_id) { - add_span_to_tree(span, &mut cache.thread_trees); - cache.processed_spans.insert(span.span_id); - } - } - cache.last_event_count = current_event_count; - } - - // Find time range - let earliest = completed_spans.iter() - .map(|s| s.start) - .chain(ongoing_spans.iter().map(|(_, _, _, _, start)| *start)) - .min() - .unwrap_or_else(Instant::now); - - let latest = completed_spans.iter() - .map(|s| s.end) - .max() - .unwrap_or(earliest); - - // Get all thread IDs (from cache and ongoing) - let mut thread_ids: Vec<_> = cache.thread_trees.keys() - .copied() - .collect(); - - for (_, _, thread_id, _, _) in &ongoing_spans { - if !thread_ids.contains(thread_id) { - thread_ids.push(*thread_id); - } - } - - thread_ids.sort_unstable(); - - let mut tracks = Vec::new(); - let current_time = view.current_time(); - - // Group ongoing spans by thread - let mut thread_ongoing: HashMap, &'static str, Instant)>> = HashMap::new(); - for (span_id, parent_id, thread_id, name, start) in ongoing_spans { - thread_ongoing.entry(thread_id).or_default().push((span_id, parent_id, name, start)); - } - - for thread_id in thread_ids { - // Get cached tree for this thread (clone so we can modify it) - let mut root_spans = cache.thread_trees.get(&thread_id) - .cloned() - .unwrap_or_default(); - - // Add ongoing spans to the tree FIRST (so they can be parents) - let ongoing = thread_ongoing.get(&thread_id).map(|v| v.as_slice()).unwrap_or(&[]); - if !ongoing.is_empty() { - add_ongoing_spans_to_tree(&mut root_spans, ongoing); - - // Now reparent any cached roots that have ongoing parents - reparent_to_ongoing(&mut root_spans); - } - - // Calculate active intervals - let completed = completed_spans.iter() - .filter(|s| s.thread_id == thread_id) - .copied() - .collect::>(); - let active_intervals = calculate_active_intervals(&completed, ongoing, current_time); - - tracks.push(ThreadTrack { - thread_id, - name: buffer.thread_names.get(&thread_id).cloned(), - root_spans, - expanded: view.is_track_expanded(thread_id), - active_intervals, - }); - } - - FrameData { - tracks, - earliest, - latest, - } - } - - // Add a single completed span to the tree cache - fn add_span_to_tree(span: &CompletedSpan, thread_trees: &mut HashMap>) { - let node = SpanNode { - span_id: span.span_id, - parent_id: span.parent_id, - name: span.name, - start: span.start, - end: Some(span.end), - children: Vec::new(), - }; - - let roots = thread_trees.entry(span.thread_id).or_default(); - - if let Some(parent_id) = span.parent_id { - // Try to find parent and add as child - if !try_add_child_to_parent(roots, parent_id, node.clone()) { - // Parent not found yet, add as root (will be reparented later) - roots.push(node); - } - } else { - // No parent, add as root - roots.push(node); - } - - // Try to reparent any existing roots that should be children of this span - reparent_orphans(roots, span.span_id); - - // Sort roots and children by start time - roots.sort_by_key(|n| n.start); - sort_tree_children(roots); - } - - // Recursively try to add a node as a child of the given parent_id - fn try_add_child_to_parent(nodes: &mut [SpanNode], parent_id: u64, child: SpanNode) -> bool { - for node in nodes { - if node.span_id == parent_id { - node.children.push(child); - node.children.sort_by_key(|n| n.start); - return true; - } - if try_add_child_to_parent(&mut node.children, parent_id, child.clone()) { - return true; - } - } - false - } - - // Find nodes at root level that should be children of span_id and reparent them - fn reparent_orphans(roots: &mut Vec, span_id: u64) { - let mut to_reparent = Vec::new(); - - // Find roots that should be children - for i in (0..roots.len()).rev() { - if let Some(parent_id) = get_parent_id_from_metadata(&roots[i]) { - if parent_id == span_id { - to_reparent.push(roots.remove(i)); - } - } - } - - // Add them as children - for orphan in to_reparent { - try_add_child_to_parent(roots, span_id, orphan); - } - } - - // Helper to get parent_id from SpanNode - fn get_parent_id_from_metadata(node: &SpanNode) -> Option { - node.parent_id - } - - fn sort_tree_children(nodes: &mut [SpanNode]) { - for node in nodes { - node.children.sort_by_key(|n| n.start); - sort_tree_children(&mut node.children); - } - } - - // Add ongoing spans to an existing tree (temporarily, not persisted to cache) - fn add_ongoing_spans_to_tree(roots: &mut Vec, ongoing: &[(u64, Option, &'static str, Instant)]) { - for &(span_id, parent_id, name, start) in ongoing { - let node = SpanNode { - span_id, - parent_id, - name, - start, - end: None, - children: Vec::new(), - }; - - if let Some(parent_id) = parent_id { - if !try_add_child_to_parent(roots, parent_id, node.clone()) { - roots.push(node); - } - } else { - roots.push(node); - } - } - - roots.sort_by_key(|n| n.start); - sort_tree_children(roots); - } - - // Reparent root nodes that should be children of ongoing spans - fn reparent_to_ongoing(roots: &mut Vec) { - // Find all ongoing span IDs (they have end: None) - let ongoing_ids: Vec = collect_ongoing_ids(roots); - - // For each ongoing span, check if any roots should be its children - for ongoing_id in ongoing_ids { - let mut to_reparent = Vec::new(); - - // Find roots that claim this ongoing span as parent - for i in (0..roots.len()).rev() { - if let Some(parent_id) = roots[i].parent_id { - if parent_id == ongoing_id && roots[i].end.is_some() { - // This is a completed span that should be a child - to_reparent.push(roots.remove(i)); - } - } - } - - // Add them as children of the ongoing span - for child in to_reparent { - try_add_child_to_parent(roots, ongoing_id, child); - } - } - - roots.sort_by_key(|n| n.start); - sort_tree_children(roots); - } - - // Collect all span IDs that are ongoing (end: None) - fn collect_ongoing_ids(nodes: &[SpanNode]) -> Vec { - let mut ids = Vec::new(); - for node in nodes { - if node.end.is_none() { - ids.push(node.span_id); - } - ids.extend(collect_ongoing_ids(&node.children)); - } - ids - } - - // Calculate when thread was active (any span executing) - fn calculate_active_intervals( - completed: &[&CompletedSpan], - ongoing: &[(u64, Option, &'static str, Instant)], - current_time: Instant, - ) -> Vec<(Instant, Instant)> { - let mut intervals = Vec::new(); - - // Collect all time points - let mut events: Vec<(Instant, bool)> = Vec::new(); // (time, is_start) - - for span in completed { - events.push((span.start, true)); - events.push((span.end, false)); - } - - for &(_, _, _, start) in ongoing { - events.push((start, true)); - // Ongoing spans don't have an end yet - } - - events.sort_by_key(|(time, _)| *time); - - let mut depth = 0; - let mut interval_start = None; - - for (time, is_start) in events { - if is_start { - if depth == 0 { - interval_start = Some(time); - } - depth += 1; - } else { - depth -= 1; - if depth == 0 { - if let Some(start) = interval_start { - intervals.push((start, time)); - interval_start = None; - } - } - } - } - - // If we have ongoing spans, add current interval using current_time - if depth > 0 { - if let Some(start) = interval_start { - intervals.push((start, current_time)); - } - } - - intervals - } fn handle_input( window: &mut Window, view: &mut ViewState, - frame_data: &FrameData, width: usize, _height: usize, ) { @@ -826,14 +479,15 @@ mod window { // Left click = box selection for zoom OR track toggle if left_down && !view.selecting && !view.mouse_down { + let cache = SPAN_CACHE.lock().unwrap(); + // Check if clicking on a track header - if let Some(track_idx) = get_track_at_y(frame_data, my, view.scroll_y) { - let track = &frame_data.tracks[track_idx]; - let track_y = get_track_y(frame_data, track_idx, view.scroll_y); + if let Some(thread_id) = get_track_at_y(&cache, my, view.scroll_y, view) { + let track_y = get_track_y(&cache, thread_id, view.scroll_y, view); // Check if click is in header region if my >= track_y && my < track_y + TRACK_HEADER_HEIGHT as f32 { - view.toggle_track(track.thread_id); + view.toggle_track(thread_id); } else { // Start box selection view.selecting = true; @@ -866,7 +520,7 @@ mod window { } // Update hover detection - update_hover(view, frame_data, mx, my); + update_hover(view, mx, my); view.mouse_down = left_down || right_down; view.last_mouse_x = mx; @@ -877,13 +531,15 @@ mod window { } } - fn get_track_at_y(frame_data: &FrameData, y: f32, scroll_y: f32) -> Option { + fn get_track_at_y(cache: &SpanCache, y: f32, scroll_y: f32, view: &ViewState) -> Option { let mut current_y = -scroll_y; + let mut thread_ids: Vec<_> = cache.thread_roots.keys().copied().collect(); + thread_ids.sort_unstable(); - for (idx, track) in frame_data.tracks.iter().enumerate() { - let track_height = track.height() as f32; + for thread_id in thread_ids { + let track_height = get_thread_height(cache, thread_id, view) as f32; if y >= current_y && y < current_y + track_height { - return Some(idx); + return Some(thread_id); } current_y += track_height; } @@ -891,45 +547,93 @@ mod window { None } - fn get_track_y(frame_data: &FrameData, track_idx: usize, scroll_y: f32) -> f32 { + fn get_track_y(cache: &SpanCache, target_thread_id: u64, scroll_y: f32, view: &ViewState) -> f32 { let mut y = -scroll_y; + let mut thread_ids: Vec<_> = cache.thread_roots.keys().copied().collect(); + thread_ids.sort_unstable(); - for (idx, track) in frame_data.tracks.iter().enumerate() { - if idx == track_idx { + for thread_id in thread_ids { + if thread_id == target_thread_id { return y; } - y += track.height() as f32; + y += get_thread_height(cache, thread_id, view) as f32; } y } - fn update_hover(view: &mut ViewState, frame_data: &FrameData, mx: f32, my: f32) { + fn get_thread_height(cache: &SpanCache, thread_id: u64, view: &ViewState) -> usize { + if view.is_track_expanded(thread_id) { + let max_depth = calculate_max_depth(cache, thread_id); + TRACK_HEADER_HEIGHT + max_depth * ROW_HEIGHT + } else { + TRACK_HEADER_HEIGHT + COLLAPSED_TRACK_HEIGHT + } + } + + fn calculate_max_depth(cache: &SpanCache, thread_id: u64) -> usize { + let roots = cache.thread_roots.get(&thread_id); + if roots.is_none() { + return 0; + } + + let mut max_depth = 0; + for &root_id in roots.unwrap() { + let depth = span_depth(cache, root_id); + max_depth = max_depth.max(depth); + } + max_depth + } + + fn span_depth(cache: &SpanCache, span_id: u64) -> usize { + if let Some(children) = cache.children.get(&span_id) { + if children.is_empty() { + 1 + } else { + 1 + children.iter().map(|&child_id| span_depth(cache, child_id)).max().unwrap_or(0) + } + } else { + 1 + } + } + + fn update_hover(view: &mut ViewState, mx: f32, my: f32) { view.hovered_span = None; + let cache = SPAN_CACHE.lock().unwrap(); let mouse_time = view.time_offset + (mx as f64 / view.time_scale); - if let Some(track_idx) = get_track_at_y(frame_data, my, view.scroll_y) { - let track = &frame_data.tracks[track_idx]; - let track_y = get_track_y(frame_data, track_idx, view.scroll_y); + if let Some(thread_id) = get_track_at_y(&cache, my, view.scroll_y, view) { + let track_y = get_track_y(&cache, thread_id, view.scroll_y, view); let relative_y = my - track_y - TRACK_HEADER_HEIGHT as f32; if relative_y >= 0.0 { - if track.expanded { + if view.is_track_expanded(thread_id) { // Find span at depth let depth = (relative_y / ROW_HEIGHT as f32) as usize; - find_span_at_depth(&track.root_spans, frame_data.earliest, mouse_time, depth, 0, view, track.thread_id); + + let earliest = get_earliest_time(&cache); + if let Some(roots) = cache.thread_roots.get(&thread_id) { + for &root_id in roots { + if find_span_at_depth(&cache, root_id, earliest, mouse_time, depth, 0, view, thread_id) { + break; + } + } + } } else { - // Collapsed - just show that thread is active - for &(start, end) in &track.active_intervals { - let start_time = (start - frame_data.earliest).as_secs_f64(); - let end_time = (end - frame_data.earliest).as_secs_f64(); + // Collapsed - show active time + let earliest = get_earliest_time(&cache); + let active_intervals = calculate_active_intervals(&cache, thread_id, view.current_time()); + + for (start, end) in active_intervals { + let start_time = (start - earliest).as_secs_f64(); + let end_time = (end - earliest).as_secs_f64(); if mouse_time >= start_time && mouse_time <= end_time { view.hovered_span = Some(HoveredSpan { name: "Active", duration_us: (end_time - start_time) * 1_000_000.0, - thread_id: track.thread_id, + thread_id, start_time, ongoing: false, }); @@ -942,53 +646,121 @@ mod window { } fn find_span_at_depth( - nodes: &[SpanNode], + cache: &SpanCache, + span_id: u64, earliest: Instant, mouse_time: f64, target_depth: usize, current_depth: usize, view: &mut ViewState, thread_id: u64, - ) { - for node in nodes { - let start_time = (node.start - earliest).as_secs_f64(); - let end_time = node.end.map(|e| (e - earliest).as_secs_f64()) - .unwrap_or_else(|| (view.current_time() - earliest).as_secs_f64()); + ) -> bool { + let span = &cache.spans[&span_id]; + let start_time = (span.start - earliest).as_secs_f64(); + let end_time = span.end + .map(|e| (e - earliest).as_secs_f64()) + .unwrap_or_else(|| (view.current_time() - earliest).as_secs_f64()); + + if mouse_time >= start_time && mouse_time <= end_time { + if current_depth == target_depth { + let duration_us = (end_time - start_time) * 1_000_000.0; + view.hovered_span = Some(HoveredSpan { + name: span.name, + duration_us, + thread_id, + start_time, + ongoing: span.end.is_none(), + }); + return true; + } - if mouse_time >= start_time && mouse_time <= end_time { - if current_depth == target_depth { - let duration_us = (end_time - start_time) * 1_000_000.0; - view.hovered_span = Some(HoveredSpan { - name: node.name, - duration_us, - thread_id, - start_time, - ongoing: node.end.is_none(), - }); - return; + if let Some(children) = cache.children.get(&span_id) { + for &child_id in children { + if find_span_at_depth(cache, child_id, earliest, mouse_time, target_depth, current_depth + 1, view, thread_id) { + return true; + } } - - find_span_at_depth(&node.children, earliest, mouse_time, target_depth, current_depth + 1, view, thread_id); } } + + false + } + + fn get_earliest_time(cache: &SpanCache) -> Instant { + cache.spans.values() + .map(|s| s.start) + .min() + .unwrap_or_else(Instant::now) + } + + fn calculate_active_intervals( + cache: &SpanCache, + thread_id: u64, + current_time: Instant, + ) -> Vec<(Instant, Instant)> { + let mut events: Vec<(Instant, bool)> = Vec::new(); + + for span in cache.spans.values() { + if span.thread_id == thread_id { + events.push((span.start, true)); + if let Some(end) = span.end { + events.push((end, false)); + } + } + } + + events.sort_by_key(|(time, _)| *time); + + let mut intervals = Vec::new(); + let mut depth = 0; + let mut interval_start = None; + + for (time, is_start) in events { + if is_start { + if depth == 0 { + interval_start = Some(time); + } + depth += 1; + } else { + depth -= 1; + if depth == 0 { + if let Some(start) = interval_start { + intervals.push((start, time)); + interval_start = None; + } + } + } + } + + // Add ongoing interval + if depth > 0 { + if let Some(start) = interval_start { + intervals.push((start, current_time)); + } + } + + intervals } fn render_frame( framebuffer: &mut [u32], width: usize, height: usize, - frame_data: &FrameData, view: &ViewState, font: &fontdue::Font, ) { - let mut y = -(view.scroll_y as i32); + let cache = SPAN_CACHE.lock().unwrap(); - for track in &frame_data.tracks { - let track_height = track.height() as i32; + let mut y = -(view.scroll_y as i32); + let mut thread_ids: Vec<_> = cache.thread_roots.keys().copied().collect(); + thread_ids.sort_unstable(); + + for thread_id in thread_ids { + let track_height = get_thread_height(&cache, thread_id, view) as i32; // Only render if visible if y + track_height > 0 && y < height as i32 { - render_track(framebuffer, width, height, y, track, frame_data, view, font); + render_track(framebuffer, width, height, y, thread_id, &cache, view, font); } y += track_height; @@ -1018,8 +790,8 @@ mod window { width: usize, height: usize, y: i32, - track: &ThreadTrack, - frame_data: &FrameData, + thread_id: u64, + cache: &SpanCache, view: &ViewState, font: &fontdue::Font, ) { @@ -1034,12 +806,13 @@ mod window { fill_rect(framebuffer, width, 0, header_y, width, header_height, HEADER_BG); // Draw expand/collapse indicator - let indicator = if track.expanded { "▼" } else { "▶" }; + let expanded = view.is_track_expanded(thread_id); + let indicator = if expanded { "▼" } else { "▶" }; draw_text(framebuffer, width, 4, header_y + 4, indicator, font, 14.0, TEXT_COLOR); // Draw thread name - let default_name = format!("Thread {}", track.thread_id); - let name = track.name.as_ref() + let default_name = format!("Thread {}", thread_id); + let name = cache.thread_names.get(&thread_id) .map(|s| s.as_str()) .unwrap_or(&default_name); draw_text(framebuffer, width, 24, header_y + 4, name, font, 14.0, TEXT_COLOR); @@ -1047,60 +820,50 @@ mod window { let content_y = y + TRACK_HEADER_HEIGHT as i32; - if track.expanded { + if view.is_track_expanded(thread_id) { // Render full call stack - render_track_expanded(framebuffer, width, height, content_y, track, frame_data, view, font); + if let Some(roots) = cache.thread_roots.get(&thread_id) { + let earliest = get_earliest_time(cache); + for &root_id in roots { + render_span(framebuffer, width, height, content_y, root_id, cache, view, font, 0, earliest); + } + } } else { // Render collapsed (active time only) - render_track_collapsed(framebuffer, width, height, content_y, track, frame_data, view); + render_track_collapsed(framebuffer, width, height, content_y, thread_id, cache, view); } } - fn render_track_expanded( - framebuffer: &mut [u32], - width: usize, - height: usize, - y: i32, - track: &ThreadTrack, - frame_data: &FrameData, - view: &ViewState, - font: &fontdue::Font, - ) { - // Render each root span and its children - for root in &track.root_spans { - render_span_node(framebuffer, width, height, y, root, frame_data, view, font, 0); - } - } - - fn render_span_node( + fn render_span( framebuffer: &mut [u32], width: usize, height: usize, base_y: i32, - node: &SpanNode, - frame_data: &FrameData, + span_id: u64, + cache: &SpanCache, view: &ViewState, font: &fontdue::Font, depth: usize, + earliest: Instant, ) { + let span = &cache.spans[&span_id]; let y = base_y + (depth * ROW_HEIGHT) as i32; if y >= height as i32 || y + (ROW_HEIGHT as i32) < 0 { return; } - let start_time = (node.start - frame_data.earliest).as_secs_f64(); - let end_time = node.end - .map(|e| (e - frame_data.earliest).as_secs_f64()) - .unwrap_or_else(|| (view.current_time() - frame_data.earliest).as_secs_f64()); + let start_time = (span.start - earliest).as_secs_f64(); + let end_time = span.end + .map(|e| (e - earliest).as_secs_f64()) + .unwrap_or_else(|| (view.current_time() - earliest).as_secs_f64()); let x1 = ((start_time - view.time_offset) * view.time_scale) as i32; let x2 = ((end_time - view.time_offset) * view.time_scale) as i32; if x2 > 0 && x1 < width as i32 { - let color = get_color_for_name(node.name); + let color = get_color_for_name(span.name); - // Clamp to visible area let visible_x1 = x1.max(0) as usize; let visible_x2 = (x2.min(width as i32) as usize).max(visible_x1); let bar_width = visible_x2.saturating_sub(visible_x1); @@ -1132,7 +895,7 @@ mod window { width, visible_x1.saturating_add(inset_x + 4), y as usize + inset_y + 2, - node.name, + span.name, font, 12.0, TEXT_COLOR, @@ -1142,8 +905,10 @@ mod window { } // Render children - for child in &node.children { - render_span_node(framebuffer, width, height, base_y, child, frame_data, view, font, depth + 1); + if let Some(children) = cache.children.get(&span_id) { + for &child_id in children { + render_span(framebuffer, width, height, base_y, child_id, cache, view, font, depth + 1, earliest); + } } } @@ -1152,8 +917,8 @@ mod window { width: usize, height: usize, y: i32, - track: &ThreadTrack, - frame_data: &FrameData, + thread_id: u64, + cache: &SpanCache, view: &ViewState, ) { if y >= height as i32 || y + (COLLAPSED_TRACK_HEIGHT as i32) < 0 { @@ -1161,17 +926,18 @@ mod window { } let y = y.max(0) as usize; + let earliest = get_earliest_time(cache); + let active_intervals = calculate_active_intervals(cache, thread_id, view.current_time()); // Draw active intervals - for &(start, end) in &track.active_intervals { - let start_time = (start - frame_data.earliest).as_secs_f64(); - let end_time = (end - frame_data.earliest).as_secs_f64(); + for (start, end) in active_intervals { + let start_time = (start - earliest).as_secs_f64(); + let end_time = (end - earliest).as_secs_f64(); let x1 = ((start_time - view.time_offset) * view.time_scale) as i32; let x2 = ((end_time - view.time_offset) * view.time_scale) as i32; if x2 > 0 && x1 < width as i32 { - // Clamp to visible area let visible_x1 = x1.max(0) as usize; let visible_x2 = (x2.min(width as i32) as usize).max(visible_x1); let bar_width = visible_x2.saturating_sub(visible_x1); @@ -1414,9 +1180,6 @@ mod window { for ch in text.chars() { let (metrics, bitmap) = font.rasterize(ch, size); - // xmin is the offset from the cursor to the left edge of the glyph - // ymin is the offset from the baseline to the bottom edge of the glyph - // The glyph's position is: (cursor + xmin, baseline - height - ymin) let glyph_x = (cursor_x + metrics.xmin).max(0) as usize; let glyph_y = (size as i32 + y as i32 - metrics.height as i32 - metrics.ymin).max(0) as usize; @@ -1436,7 +1199,6 @@ mod window { } } - // Advance cursor by the advance_width (includes spacing) cursor_x += metrics.advance_width as i32; } } @@ -1446,14 +1208,12 @@ mod window { COLORS[(hash as usize) % COLORS.len()] } - // Format a number with thousands separators fn format_number(n: f64) -> String { let s = format!("{:.2}", n); let parts: Vec<&str> = s.split('.').collect(); let integer_part = parts[0]; let decimal_part = if parts.len() > 1 { parts[1] } else { "00" }; - // Add commas to integer part let mut result = String::new(); let chars: Vec = integer_part.chars().collect(); for (i, ch) in chars.iter().enumerate() {