2026-04-13 23:11:35 -03:00
use serde ::{ Deserialize , Serialize } ;
use std ::collections ::VecDeque ;
2026-04-16 19:19:37 -03:00
use std ::fmt ::Write as _ ;
2026-04-13 23:11:35 -03:00
use super ::state ::{ InputRouting , LauncherState , ViewMode } ;
#[ derive(Debug, Clone, Serialize, Deserialize, PartialEq) ]
pub struct PerformanceSample {
pub rtt_ms : f32 ,
2026-04-16 21:18:34 -03:00
pub jitter_ms : f32 ,
2026-04-13 23:11:35 -03:00
pub input_latency_ms : f32 ,
2026-04-16 21:18:34 -03:00
pub probe_loss_pct : f32 ,
pub video_loss_pct : f32 ,
pub left_receive_fps : f32 ,
pub left_present_fps : f32 ,
pub left_server_fps : f32 ,
pub right_receive_fps : f32 ,
pub right_present_fps : f32 ,
pub right_server_fps : f32 ,
2026-04-13 23:11:35 -03:00
pub dropped_frames : u64 ,
pub queue_depth : u32 ,
}
#[ derive(Debug, Clone, Serialize, Deserialize) ]
pub struct DiagnosticsLog {
capacity : usize ,
history : VecDeque < PerformanceSample > ,
}
impl DiagnosticsLog {
pub fn new ( capacity : usize ) -> Self {
let capacity = capacity . max ( 1 ) ;
Self {
capacity ,
history : VecDeque ::with_capacity ( capacity ) ,
}
}
pub fn record ( & mut self , sample : PerformanceSample ) {
if self . history . len ( ) = = self . capacity {
let _ = self . history . pop_front ( ) ;
}
self . history . push_back ( sample ) ;
}
pub fn latest ( & self ) -> Option < & PerformanceSample > {
self . history . back ( )
}
pub fn len ( & self ) -> usize {
self . history . len ( )
}
pub fn is_empty ( & self ) -> bool {
self . history . is_empty ( )
}
pub fn iter ( & self ) -> impl Iterator < Item = & PerformanceSample > {
self . history . iter ( )
}
}
#[ derive(Debug, Clone, Serialize, Deserialize) ]
pub struct SnapshotReport {
2026-04-16 19:19:37 -03:00
pub client_version : String ,
pub server_version : Option < String > ,
pub server_available : bool ,
2026-04-13 23:11:35 -03:00
pub routing : InputRouting ,
pub view_mode : ViewMode ,
pub remote_active : bool ,
2026-04-16 19:19:37 -03:00
pub power_state : String ,
pub preview_source : String ,
2026-04-17 00:21:18 -03:00
pub client_display_limit : String ,
2026-04-16 19:19:37 -03:00
pub left_surface : String ,
pub left_capture_profile : String ,
2026-04-16 22:15:59 -03:00
pub left_capture_transport : String ,
2026-04-16 19:19:37 -03:00
pub left_breakout_profile : String ,
pub right_surface : String ,
pub right_capture_profile : String ,
2026-04-16 22:15:59 -03:00
pub right_capture_transport : String ,
2026-04-16 19:19:37 -03:00
pub right_breakout_profile : String ,
2026-04-13 23:11:35 -03:00
pub selected_camera : Option < String > ,
pub selected_microphone : Option < String > ,
pub selected_speaker : Option < String > ,
2026-04-16 19:19:37 -03:00
pub selected_keyboard : Option < String > ,
pub selected_mouse : Option < String > ,
2026-04-13 23:11:35 -03:00
pub status : String ,
pub recent_samples : Vec < PerformanceSample > ,
pub notes : Vec < String > ,
2026-04-16 19:19:37 -03:00
pub recommendations : Vec < String > ,
2026-04-13 23:11:35 -03:00
pub probe_command : String ,
}
impl SnapshotReport {
pub fn from_state ( state : & LauncherState , log : & DiagnosticsLog , probe_command : String ) -> Self {
2026-04-16 19:19:37 -03:00
let left_capture = state . capture_size_choice ( 0 ) ;
let right_capture = state . capture_size_choice ( 1 ) ;
let left_breakout = state . breakout_size_choice ( 0 ) ;
let right_breakout = state . breakout_size_choice ( 1 ) ;
2026-04-13 23:11:35 -03:00
Self {
2026-04-16 19:19:37 -03:00
client_version : crate ::VERSION . to_string ( ) ,
server_version : state . server_version . clone ( ) ,
server_available : state . server_available ,
2026-04-13 23:11:35 -03:00
routing : state . routing ,
view_mode : state . view_mode ,
remote_active : state . remote_active ,
2026-04-16 19:19:37 -03:00
power_state : format ! (
" {} | {} | leases {} " ,
state . capture_power . mode ,
state . capture_power . detail ,
state . capture_power . active_leases
) ,
preview_source : format ! (
" {}x{} @ {} fps " ,
state . preview_source . width , state . preview_source . height , state . preview_source . fps
) ,
2026-04-17 00:21:18 -03:00
client_display_limit : format ! (
2026-04-16 19:19:37 -03:00
" {}x{} " ,
state . breakout_display . width , state . breakout_display . height
) ,
left_surface : state . display_surface ( 0 ) . label ( ) . to_string ( ) ,
left_capture_profile : format ! (
" {} | {}x{} | {} fps | {} kbit " ,
2026-04-16 22:15:59 -03:00
left_capture . preset . label ( ) ,
2026-04-16 19:19:37 -03:00
left_capture . width ,
left_capture . height ,
left_capture . fps ,
left_capture . max_bitrate_kbit
) ,
2026-04-16 22:15:59 -03:00
left_capture_transport : left_capture . preset . transport_label ( ) . to_string ( ) ,
2026-04-16 19:19:37 -03:00
left_breakout_profile : format ! (
" {} | {}x{} " ,
2026-04-16 22:15:59 -03:00
left_breakout . preset . label ( ) ,
2026-04-16 19:19:37 -03:00
left_breakout . width ,
left_breakout . height
) ,
right_surface : state . display_surface ( 1 ) . label ( ) . to_string ( ) ,
right_capture_profile : format ! (
" {} | {}x{} | {} fps | {} kbit " ,
2026-04-16 22:15:59 -03:00
right_capture . preset . label ( ) ,
2026-04-16 19:19:37 -03:00
right_capture . width ,
right_capture . height ,
right_capture . fps ,
right_capture . max_bitrate_kbit
) ,
2026-04-16 22:15:59 -03:00
right_capture_transport : right_capture . preset . transport_label ( ) . to_string ( ) ,
2026-04-16 19:19:37 -03:00
right_breakout_profile : format ! (
" {} | {}x{} " ,
2026-04-16 22:15:59 -03:00
right_breakout . preset . label ( ) ,
2026-04-16 19:19:37 -03:00
right_breakout . width ,
right_breakout . height
) ,
2026-04-13 23:11:35 -03:00
selected_camera : state . devices . camera . clone ( ) ,
selected_microphone : state . devices . microphone . clone ( ) ,
selected_speaker : state . devices . speaker . clone ( ) ,
2026-04-16 19:19:37 -03:00
selected_keyboard : state . devices . keyboard . clone ( ) ,
selected_mouse : state . devices . mouse . clone ( ) ,
2026-04-13 23:11:35 -03:00
status : state . status_line ( ) ,
recent_samples : log . iter ( ) . cloned ( ) . collect ( ) ,
notes : state . notes . clone ( ) ,
2026-04-16 19:19:37 -03:00
recommendations : recommendations_for ( state , log ) ,
2026-04-13 23:11:35 -03:00
probe_command ,
}
}
pub fn to_pretty_json ( & self ) -> Result < String , serde_json ::Error > {
serde_json ::to_string_pretty ( self )
}
2026-04-16 19:19:37 -03:00
pub fn to_pretty_text ( & self ) -> String {
let mut text = String ::new ( ) ;
let server_version = self . server_version . as_deref ( ) . unwrap_or ( " unknown " ) ;
let server_state = if self . server_available {
" reachable "
} else {
" unreachable "
} ;
let _ = writeln! ( text , " Lesavka Diagnostics " ) ;
let _ = writeln! ( text , " client: v{} " , self . client_version ) ;
let _ = writeln! ( text , " server: {server_version} ({server_state}) " ) ;
let _ = writeln! (
text ,
" session: routing={:?} view={:?} relay_active={} power={} " ,
self . routing , self . view_mode , self . remote_active , self . power_state
) ;
let _ = writeln! ( text , " source feed: {} " , self . preview_source ) ;
2026-04-17 00:21:18 -03:00
let _ = writeln! ( text , " client display limit: {} " , self . client_display_limit ) ;
2026-04-16 19:19:37 -03:00
let _ = writeln! ( text ) ;
let _ = writeln! ( text , " left eye " ) ;
let _ = writeln! ( text , " surface: {} " , self . left_surface ) ;
let _ = writeln! ( text , " capture: {} " , self . left_capture_profile ) ;
2026-04-16 22:15:59 -03:00
let _ = writeln! ( text , " transport: {} " , self . left_capture_transport ) ;
2026-04-16 19:19:37 -03:00
let _ = writeln! ( text , " breakout: {} " , self . left_breakout_profile ) ;
let _ = writeln! ( text , " right eye " ) ;
let _ = writeln! ( text , " surface: {} " , self . right_surface ) ;
let _ = writeln! ( text , " capture: {} " , self . right_capture_profile ) ;
2026-04-16 22:15:59 -03:00
let _ = writeln! ( text , " transport: {} " , self . right_capture_transport ) ;
2026-04-16 19:19:37 -03:00
let _ = writeln! ( text , " breakout: {} " , self . right_breakout_profile ) ;
let _ = writeln! ( text ) ;
let _ = writeln! ( text , " device staging " ) ;
let _ = writeln! (
text ,
" camera: {} " ,
self . selected_camera . as_deref ( ) . unwrap_or ( " auto " )
) ;
let _ = writeln! (
text ,
" microphone: {} " ,
self . selected_microphone . as_deref ( ) . unwrap_or ( " auto " )
) ;
let _ = writeln! (
text ,
" speaker: {} " ,
self . selected_speaker . as_deref ( ) . unwrap_or ( " auto " )
) ;
let _ = writeln! (
text ,
" keyboard: {} " ,
self . selected_keyboard . as_deref ( ) . unwrap_or ( " all " )
) ;
let _ = writeln! (
text ,
" mouse: {} " ,
self . selected_mouse . as_deref ( ) . unwrap_or ( " all " )
) ;
let _ = writeln! ( text ) ;
let _ = writeln! ( text , " launcher status " ) ;
let _ = writeln! ( text , " {} " , self . status ) ;
let _ = writeln! ( text ) ;
let _ = writeln! ( text , " recent samples " ) ;
if self . recent_samples . is_empty ( ) {
let _ = writeln! (
text ,
" no live RTT/jitter/loss samples yet; this report is currently a launcher state snapshot. "
) ;
} else {
for sample in & self . recent_samples {
let _ = writeln! (
text ,
2026-04-16 21:18:34 -03:00
" rtt={:.1}ms jitter={:.1}ms input-floor={:.1}ms probe-loss={:.1}% video-loss={:.1}% left={:.1}/{:.1}/{:.1}fps right={:.1}/{:.1}/{:.1}fps dropped={} queue={} " ,
2026-04-16 19:19:37 -03:00
sample . rtt_ms ,
2026-04-16 21:18:34 -03:00
sample . jitter_ms ,
2026-04-16 19:19:37 -03:00
sample . input_latency_ms ,
2026-04-16 21:18:34 -03:00
sample . probe_loss_pct ,
sample . video_loss_pct ,
sample . left_receive_fps ,
sample . left_present_fps ,
sample . left_server_fps ,
sample . right_receive_fps ,
sample . right_present_fps ,
sample . right_server_fps ,
2026-04-16 19:19:37 -03:00
sample . dropped_frames ,
sample . queue_depth
) ;
}
}
let _ = writeln! ( text ) ;
let _ = writeln! ( text , " recommendations " ) ;
for item in & self . recommendations {
let _ = writeln! ( text , " - {item} " ) ;
}
if ! self . notes . is_empty ( ) {
let _ = writeln! ( text ) ;
let _ = writeln! ( text , " notes " ) ;
for item in & self . notes {
let _ = writeln! ( text , " - {item} " ) ;
}
}
let _ = writeln! ( text ) ;
let _ = writeln! ( text , " quality probe " ) ;
let _ = writeln! ( text , " {} " , self . probe_command ) ;
text
}
2026-04-13 23:11:35 -03:00
}
pub fn quality_probe_command ( ) -> & 'static str {
" scripts/ci/hygiene_gate.sh && scripts/ci/quality_gate.sh "
}
2026-04-16 19:19:37 -03:00
fn recommendations_for ( state : & LauncherState , log : & DiagnosticsLog ) -> Vec < String > {
let mut items = Vec ::new ( ) ;
if ! state . server_available {
items . push (
" The server is not reachable from this launcher yet, so stream-quality results would not be meaningful. "
. to_string ( ) ,
) ;
}
if log . is_empty ( ) {
items . push (
2026-04-16 21:18:34 -03:00
" Live stream samples will appear here after the launcher collects a few probe windows. Leave the relay up for a few seconds to populate RTT, jitter, loss, and fps. "
2026-04-16 19:19:37 -03:00
. to_string ( ) ,
) ;
}
2026-04-16 21:18:34 -03:00
if let Some ( sample ) = log . latest ( ) {
if sample . probe_loss_pct > = 3.0 | | sample . jitter_ms > = 18.0 {
items . push (
" Probe jitter/loss is elevated. A wired client connection or a gentler capture profile will usually help more than changing the breakout size. "
. to_string ( ) ,
) ;
}
if sample . video_loss_pct > = 2.0 | | sample . dropped_frames > 0 {
items . push (
" Video packets are arriving with gaps or server-side drops. Try 900p or 720p capture first, then watch whether dropped frames and video-loss fall. "
. to_string ( ) ,
) ;
}
if sample . left_present_fps + 1.0 < sample . left_receive_fps
| | sample . right_present_fps + 1.0 < sample . right_receive_fps
{
items . push (
" The client is receiving more frames than it is presenting. That points at local decode/render pressure, so prefer lighter breakout sizes or hardware decode. "
. to_string ( ) ,
) ;
}
if sample . queue_depth > 8 {
items . push (
" The preview queue is backing up. When queue depth climbs, expect laggy mouse feel and delayed visual response even if raw fps still looks okay. "
. to_string ( ) ,
) ;
}
}
2026-04-16 19:19:37 -03:00
let heavy_capture = state . capture_sizes . iter ( ) . any ( | preset | {
matches! (
preset ,
super ::state ::CaptureSizePreset ::Source
| super ::state ::CaptureSizePreset ::P1080
| super ::state ::CaptureSizePreset ::P1440
)
} ) ;
if heavy_capture {
items . push (
" If motion artifacting spikes, try a 900p or 720p capture profile before shrinking the breakout window; that usually lowers WAN pressure faster. "
. to_string ( ) ,
) ;
}
2026-04-16 22:15:59 -03:00
let source_passthrough = state
. capture_sizes
. iter ( )
. any ( | preset | matches! ( preset , super ::state ::CaptureSizePreset ::Source ) ) ;
if source_passthrough {
items . push (
" Source capture uses the HDMI device's own H.264 stream. If motion damage lingers for seconds, switch that eye to 1080p, 900p, or 720p so the server re-encodes with a tighter keyframe cadence. "
. to_string ( ) ,
) ;
}
if let Some ( sample ) = log . latest ( )
& & sample . video_loss_pct < 0.5
& & sample . dropped_frames = = 0
& & ( ( sample . left_server_fps - sample . left_receive_fps ) > 6.0
| | ( sample . right_server_fps - sample . right_receive_fps ) > 6.0 )
{
items . push (
" Receive fps is well below the target without packet loss. That usually points at source cadence or local decode pressure more than WAN loss, so compare Source against 1080p/900p and watch which side stays steadier. "
. to_string ( ) ,
) ;
}
2026-04-16 19:19:37 -03:00
if state . breakout_count ( ) = = 2 {
items . push (
" Both eye feeds are broken out right now. If the client starts struggling, compare in-launcher preview smoothness against full-window decode. "
. to_string ( ) ,
) ;
}
if items . is_empty ( ) {
items . push ( " Session state looks stable. Collect a few real samples before changing capture settings. " . to_string ( ) ) ;
}
items
}
2026-04-13 23:11:35 -03:00
#[ cfg(test) ]
mod tests {
use super ::* ;
use crate ::launcher ::state ::{ DeviceSelection , LauncherState } ;
fn sample ( n : u64 ) -> PerformanceSample {
PerformanceSample {
rtt_ms : 20.0 + n as f32 ,
2026-04-16 21:18:34 -03:00
jitter_ms : 3.0 + n as f32 ,
2026-04-13 23:11:35 -03:00
input_latency_ms : 10.0 + n as f32 ,
2026-04-16 21:18:34 -03:00
probe_loss_pct : n as f32 ,
video_loss_pct : ( n as f32 ) * 0.5 ,
left_receive_fps : 30.0 ,
left_present_fps : 29.0 ,
left_server_fps : 30.0 ,
right_receive_fps : 30.0 ,
right_present_fps : 28.0 ,
right_server_fps : 30.0 ,
2026-04-13 23:11:35 -03:00
dropped_frames : n ,
queue_depth : n as u32 ,
}
}
#[ test ]
fn diagnostics_log_keeps_only_latest_samples_with_capacity ( ) {
let mut log = DiagnosticsLog ::new ( 2 ) ;
log . record ( sample ( 1 ) ) ;
log . record ( sample ( 2 ) ) ;
log . record ( sample ( 3 ) ) ;
let kept : Vec < u64 > = log . iter ( ) . map ( | item | item . dropped_frames ) . collect ( ) ;
assert_eq! ( kept , vec! [ 2 , 3 ] ) ;
assert_eq! ( log . latest ( ) . map ( | s | s . dropped_frames ) , Some ( 3 ) ) ;
}
#[ test ]
fn diagnostics_log_enforces_minimum_capacity ( ) {
let mut log = DiagnosticsLog ::new ( 0 ) ;
log . record ( sample ( 1 ) ) ;
log . record ( sample ( 2 ) ) ;
assert_eq! ( log . len ( ) , 1 ) ;
assert_eq! ( log . latest ( ) . map ( | s | s . dropped_frames ) , Some ( 2 ) ) ;
}
#[ test ]
fn snapshot_report_contains_state_fields_and_samples ( ) {
let mut state = LauncherState ::new ( ) ;
state . devices = DeviceSelection {
camera : Some ( " /dev/video0 " . to_string ( ) ) ,
microphone : Some ( " alsa_input.usb " . to_string ( ) ) ,
speaker : Some ( " alsa_output.usb " . to_string ( ) ) ,
2026-04-16 12:58:05 -03:00
keyboard : Some ( " /dev/input/event10 " . to_string ( ) ) ,
mouse : Some ( " /dev/input/event11 " . to_string ( ) ) ,
2026-04-13 23:11:35 -03:00
} ;
state . push_note ( " first note " ) ;
let mut log = DiagnosticsLog ::new ( 4 ) ;
log . record ( sample ( 7 ) ) ;
let report = SnapshotReport ::from_state ( & state , & log , quality_probe_command ( ) . to_string ( ) ) ;
assert_eq! ( report . selected_camera . as_deref ( ) , Some ( " /dev/video0 " ) ) ;
2026-04-14 23:03:18 -03:00
assert_eq! (
report . selected_microphone . as_deref ( ) ,
Some ( " alsa_input.usb " )
) ;
2026-04-13 23:11:35 -03:00
assert_eq! ( report . selected_speaker . as_deref ( ) , Some ( " alsa_output.usb " ) ) ;
2026-04-16 19:19:37 -03:00
assert_eq! (
report . selected_keyboard . as_deref ( ) ,
Some ( " /dev/input/event10 " )
) ;
assert_eq! ( report . selected_mouse . as_deref ( ) , Some ( " /dev/input/event11 " ) ) ;
2026-04-13 23:11:35 -03:00
assert_eq! ( report . recent_samples . len ( ) , 1 ) ;
assert_eq! ( report . notes , vec! [ " first note " . to_string ( ) ] ) ;
2026-04-14 18:44:40 -03:00
assert! ( report . status . contains ( " mode=remote " ) ) ;
2026-04-16 19:19:37 -03:00
assert! ( report . client_version . starts_with ( " 0. " ) ) ;
assert! ( report . left_capture_profile . contains ( " fps " ) ) ;
2026-04-16 22:15:59 -03:00
assert_eq! ( report . left_capture_transport , " source pass-through " ) ;
2026-04-13 23:11:35 -03:00
}
#[ test ]
fn snapshot_json_is_serializable_and_mentions_probe_command ( ) {
let report = SnapshotReport ::from_state (
& LauncherState ::new ( ) ,
& DiagnosticsLog ::new ( 1 ) ,
quality_probe_command ( ) . to_string ( ) ,
) ;
let json = report . to_pretty_json ( ) . expect ( " serialize " ) ;
assert! ( json . contains ( " quality_gate.sh " ) ) ;
assert! ( json . contains ( " routing " ) ) ;
assert! ( json . contains ( " view_mode " ) ) ;
}
2026-04-16 19:19:37 -03:00
#[ test ]
fn snapshot_text_mentions_versions_profiles_and_recommendations ( ) {
let report = SnapshotReport ::from_state (
& LauncherState ::new ( ) ,
& DiagnosticsLog ::new ( 1 ) ,
quality_probe_command ( ) . to_string ( ) ,
) ;
let text = report . to_pretty_text ( ) ;
assert! ( text . contains ( " Lesavka Diagnostics " ) ) ;
assert! ( text . contains ( " client: v " ) ) ;
assert! ( text . contains ( " left eye " ) ) ;
2026-04-16 22:15:59 -03:00
assert! ( text . contains ( " transport: " ) ) ;
2026-04-16 19:19:37 -03:00
assert! ( text . contains ( " recommendations " ) ) ;
}
2026-04-13 23:11:35 -03:00
#[ test ]
fn quality_probe_command_mentions_both_gates ( ) {
let cmd = quality_probe_command ( ) ;
assert! ( cmd . contains ( " hygiene_gate.sh " ) ) ;
assert! ( cmd . contains ( " quality_gate.sh " ) ) ;
}
}