polish the rendering a bit, add a macro

This commit is contained in:
2025-12-16 23:20:34 +01:00
parent 11d9ebe2b6
commit cd790cbf7e
4 changed files with 232 additions and 101 deletions

View File

@@ -2,7 +2,7 @@
A lightweight, debug-only telemetry profiler for Rust applications. Shows thread activity and call stack hierarchy in real-time. A lightweight, debug-only telemetry profiler for Rust applications. Shows thread activity and call stack hierarchy in real-time.
Inspired by RAD Telemetry - built in ~400 LOC with minimal dependencies. Inspired by RAD Telemetry - built in ~1200 LOC with 'minimal' dependencies (for a Rust project).
## Features ## Features
@@ -15,10 +15,14 @@ Inspired by RAD Telemetry - built in ~400 LOC with minimal dependencies.
## Dependencies ## Dependencies
Only 3 dependencies (~15 total including transitive): Only 7 dependencies (~70 total including transitive):
- `minifb` - Window and framebuffer - `minifb` - Window and framebuffer
- `crossbeam-channel` - Lock-free MPSC - `crossbeam-channel` - Lock-free MPSC
- `once_cell` - Lazy statics - `once_cell` - Lazy statics
- `fontdue`
- `procmacro2`
- `syn`
- `quote`
## Usage ## Usage
@@ -26,7 +30,7 @@ Only 3 dependencies (~15 total including transitive):
```toml ```toml
[dependencies] [dependencies]
teleprof = { path = "../teleprof" } # or from crates.io when published teleprof = { path = "../teleprof" }
``` ```
### In your code: ### In your code:
@@ -99,26 +103,6 @@ let work = || {
- **Separate window**: Doesn't interfere with your app's rendering - **Separate window**: Doesn't interfere with your app's rendering
- **Simple API**: Just `span!("name")` and you're done - **Simple API**: Just `span!("name")` and you're done
## Example Output
```
┌─────────────────────────────────────┐
│ Icicle Graph (Call Stack) │
│ ┌──────────────────────────┐ │
│ │ frame_work │ │
│ ├──────────┬───────────────┤ │
│ │ physics │ render │ │
│ ├────┬─────┤ │ │
│ │ w0 │ w1 │ │ │
│ └────┴─────┴───────────────┘ │
├─────────────────────────────────────┤
│ Thread Timeline │
│ Main: ████████████████████ │
│ Work 0: ░░██████░░░░░░░░░░░ │
│ Work 1: ░░░░░░██████░░░░░░░ │
└─────────────────────────────────────┘
```
## Examples ## Examples
Run the included examples: Run the included examples:
@@ -138,4 +122,4 @@ The bouncing ball example demonstrates:
## License ## License
MIT / Apache-2.0 (choose whichever you prefer) ???

View File

@@ -32,6 +32,7 @@ impl Ball {
} }
} }
#[instrument]
fn main() { fn main() {
// Start the telemetry window // Start the telemetry window
teleprof::start(); teleprof::start();
@@ -102,6 +103,7 @@ fn main_frame(
teleprof::set_thread_name(format!("ColorPicker-{}", id)); teleprof::set_thread_name(format!("ColorPicker-{}", id));
pick_new_color(ball_clone); pick_new_color(ball_clone);
}); });
let _ = COLOR_PICKER_COUNTER.fetch_update(Ordering::Relaxed,Ordering::Relaxed,|cnt| Some(cnt -1));
} }
// Render - also instrumented, so dedup will handle it // Render - also instrumented, so dedup will handle it
@@ -118,7 +120,7 @@ fn main_frame(
} }
} }
#[instrument]
fn update_physics(ball: &Arc<Mutex<Ball>>, dt: f32) -> bool { fn update_physics(ball: &Arc<Mutex<Ball>>, dt: f32) -> bool {
let mut ball = ball.lock().unwrap(); let mut ball = ball.lock().unwrap();
@@ -163,7 +165,6 @@ fn pick_new_color(ball: Arc<Mutex<Ball>>) {
} }
// Using instrument_calls here too to see the breakdown of rendering // Using instrument_calls here too to see the breakdown of rendering
#[instrument_calls]
fn render(ball: &Arc<Mutex<Ball>>, framebuffer: &mut [u32]) { fn render(ball: &Arc<Mutex<Ball>>, framebuffer: &mut [u32]) {
clear_background(framebuffer); clear_background(framebuffer);
draw_ball(ball, framebuffer); draw_ball(ball, framebuffer);
@@ -203,7 +204,6 @@ fn submit_frame() {
thread::sleep(Duration::from_millis(2)); thread::sleep(Duration::from_millis(2));
} }
#[instrument]
fn print_status(ball: &Arc<Mutex<Ball>>, frame: u32) { fn print_status(ball: &Arc<Mutex<Ball>>, frame: u32) {
let ball = ball.lock().unwrap(); let ball = ball.lock().unwrap();
println!( println!(

View File

@@ -66,7 +66,22 @@ pub fn instrument_calls(_args: TokenStream, input: TokenStream) -> TokenStream {
let mut visitor = CallInstrumenter; let mut visitor = CallInstrumenter;
visitor.visit_block_mut(&mut input_fn.block); visitor.visit_block_mut(&mut input_fn.block);
TokenStream::from(quote! { #input_fn }) // Add an outer span for the function itself
let fn_name = input_fn.sig.ident.to_string();
let fn_vis = &input_fn.vis;
let fn_sig = &input_fn.sig;
let fn_block = &input_fn.block;
let fn_attrs = &input_fn.attrs;
let instrumented = quote! {
#(#fn_attrs)*
#fn_vis #fn_sig {
let _guard = ::teleprof::SpanGuard::new(#fn_name);
#fn_block
}
};
TokenStream::from(instrumented)
} }
struct CallInstrumenter; struct CallInstrumenter;

View File

@@ -509,14 +509,26 @@ mod window {
fn update_hover(view: &mut ViewState, buffer: &RingBuffer, earliest: Instant, mx: f32, my: f32, icicle_height: usize) { fn update_hover(view: &mut ViewState, buffer: &RingBuffer, earliest: Instant, mx: f32, my: f32, icicle_height: usize) {
view.hovered_span = None; 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); let mouse_time = view.icicle_time_offset + (mx as f64 / view.icicle_time_scale);
// Find span under cursor if my < icicle_height as f32 {
// Icicle view hover
let row_height = 24;
let scroll_offset = view.icicle_scroll_y as i32;
let adjusted_y = my as i32 + scroll_offset;
let mouse_depth = (adjusted_y / row_height as i32).max(0) as usize;
// Collect all spans and calculate their depths
let spans: Vec<_> = buffer.iter().collect();
let depths = calculate_span_depths(&spans);
// Find span under cursor that matches both time AND depth
for span in buffer.iter() { for span in buffer.iter() {
if let Some(&span_depth) = depths.get(&span.span_id) {
if span_depth != mouse_depth {
continue;
}
let start_time = (span.start - earliest).as_secs_f64(); let start_time = (span.start - earliest).as_secs_f64();
let end_time = (span.end - earliest).as_secs_f64(); let end_time = (span.end - earliest).as_secs_f64();
@@ -528,10 +540,83 @@ mod window {
thread_id: span.thread_id, thread_id: span.thread_id,
start_time, start_time,
}); });
break; // Take first match (could be improved to find best match by y-position) break;
} }
} }
} }
} else {
// Timeline view hover
let timeline_y = my - icicle_height as f32;
// Collect all spans and group by thread
let spans: Vec<_> = buffer.iter().collect();
let mut threads: HashMap<u64, Vec<&CompletedSpan>> = HashMap::new();
for span in &spans {
threads.entry(span.thread_id).or_default().push(*span);
}
let mut thread_ids: Vec<_> = threads.keys().copied().collect();
thread_ids.sort_unstable();
let num_threads = thread_ids.len();
if num_threads > 0 {
let timeline_height = icicle_height; // Assuming timeline takes remaining half
let row_height = (timeline_height / num_threads.max(1)).max(20);
let mouse_thread_row = (timeline_y as usize) / row_height;
if mouse_thread_row < thread_ids.len() {
let thread_id = thread_ids[mouse_thread_row];
let thread_spans = &threads[&thread_id];
// Find span at this time in this thread
for span in thread_spans {
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;
}
}
}
}
}
}
fn calculate_span_depths(spans: &[&CompletedSpan]) -> HashMap<u64, usize> {
let mut children: HashMap<u64, Vec<&CompletedSpan>> = HashMap::new();
let mut depths: HashMap<u64, usize> = HashMap::new();
let mut roots = Vec::new();
for span in spans {
if let Some(parent) = span.parent_id {
children.entry(parent).or_default().push(span);
} else {
roots.push(span.span_id);
depths.insert(span.span_id, 0);
}
}
// Compute depths for all spans using BFS
let mut queue: Vec<u64> = roots.clone();
while let Some(span_id) = queue.pop() {
let current_depth = depths[&span_id];
if let Some(child_list) = children.get(&span_id) {
for child in child_list {
depths.insert(child.span_id, current_depth + 1);
queue.push(child.span_id);
}
}
}
depths
}
fn render_frame( fn render_frame(
framebuffer: &mut [u32], framebuffer: &mut [u32],
@@ -662,10 +747,31 @@ mod window {
let tooltip_w = 280; let tooltip_w = 280;
let tooltip_h = 80; let tooltip_h = 80;
let padding = 8; let padding = 8;
let margin = 10; // Margin from screen edges
let offset = 20; // Offset from cursor
// Position tooltip near cursor but keep it on screen // Calculate initial position (offset from cursor)
let tooltip_x = (view.mouse_x as usize + 20).min(width.saturating_sub(tooltip_w + 10)); let mut tooltip_x = view.mouse_x as i32 + offset;
let tooltip_y = (view.mouse_y as usize + 20).min(height.saturating_sub(tooltip_h + 10)); let mut tooltip_y = view.mouse_y as i32 + offset;
// Check if tooltip would go off right edge
if tooltip_x + tooltip_w as i32 + margin > width as i32 {
// Position to the left of cursor instead
tooltip_x = view.mouse_x as i32 - tooltip_w as i32 - offset;
}
// Check if tooltip would go off bottom edge
if tooltip_y + tooltip_h as i32 + margin > height as i32 {
// Position above cursor instead
tooltip_y = view.mouse_y as i32 - tooltip_h as i32 - offset;
}
// Clamp to screen bounds with margin
tooltip_x = tooltip_x.clamp(margin, (width as i32 - tooltip_w as i32 - margin).max(margin));
tooltip_y = tooltip_y.clamp(margin, (height as i32 - tooltip_h as i32 - margin).max(margin));
let tooltip_x = tooltip_x as usize;
let tooltip_y = tooltip_y as usize;
// Draw tooltip background // Draw tooltip background
for dy in 0..tooltip_h { for dy in 0..tooltip_h {
@@ -767,31 +873,8 @@ mod window {
view: &ViewState, view: &ViewState,
font: &fontdue::Font, font: &fontdue::Font,
) { ) {
// Build tree structure and compute depths // Calculate depths for all spans
let mut children: HashMap<u64, Vec<&CompletedSpan>> = HashMap::new(); let depths = calculate_span_depths(spans);
let mut depths: HashMap<u64, usize> = HashMap::new();
let mut roots = Vec::new();
for span in spans {
if let Some(parent) = span.parent_id {
children.entry(parent).or_default().push(span);
} else {
roots.push(span.span_id);
depths.insert(span.span_id, 0);
}
}
// Compute depths for all spans using BFS
let mut queue: Vec<u64> = roots.clone();
while let Some(span_id) = queue.pop() {
let current_depth = depths[&span_id];
if let Some(child_list) = children.get(&span_id) {
for child in child_list {
depths.insert(child.span_id, current_depth + 1);
queue.push(child.span_id);
}
}
}
// Group spans by depth level // Group spans by depth level
let mut spans_by_depth: HashMap<usize, Vec<&CompletedSpan>> = HashMap::new(); let mut spans_by_depth: HashMap<usize, Vec<&CompletedSpan>> = HashMap::new();
@@ -859,14 +942,52 @@ mod window {
let color = get_color_for_name(span.name); let color = get_color_for_name(span.name);
let bar_width = (x2 - x1).max(1) as usize; let bar_width = (x2 - x1).max(1) as usize;
fill_rect(framebuffer, width, x1.max(0) as usize, y, bar_width, row_height - 2, color); // Add inset padding
let inset_x = 2;
let inset_height = 1;
// Render text if there's enough space, otherwise render to the right let actual_width = if bar_width > inset_x * 2 {
if bar_width > 40 { bar_width.saturating_sub(inset_x * 2)
draw_text(framebuffer, width, x1.max(0) as usize + 4, y + 4, span.name, font, 14.0, TEXT_COLOR); } else { bar_width };
let actual_height = row_height.saturating_sub(inset_height + 1); // +1 for bottom gap
if actual_width > 0 && actual_height > 0 {
fill_rect(
framebuffer,
width,
(x1.max(0) as usize).saturating_add(inset_x),
y.saturating_add(inset_height),
actual_width,
actual_height,
color
);
// Render text if there's enough space
if actual_width > 40 {
draw_text(
framebuffer,
width,
(x1.max(0) as usize).saturating_add(inset_x + 4),
y.saturating_add(inset_height + 4),
span.name,
font,
14.0,
TEXT_COLOR
);
} else if x2 >= 0 && x2 < width as i32 { } else if x2 >= 0 && x2 < width as i32 {
// Draw text to the right of the bar // 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); draw_text(
framebuffer,
width,
x2.max(0) as usize + 4,
y + 4,
span.name,
font,
14.0,
TEXT_COLOR
);
}
} }
} }
} }
@@ -924,24 +1045,34 @@ mod window {
// Only render if visible // Only render if visible
if x2 > 0 && x1 < width as i32 { if x2 > 0 && x1 < width as i32 {
let color = get_color_for_name(span.name); let color = get_color_for_name(span.name);
let bar_width = (x2 - x1).max(1) as usize;
// Add inset padding: 2px horizontal, 1px vertical
let inset_x = 2;
let inset_y = 1;
let actual_width = if bar_width > inset_x * 2 {
bar_width.saturating_sub(inset_x * 2)
} else { bar_width };
let actual_height = row_height.saturating_sub(inset_y * 2 + 2); // Additional gap
if actual_width > 0 && actual_height > 0 {
fill_rect( fill_rect(
framebuffer, framebuffer,
width, width,
x1.max(0) as usize, (x1.max(0) as usize).saturating_add(inset_x),
y, y.saturating_add(inset_y),
(x2 - x1).max(1) as usize, actual_width,
row_height - 4, actual_height,
color color
); );
// Render text if there's enough space // Render text if there's enough space
let text_width = (x2 - x1) as usize; if actual_width > 60 {
if text_width > 60 {
draw_text( draw_text(
framebuffer, framebuffer,
width, width,
x1.max(0) as usize + 4, (x1.max(0) as usize).saturating_add(inset_x + 4),
y + row_height / 2 - 6, y + row_height / 2 - 6,
span.name, span.name,
font, font,
@@ -953,6 +1084,7 @@ mod window {
} }
} }
} }
}
fn fill_rect(framebuffer: &mut [u32], width: usize, x: usize, y: usize, w: usize, h: usize, color: u32) { fn fill_rect(framebuffer: &mut [u32], width: usize, x: usize, y: usize, w: usize, h: usize, color: u32) {
for dy in 0..h { for dy in 0..h {