trustformers_debug/dashboard_ws/
websocket.rs1use std::net::SocketAddr;
38use std::sync::atomic::{AtomicBool, Ordering};
39use std::sync::Arc;
40
41use anyhow::Result;
42use serde::{Deserialize, Serialize};
43use serde_json::Value;
44use tokio::io::{AsyncReadExt, AsyncWriteExt};
45use tokio::net::{TcpListener, TcpStream};
46use tokio::sync::broadcast;
47
48use crate::ring_buffer::LockFreeRingBuffer;
49
50#[derive(Serialize, Deserialize, Clone, Debug)]
74pub enum DashboardEvent {
75 TrainingStep {
77 step: u64,
79 loss: f32,
81 learning_rate: f32,
83 throughput_tokens_per_sec: f32,
85 },
86 MemorySnapshot {
88 timestamp_ms: u64,
90 allocated_bytes: u64,
92 peak_bytes: u64,
94 },
95 LayerProfile {
97 layer_name: String,
99 duration_us: u64,
101 memory_bytes: u64,
103 },
104 Custom {
106 name: String,
108 value: Value,
110 },
111}
112
113pub struct DashboardConfig {
132 pub bind_addr: SocketAddr,
134 pub max_clients: usize,
136 pub event_buffer_size: usize,
138}
139
140pub struct DashboardServer {
156 config: DashboardConfig,
157 event_buffer: Arc<LockFreeRingBuffer<u64>>,
158 sender: broadcast::Sender<String>,
160 running: Arc<AtomicBool>,
162}
163
164impl DashboardServer {
165 pub fn new(config: DashboardConfig) -> Result<Self> {
187 let buf_size = config.event_buffer_size;
188 if buf_size == 0 {
189 anyhow::bail!("event_buffer_size must be at least 1");
190 }
191 let event_buffer = Arc::new(LockFreeRingBuffer::new(buf_size));
192 let channel_cap = config.max_clients.max(1);
194 let (sender, _) = broadcast::channel(channel_cap * 8);
195 Ok(Self {
196 config,
197 event_buffer,
198 sender,
199 running: Arc::new(AtomicBool::new(false)),
200 })
201 }
202
203 pub fn is_running(&self) -> bool {
205 self.running.load(Ordering::Acquire)
206 }
207
208 pub fn push_event(&self, event: DashboardEvent) -> Result<()> {
237 let json = serde_json::to_string(&event)?;
238 let sse_frame = format!("data: {}\n\n", json);
239
240 let _ = self.event_buffer.push(sse_frame.len() as u64);
244
245 let _ = self.sender.send(sse_frame);
247 Ok(())
248 }
249
250 pub async fn start(&self) -> Result<()> {
278 let listener = TcpListener::bind(self.config.bind_addr).await?;
279 tracing::debug!(
280 "SSE dashboard listening on {}",
281 listener.local_addr().unwrap_or(self.config.bind_addr)
282 );
283
284 let sender = self.sender.clone();
285 let running = Arc::clone(&self.running);
286 running.store(true, Ordering::Release);
287 let running_flag = Arc::clone(&self.running);
288
289 tokio::spawn(async move {
290 while running_flag.load(Ordering::Acquire) {
291 match listener.accept().await {
292 Ok((stream, peer)) => {
293 tracing::debug!("dashboard: accepted connection from {}", peer);
294 let rx = sender.subscribe();
295 tokio::spawn(handle_client(stream, rx));
296 },
297 Err(e) => {
298 tracing::warn!("dashboard: accept error: {}", e);
299 break;
300 },
301 }
302 }
303 });
304
305 Ok(())
306 }
307
308 pub fn stop(&self) -> Result<()> {
317 self.running.store(false, Ordering::Release);
318 tracing::debug!("dashboard server stopping");
319 Ok(())
320 }
321}
322
323async fn handle_client(mut stream: TcpStream, mut rx: broadcast::Receiver<String>) {
332 let mut buf = [0u8; 512];
334 match stream.read(&mut buf).await {
335 Ok(0) | Err(_) => return,
336 Ok(_) => {},
337 }
338
339 let request = std::str::from_utf8(&buf).unwrap_or("");
340 let is_events = request.starts_with("GET /events") || request.contains("GET /events");
341
342 let (status, body) = if is_events {
343 (
344 "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\nConnection: keep-alive\r\nAccess-Control-Allow-Origin: *\r\n\r\n",
345 None,
346 )
347 } else {
348 (
349 "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n",
350 Some(DASHBOARD_HTML),
351 )
352 };
353
354 if stream.write_all(status.as_bytes()).await.is_err() {
355 return;
356 }
357
358 if let Some(html) = body {
359 let _ = stream.write_all(html.as_bytes()).await;
360 return;
361 }
362
363 loop {
365 match rx.recv().await {
366 Ok(frame) => {
367 if stream.write_all(frame.as_bytes()).await.is_err() {
368 break;
369 }
370 },
371 Err(broadcast::error::RecvError::Lagged(n)) => {
372 tracing::warn!("dashboard client lagged by {} events", n);
373 },
374 Err(broadcast::error::RecvError::Closed) => break,
375 }
376 }
377}
378
379const DASHBOARD_HTML: &str = r#"<!DOCTYPE html>
381<html lang="en">
382<head><meta charset="utf-8"><title>TrustformeRS Dashboard</title></head>
383<body>
384<h1>TrustformeRS Streaming Dashboard</h1>
385<pre id="log"></pre>
386<script>
387 const log = document.getElementById('log');
388 const es = new EventSource('/events');
389 es.onmessage = e => {
390 const line = document.createTextNode(e.data + '\n');
391 log.appendChild(line);
392 };
393</script>
394</body>
395</html>"#;
396
397#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
408 fn test_training_step_serialises() {
409 let ev = DashboardEvent::TrainingStep {
410 step: 5,
411 loss: 0.5,
412 learning_rate: 1e-4,
413 throughput_tokens_per_sec: 1024.0,
414 };
415 let json = serde_json::to_string(&ev).unwrap();
416 assert!(json.contains("TrainingStep"));
417 assert!(json.contains("\"step\":5"));
418 }
419
420 #[test]
421 fn test_memory_snapshot_serialises() {
422 let ev = DashboardEvent::MemorySnapshot {
423 timestamp_ms: 1000,
424 allocated_bytes: 1_073_741_824,
425 peak_bytes: 2_147_483_648,
426 };
427 let json = serde_json::to_string(&ev).unwrap();
428 assert!(json.contains("MemorySnapshot"));
429 assert!(json.contains("allocated_bytes"));
430 }
431
432 #[test]
433 fn test_layer_profile_serialises() {
434 let ev = DashboardEvent::LayerProfile {
435 layer_name: "attention".to_string(),
436 duration_us: 1500,
437 memory_bytes: 256 * 1024 * 1024,
438 };
439 let json = serde_json::to_string(&ev).unwrap();
440 assert!(json.contains("LayerProfile"));
441 assert!(json.contains("\"layer_name\":\"attention\""));
442 }
443
444 #[test]
445 fn test_custom_event_serialises() {
446 let ev = DashboardEvent::Custom {
447 name: "epoch_end".to_string(),
448 value: serde_json::json!({"epoch": 3, "val_loss": 0.22}),
449 };
450 let json = serde_json::to_string(&ev).unwrap();
451 assert!(json.contains("Custom"));
452 assert!(json.contains("epoch_end"));
453 }
454
455 #[test]
456 fn test_event_roundtrip_deserialise() {
457 let ev = DashboardEvent::TrainingStep {
458 step: 42,
459 loss: 0.1,
460 learning_rate: 3e-5,
461 throughput_tokens_per_sec: 4096.0,
462 };
463 let json = serde_json::to_string(&ev).unwrap();
464 let ev2: DashboardEvent = serde_json::from_str(&json).unwrap();
465 if let DashboardEvent::TrainingStep { step, .. } = ev2 {
466 assert_eq!(step, 42);
467 } else {
468 panic!("unexpected variant after roundtrip");
469 }
470 }
471
472 #[test]
475 fn test_server_creation() {
476 let cfg = DashboardConfig {
477 bind_addr: "127.0.0.1:0".parse().unwrap(),
478 max_clients: 4,
479 event_buffer_size: 32,
480 };
481 let server = DashboardServer::new(cfg).unwrap();
482 assert!(!server.is_running());
483 }
484
485 #[test]
486 fn test_push_event_without_clients() {
487 let cfg = DashboardConfig {
488 bind_addr: "127.0.0.1:0".parse().unwrap(),
489 max_clients: 4,
490 event_buffer_size: 32,
491 };
492 let server = DashboardServer::new(cfg).unwrap();
493 server
495 .push_event(DashboardEvent::TrainingStep {
496 step: 1,
497 loss: 0.4,
498 learning_rate: 1e-4,
499 throughput_tokens_per_sec: 2048.0,
500 })
501 .unwrap();
502 assert!(!server.event_buffer.is_empty());
504 }
505
506 #[test]
507 fn test_stop_before_start() {
508 let cfg = DashboardConfig {
509 bind_addr: "127.0.0.1:0".parse().unwrap(),
510 max_clients: 2,
511 event_buffer_size: 8,
512 };
513 let server = DashboardServer::new(cfg).unwrap();
514 server.stop().unwrap();
515 assert!(!server.is_running());
516 }
517
518 #[tokio::test]
519 async fn test_start_and_stop() {
520 let cfg = DashboardConfig {
521 bind_addr: "127.0.0.1:0".parse().unwrap(),
522 max_clients: 2,
523 event_buffer_size: 16,
524 };
525 let server = DashboardServer::new(cfg).unwrap();
526 server.start().await.unwrap();
527 assert!(server.is_running());
528 server.stop().unwrap();
529 assert!(!server.is_running());
530 }
531
532 #[tokio::test]
533 async fn test_push_event_with_subscriber() {
534 let cfg = DashboardConfig {
535 bind_addr: "127.0.0.1:0".parse().unwrap(),
536 max_clients: 4,
537 event_buffer_size: 32,
538 };
539 let server = DashboardServer::new(cfg).unwrap();
540
541 let mut rx = server.sender.subscribe();
543
544 server
545 .push_event(DashboardEvent::Custom {
546 name: "test".to_string(),
547 value: serde_json::json!(42),
548 })
549 .unwrap();
550
551 let frame = rx.try_recv().expect("should receive frame");
552 assert!(frame.starts_with("data: "));
553 assert!(frame.contains("\"test\""));
554 }
555
556 #[test]
559 fn test_dashboard_event_training_step_serialization() {
560 let ev = DashboardEvent::TrainingStep {
561 step: 42,
562 loss: 0.25,
563 learning_rate: 3e-4,
564 throughput_tokens_per_sec: 1500.0,
565 };
566 let json = serde_json::to_string(&ev).expect("serialize should succeed");
567 assert!(json.contains("TrainingStep"));
568 assert!(json.contains("42"));
569 }
570
571 #[test]
572 fn test_dashboard_event_memory_snapshot_serialization() {
573 let ev = DashboardEvent::MemorySnapshot {
574 timestamp_ms: 12345,
575 allocated_bytes: 1024 * 1024,
576 peak_bytes: 2 * 1024 * 1024,
577 };
578 let json = serde_json::to_string(&ev).expect("serialize should succeed");
579 assert!(json.contains("MemorySnapshot"));
580 }
581
582 #[test]
583 fn test_dashboard_event_layer_profile_serialization() {
584 let ev = DashboardEvent::LayerProfile {
585 layer_name: "attention".to_string(),
586 duration_us: 1500,
587 memory_bytes: 4096,
588 };
589 let json = serde_json::to_string(&ev).expect("serialize should succeed");
590 assert!(json.contains("attention"));
591 assert!(json.contains("1500"));
592 }
593
594 #[test]
595 fn test_dashboard_event_custom_roundtrip() {
596 let ev = DashboardEvent::Custom {
597 name: "my_event".to_string(),
598 value: serde_json::json!({"key": "value"}),
599 };
600 let json = serde_json::to_string(&ev).expect("serialize should succeed");
601 let decoded: DashboardEvent =
602 serde_json::from_str(&json).expect("deserialize should succeed");
603 if let DashboardEvent::Custom { name, .. } = decoded {
604 assert_eq!(name, "my_event");
605 } else {
606 panic!("expected Custom variant");
607 }
608 }
609
610 #[test]
611 fn test_dashboard_config_fields() {
612 let cfg = DashboardConfig {
613 bind_addr: "0.0.0.0:8080".parse().expect("parse addr"),
614 max_clients: 16,
615 event_buffer_size: 128,
616 };
617 assert_eq!(cfg.max_clients, 16);
618 assert_eq!(cfg.event_buffer_size, 128);
619 }
620
621 #[test]
622 fn test_dashboard_server_zero_buffer_errors() {
623 let cfg = DashboardConfig {
624 bind_addr: "127.0.0.1:0".parse().expect("parse addr"),
625 max_clients: 4,
626 event_buffer_size: 0, };
628 let result = DashboardServer::new(cfg);
629 assert!(result.is_err());
630 }
631
632 #[test]
633 fn test_push_multiple_events_fills_buffer() {
634 let cfg = DashboardConfig {
635 bind_addr: "127.0.0.1:0".parse().expect("parse addr"),
636 max_clients: 4,
637 event_buffer_size: 32,
638 };
639 let server = DashboardServer::new(cfg).expect("create server");
640 for i in 0..5_u64 {
641 server
642 .push_event(DashboardEvent::TrainingStep {
643 step: i,
644 loss: 0.5,
645 learning_rate: 1e-4,
646 throughput_tokens_per_sec: 1000.0,
647 })
648 .expect("push should succeed");
649 }
650 assert!(!server.event_buffer.is_empty());
651 }
652
653 #[test]
654 fn test_dashboard_event_deserialize_training_step() {
655 let json = r#"{"TrainingStep":{"step":10,"loss":0.3,"learning_rate":0.001,"throughput_tokens_per_sec":2048.0}}"#;
656 let ev: DashboardEvent = serde_json::from_str(json).expect("deserialize should succeed");
657 if let DashboardEvent::TrainingStep { step, .. } = ev {
658 assert_eq!(step, 10);
659 } else {
660 panic!("expected TrainingStep");
661 }
662 }
663}