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.
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
@@ -15,10 +15,14 @@ Inspired by RAD Telemetry - built in ~400 LOC with minimal dependencies.
## Dependencies
Only 3 dependencies (~15 total including transitive):
Only 7 dependencies (~70 total including transitive):
- `minifb` - Window and framebuffer
- `crossbeam-channel` - Lock-free MPSC
- `once_cell` - Lazy statics
- `fontdue`
- `procmacro2`
- `syn`
- `quote`
## Usage
@@ -26,7 +30,7 @@ Only 3 dependencies (~15 total including transitive):
```toml
[dependencies]
teleprof = { path = "../teleprof" } # or from crates.io when published
teleprof = { path = "../teleprof" }
```
### In your code:
@@ -99,26 +103,6 @@ let work = || {
- **Separate window**: Doesn't interfere with your app's rendering
- **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
Run the included examples:
@@ -138,4 +122,4 @@ The bouncing ball example demonstrates:
## License
MIT / Apache-2.0 (choose whichever you prefer)
???

View File

@@ -32,6 +32,7 @@ impl Ball {
}
}
#[instrument]
fn main() {
// Start the telemetry window
teleprof::start();
@@ -49,7 +50,7 @@ fn main() {
println!("- Flame graph rendering (all depth-N spans on row N)");
println!("- instrument_calls macro for automatic call instrumentation");
println!("- Runtime deduplication preventing double-wrapping");
println!();
println!();
let mut window = Window::new(
"Bouncing Ball",
@@ -102,6 +103,7 @@ fn main_frame(
teleprof::set_thread_name(format!("ColorPicker-{}", id));
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
@@ -118,7 +120,7 @@ fn main_frame(
}
}
#[instrument]
fn update_physics(ball: &Arc<Mutex<Ball>>, dt: f32) -> bool {
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
#[instrument_calls]
fn render(ball: &Arc<Mutex<Ball>>, framebuffer: &mut [u32]) {
clear_background(framebuffer);
draw_ball(ball, framebuffer);
@@ -203,7 +204,6 @@ fn submit_frame() {
thread::sleep(Duration::from_millis(2));
}
#[instrument]
fn print_status(ball: &Arc<Mutex<Ball>>, frame: u32) {
let ball = ball.lock().unwrap();
println!(

View File

@@ -66,7 +66,22 @@ pub fn instrument_calls(_args: TokenStream, input: TokenStream) -> TokenStream {
let mut visitor = CallInstrumenter;
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;

View File

@@ -505,34 +505,119 @@ mod window {
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 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;
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)
// 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() {
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 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;
}
}
}
} 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(
framebuffer: &mut [u32],
width: usize,
@@ -662,10 +747,31 @@ mod window {
let tooltip_w = 280;
let tooltip_h = 80;
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
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));
// Calculate initial position (offset from cursor)
let mut tooltip_x = view.mouse_x as i32 + offset;
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
for dy in 0..tooltip_h {
@@ -767,31 +873,8 @@ mod window {
view: &ViewState,
font: &fontdue::Font,
) {
// Build tree structure and compute depths
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);
}
}
}
// Calculate depths for all spans
let depths = calculate_span_depths(spans);
// Group spans by depth level
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 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;
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_height + 1); // +1 for bottom gap
// 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);
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 {
// 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
);
}
}
}
}
@@ -924,30 +1045,41 @@ 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 - 4,
color
);
// Add inset padding: 2px horizontal, 1px vertical
let inset_x = 2;
let inset_y = 1;
// Render text if there's enough space
let text_width = (x2 - x1) as usize;
if text_width > 60 {
draw_text(
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(
framebuffer,
width,
x1.max(0) as usize + 4,
y + row_height / 2 - 6,
span.name,
font,
12.0,
TEXT_COLOR
(x1.max(0) as usize).saturating_add(inset_x),
y.saturating_add(inset_y),
actual_width,
actual_height,
color
);
// Render text if there's enough space
if actual_width > 60 {
draw_text(
framebuffer,
width,
(x1.max(0) as usize).saturating_add(inset_x + 4),
y + row_height / 2 - 6,
span.name,
font,
12.0,
TEXT_COLOR
);
}
}
}
}