better but still not workable

This commit is contained in:
2025-12-11 00:12:16 +01:00
parent 5e9336c6c7
commit 4930d86e23
10 changed files with 945 additions and 566 deletions

65
Cargo.lock generated
View File

@@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "autocfg"
version = "1.5.0"
@@ -72,6 +78,12 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
@@ -94,6 +106,22 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "fontdue"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b"
dependencies = [
"hashbrown",
"ttf-parser",
]
[[package]]
name = "futures"
version = "0.3.31"
@@ -206,6 +234,17 @@ dependencies = [
"wasip2",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]]
name = "instant"
version = "0.1.13"
@@ -546,9 +585,20 @@ name = "teleprof"
version = "0.1.0"
dependencies = [
"crossbeam-channel",
"fontdue",
"minifb",
"once_cell",
"rand",
"teleprof-macros",
]
[[package]]
name = "teleprof-macros"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
@@ -564,6 +614,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "ttf-parser"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
[[package]]
name = "unicode-ident"
version = "1.0.22"
@@ -606,12 +662,13 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.45"
version = "0.4.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b"
checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
dependencies = [
"cfg-if",
"js-sys",
"once_cell",
"wasm-bindgen",
"web-sys",
]
@@ -723,9 +780,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.72"
version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
dependencies = [
"js-sys",
"wasm-bindgen",

View File

@@ -1,13 +1,7 @@
[package]
name = "teleprof"
version = "0.1.0"
edition = "2021"
[workspace]
members = ["teleprof", "teleprof-macros"]
resolver = "2"
[dependencies]
minifb = "0.27"
[workspace.dependencies]
crossbeam-channel = "0.5"
once_cell = "1.19"
[dev-dependencies]
rand = "0.8"
minifb = "0.27"
once_cell = "1.19"

View File

@@ -3,12 +3,12 @@ use std::thread;
use std::time::{Duration, Instant};
use std::sync::{Arc, Mutex};
use rand::Rng;
use teleprof::instrument;
const WIDTH: usize = 800;
const HEIGHT: usize = 600;
const BALL_RADIUS: usize = 20;
// Simple ball state
struct Ball {
x: f32,
y: f32,
@@ -22,9 +22,9 @@ impl Ball {
Self {
x: 400.0,
y: 300.0,
vx: 200.0, // pixels per second
vx: 200.0,
vy: 150.0,
color: 0xFF6464FF, // Red-ish
color: 0xFF6464FF,
}
}
}
@@ -32,10 +32,14 @@ impl Ball {
fn main() {
// Start the telemetry window
teleprof::start();
// Name the main thread
teleprof::set_thread_name("Main");
println!("Bouncing Ball Demo");
println!("The ball window should appear alongside the profiler");
println!("Press Space in profiler window to pause");
println!("Mouse wheel to zoom, drag to pan in profiler");
println!("Press Escape in either window to quit");
println!();
@@ -52,49 +56,14 @@ fn main() {
let ball = Arc::new(Mutex::new(Ball::new()));
let mut framebuffer = vec![0u32; WIDTH * HEIGHT];
// Target 30 FPS
let frame_time = Duration::from_millis(33);
let mut frame_count = 0;
while window.is_open() && !window.is_key_down(Key::Escape) {
let frame_start = Instant::now();
teleprof::span!("main_frame");
// Check if paused
if teleprof::PAUSE.try_lock().is_err() {
println!("Paused!");
while teleprof::PAUSE.try_lock().is_err() {
thread::sleep(Duration::from_millis(100));
}
println!("Resumed!");
}
main_frame(&ball, &mut framebuffer, &mut window, &mut frame_count, frame_time);
// Update physics
let hit_wall = update_physics(&ball, frame_time.as_secs_f32());
// If we hit a wall, spawn a thread to pick a new color
if hit_wall {
let ball_clone = Arc::clone(&ball);
thread::spawn(move || {
pick_new_color(ball_clone);
});
}
// Render
render(&ball, &mut framebuffer);
// Update window
window
.update_with_buffer(&framebuffer, WIDTH, HEIGHT)
.expect("Failed to update window");
frame_count += 1;
if frame_count % 30 == 0 {
print_status(&ball, frame_count);
}
// Sleep to maintain 30fps
let elapsed = frame_start.elapsed();
if elapsed < frame_time {
thread::sleep(frame_time - elapsed);
@@ -102,18 +71,58 @@ fn main() {
}
}
fn update_physics(ball: &Arc<Mutex<Ball>>, dt: f32) -> bool {
teleprof::span!("update_physics");
#[instrument]
fn main_frame(
ball: &Arc<Mutex<Ball>>,
framebuffer: &mut [u32],
window: &mut Window,
frame_count: &mut u32,
frame_time: Duration,
) {
// Check if paused
if teleprof::PAUSE.try_lock().is_err() {
println!("Paused!");
while teleprof::PAUSE.try_lock().is_err() {
thread::sleep(Duration::from_millis(100));
}
println!("Resumed!");
}
// Update physics
let hit_wall = update_physics(ball, frame_time.as_secs_f32());
// If we hit a wall, spawn a thread to pick a new color
if hit_wall {
let ball_clone = Arc::clone(ball);
thread::spawn(move || {
teleprof::set_thread_name("ColorPicker");
pick_new_color(ball_clone);
});
}
// Render
render(ball, framebuffer);
// Update window
window
.update_with_buffer(framebuffer, WIDTH, HEIGHT)
.expect("Failed to update window");
*frame_count += 1;
if *frame_count % 30 == 0 {
print_status(ball, *frame_count);
}
}
#[instrument]
fn update_physics(ball: &Arc<Mutex<Ball>>, dt: f32) -> bool {
let mut ball = ball.lock().unwrap();
// Update position
ball.x += ball.vx * dt;
ball.y += ball.vy * dt;
let mut hit_wall = false;
// Bounce off walls
let radius = BALL_RADIUS as f32;
if ball.x - radius < 0.0 || ball.x + radius > WIDTH as f32 {
ball.vx = -ball.vx;
@@ -127,16 +136,13 @@ fn update_physics(ball: &Arc<Mutex<Ball>>, dt: f32) -> bool {
hit_wall = true;
}
// Simulate some physics computation
thread::sleep(Duration::from_millis(5));
hit_wall
}
#[instrument]
fn pick_new_color(ball: Arc<Mutex<Ball>>) {
teleprof::span!("pick_new_color");
// Simulate some "expensive" color selection
thread::sleep(Duration::from_millis(10));
let mut rng = rand::thread_rng();
@@ -152,50 +158,48 @@ fn pick_new_color(ball: Arc<Mutex<Ball>>) {
println!(" → New color selected: RGB({}, {}, {})", r, g, b);
}
#[instrument]
fn render(ball: &Arc<Mutex<Ball>>, framebuffer: &mut [u32]) {
teleprof::span!("render");
clear_background(framebuffer);
draw_ball(ball, framebuffer);
submit_frame();
}
#[instrument]
fn clear_background(framebuffer: &mut [u32]) {
framebuffer.fill(0x2A2A2AFF);
}
#[instrument]
fn draw_ball(ball: &Arc<Mutex<Ball>>, framebuffer: &mut [u32]) {
let ball = ball.lock().unwrap();
{
teleprof::span!("clear_background");
// Clear to dark gray
framebuffer.fill(0x2A2A2AFF);
}
let cx = ball.x as i32;
let cy = ball.y as i32;
let radius = BALL_RADIUS as i32;
{
teleprof::span!("draw_ball");
let ball = ball.lock().unwrap();
// Draw ball as a filled circle
let cx = ball.x as i32;
let cy = ball.y as i32;
let radius = BALL_RADIUS as i32;
for dy in -radius..=radius {
for dx in -radius..=radius {
// Check if point is inside circle
if dx * dx + dy * dy <= radius * radius {
let x = cx + dx;
let y = cy + dy;
if x >= 0 && x < WIDTH as i32 && y >= 0 && y < HEIGHT as i32 {
let idx = y as usize * WIDTH + x as usize;
framebuffer[idx] = ball.color;
}
for dy in -radius..=radius {
for dx in -radius..=radius {
if dx * dx + dy * dy <= radius * radius {
let x = cx + dx;
let y = cy + dy;
if x >= 0 && x < WIDTH as i32 && y >= 0 && y < HEIGHT as i32 {
let idx = y as usize * WIDTH + x as usize;
framebuffer[idx] = ball.color;
}
}
}
}
{
teleprof::span!("submit_frame");
// Simulate GPU submission
thread::sleep(Duration::from_millis(2));
}
}
#[instrument]
fn submit_frame() {
thread::sleep(Duration::from_millis(2));
}
#[instrument]
fn print_status(ball: &Arc<Mutex<Ball>>, frame: u32) {
teleprof::span!("print_status");
let ball = ball.lock().unwrap();
println!(
"Frame {}: Ball at ({:.1}, {:.1})",

View File

@@ -1,5 +1,6 @@
use std::thread;
use std::time::Duration;
use teleprof::instrument;
fn main() {
// Start the telemetry window
@@ -7,7 +8,9 @@ fn main() {
println!("Teleprof demo running...");
println!("Press Space in the profiler window to pause/unpause");
println!("Mouse wheel to zoom, drag to pan");
println!("Press Escape in the profiler window to quit");
println!();
// Simulate some work
for i in 0..1000 {
@@ -26,9 +29,8 @@ fn main() {
}
}
#[instrument]
fn frame_work(frame: u32) {
teleprof::span!("frame_work");
physics_update();
render();
@@ -37,9 +39,8 @@ fn frame_work(frame: u32) {
}
}
#[instrument]
fn physics_update() {
teleprof::span!("physics_update");
// Spawn some worker threads
let handles: Vec<_> = (0..3).map(|i| {
thread::spawn(move || {
@@ -52,32 +53,30 @@ fn physics_update() {
}
}
#[instrument]
fn physics_worker(id: u32) {
teleprof::span!("physics_worker");
// Simulate work
let work_ms = 5 + (id * 2);
thread::sleep(Duration::from_millis(work_ms as u64));
}
#[instrument]
fn render() {
teleprof::span!("render");
build_command_buffer();
submit_to_gpu();
}
#[instrument]
fn build_command_buffer() {
teleprof::span!("build_command_buffer");
thread::sleep(Duration::from_millis(3));
}
#[instrument]
fn submit_to_gpu() {
teleprof::span!("submit_to_gpu");
thread::sleep(Duration::from_millis(2));
}
#[instrument]
fn occasional_task() {
teleprof::span!("occasional_task");
thread::sleep(Duration::from_millis(10));
}
}

BIN
fonts/DejaVuSansMono.ttf Normal file

Binary file not shown.

View File

@@ -1,458 +0,0 @@
use crossbeam_channel::{unbounded, Receiver, Sender};
use once_cell::sync::Lazy;
use std::cell::Cell;
use std::sync::{Arc, Mutex};
use std::time::Instant;
// ============================================================================
// Public API
// ============================================================================
/// Global PAUSE lock - acquire this to pause the application
pub static PAUSE: Lazy<Arc<Mutex<()>>> = Lazy::new(|| Arc::new(Mutex::new(())));
/// Start the telemetry window in a separate thread
pub fn start() {
std::thread::spawn(|| {
if let Err(e) = window::run() {
eprintln!("Teleprof window error: {}", e);
}
});
}
/// Create a profiling span - use via the `span!` macro
pub struct SpanGuard {
span_id: u64,
}
impl SpanGuard {
pub fn new(name: &'static str) -> Self {
let span_id = next_span_id();
let thread_id = std::thread::current().id();
let parent_id = PARENT_SPAN.with(|p| p.get());
// Set ourselves as the current parent for nested spans
PARENT_SPAN.with(|p| p.set(Some(span_id)));
EVENT_SENDER.send(Event::SpanStart {
span_id,
parent_id,
thread_id: thread_id_to_u64(thread_id),
name,
timestamp: Instant::now(),
}).ok();
Self { span_id }
}
}
impl Drop for SpanGuard {
fn drop(&mut self) {
EVENT_SENDER.send(Event::SpanEnd {
span_id: self.span_id,
timestamp: Instant::now(),
}).ok();
// Pop back to parent
PARENT_SPAN.with(|p| {
// Find parent by looking at active spans (simplified - just clear for now)
p.set(None);
});
}
}
/// Macro for creating a span
#[macro_export]
macro_rules! span {
($name:expr) => {
let _span_guard = $crate::SpanGuard::new($name);
};
}
// ============================================================================
// Internal types and state
// ============================================================================
#[derive(Debug, Clone)]
pub(crate) enum Event {
SpanStart {
span_id: u64,
parent_id: Option<u64>,
thread_id: u64,
name: &'static str,
timestamp: Instant,
},
SpanEnd {
span_id: u64,
timestamp: Instant,
},
}
thread_local! {
static PARENT_SPAN: Cell<Option<u64>> = Cell::new(None);
}
static EVENT_SENDER: Lazy<Sender<Event>> = Lazy::new(|| {
let (tx, rx) = unbounded();
*EVENT_RECEIVER.lock().unwrap() = Some(rx);
tx
});
static EVENT_RECEIVER: Lazy<Arc<Mutex<Option<Receiver<Event>>>>> =
Lazy::new(|| Arc::new(Mutex::new(None)));
static SPAN_ID_COUNTER: Lazy<Arc<Mutex<u64>>> = Lazy::new(|| Arc::new(Mutex::new(0)));
fn next_span_id() -> u64 {
let mut counter = SPAN_ID_COUNTER.lock().unwrap();
let id = *counter;
*counter += 1;
id
}
fn thread_id_to_u64(id: std::thread::ThreadId) -> u64 {
// Hack: ThreadId doesn't expose inner value, so we format and parse
let s = format!("{:?}", id);
s.trim_start_matches("ThreadId(")
.trim_end_matches(")")
.parse()
.unwrap_or(0)
}
// ============================================================================
// Window rendering
// ============================================================================
mod window {
use super::*;
use minifb::{Key, Window, WindowOptions};
use std::collections::HashMap;
const INITIAL_WIDTH: usize = 1280;
const INITIAL_HEIGHT: usize = 720;
const MAX_EVENTS: usize = 1_000_000; // ~16MB at 16 bytes per event
// Monokai palette
const COLORS: [u32; 8] = [
0xF92672, // Pink
0xA6E22E, // Green
0xFD971F, // Orange
0x66D9EF, // Cyan
0xAE81FF, // Purple
0xE6DB74, // Yellow
0xF8F8F2, // White
0x75715E, // Gray
];
const BG_COLOR: u32 = 0x272822;
const GRID_COLOR: u32 = 0x3E3D32;
struct CompletedSpan {
span_id: u64,
parent_id: Option<u64>,
thread_id: u64,
name: &'static str,
start: Instant,
end: Instant,
}
struct RingBuffer {
spans: Vec<CompletedSpan>,
head: usize,
pending_starts: HashMap<u64, (u64, Option<u64>, u64, &'static str, Instant)>,
}
impl RingBuffer {
fn new() -> Self {
Self {
spans: Vec::with_capacity(MAX_EVENTS),
head: 0,
pending_starts: HashMap::new(),
}
}
fn push_event(&mut self, event: Event) {
match event {
Event::SpanStart { span_id, parent_id, thread_id, name, timestamp } => {
self.pending_starts.insert(span_id, (span_id, parent_id, thread_id, name, timestamp));
}
Event::SpanEnd { span_id, timestamp } => {
if let Some((span_id, parent_id, thread_id, name, start)) = self.pending_starts.remove(&span_id) {
let span = CompletedSpan {
span_id,
parent_id,
thread_id,
name,
start,
end: timestamp,
};
if self.spans.len() < MAX_EVENTS {
self.spans.push(span);
} else {
self.spans[self.head] = span;
self.head = (self.head + 1) % MAX_EVENTS;
}
}
}
}
}
fn iter(&self) -> Box<dyn Iterator<Item = &CompletedSpan> + '_> {
if self.spans.len() < MAX_EVENTS {
Box::new(self.spans.iter())
} else {
// Return items in chronological order from ringbuffer
Box::new(self.spans[self.head..].iter().chain(self.spans[..self.head].iter()))
}
}
}
struct ViewState {
time_offset: f64, // seconds
time_scale: f64, // pixels per second
paused: bool,
pause_guard: Option<std::sync::MutexGuard<'static, ()>>,
}
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut window = Window::new(
"Teleprof",
INITIAL_WIDTH,
INITIAL_HEIGHT,
WindowOptions::default(),
)?;
window.set_target_fps(60);
let receiver = EVENT_RECEIVER.lock().unwrap().take()
.ok_or("Event receiver not initialized")?;
let mut buffer = RingBuffer::new();
let mut view = ViewState {
time_offset: 0.0,
time_scale: 100.0, // 100 pixels per second
paused: false,
pause_guard: None,
};
let mut framebuffer = vec![BG_COLOR; INITIAL_WIDTH * INITIAL_HEIGHT];
while window.is_open() && !window.is_key_down(Key::Escape) {
// Drain events from channel
while let Ok(event) = receiver.try_recv() {
buffer.push_event(event);
}
// Handle input
if window.is_key_pressed(Key::Space, minifb::KeyRepeat::No) {
view.paused = !view.paused;
if view.paused {
view.pause_guard = PAUSE.try_lock().ok();
} else {
view.pause_guard = None;
}
}
// Get current window size
let (width, height) = window.get_size();
if framebuffer.len() != width * height {
framebuffer.resize(width * height, BG_COLOR);
}
// Clear framebuffer
framebuffer.fill(BG_COLOR);
// Render
render_frame(&mut framebuffer, width, height, &buffer, &view);
window.update_with_buffer(&framebuffer, width, height)?;
}
Ok(())
}
fn render_frame(
framebuffer: &mut [u32],
width: usize,
height: usize,
buffer: &RingBuffer,
view: &ViewState,
) {
let icicle_height = height / 2;
let timeline_height = height - icicle_height;
// Find time range
let spans: Vec<_> = buffer.iter().collect();
if spans.is_empty() {
return;
}
let earliest = spans.iter().map(|s| s.start).min().unwrap();
let latest = spans.iter().map(|s| s.end).max().unwrap();
let duration = (latest - earliest).as_secs_f64();
// Draw icicle graph (top half)
render_icicle(framebuffer, width, icicle_height, &spans, earliest, duration, view);
// Draw timeline (bottom half)
render_timeline(
framebuffer,
width,
height,
icicle_height,
timeline_height,
&spans,
earliest,
duration,
view,
);
}
fn render_icicle(
framebuffer: &mut [u32],
width: usize,
height: usize,
spans: &[&CompletedSpan],
earliest: Instant,
duration: f64,
view: &ViewState,
) {
// Build tree structure
let mut roots = Vec::new();
let mut children: HashMap<u64, Vec<&CompletedSpan>> = HashMap::new();
for span in spans {
if let Some(parent) = span.parent_id {
children.entry(parent).or_default().push(span);
} else {
roots.push(*span);
}
}
// Render each root and its children recursively
let y_start = 0;
let row_height = 20;
for root in roots {
render_icicle_span(
framebuffer,
width,
height,
root,
&children,
earliest,
duration,
y_start,
row_height,
);
}
}
fn render_icicle_span(
framebuffer: &mut [u32],
width: usize,
height: usize,
span: &CompletedSpan,
children: &HashMap<u64, Vec<&CompletedSpan>>,
earliest: Instant,
duration: f64,
y: usize,
row_height: usize,
) {
if y + row_height > height {
return;
}
let start_time = (span.start - earliest).as_secs_f64();
let end_time = (span.end - earliest).as_secs_f64();
let x1 = ((start_time / duration) * width as f64) as usize;
let x2 = ((end_time / duration) * width as f64) as usize;
let color = get_color_for_name(span.name);
fill_rect(framebuffer, width, x1, y, x2 - x1, row_height, color);
// Render children
if let Some(child_spans) = children.get(&span.span_id) {
let child_y = y + row_height;
for child in child_spans {
render_icicle_span(
framebuffer,
width,
height,
child,
children,
earliest,
duration,
child_y,
row_height,
);
}
}
}
fn render_timeline(
framebuffer: &mut [u32],
width: usize,
_height: usize,
y_offset: usize,
timeline_height: usize,
spans: &[&CompletedSpan],
earliest: Instant,
duration: f64,
_view: &ViewState,
) {
// Group by thread
let mut threads: HashMap<u64, Vec<&CompletedSpan>> = HashMap::new();
for span in spans {
threads.entry(span.thread_id).or_default().push(*span);
}
let thread_ids: Vec<_> = threads.keys().copied().collect();
let num_threads = thread_ids.len();
if num_threads == 0 {
return;
}
let row_height = timeline_height / num_threads.max(1);
for (i, thread_id) in thread_ids.iter().enumerate() {
let y = y_offset + i * row_height;
let thread_spans = &threads[thread_id];
for span in thread_spans {
let start_time = (span.start - earliest).as_secs_f64();
let end_time = (span.end - earliest).as_secs_f64();
let x1 = ((start_time / duration) * width as f64) as usize;
let x2 = ((end_time / duration) * width as f64).max(x1 as f64 + 1.0) as usize;
let color = get_color_for_name(span.name);
fill_rect(framebuffer, width, x1, y, x2 - x1, row_height - 2, color);
}
}
}
fn fill_rect(framebuffer: &mut [u32], width: usize, x: usize, y: usize, w: usize, h: usize, color: u32) {
for dy in 0..h {
let row = y + dy;
for dx in 0..w {
let col = x + dx;
if col < width {
let idx = row * width + col;
if idx < framebuffer.len() {
framebuffer[idx] = color;
}
}
}
}
}
fn get_color_for_name(name: &str) -> u32 {
let hash = name.bytes().fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32));
COLORS[(hash as usize) % COLORS.len()]
}
}

View File

@@ -0,0 +1,12 @@
[package]
name = "teleprof-macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
syn = { version = "2.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"

View File

@@ -0,0 +1,58 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn, Lit};
#[proc_macro_attribute]
pub fn instrument(args: TokenStream, input: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(input as ItemFn);
// Parse custom name from attributes
let mut custom_name: Option<String> = None;
let args_parser = syn::meta::parser(|meta| {
if meta.path.is_ident("name") {
let value: Lit = meta.value()?.parse()?;
if let Lit::Str(lit_str) = value {
custom_name = Some(lit_str.value());
}
Ok(())
} else {
Err(meta.error("unsupported attribute"))
}
});
let _ = parse_macro_input!(args with args_parser);
let fn_name = &input_fn.sig.ident;
let span_name = custom_name.unwrap_or_else(|| fn_name.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;
// Check if function is async
let is_async = fn_sig.asyncness.is_some();
let instrumented = if is_async {
// For async functions, we need to instrument the future
quote! {
#(#fn_attrs)*
#fn_vis #fn_sig {
let _guard = ::teleprof::SpanGuard::new(#span_name);
async move #fn_block
}
}
} else {
// For sync functions, straightforward instrumentation
quote! {
#(#fn_attrs)*
#fn_vis #fn_sig {
let _guard = ::teleprof::SpanGuard::new(#span_name);
#fn_block
}
}
};
TokenStream::from(instrumented)
}

22
teleprof/Cargo.toml Normal file
View File

@@ -0,0 +1,22 @@
[package]
name = "teleprof"
version = "0.1.0"
edition = "2021"
[dependencies]
teleprof-macros = { path = "../teleprof-macros" }
crossbeam-channel = { workspace = true }
once_cell = { workspace = true }
minifb = "0.27"
fontdue = "0.9"
[dev-dependencies]
rand = "0.8"
[[example]]
name = "demo"
path = "../examples/demo.rs"
[[example]]
name = "bouncing_ball"
path = "../examples/bouncing_ball.rs"

691
teleprof/src/lib.rs Normal file
View File

@@ -0,0 +1,691 @@
use crossbeam_channel::{unbounded, Receiver, Sender};
use once_cell::sync::Lazy;
use std::cell::Cell;
use std::sync::{Arc, Mutex};
use std::time::Instant;
// Re-export the macro
pub use teleprof_macros::instrument;
// ============================================================================
// Public API
// ============================================================================
/// Global PAUSE lock - acquire this to pause the application
pub static PAUSE: Lazy<Arc<Mutex<()>>> = Lazy::new(|| Arc::new(Mutex::new(())));
/// Start the telemetry window in a separate thread
pub fn start() {
std::thread::spawn(|| {
if let Err(e) = window::run() {
eprintln!("Teleprof window error: {}", e);
}
});
}
/// Set the name for the current thread (will appear in timeline view)
pub fn set_thread_name(name: impl Into<String>) {
let thread_id = std::thread::current().id();
EVENT_SENDER.send(Event::ThreadName {
thread_id: thread_id_to_u64(thread_id),
name: name.into(),
}).ok();
}
/// Create a profiling span
pub struct SpanGuard {
span_id: u64,
}
impl SpanGuard {
pub fn new(name: &'static str) -> Self {
let span_id = next_span_id();
let thread_id = std::thread::current().id();
let parent_id = PARENT_SPAN.with(|p| p.get());
PARENT_SPAN.with(|p| p.set(Some(span_id)));
EVENT_SENDER.send(Event::SpanStart {
span_id,
parent_id,
thread_id: thread_id_to_u64(thread_id),
name,
timestamp: Instant::now(),
}).ok();
Self { span_id }
}
}
impl Drop for SpanGuard {
fn drop(&mut self) {
EVENT_SENDER.send(Event::SpanEnd {
span_id: self.span_id,
timestamp: Instant::now(),
}).ok();
// Restore parent
PARENT_SPAN.with(|p| p.set(None));
}
}
/// Macro for creating a span manually
#[macro_export]
macro_rules! span {
($name:expr) => {
let _span_guard = $crate::SpanGuard::new($name);
};
}
// ============================================================================
// Internal types and state
// ============================================================================
#[derive(Debug, Clone)]
pub(crate) enum Event {
SpanStart {
span_id: u64,
parent_id: Option<u64>,
thread_id: u64,
name: &'static str,
timestamp: Instant,
},
SpanEnd {
span_id: u64,
timestamp: Instant,
},
ThreadName {
thread_id: u64,
name: String,
},
}
thread_local! {
static PARENT_SPAN: Cell<Option<u64>> = Cell::new(None);
}
static EVENT_SENDER: Lazy<Sender<Event>> = Lazy::new(|| {
let (tx, rx) = unbounded();
*EVENT_RECEIVER.lock().unwrap() = Some(rx);
tx
});
static EVENT_RECEIVER: Lazy<Arc<Mutex<Option<Receiver<Event>>>>> =
Lazy::new(|| Arc::new(Mutex::new(None)));
static SPAN_ID_COUNTER: Lazy<Arc<Mutex<u64>>> = Lazy::new(|| Arc::new(Mutex::new(0)));
fn next_span_id() -> u64 {
let mut counter = SPAN_ID_COUNTER.lock().unwrap();
let id = *counter;
*counter += 1;
id
}
fn thread_id_to_u64(id: std::thread::ThreadId) -> u64 {
let s = format!("{:?}", id);
s.trim_start_matches("ThreadId(")
.trim_end_matches(")")
.parse()
.unwrap_or(0)
}
// ============================================================================
// Window rendering
// ============================================================================
mod window {
use super::*;
use minifb::{Key, MouseButton, MouseMode, Window, WindowOptions};
use std::collections::HashMap;
const INITIAL_WIDTH: usize = 1280;
const INITIAL_HEIGHT: usize = 720;
const MAX_EVENTS: usize = 1_000_000;
// Monokai palette
const COLORS: [u32; 8] = [
0xF92672, // Pink
0xA6E22E, // Green
0xFD971F, // Orange
0x66D9EF, // Cyan
0xAE81FF, // Purple
0xE6DB74, // Yellow
0xF8F8F2, // White
0x75715E, // Gray
];
const BG_COLOR: u32 = 0x272822;
const TEXT_COLOR: u32 = 0xF8F8F2;
struct CompletedSpan {
span_id: u64,
parent_id: Option<u64>,
thread_id: u64,
name: &'static str,
start: Instant,
end: Instant,
}
struct RingBuffer {
spans: Vec<CompletedSpan>,
head: usize,
pending_starts: HashMap<u64, (u64, Option<u64>, u64, &'static str, Instant)>,
thread_names: HashMap<u64, String>,
}
impl RingBuffer {
fn new() -> Self {
Self {
spans: Vec::with_capacity(MAX_EVENTS),
head: 0,
pending_starts: HashMap::new(),
thread_names: HashMap::new(),
}
}
fn push_event(&mut self, event: Event) {
match event {
Event::SpanStart { span_id, parent_id, thread_id, name, timestamp } => {
self.pending_starts.insert(span_id, (span_id, parent_id, thread_id, name, timestamp));
}
Event::SpanEnd { span_id, timestamp } => {
if let Some((span_id, parent_id, thread_id, name, start)) = self.pending_starts.remove(&span_id) {
let span = CompletedSpan {
span_id,
parent_id,
thread_id,
name,
start,
end: timestamp,
};
if self.spans.len() < MAX_EVENTS {
self.spans.push(span);
} else {
self.spans[self.head] = span;
self.head = (self.head + 1) % MAX_EVENTS;
}
}
}
Event::ThreadName { thread_id, name } => {
self.thread_names.insert(thread_id, name);
}
}
}
fn iter(&self) -> Box<dyn Iterator<Item = &CompletedSpan> + '_> {
if self.spans.len() < MAX_EVENTS {
Box::new(self.spans.iter())
} else {
Box::new(self.spans[self.head..].iter().chain(self.spans[..self.head].iter()))
}
}
}
struct ViewState {
// Pan/zoom (synchronized between views)
icicle_time_offset: f64,
icicle_time_scale: f64,
timeline_time_offset: f64,
timeline_time_scale: f64,
// Mouse state
mouse_down: bool,
last_mouse_x: f32,
last_mouse_y: f32,
mouse_visible: bool,
mouse_x: f32,
mouse_y: f32,
// Pause state
paused: bool,
pause_guard: Option<std::sync::MutexGuard<'static, ()>>,
}
impl ViewState {
fn new() -> Self {
Self {
icicle_time_offset: 0.0,
icicle_time_scale: 100.0,
timeline_time_offset: 0.0,
timeline_time_scale: 100.0,
mouse_down: false,
last_mouse_x: 0.0,
last_mouse_y: 0.0,
mouse_visible: false,
mouse_x: 0.0,
mouse_y: 0.0,
paused: false,
pause_guard: None,
}
}
}
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut window = Window::new(
"Teleprof",
INITIAL_WIDTH,
INITIAL_HEIGHT,
WindowOptions::default(),
)?;
window.set_target_fps(60);
let receiver = EVENT_RECEIVER.lock().unwrap().take()
.ok_or("Event receiver not initialized")?;
let mut buffer = RingBuffer::new();
let mut view = ViewState::new();
let mut framebuffer = vec![BG_COLOR; INITIAL_WIDTH * INITIAL_HEIGHT];
// Load font
let font_data = include_bytes!("../../fonts/DejaVuSansMono.ttf");
let font = fontdue::Font::from_bytes(font_data as &[u8], fontdue::FontSettings::default())?;
while window.is_open() && !window.is_key_down(Key::Escape) {
// Drain events from channel
while let Ok(event) = receiver.try_recv() {
buffer.push_event(event);
}
let (width, height) = window.get_size();
// Handle input
handle_input(&mut window, &mut view, width, height);
// Resize framebuffer if needed
if framebuffer.len() != width * height {
framebuffer.resize(width * height, BG_COLOR);
}
framebuffer.fill(BG_COLOR);
// Render
render_frame(&mut framebuffer, width, height, &buffer, &view, &font);
window.update_with_buffer(&framebuffer, width, height)?;
}
Ok(())
}
fn handle_input(window: &mut Window, view: &mut ViewState, _width: usize, height: usize) {
// Pause/unpause
if window.is_key_pressed(Key::Space, minifb::KeyRepeat::No) {
view.paused = !view.paused;
if view.paused {
view.pause_guard = PAUSE.try_lock().ok();
} else {
view.pause_guard = None;
}
}
// Get mouse position
if let Some((mx, my)) = window.get_mouse_pos(MouseMode::Clamp) {
view.mouse_visible = true;
view.mouse_x = mx;
view.mouse_y = my;
let icicle_height = height / 2;
let _is_icicle = my < icicle_height as f32;
// Handle mouse wheel (zoom)
if let Some((_, scroll_y)) = window.get_scroll_wheel() {
if scroll_y != 0.0 {
let zoom_factor = if scroll_y > 0.0 { 1.2 } else { 1.0 / 1.2 };
// Synchronize both views - zoom changes scale for both
let old_scale = view.icicle_time_scale;
let new_scale = old_scale * zoom_factor;
// Calculate mouse time in world space (same for both views since synchronized)
let mouse_time = view.icicle_time_offset + (mx as f64 / old_scale);
// Update scale and adjust offset to keep mouse position stable
view.icicle_time_scale = new_scale;
view.timeline_time_scale = new_scale;
let new_offset = mouse_time - (mx as f64 / new_scale);
view.icicle_time_offset = new_offset;
view.timeline_time_offset = new_offset;
}
}
// Handle mouse drag (pan)
let mouse_down = window.get_mouse_down(MouseButton::Left);
if mouse_down && view.mouse_down {
let dx = mx - view.last_mouse_x;
// Pan both views together (synchronized)
let delta = dx as f64 / view.icicle_time_scale;
view.icicle_time_offset -= delta;
view.timeline_time_offset -= delta;
}
view.mouse_down = mouse_down;
view.last_mouse_x = mx;
view.last_mouse_y = my;
} else {
view.mouse_visible = false;
}
}
fn render_frame(
framebuffer: &mut [u32],
width: usize,
height: usize,
buffer: &RingBuffer,
view: &ViewState,
font: &fontdue::Font,
) {
let icicle_height = height / 2;
let timeline_height = height - icicle_height;
let spans: Vec<_> = buffer.iter().collect();
if spans.is_empty() {
return;
}
let earliest = spans.iter().map(|s| s.start).min().unwrap();
let _latest = spans.iter().map(|s| s.end).max().unwrap();
// Render icicle graph (top half)
render_icicle(framebuffer, width, icicle_height, &spans, earliest, view, font);
// Draw separator line
for x in 0..width {
framebuffer[icicle_height * width + x] = 0x505050;
}
// Render timeline (bottom half)
render_timeline(
framebuffer,
width,
height,
icicle_height,
timeline_height,
buffer,
earliest,
view,
font,
);
// Draw cursor
if view.mouse_visible {
let cursor_x = view.mouse_x as usize;
// Draw vertical line
for y in 0..height {
if cursor_x < width {
let idx = y * width + cursor_x;
if idx < framebuffer.len() {
// Make cursor semi-transparent by blending
let bg = framebuffer[idx];
let bg_r = ((bg >> 16) & 0xFF) as u32;
let bg_g = ((bg >> 8) & 0xFF) as u32;
let bg_b = (bg & 0xFF) as u32;
let alpha = 0.5;
let r = (bg_r as f32 * (1.0 - alpha) + 255.0 * alpha) as u32;
let g = (bg_g as f32 * (1.0 - alpha) + 255.0 * alpha) as u32;
let b = (bg_b as f32 * (1.0 - alpha) + 255.0 * alpha) as u32;
framebuffer[idx] = (r << 16) | (g << 8) | b;
}
}
}
}
}
fn render_icicle(
framebuffer: &mut [u32],
width: usize,
height: usize,
spans: &[&CompletedSpan],
earliest: Instant,
view: &ViewState,
font: &fontdue::Font,
) {
// Build tree structure
let mut roots = Vec::new();
let mut children: HashMap<u64, Vec<&CompletedSpan>> = HashMap::new();
for span in spans {
if let Some(parent) = span.parent_id {
children.entry(parent).or_default().push(span);
} else {
roots.push(*span);
}
}
// Render each root and its children recursively
let row_height = 24;
let mut y_offset = 0;
for root in roots {
y_offset = render_icicle_span(
framebuffer,
width,
height,
root,
&children,
earliest,
y_offset,
row_height,
view,
font,
);
}
}
fn render_icicle_span(
framebuffer: &mut [u32],
width: usize,
height: usize,
span: &CompletedSpan,
children: &HashMap<u64, Vec<&CompletedSpan>>,
earliest: Instant,
y: usize,
row_height: usize,
view: &ViewState,
font: &fontdue::Font,
) -> usize {
if y + row_height > height {
return y;
}
let start_time = (span.start - earliest).as_secs_f64();
let end_time = (span.end - earliest).as_secs_f64();
// Apply zoom and pan
let x1 = ((start_time - view.icicle_time_offset) * view.icicle_time_scale) as i32;
let x2 = ((end_time - view.icicle_time_offset) * view.icicle_time_scale) as i32;
// Only render if visible
if x2 > 0 && x1 < width as i32 {
let color = get_color_for_name(span.name);
fill_rect(framebuffer, width, x1.max(0) as usize, y, (x2 - x1).max(1) as usize, row_height - 2, color);
// Render text if there's enough space
let text_width = (x2 - x1) as usize;
if text_width > 40 {
draw_text(framebuffer, width, x1.max(0) as usize + 4, y + 4, span.name, font, 14.0, TEXT_COLOR);
}
}
// Render children
let mut next_y = y + row_height;
if let Some(child_spans) = children.get(&span.span_id) {
for child in child_spans {
next_y = render_icicle_span(
framebuffer,
width,
height,
child,
children,
earliest,
next_y,
row_height,
view,
font,
);
}
}
next_y
}
fn render_timeline(
framebuffer: &mut [u32],
width: usize,
_height: usize,
y_offset: usize,
timeline_height: usize,
buffer: &RingBuffer,
earliest: Instant,
view: &ViewState,
font: &fontdue::Font,
) {
// Collect all spans
let spans: Vec<_> = buffer.iter().collect();
// Group by thread and sort for stable ordering
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 {
return;
}
let row_height = (timeline_height / num_threads.max(1)).max(20);
for (i, thread_id) in thread_ids.iter().enumerate() {
let y = y_offset + i * row_height;
let thread_spans = &threads[thread_id];
// Draw thread label with name if available
let label = if let Some(name) = buffer.thread_names.get(thread_id) {
name.clone()
} else {
format!("Thread {}", thread_id)
};
draw_text(framebuffer, width, 4, y + 4, &label, font, 12.0, TEXT_COLOR);
for span in thread_spans {
let start_time = (span.start - earliest).as_secs_f64();
let end_time = (span.end - earliest).as_secs_f64();
// Apply zoom and pan
let x1 = ((start_time - view.timeline_time_offset) * view.timeline_time_scale) as i32;
let x2 = ((end_time - view.timeline_time_offset) * view.timeline_time_scale) as i32;
// Only render if visible
if x2 > 0 && x1 < width as i32 {
let color = get_color_for_name(span.name);
fill_rect(
framebuffer,
width,
x1.max(0) as usize,
y,
(x2 - x1).max(1) as usize,
row_height - 4,
color
);
// Render text if there's enough space
let text_width = (x2 - x1) as usize;
if text_width > 60 {
draw_text(
framebuffer,
width,
x1.max(0) as usize + 4,
y + row_height / 2 - 6,
span.name,
font,
12.0,
TEXT_COLOR
);
}
}
}
}
}
fn fill_rect(framebuffer: &mut [u32], width: usize, x: usize, y: usize, w: usize, h: usize, color: u32) {
for dy in 0..h {
let row = y + dy;
for dx in 0..w {
let col = x + dx;
if col < width {
let idx = row * width + col;
if idx < framebuffer.len() {
framebuffer[idx] = color;
}
}
}
}
}
fn draw_text(
framebuffer: &mut [u32],
width: usize,
x: usize,
y: usize,
text: &str,
font: &fontdue::Font,
size: f32,
color: u32,
) {
let mut cursor_x = x as f32;
for ch in text.chars() {
let (metrics, bitmap) = font.rasterize(ch, size);
for (i, &alpha) in bitmap.iter().enumerate() {
if alpha > 0 {
let px = cursor_x as usize + (i % metrics.width);
let py = y + (i / metrics.width) + (size as usize - metrics.height);
if px < width {
let idx = py * width + px;
if idx < framebuffer.len() {
// Alpha blend
let alpha_f = alpha as f32 / 255.0;
let bg = framebuffer[idx];
let bg_r = ((bg >> 16) & 0xFF) as f32;
let bg_g = ((bg >> 8) & 0xFF) as f32;
let bg_b = (bg & 0xFF) as f32;
let fg_r = ((color >> 16) & 0xFF) as f32;
let fg_g = ((color >> 8) & 0xFF) as f32;
let fg_b = (color & 0xFF) as f32;
let r = (bg_r * (1.0 - alpha_f) + fg_r * alpha_f) as u32;
let g = (bg_g * (1.0 - alpha_f) + fg_g * alpha_f) as u32;
let b = (bg_b * (1.0 - alpha_f) + fg_b * alpha_f) as u32;
framebuffer[idx] = (r << 16) | (g << 8) | b;
}
}
}
}
cursor_x += metrics.advance_width;
}
}
fn get_color_for_name(name: &str) -> u32 {
let hash = name.bytes().fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32));
COLORS[(hash as usize) % COLORS.len()]
}
}