first commit, not pretty

This commit is contained in:
2025-12-10 23:50:28 +01:00
commit 5e9336c6c7
9 changed files with 1748 additions and 0 deletions

458
src/lib.rs Normal file
View File

@@ -0,0 +1,458 @@
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()]
}
}