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.
|
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)
|
???
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user