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.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allocator-api2"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -72,6 +78,12 @@ version = "1.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@@ -94,6 +106,22 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
|
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]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -206,6 +234,17 @@ dependencies = [
|
|||||||
"wasip2",
|
"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]]
|
[[package]]
|
||||||
name = "instant"
|
name = "instant"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
@@ -546,9 +585,20 @@ name = "teleprof"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
|
"fontdue",
|
||||||
"minifb",
|
"minifb",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rand",
|
"rand",
|
||||||
|
"teleprof-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "teleprof-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -564,6 +614,12 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ttf-parser"
|
||||||
|
version = "0.21.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
@@ -606,12 +662,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-futures"
|
name = "wasm-bindgen-futures"
|
||||||
version = "0.4.45"
|
version = "0.4.56"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b"
|
checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
"once_cell",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
@@ -723,9 +780,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.72"
|
version = "0.3.83"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
|
checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
|||||||
14
Cargo.toml
14
Cargo.toml
@@ -1,13 +1,7 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "teleprof"
|
members = ["teleprof", "teleprof-macros"]
|
||||||
version = "0.1.0"
|
resolver = "2"
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
[workspace.dependencies]
|
||||||
minifb = "0.27"
|
|
||||||
crossbeam-channel = "0.5"
|
crossbeam-channel = "0.5"
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
rand = "0.8"
|
|
||||||
minifb = "0.27"
|
|
||||||
@@ -3,12 +3,12 @@ use std::thread;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
use teleprof::instrument;
|
||||||
|
|
||||||
const WIDTH: usize = 800;
|
const WIDTH: usize = 800;
|
||||||
const HEIGHT: usize = 600;
|
const HEIGHT: usize = 600;
|
||||||
const BALL_RADIUS: usize = 20;
|
const BALL_RADIUS: usize = 20;
|
||||||
|
|
||||||
// Simple ball state
|
|
||||||
struct Ball {
|
struct Ball {
|
||||||
x: f32,
|
x: f32,
|
||||||
y: f32,
|
y: f32,
|
||||||
@@ -22,9 +22,9 @@ impl Ball {
|
|||||||
Self {
|
Self {
|
||||||
x: 400.0,
|
x: 400.0,
|
||||||
y: 300.0,
|
y: 300.0,
|
||||||
vx: 200.0, // pixels per second
|
vx: 200.0,
|
||||||
vy: 150.0,
|
vy: 150.0,
|
||||||
color: 0xFF6464FF, // Red-ish
|
color: 0xFF6464FF,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,9 +33,13 @@ fn main() {
|
|||||||
// Start the telemetry window
|
// Start the telemetry window
|
||||||
teleprof::start();
|
teleprof::start();
|
||||||
|
|
||||||
|
// Name the main thread
|
||||||
|
teleprof::set_thread_name("Main");
|
||||||
|
|
||||||
println!("Bouncing Ball Demo");
|
println!("Bouncing Ball Demo");
|
||||||
println!("The ball window should appear alongside the profiler");
|
println!("The ball window should appear alongside the profiler");
|
||||||
println!("Press Space in profiler window to pause");
|
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!("Press Escape in either window to quit");
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
@@ -52,15 +56,29 @@ fn main() {
|
|||||||
let ball = Arc::new(Mutex::new(Ball::new()));
|
let ball = Arc::new(Mutex::new(Ball::new()));
|
||||||
let mut framebuffer = vec![0u32; WIDTH * HEIGHT];
|
let mut framebuffer = vec![0u32; WIDTH * HEIGHT];
|
||||||
|
|
||||||
// Target 30 FPS
|
|
||||||
let frame_time = Duration::from_millis(33);
|
let frame_time = Duration::from_millis(33);
|
||||||
let mut frame_count = 0;
|
let mut frame_count = 0;
|
||||||
|
|
||||||
while window.is_open() && !window.is_key_down(Key::Escape) {
|
while window.is_open() && !window.is_key_down(Key::Escape) {
|
||||||
let frame_start = Instant::now();
|
let frame_start = Instant::now();
|
||||||
|
|
||||||
teleprof::span!("main_frame");
|
main_frame(&ball, &mut framebuffer, &mut window, &mut frame_count, frame_time);
|
||||||
|
|
||||||
|
let elapsed = frame_start.elapsed();
|
||||||
|
if elapsed < frame_time {
|
||||||
|
thread::sleep(frame_time - elapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
fn main_frame(
|
||||||
|
ball: &Arc<Mutex<Ball>>,
|
||||||
|
framebuffer: &mut [u32],
|
||||||
|
window: &mut Window,
|
||||||
|
frame_count: &mut u32,
|
||||||
|
frame_time: Duration,
|
||||||
|
) {
|
||||||
// Check if paused
|
// Check if paused
|
||||||
if teleprof::PAUSE.try_lock().is_err() {
|
if teleprof::PAUSE.try_lock().is_err() {
|
||||||
println!("Paused!");
|
println!("Paused!");
|
||||||
@@ -71,49 +89,40 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update physics
|
// Update physics
|
||||||
let hit_wall = update_physics(&ball, frame_time.as_secs_f32());
|
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 we hit a wall, spawn a thread to pick a new color
|
||||||
if hit_wall {
|
if hit_wall {
|
||||||
let ball_clone = Arc::clone(&ball);
|
let ball_clone = Arc::clone(ball);
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
|
teleprof::set_thread_name("ColorPicker");
|
||||||
pick_new_color(ball_clone);
|
pick_new_color(ball_clone);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
render(&ball, &mut framebuffer);
|
render(ball, framebuffer);
|
||||||
|
|
||||||
// Update window
|
// Update window
|
||||||
window
|
window
|
||||||
.update_with_buffer(&framebuffer, WIDTH, HEIGHT)
|
.update_with_buffer(framebuffer, WIDTH, HEIGHT)
|
||||||
.expect("Failed to update window");
|
.expect("Failed to update window");
|
||||||
|
|
||||||
frame_count += 1;
|
*frame_count += 1;
|
||||||
if frame_count % 30 == 0 {
|
if *frame_count % 30 == 0 {
|
||||||
print_status(&ball, frame_count);
|
print_status(ball, *frame_count);
|
||||||
}
|
|
||||||
|
|
||||||
// Sleep to maintain 30fps
|
|
||||||
let elapsed = frame_start.elapsed();
|
|
||||||
if elapsed < frame_time {
|
|
||||||
thread::sleep(frame_time - elapsed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
fn update_physics(ball: &Arc<Mutex<Ball>>, dt: f32) -> bool {
|
fn update_physics(ball: &Arc<Mutex<Ball>>, dt: f32) -> bool {
|
||||||
teleprof::span!("update_physics");
|
|
||||||
|
|
||||||
let mut ball = ball.lock().unwrap();
|
let mut ball = ball.lock().unwrap();
|
||||||
|
|
||||||
// Update position
|
|
||||||
ball.x += ball.vx * dt;
|
ball.x += ball.vx * dt;
|
||||||
ball.y += ball.vy * dt;
|
ball.y += ball.vy * dt;
|
||||||
|
|
||||||
let mut hit_wall = false;
|
let mut hit_wall = false;
|
||||||
|
|
||||||
// Bounce off walls
|
|
||||||
let radius = BALL_RADIUS as f32;
|
let radius = BALL_RADIUS as f32;
|
||||||
if ball.x - radius < 0.0 || ball.x + radius > WIDTH as f32 {
|
if ball.x - radius < 0.0 || ball.x + radius > WIDTH as f32 {
|
||||||
ball.vx = -ball.vx;
|
ball.vx = -ball.vx;
|
||||||
@@ -127,16 +136,13 @@ fn update_physics(ball: &Arc<Mutex<Ball>>, dt: f32) -> bool {
|
|||||||
hit_wall = true;
|
hit_wall = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simulate some physics computation
|
|
||||||
thread::sleep(Duration::from_millis(5));
|
thread::sleep(Duration::from_millis(5));
|
||||||
|
|
||||||
hit_wall
|
hit_wall
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
fn pick_new_color(ball: Arc<Mutex<Ball>>) {
|
fn pick_new_color(ball: Arc<Mutex<Ball>>) {
|
||||||
teleprof::span!("pick_new_color");
|
|
||||||
|
|
||||||
// Simulate some "expensive" color selection
|
|
||||||
thread::sleep(Duration::from_millis(10));
|
thread::sleep(Duration::from_millis(10));
|
||||||
|
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
@@ -152,27 +158,28 @@ fn pick_new_color(ball: Arc<Mutex<Ball>>) {
|
|||||||
println!(" → New color selected: RGB({}, {}, {})", r, g, b);
|
println!(" → New color selected: RGB({}, {}, {})", r, g, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
fn render(ball: &Arc<Mutex<Ball>>, framebuffer: &mut [u32]) {
|
fn render(ball: &Arc<Mutex<Ball>>, framebuffer: &mut [u32]) {
|
||||||
teleprof::span!("render");
|
clear_background(framebuffer);
|
||||||
|
draw_ball(ball, framebuffer);
|
||||||
|
submit_frame();
|
||||||
|
}
|
||||||
|
|
||||||
{
|
#[instrument]
|
||||||
teleprof::span!("clear_background");
|
fn clear_background(framebuffer: &mut [u32]) {
|
||||||
// Clear to dark gray
|
|
||||||
framebuffer.fill(0x2A2A2AFF);
|
framebuffer.fill(0x2A2A2AFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
#[instrument]
|
||||||
teleprof::span!("draw_ball");
|
fn draw_ball(ball: &Arc<Mutex<Ball>>, framebuffer: &mut [u32]) {
|
||||||
let ball = ball.lock().unwrap();
|
let ball = ball.lock().unwrap();
|
||||||
|
|
||||||
// Draw ball as a filled circle
|
|
||||||
let cx = ball.x as i32;
|
let cx = ball.x as i32;
|
||||||
let cy = ball.y as i32;
|
let cy = ball.y as i32;
|
||||||
let radius = BALL_RADIUS as i32;
|
let radius = BALL_RADIUS as i32;
|
||||||
|
|
||||||
for dy in -radius..=radius {
|
for dy in -radius..=radius {
|
||||||
for dx in -radius..=radius {
|
for dx in -radius..=radius {
|
||||||
// Check if point is inside circle
|
|
||||||
if dx * dx + dy * dy <= radius * radius {
|
if dx * dx + dy * dy <= radius * radius {
|
||||||
let x = cx + dx;
|
let x = cx + dx;
|
||||||
let y = cy + dy;
|
let y = cy + dy;
|
||||||
@@ -184,18 +191,15 @@ fn render(ball: &Arc<Mutex<Ball>>, framebuffer: &mut [u32]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
teleprof::span!("submit_frame");
|
|
||||||
// Simulate GPU submission
|
|
||||||
thread::sleep(Duration::from_millis(2));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_status(ball: &Arc<Mutex<Ball>>, frame: u32) {
|
#[instrument]
|
||||||
teleprof::span!("print_status");
|
fn submit_frame() {
|
||||||
|
thread::sleep(Duration::from_millis(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
fn print_status(ball: &Arc<Mutex<Ball>>, frame: u32) {
|
||||||
let ball = ball.lock().unwrap();
|
let ball = ball.lock().unwrap();
|
||||||
println!(
|
println!(
|
||||||
"Frame {}: Ball at ({:.1}, {:.1})",
|
"Frame {}: Ball at ({:.1}, {:.1})",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use teleprof::instrument;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Start the telemetry window
|
// Start the telemetry window
|
||||||
@@ -7,7 +8,9 @@ fn main() {
|
|||||||
|
|
||||||
println!("Teleprof demo running...");
|
println!("Teleprof demo running...");
|
||||||
println!("Press Space in the profiler window to pause/unpause");
|
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!("Press Escape in the profiler window to quit");
|
||||||
|
println!();
|
||||||
|
|
||||||
// Simulate some work
|
// Simulate some work
|
||||||
for i in 0..1000 {
|
for i in 0..1000 {
|
||||||
@@ -26,9 +29,8 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
fn frame_work(frame: u32) {
|
fn frame_work(frame: u32) {
|
||||||
teleprof::span!("frame_work");
|
|
||||||
|
|
||||||
physics_update();
|
physics_update();
|
||||||
render();
|
render();
|
||||||
|
|
||||||
@@ -37,9 +39,8 @@ fn frame_work(frame: u32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
fn physics_update() {
|
fn physics_update() {
|
||||||
teleprof::span!("physics_update");
|
|
||||||
|
|
||||||
// Spawn some worker threads
|
// Spawn some worker threads
|
||||||
let handles: Vec<_> = (0..3).map(|i| {
|
let handles: Vec<_> = (0..3).map(|i| {
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
@@ -52,32 +53,30 @@ fn physics_update() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
fn physics_worker(id: u32) {
|
fn physics_worker(id: u32) {
|
||||||
teleprof::span!("physics_worker");
|
|
||||||
|
|
||||||
// Simulate work
|
// Simulate work
|
||||||
let work_ms = 5 + (id * 2);
|
let work_ms = 5 + (id * 2);
|
||||||
thread::sleep(Duration::from_millis(work_ms as u64));
|
thread::sleep(Duration::from_millis(work_ms as u64));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
fn render() {
|
fn render() {
|
||||||
teleprof::span!("render");
|
|
||||||
|
|
||||||
build_command_buffer();
|
build_command_buffer();
|
||||||
submit_to_gpu();
|
submit_to_gpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
fn build_command_buffer() {
|
fn build_command_buffer() {
|
||||||
teleprof::span!("build_command_buffer");
|
|
||||||
thread::sleep(Duration::from_millis(3));
|
thread::sleep(Duration::from_millis(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
fn submit_to_gpu() {
|
fn submit_to_gpu() {
|
||||||
teleprof::span!("submit_to_gpu");
|
|
||||||
thread::sleep(Duration::from_millis(2));
|
thread::sleep(Duration::from_millis(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
fn occasional_task() {
|
fn occasional_task() {
|
||||||
teleprof::span!("occasional_task");
|
|
||||||
thread::sleep(Duration::from_millis(10));
|
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