Compare commits
2 Commits
09457e56d4
...
873e6419b8
| Author | SHA1 | Date | |
|---|---|---|---|
| 873e6419b8 | |||
| a9d0d7ec42 |
59
README.md
59
README.md
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user