466 lines
20 KiB
Rust
466 lines
20 KiB
Rust
impl SnapshotReport {
|
|
pub fn from_state(state: &LauncherState, log: &DiagnosticsLog, probe_command: String) -> Self {
|
|
let left_capture = state
|
|
.display_capture_size_choice(0)
|
|
.unwrap_or_else(|| state.capture_size_choice(0));
|
|
let right_capture = state
|
|
.display_capture_size_choice(1)
|
|
.unwrap_or_else(|| state.capture_size_choice(1));
|
|
let left_breakout = state.breakout_size_choice(0);
|
|
let right_breakout = state.breakout_size_choice(1);
|
|
let latest = log.latest();
|
|
let left_stream_caps = latest
|
|
.map(|sample| sample.left_stream_caps_label.clone())
|
|
.unwrap_or_default();
|
|
let right_stream_caps = latest
|
|
.map(|sample| sample.right_stream_caps_label.clone())
|
|
.unwrap_or_default();
|
|
Self {
|
|
client_version: crate::VERSION.to_string(),
|
|
server_version: state.server_version.clone(),
|
|
server_available: state.server_available,
|
|
routing: state.routing,
|
|
view_mode: state.view_mode,
|
|
remote_active: state.remote_active,
|
|
power_state: format!(
|
|
"{} | {} | leases {}",
|
|
state.capture_power.mode,
|
|
state.capture_power.detail,
|
|
state.capture_power.active_leases
|
|
),
|
|
client_process_cpu_pct: latest
|
|
.map(|sample| sample.client_process_cpu_pct)
|
|
.unwrap_or(0.0),
|
|
server_process_cpu_pct: latest
|
|
.map(|sample| sample.server_process_cpu_pct)
|
|
.unwrap_or(0.0),
|
|
preview_source: format!(
|
|
"{}x{} @ {} fps",
|
|
state.preview_source.width, state.preview_source.height, state.preview_source.fps
|
|
),
|
|
client_display_limit: format!(
|
|
"{}x{}",
|
|
state.breakout_display.width, state.breakout_display.height
|
|
),
|
|
left_surface: state.display_surface(0).label().to_string(),
|
|
left_feed_source: match state.feed_source_preset(0) {
|
|
super::state::FeedSourcePreset::ThisEye => "Left Eye".to_string(),
|
|
super::state::FeedSourcePreset::OtherEye => "Right Eye (mirrored)".to_string(),
|
|
super::state::FeedSourcePreset::Off => "Off".to_string(),
|
|
},
|
|
left_capture_profile: capture_profile_label(&left_capture, &left_stream_caps),
|
|
left_capture_transport: left_capture.preset.transport_label().to_string(),
|
|
left_breakout_profile: format!(
|
|
"{} | {}x{}",
|
|
left_breakout.preset.label(),
|
|
left_breakout.width,
|
|
left_breakout.height
|
|
),
|
|
left_decoder_label: latest
|
|
.map(|sample| {
|
|
if sample.left_decoder_label.is_empty() {
|
|
"pending".to_string()
|
|
} else {
|
|
sample.left_decoder_label.clone()
|
|
}
|
|
})
|
|
.unwrap_or_else(|| "pending".to_string()),
|
|
left_stream_spread_ms: latest
|
|
.map(|sample| sample.left_stream_spread_ms)
|
|
.unwrap_or(0.0),
|
|
left_packet_gap_peak_ms: latest
|
|
.map(|sample| sample.left_packet_gap_peak_ms)
|
|
.unwrap_or(0.0),
|
|
left_present_gap_peak_ms: latest
|
|
.map(|sample| sample.left_present_gap_peak_ms)
|
|
.unwrap_or(0.0),
|
|
left_queue_depth: latest.map(|sample| sample.left_queue_depth).unwrap_or(0),
|
|
left_queue_peak: latest.map(|sample| sample.left_queue_peak).unwrap_or(0),
|
|
left_server_source_gap_peak_ms: latest
|
|
.map(|sample| sample.left_server_source_gap_peak_ms)
|
|
.unwrap_or(0.0),
|
|
left_server_send_gap_peak_ms: latest
|
|
.map(|sample| sample.left_server_send_gap_peak_ms)
|
|
.unwrap_or(0.0),
|
|
left_server_queue_peak: latest
|
|
.map(|sample| sample.left_server_queue_peak)
|
|
.unwrap_or(0),
|
|
left_server_encoder_label: latest
|
|
.map(|sample| {
|
|
if sample.left_server_encoder_label.is_empty() {
|
|
"pending".to_string()
|
|
} else {
|
|
sample.left_server_encoder_label.clone()
|
|
}
|
|
})
|
|
.unwrap_or_else(|| "pending".to_string()),
|
|
left_stream_caps_label: latest
|
|
.map(|sample| {
|
|
if sample.left_stream_caps_label.is_empty() {
|
|
"pending".to_string()
|
|
} else {
|
|
sample.left_stream_caps_label.clone()
|
|
}
|
|
})
|
|
.unwrap_or_else(|| "pending".to_string()),
|
|
left_decoded_caps_label: latest
|
|
.map(|sample| {
|
|
if sample.left_decoded_caps_label.is_empty() {
|
|
"pending".to_string()
|
|
} else {
|
|
sample.left_decoded_caps_label.clone()
|
|
}
|
|
})
|
|
.unwrap_or_else(|| "pending".to_string()),
|
|
left_rendered_caps_label: latest
|
|
.map(|sample| {
|
|
if sample.left_rendered_caps_label.is_empty() {
|
|
"pending".to_string()
|
|
} else {
|
|
sample.left_rendered_caps_label.clone()
|
|
}
|
|
})
|
|
.unwrap_or_else(|| "pending".to_string()),
|
|
right_surface: state.display_surface(1).label().to_string(),
|
|
right_feed_source: match state.feed_source_preset(1) {
|
|
super::state::FeedSourcePreset::ThisEye => "Right Eye".to_string(),
|
|
super::state::FeedSourcePreset::OtherEye => "Left Eye (mirrored)".to_string(),
|
|
super::state::FeedSourcePreset::Off => "Off".to_string(),
|
|
},
|
|
right_capture_profile: capture_profile_label(&right_capture, &right_stream_caps),
|
|
right_capture_transport: right_capture.preset.transport_label().to_string(),
|
|
right_breakout_profile: format!(
|
|
"{} | {}x{}",
|
|
right_breakout.preset.label(),
|
|
right_breakout.width,
|
|
right_breakout.height
|
|
),
|
|
right_decoder_label: latest
|
|
.map(|sample| {
|
|
if sample.right_decoder_label.is_empty() {
|
|
"pending".to_string()
|
|
} else {
|
|
sample.right_decoder_label.clone()
|
|
}
|
|
})
|
|
.unwrap_or_else(|| "pending".to_string()),
|
|
right_stream_spread_ms: latest
|
|
.map(|sample| sample.right_stream_spread_ms)
|
|
.unwrap_or(0.0),
|
|
right_packet_gap_peak_ms: latest
|
|
.map(|sample| sample.right_packet_gap_peak_ms)
|
|
.unwrap_or(0.0),
|
|
right_present_gap_peak_ms: latest
|
|
.map(|sample| sample.right_present_gap_peak_ms)
|
|
.unwrap_or(0.0),
|
|
right_queue_depth: latest.map(|sample| sample.right_queue_depth).unwrap_or(0),
|
|
right_queue_peak: latest.map(|sample| sample.right_queue_peak).unwrap_or(0),
|
|
right_server_source_gap_peak_ms: latest
|
|
.map(|sample| sample.right_server_source_gap_peak_ms)
|
|
.unwrap_or(0.0),
|
|
right_server_send_gap_peak_ms: latest
|
|
.map(|sample| sample.right_server_send_gap_peak_ms)
|
|
.unwrap_or(0.0),
|
|
right_server_queue_peak: latest
|
|
.map(|sample| sample.right_server_queue_peak)
|
|
.unwrap_or(0),
|
|
right_server_encoder_label: latest
|
|
.map(|sample| {
|
|
if sample.right_server_encoder_label.is_empty() {
|
|
"pending".to_string()
|
|
} else {
|
|
sample.right_server_encoder_label.clone()
|
|
}
|
|
})
|
|
.unwrap_or_else(|| "pending".to_string()),
|
|
right_stream_caps_label: latest
|
|
.map(|sample| {
|
|
if sample.right_stream_caps_label.is_empty() {
|
|
"pending".to_string()
|
|
} else {
|
|
sample.right_stream_caps_label.clone()
|
|
}
|
|
})
|
|
.unwrap_or_else(|| "pending".to_string()),
|
|
right_decoded_caps_label: latest
|
|
.map(|sample| {
|
|
if sample.right_decoded_caps_label.is_empty() {
|
|
"pending".to_string()
|
|
} else {
|
|
sample.right_decoded_caps_label.clone()
|
|
}
|
|
})
|
|
.unwrap_or_else(|| "pending".to_string()),
|
|
right_rendered_caps_label: latest
|
|
.map(|sample| {
|
|
if sample.right_rendered_caps_label.is_empty() {
|
|
"pending".to_string()
|
|
} else {
|
|
sample.right_rendered_caps_label.clone()
|
|
}
|
|
})
|
|
.unwrap_or_else(|| "pending".to_string()),
|
|
selected_camera: state.devices.camera.clone(),
|
|
camera_quality_label: state
|
|
.camera_quality
|
|
.map(CameraMode::short_label)
|
|
.unwrap_or_else(|| "default".to_string()),
|
|
selected_microphone: state.devices.microphone.clone(),
|
|
selected_speaker: state.devices.speaker.clone(),
|
|
media_channels: MediaChannelState {
|
|
camera: state.channels.camera,
|
|
microphone: state.channels.microphone,
|
|
audio: state.channels.audio,
|
|
},
|
|
audio_gain_label: state.audio_gain_label(),
|
|
mic_gain_label: state.mic_gain_label(),
|
|
upstream_camera: latest
|
|
.map(|sample| sample.upstream_camera.clone())
|
|
.unwrap_or_default(),
|
|
upstream_microphone: latest
|
|
.map(|sample| sample.upstream_microphone.clone())
|
|
.unwrap_or_default(),
|
|
selected_keyboard: state.devices.keyboard.clone(),
|
|
selected_mouse: state.devices.mouse.clone(),
|
|
status: state.status_line(),
|
|
recent_samples: log.iter().cloned().collect(),
|
|
notes: state.notes.clone(),
|
|
recommendations: recommendations_for(state, log),
|
|
probe_command,
|
|
}
|
|
}
|
|
|
|
pub fn to_pretty_json(&self) -> Result<String, serde_json::Error> {
|
|
serde_json::to_string_pretty(self)
|
|
}
|
|
|
|
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={} capture_power={}",
|
|
self.routing,
|
|
self.view_mode,
|
|
if self.remote_active { "active" } else { "idle" },
|
|
self.power_state
|
|
);
|
|
let _ = writeln!(
|
|
text,
|
|
"runtime: client CPU {:.1}% | server CPU {:.1}%",
|
|
self.client_process_cpu_pct, self.server_process_cpu_pct
|
|
);
|
|
let _ = writeln!(text, "source feed: {}", self.preview_source);
|
|
let _ = writeln!(text, "display limit: {}", self.client_display_limit);
|
|
let _ = writeln!(text);
|
|
let _ = writeln!(text, "left eye");
|
|
let _ = writeln!(text, " surface: {}", self.left_surface);
|
|
let _ = writeln!(text, " source: {}", self.left_feed_source);
|
|
let _ = writeln!(text, " capture: {}", self.left_capture_profile);
|
|
let _ = writeln!(text, " transport: {}", self.left_capture_transport);
|
|
let _ = writeln!(text, " breakout: {}", self.left_breakout_profile);
|
|
let _ = writeln!(
|
|
text,
|
|
" live: decoder={} spread={:.1}ms gaps={:.0}/{:.0}ms queue={}/{}",
|
|
self.left_decoder_label,
|
|
self.left_stream_spread_ms,
|
|
self.left_packet_gap_peak_ms,
|
|
self.left_present_gap_peak_ms,
|
|
self.left_queue_depth,
|
|
self.left_queue_peak
|
|
);
|
|
let _ = writeln!(text, " stream caps: {}", self.left_stream_caps_label);
|
|
let _ = writeln!(text, " decoded caps: {}", self.left_decoded_caps_label);
|
|
let _ = writeln!(text, " rendered caps: {}", self.left_rendered_caps_label);
|
|
let _ = writeln!(
|
|
text,
|
|
" server: encoder={} cpu={:.1}% gaps={:.0}/{:.0}ms queue-peak={}",
|
|
self.left_server_encoder_label,
|
|
self.server_process_cpu_pct,
|
|
self.left_server_source_gap_peak_ms,
|
|
self.left_server_send_gap_peak_ms,
|
|
self.left_server_queue_peak
|
|
);
|
|
let _ = writeln!(text, "right eye");
|
|
let _ = writeln!(text, " surface: {}", self.right_surface);
|
|
let _ = writeln!(text, " source: {}", self.right_feed_source);
|
|
let _ = writeln!(text, " capture: {}", self.right_capture_profile);
|
|
let _ = writeln!(text, " transport: {}", self.right_capture_transport);
|
|
let _ = writeln!(text, " breakout: {}", self.right_breakout_profile);
|
|
let _ = writeln!(
|
|
text,
|
|
" live: decoder={} spread={:.1}ms gaps={:.0}/{:.0}ms queue={}/{}",
|
|
self.right_decoder_label,
|
|
self.right_stream_spread_ms,
|
|
self.right_packet_gap_peak_ms,
|
|
self.right_present_gap_peak_ms,
|
|
self.right_queue_depth,
|
|
self.right_queue_peak
|
|
);
|
|
let _ = writeln!(text, " stream caps: {}", self.right_stream_caps_label);
|
|
let _ = writeln!(text, " decoded caps: {}", self.right_decoded_caps_label);
|
|
let _ = writeln!(text, " rendered caps: {}", self.right_rendered_caps_label);
|
|
let _ = writeln!(
|
|
text,
|
|
" server: encoder={} cpu={:.1}% gaps={:.0}/{:.0}ms queue-peak={}",
|
|
self.right_server_encoder_label,
|
|
self.server_process_cpu_pct,
|
|
self.right_server_source_gap_peak_ms,
|
|
self.right_server_send_gap_peak_ms,
|
|
self.right_server_queue_peak
|
|
);
|
|
let _ = writeln!(text);
|
|
let _ = writeln!(text, "media staging");
|
|
let _ = writeln!(
|
|
text,
|
|
" camera: {} | quality={} | enabled={}",
|
|
self.selected_camera.as_deref().unwrap_or("auto"),
|
|
self.camera_quality_label,
|
|
self.media_channels.camera
|
|
);
|
|
let _ = writeln!(
|
|
text,
|
|
" speaker: {} | volume={} | enabled={}",
|
|
self.selected_speaker.as_deref().unwrap_or("auto"),
|
|
self.audio_gain_label,
|
|
self.media_channels.audio
|
|
);
|
|
let _ = writeln!(
|
|
text,
|
|
" microphone: {} | gain={} | enabled={}",
|
|
self.selected_microphone.as_deref().unwrap_or("auto"),
|
|
self.mic_gain_label,
|
|
self.media_channels.microphone
|
|
);
|
|
let _ = writeln!(
|
|
text,
|
|
" uplink camera: {}",
|
|
uplink_summary(&self.upstream_camera)
|
|
);
|
|
let _ = writeln!(
|
|
text,
|
|
" uplink microphone: {}",
|
|
uplink_summary(&self.upstream_microphone)
|
|
);
|
|
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, "current UI state");
|
|
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/probe-spread/loss samples yet; this report is currently a launcher state snapshot."
|
|
);
|
|
} else {
|
|
for sample in &self.recent_samples {
|
|
let _ = writeln!(
|
|
text,
|
|
" rtt={:.1}ms probe-spread={:.1}ms input-floor={:.1}ms cpu={:.1}/{:.1}% probe-loss={:.1}% video-loss={:.1}% left={:.1}/{:.1}/{:.1}fps right={:.1}/{:.1}/{:.1}fps dropped={} queue={}/{} peaks=l{:.0}/{:.0}ms r{:.0}/{:.0}ms server=l{}:{:.0}/{:.0}/{} r{}:{:.0}/{:.0}/{}",
|
|
sample.rtt_ms,
|
|
sample.probe_spread_ms,
|
|
sample.input_latency_ms,
|
|
sample.client_process_cpu_pct,
|
|
sample.server_process_cpu_pct,
|
|
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,
|
|
sample.dropped_frames,
|
|
sample.queue_depth,
|
|
sample.left_queue_peak.max(sample.right_queue_peak),
|
|
sample.left_packet_gap_peak_ms,
|
|
sample.left_present_gap_peak_ms,
|
|
sample.right_packet_gap_peak_ms,
|
|
sample.right_present_gap_peak_ms,
|
|
sample.left_server_encoder_label,
|
|
sample.left_server_source_gap_peak_ms,
|
|
sample.left_server_send_gap_peak_ms,
|
|
sample.left_server_queue_peak,
|
|
sample.right_server_encoder_label,
|
|
sample.right_server_source_gap_peak_ms,
|
|
sample.right_server_send_gap_peak_ms,
|
|
sample.right_server_queue_peak
|
|
);
|
|
let _ = writeln!(
|
|
text,
|
|
" uplink: cam={} mic={}",
|
|
uplink_summary(&sample.upstream_camera),
|
|
uplink_summary(&sample.upstream_microphone)
|
|
);
|
|
}
|
|
}
|
|
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
|
|
}
|
|
}
|
|
|
|
fn uplink_summary(stream: &crate::uplink_telemetry::UpstreamStreamTelemetry) -> String {
|
|
if !stream.enabled {
|
|
return "disabled".to_string();
|
|
}
|
|
let connection = if stream.connected {
|
|
"live"
|
|
} else if stream.reconnect_count > 0 {
|
|
"reconnecting"
|
|
} else {
|
|
"idle"
|
|
};
|
|
let error = if stream.last_error.is_empty() {
|
|
"ok".to_string()
|
|
} else {
|
|
stream.last_error.clone()
|
|
};
|
|
format!(
|
|
"{connection} queue={}/{} enq-age={:.0}/{:.0}ms delivery={:.0}/{:.0}ms block-peak={:.0}ms reconnects={} streamed={} drops(total/full/stale)={}/{}/{} error={error}",
|
|
stream.queue_depth,
|
|
stream.queue_peak,
|
|
stream.latest_enqueue_age_ms,
|
|
stream.enqueue_age_peak_ms,
|
|
stream.latest_delivery_age_ms,
|
|
stream.delivery_age_peak_ms,
|
|
stream.enqueue_block_peak_ms,
|
|
stream.reconnect_count,
|
|
stream.packets_streamed,
|
|
stream.dropped_packets,
|
|
stream.dropped_queue_full_packets,
|
|
stream.dropped_stale_packets
|
|
)
|
|
}
|