better but still not workable
This commit is contained in:
65
Cargo.lock
generated
65
Cargo.lock
generated
@@ -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",
|
||||
|
||||
16
Cargo.toml
16
Cargo.toml
@@ -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"
|
||||
@@ -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})",
|
||||
|
||||
@@ -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
BIN
fonts/DejaVuSansMono.ttf
Normal file
Binary file not shown.
458
src/lib.rs
458
src/lib.rs
@@ -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()]
|
||||
}
|
||||
}
|
||||
12
teleprof-macros/Cargo.toml
Normal file
12
teleprof-macros/Cargo.toml
Normal 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"
|
||||
58
teleprof-macros/src/lib.rs
Normal file
58
teleprof-macros/src/lib.rs
Normal 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
22
teleprof/Cargo.toml
Normal 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
691
teleprof/src/lib.rs
Normal 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()]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user