Compare commits

..

2 Commits

Author SHA1 Message Date
873e6419b8 update docs 2025-12-17 00:36:21 +01:00
a9d0d7ec42 fix text rendering 2025-12-17 00:33:23 +01:00
2 changed files with 61 additions and 43 deletions

View File

@@ -2,64 +2,57 @@
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 ~1200 LOC with 'minimal' dependencies (for a Rust project). Inspired by RAD Telemetry - built in ~1200 LOC with minimal dependencies.
## Features ## Features
- **Icicle graph** showing call stack hierarchy (top half) - **Unified thread tracks** with expandable call stacks (click headers to toggle)
- **Thread timeline** showing per-thread activity over time (bottom half) - **Collapsed view** shows when threads are active (easy to spot blocking)
- **Expanded view** shows full call stack hierarchy with flame graph visualization
- **Ongoing span support** for long-running functions (main loops, render threads)
- **Pause mechanism** to freeze your application for inspection (Space bar)
- **Monokai color palette** for easy visual distinction - **Monokai color palette** for easy visual distinction
- **Pause mechanism** to freeze your application for inspection - **Incremental tree building** - only processes new spans each frame
- **Ringbuffer storage** (~16MB, 1M events) for recent history - **Ringbuffer storage** (~16MB, 1M events) for recent history
- **Lock-free event recording** via MPSC channels - **Lock-free event recording** via MPSC channels
## Dependencies ## Dependencies
Only 7 dependencies (~70 total including transitive): Only 7 direct 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` - `fontdue` - Font rasterization
- `procmacro2` - `procmacro2`, `syn`, `quote` - Proc macros
- `syn`
- `quote`
## Usage ## Usage
### Add to your `Cargo.toml`: ### Add to your `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
teleprof = { path = "../teleprof" } teleprof = { path = "../teleprof" }
``` ```
### In your code: ### In your code:
```rust ```rust
fn main() { fn main() {
// Start the profiler window (separate thread) // Start the profiler window (separate thread)
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
teleprof::start(); teleprof::start();
// Your application code // Name your thread (optional, shows in UI)
#[cfg(debug_assertions)]
teleprof::set_thread_name("main");
game_loop(); game_loop();
} }
fn game_loop() { fn game_loop() {
loop { loop {
// Profile a scope teleprof::span!("main_frame");
teleprof::span!("game_loop");
update(); update();
render(); render();
// Check if paused (optional)
if teleprof::PAUSE.try_lock().is_err() {
// Wait until unpaused
while teleprof::PAUSE.try_lock().is_err() {
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
} }
} }
@@ -74,18 +67,13 @@ fn render() {
} }
``` ```
### For closures:
```rust
let work = || {
teleprof::span!("my_closure");
// work...
};
```
## Controls ## Controls
- **Space**: Toggle pause (acquires `PAUSE` lock to freeze your app) - **Space**: Toggle pause (freezes ongoing spans at current time)
- **Left click + drag**: Box select to zoom (click background not function)
- **Right click + drag**: Pan timeline
- **Scroll**: Zoom timeline horizontally
- **Click track header**: Expand/collapse thread's call stack
- **Escape**: Close profiler window - **Escape**: Close profiler window
## How it works ## How it works
@@ -94,19 +82,20 @@ let work = || {
2. When the guard drops, sends `SpanEnd` 2. When the guard drops, sends `SpanEnd`
3. Events are sent via lock-free MPSC channel 3. Events are sent via lock-free MPSC channel
4. Window thread drains events into a fixed-size ringbuffer 4. Window thread drains events into a fixed-size ringbuffer
5. Renders icicle graph (call hierarchy) and timeline (per-thread activity) 5. Incrementally builds per-thread call trees (only processes new spans)
6. Renders unified thread tracks with expandable call stacks
## Design Goals ## Design Goals
- **Minimal overhead**: Lock-free event recording - **Minimal overhead**: Lock-free event recording, incremental tree building
- **Debug-only**: Compile out in release builds with `#[cfg(debug_assertions)]` - **Debug-only**: Compile out in release builds with `#[cfg(debug_assertions)]`
- **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
- **Handle any thread pattern**: Long-lived, short-lived, thread pools (Rayon, etc.)
## Examples ## Examples
Run the included examples: Run the included examples:
```bash ```bash
# Multi-threaded physics simulation # Multi-threaded physics simulation
cargo run --example demo cargo run --example demo
@@ -122,4 +111,4 @@ The bouncing ball example demonstrates:
## License ## License
??? ???

View File

@@ -1372,7 +1372,7 @@ mod window {
draw_text(framebuffer, width, text_x, text_y, hover.name, font, 14.0, 0xFFFFFF); draw_text(framebuffer, width, text_x, text_y, hover.name, font, 14.0, 0xFFFFFF);
draw_text(framebuffer, width, text_x, text_y + 20, draw_text(framebuffer, width, text_x, text_y + 20,
&format!("Duration: {:.2} μs", hover.duration_us), font, 12.0, 0xCCCCCC); &format!("Duration: {} μs", format_number(hover.duration_us)), font, 12.0, 0xCCCCCC);
draw_text(framebuffer, width, text_x, text_y + 38, draw_text(framebuffer, width, text_x, text_y + 38,
&format!("Thread: {}", hover.thread_id), font, 12.0, 0xCCCCCC); &format!("Thread: {}", hover.thread_id), font, 12.0, 0xCCCCCC);
draw_text(framebuffer, width, text_x, text_y + 56, draw_text(framebuffer, width, text_x, text_y + 56,
@@ -1409,17 +1409,23 @@ mod window {
size: f32, size: f32,
color: u32, color: u32,
) { ) {
let mut cursor_x = x as f32; let mut cursor_x = x as i32;
for ch in text.chars() { for ch in text.chars() {
let (metrics, bitmap) = font.rasterize(ch, size); 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;
for (i, &alpha) in bitmap.iter().enumerate() { for (i, &alpha) in bitmap.iter().enumerate() {
if alpha > 0 { if alpha > 0 {
let px = cursor_x as usize + (i % metrics.width); let px = glyph_x + (i % metrics.width);
let py = y + (i / metrics.width) + (size as usize - metrics.height); let py = glyph_y + (i / metrics.width);
if px < width { if px < width && py < framebuffer.len() / width {
let idx = py * width + px; let idx = py * width + px;
if idx < framebuffer.len() { if idx < framebuffer.len() {
let alpha_f = alpha as f32 / 255.0; let alpha_f = alpha as f32 / 255.0;
@@ -1430,7 +1436,8 @@ mod window {
} }
} }
cursor_x += metrics.advance_width; // Advance cursor by the advance_width (includes spacing)
cursor_x += metrics.advance_width as i32;
} }
} }
@@ -1438,4 +1445,26 @@ mod window {
let hash = name.bytes().fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32)); let hash = name.bytes().fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32));
COLORS[(hash as usize) % COLORS.len()] 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<char> = integer_part.chars().collect();
for (i, ch) in chars.iter().enumerate() {
if i > 0 && (chars.len() - i) % 3 == 0 {
result.push(',');
}
result.push(*ch);
}
result.push('.');
result.push_str(decimal_part);
result
}
} }