polish the rendering a bit, add a macro
This commit is contained in:
32
README.md
32
README.md
@@ -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)
|
||||
???
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user