first commit, not pretty
This commit is contained in:
458
src/lib.rs
Normal file
458
src/lib.rs
Normal 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()]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user