Improve GUI plot workflow and performance
This commit is contained in:
@@ -28,6 +28,9 @@ impl<T> RingBuffer<T> {
|
|||||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||||
self.buf.iter()
|
self.buf.iter()
|
||||||
}
|
}
|
||||||
|
pub fn get(&self, index: usize) -> Option<&T> {
|
||||||
|
self.buf.get(index)
|
||||||
|
}
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.buf.clear();
|
self.buf.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ impl MixedTextPlotDecoder {
|
|||||||
Endian::Big => 1,
|
Endian::Big => 1,
|
||||||
});
|
});
|
||||||
packet.push(channels.min(u8::MAX as usize) as u8);
|
packet.push(channels.min(u8::MAX as usize) as u8);
|
||||||
packet.extend_from_slice(&(samples_per_channel.min(u16::MAX as usize) as u16).to_le_bytes());
|
packet
|
||||||
|
.extend_from_slice(&(samples_per_channel.min(u16::MAX as usize) as u16).to_le_bytes());
|
||||||
packet.extend_from_slice(&(payload.len().min(u32::MAX as usize) as u32).to_le_bytes());
|
packet.extend_from_slice(&(payload.len().min(u32::MAX as usize) as u32).to_le_bytes());
|
||||||
packet.extend_from_slice(payload);
|
packet.extend_from_slice(payload);
|
||||||
packet
|
packet
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use tokio::time::timeout;
|
|||||||
|
|
||||||
use xserial_core::frame::{
|
use xserial_core::frame::{
|
||||||
Endian, Framer,
|
Endian, Framer,
|
||||||
cobs::cobs_encode as raw_cobs_encode,
|
|
||||||
cobs::CobsFramer,
|
cobs::CobsFramer,
|
||||||
|
cobs::cobs_encode as raw_cobs_encode,
|
||||||
fixed::FixedLengthFramer,
|
fixed::FixedLengthFramer,
|
||||||
length::{LengthConfig, LengthPrefixedFramer},
|
length::{LengthConfig, LengthPrefixedFramer},
|
||||||
line::{LineConfig, LineFramer},
|
line::{LineConfig, LineFramer},
|
||||||
@@ -298,10 +298,7 @@ async fn tcp_mixed_text_and_plot_single_stream() {
|
|||||||
conn.disconnect().await.unwrap();
|
conn.disconnect().await.unwrap();
|
||||||
|
|
||||||
let decoder = MixedTextPlotDecoder::new(MixedTextPlotConfig::default());
|
let decoder = MixedTextPlotDecoder::new(MixedTextPlotConfig::default());
|
||||||
let results: Vec<DecodedData> = frames
|
let results: Vec<DecodedData> = frames.iter().filter_map(|f| decoder.decode(f)).collect();
|
||||||
.iter()
|
|
||||||
.filter_map(|f| decoder.decode(f))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
assert_eq!(results.len(), 3);
|
assert_eq!(results.len(), 3);
|
||||||
assert!(matches!(&results[0], DecodedData::Text(s) if s == "status ok"));
|
assert!(matches!(&results[0], DecodedData::Text(s) if s == "status ok"));
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use crate::app_state::{self, PersistedGuiState};
|
use crate::app_state::{self, PersistedGuiState};
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use crate::buffers::{HexBuffer, PlotBuffer, TextBuffer};
|
use crate::buffers::{HexBuffer, PlotBuffer, TextBuffer};
|
||||||
use crate::panels::{config, console, hex_view, plot_view, sidebar};
|
use crate::panels::{config, console, hex_view, plot_view, sidebar};
|
||||||
|
use crate::perf::{DrainStats, GuiProfiler, GuiSnapshot};
|
||||||
use crate::ui_fonts::{self, FontCandidate, FontChoice, UiFontSettings};
|
use crate::ui_fonts::{self, FontCandidate, FontChoice, UiFontSettings};
|
||||||
use egui::{Color32, Layout, Panel, Pos2, Rect, TextEdit, UiBuilder};
|
use egui::{Color32, Layout, Panel, Pos2, Rect, TextEdit, UiBuilder};
|
||||||
use xserial_client::SessionManager;
|
use xserial_client::SessionManager;
|
||||||
@@ -12,6 +13,8 @@ use xserial_client::session::SessionEvent;
|
|||||||
use xserial_core::protocol::DecodedData;
|
use xserial_core::protocol::DecodedData;
|
||||||
use xserial_core::transport::TransportConfig;
|
use xserial_core::transport::TransportConfig;
|
||||||
|
|
||||||
|
const DATA_REPAINT_INTERVAL: Duration = Duration::from_millis(33);
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum ConnectionStatus {
|
pub enum ConnectionStatus {
|
||||||
Connected,
|
Connected,
|
||||||
@@ -86,6 +89,7 @@ pub struct XserialApp {
|
|||||||
config_form: config::ConfigForm,
|
config_form: config::ConfigForm,
|
||||||
event_rx: mpsc::Receiver<SessionEvent>,
|
event_rx: mpsc::Receiver<SessionEvent>,
|
||||||
pending: Vec<SessionEvent>,
|
pending: Vec<SessionEvent>,
|
||||||
|
profiler: GuiProfiler,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl XserialApp {
|
impl XserialApp {
|
||||||
@@ -94,16 +98,32 @@ impl XserialApp {
|
|||||||
mut rx: tokio::sync::broadcast::Receiver<SessionEvent>,
|
mut rx: tokio::sync::broadcast::Receiver<SessionEvent>,
|
||||||
ctx: egui::Context,
|
ctx: egui::Context,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let profiler = GuiProfiler::from_env();
|
||||||
|
let repaint_counter = profiler.repaint_counter();
|
||||||
let (event_tx, event_rx) = mpsc::channel();
|
let (event_tx, event_rx) = mpsc::channel();
|
||||||
let repaint_ctx = ctx.clone();
|
let repaint_ctx = ctx.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
let mut last_repaint = Instant::now()
|
||||||
|
.checked_sub(DATA_REPAINT_INTERVAL)
|
||||||
|
.unwrap_or_else(Instant::now);
|
||||||
|
let mut delayed_repaint_pending = false;
|
||||||
loop {
|
loop {
|
||||||
match rx.recv().await {
|
match rx.recv().await {
|
||||||
Ok(event) => {
|
Ok(event) => {
|
||||||
if event_tx.send(event).is_err() {
|
if event_tx.send(event).is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
let elapsed = last_repaint.elapsed();
|
||||||
|
if elapsed >= DATA_REPAINT_INTERVAL {
|
||||||
|
repaint_counter.increment();
|
||||||
repaint_ctx.request_repaint();
|
repaint_ctx.request_repaint();
|
||||||
|
last_repaint = Instant::now();
|
||||||
|
delayed_repaint_pending = false;
|
||||||
|
} else if !delayed_repaint_pending {
|
||||||
|
repaint_counter.increment();
|
||||||
|
repaint_ctx.request_repaint_after(DATA_REPAINT_INTERVAL - elapsed);
|
||||||
|
delayed_repaint_pending = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
|
||||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
||||||
@@ -139,6 +159,7 @@ impl XserialApp {
|
|||||||
config_form: config::ConfigForm::default(),
|
config_form: config::ConfigForm::default(),
|
||||||
event_rx,
|
event_rx,
|
||||||
pending: Vec::new(),
|
pending: Vec::new(),
|
||||||
|
profiler,
|
||||||
};
|
};
|
||||||
app.restore_saved_sessions(saved_state);
|
app.restore_saved_sessions(saved_state);
|
||||||
app
|
app
|
||||||
@@ -230,11 +251,22 @@ impl XserialApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn drain_events(&mut self) {
|
fn drain_events(&mut self) {
|
||||||
|
let started = Instant::now();
|
||||||
|
let mut stats = DrainStats {
|
||||||
|
drained_events: 0,
|
||||||
|
drained_data_events: 0,
|
||||||
|
drained_text_events: 0,
|
||||||
|
drained_hex_events: 0,
|
||||||
|
drained_plot_events: 0,
|
||||||
|
pending_events: 0,
|
||||||
|
};
|
||||||
while let Ok(event) = self.event_rx.try_recv() {
|
while let Ok(event) = self.event_rx.try_recv() {
|
||||||
self.pending.push(event);
|
self.pending.push(event);
|
||||||
}
|
}
|
||||||
|
stats.pending_events = self.pending.len();
|
||||||
|
|
||||||
for event in self.pending.drain(..) {
|
for event in self.pending.drain(..) {
|
||||||
|
stats.drained_events += 1;
|
||||||
match event {
|
match event {
|
||||||
SessionEvent::Connected(id) => {
|
SessionEvent::Connected(id) => {
|
||||||
if let Some(tab) = self.tabs.iter_mut().find(|tab| tab.id == id) {
|
if let Some(tab) = self.tabs.iter_mut().find(|tab| tab.id == id) {
|
||||||
@@ -252,26 +284,62 @@ impl XserialApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SessionEvent::Data(id, entry) => {
|
SessionEvent::Data(id, entry) => {
|
||||||
|
stats.drained_data_events += 1;
|
||||||
if let Some(tab) = self.tabs.iter_mut().find(|tab| tab.id == id) {
|
if let Some(tab) = self.tabs.iter_mut().find(|tab| tab.id == id) {
|
||||||
match &entry.data {
|
match &entry.data {
|
||||||
DecodedData::Text(_) => tab.console.push(&entry),
|
DecodedData::Text(_) => {
|
||||||
DecodedData::Hex(_) => tab.hex.push(&entry),
|
stats.drained_text_events += 1;
|
||||||
DecodedData::Plot(_) => tab.plot.push(&entry),
|
tab.console.push(&entry);
|
||||||
|
}
|
||||||
|
DecodedData::Hex(_) => {
|
||||||
|
stats.drained_hex_events += 1;
|
||||||
|
tab.hex.push(&entry);
|
||||||
|
}
|
||||||
|
DecodedData::Plot(_) => {
|
||||||
|
stats.drained_plot_events += 1;
|
||||||
|
tab.plot.push(&entry);
|
||||||
|
}
|
||||||
DecodedData::Binary(_) => {}
|
DecodedData::Binary(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.profiler.record_drain(started.elapsed(), stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_live_plot_repaint(&self) -> bool {
|
fn wants_live_plot_repaint(&self) -> bool {
|
||||||
self.tabs
|
self.tabs.iter().enumerate().any(|(index, tab)| {
|
||||||
.get(self.active)
|
matches!(tab.status, ConnectionStatus::Connected)
|
||||||
.map(|tab| {
|
&& (tab.plot_view.detached
|
||||||
matches!(tab.view, View::Plot) && matches!(tab.status, ConnectionStatus::Connected)
|
|| (index == self.active && matches!(tab.view, View::Plot)))
|
||||||
})
|
})
|
||||||
.unwrap_or(false)
|
}
|
||||||
|
|
||||||
|
fn profiler_snapshot(&self) -> GuiSnapshot {
|
||||||
|
if let Some(tab) = self.tabs.get(self.active) {
|
||||||
|
GuiSnapshot {
|
||||||
|
tabs: self.tabs.len(),
|
||||||
|
active_view: match tab.view {
|
||||||
|
View::Text => "text",
|
||||||
|
View::Hex => "hex",
|
||||||
|
View::Plot => "plot",
|
||||||
|
},
|
||||||
|
active_text_lines: tab.console.len(),
|
||||||
|
active_hex_lines: tab.hex.len(),
|
||||||
|
active_plot_series: tab.plot.series_len(),
|
||||||
|
active_plot_points: tab.plot.total_points(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
GuiSnapshot {
|
||||||
|
tabs: self.tabs.len(),
|
||||||
|
active_view: "none",
|
||||||
|
active_text_lines: 0,
|
||||||
|
active_hex_lines: 0,
|
||||||
|
active_plot_series: 0,
|
||||||
|
active_plot_points: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_config_window(&mut self, ctx: &egui::Context) {
|
fn render_config_window(&mut self, ctx: &egui::Context) {
|
||||||
@@ -313,6 +381,7 @@ impl XserialApp {
|
|||||||
|
|
||||||
fn render_sidebar(&mut self, ui: &mut egui::Ui) {
|
fn render_sidebar(&mut self, ui: &mut egui::Ui) {
|
||||||
let mut on_new = false;
|
let mut on_new = false;
|
||||||
|
let mut on_edit = None;
|
||||||
let mut on_delete = None;
|
let mut on_delete = None;
|
||||||
let previous_active = self.active;
|
let previous_active = self.active;
|
||||||
|
|
||||||
@@ -328,13 +397,26 @@ impl XserialApp {
|
|||||||
transport_summary: transport_summary(&tab.session_config.transport),
|
transport_summary: transport_summary(&tab.session_config.transport),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
sidebar::render(ui, &sessions, &mut self.active, &mut on_new, &mut on_delete);
|
sidebar::render(
|
||||||
|
ui,
|
||||||
|
&sessions,
|
||||||
|
&mut self.active,
|
||||||
|
&mut on_new,
|
||||||
|
&mut on_edit,
|
||||||
|
&mut on_delete,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if on_new {
|
if on_new {
|
||||||
self.open_create_config();
|
self.open_create_config();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(index) = on_edit
|
||||||
|
&& let Some(tab) = self.tabs.get(index)
|
||||||
|
{
|
||||||
|
self.open_edit_config(tab.id);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(index) = on_delete {
|
if let Some(index) = on_delete {
|
||||||
if let Some(tab) = self.tabs.get(index) {
|
if let Some(tab) = self.tabs.get(index) {
|
||||||
self.manager.remove(tab.id);
|
self.manager.remove(tab.id);
|
||||||
@@ -362,8 +444,10 @@ impl XserialApp {
|
|||||||
|
|
||||||
let manager = self.manager.clone();
|
let manager = self.manager.clone();
|
||||||
let display = &mut self.display;
|
let display = &mut self.display;
|
||||||
let mut edit_session = None;
|
|
||||||
let mut persist_state = false;
|
let mut persist_state = false;
|
||||||
|
let mut text_render = None;
|
||||||
|
let mut hex_render = None;
|
||||||
|
let mut plot_render = None;
|
||||||
|
|
||||||
{
|
{
|
||||||
let tab = &mut self.tabs[self.active];
|
let tab = &mut self.tabs[self.active];
|
||||||
@@ -392,11 +476,7 @@ impl XserialApp {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let (configure_clicked, auto_reconnect_changed) =
|
let auto_reconnect_changed = render_session_controls(ui, &manager, tab);
|
||||||
render_session_controls(ui, &manager, tab);
|
|
||||||
if configure_clicked {
|
|
||||||
edit_session = Some(tab.id);
|
|
||||||
}
|
|
||||||
persist_state |= auto_reconnect_changed;
|
persist_state |= auto_reconnect_changed;
|
||||||
|
|
||||||
let mut display_changed = false;
|
let mut display_changed = false;
|
||||||
@@ -431,10 +511,41 @@ impl XserialApp {
|
|||||||
UiBuilder::new()
|
UiBuilder::new()
|
||||||
.max_rect(receive_rect)
|
.max_rect(receive_rect)
|
||||||
.layout(Layout::top_down(egui::Align::Min).with_cross_justify(true)),
|
.layout(Layout::top_down(egui::Align::Min).with_cross_justify(true)),
|
||||||
|ui| match tab.view {
|
|ui| {
|
||||||
View::Text => console::render(ui, &tab.console, *display),
|
let started = Instant::now();
|
||||||
View::Hex => hex_view::render(ui, &tab.hex, *display),
|
match tab.view {
|
||||||
View::Plot => plot_view::render(ui, &tab.plot, tab.id, &mut tab.plot_view),
|
View::Text => {
|
||||||
|
let line_count = console::render(ui, &tab.console, *display);
|
||||||
|
text_render = Some((started.elapsed(), line_count));
|
||||||
|
}
|
||||||
|
View::Hex => {
|
||||||
|
let line_count = hex_view::render(ui, &tab.hex, *display);
|
||||||
|
hex_render = Some((started.elapsed(), line_count));
|
||||||
|
}
|
||||||
|
View::Plot => {
|
||||||
|
if tab.plot_view.detached {
|
||||||
|
ui.heading(format!(
|
||||||
|
"Plot window detached for Session {}",
|
||||||
|
tab.id
|
||||||
|
));
|
||||||
|
ui.label("The plot is currently shown in a floating window.");
|
||||||
|
if ui.button("Dock Plot Back").clicked() {
|
||||||
|
tab.plot_view.detached = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let output = plot_view::render(
|
||||||
|
ui,
|
||||||
|
&mut tab.plot,
|
||||||
|
tab.id,
|
||||||
|
&mut tab.plot_view,
|
||||||
|
);
|
||||||
|
if output.toggle_detached {
|
||||||
|
tab.plot_view.detached = true;
|
||||||
|
}
|
||||||
|
plot_render = Some((started.elapsed(), output.stats));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -446,8 +557,14 @@ impl XserialApp {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(id) = edit_session {
|
if let Some((elapsed, line_count)) = text_render {
|
||||||
self.open_edit_config(id);
|
self.profiler.record_text_render(elapsed, line_count);
|
||||||
|
}
|
||||||
|
if let Some((elapsed, line_count)) = hex_render {
|
||||||
|
self.profiler.record_hex_render(elapsed, line_count);
|
||||||
|
}
|
||||||
|
if let Some((elapsed, stats)) = plot_render {
|
||||||
|
self.profiler.record_plot_render(elapsed, stats);
|
||||||
}
|
}
|
||||||
if persist_state {
|
if persist_state {
|
||||||
self.persist_gui_state();
|
self.persist_gui_state();
|
||||||
@@ -455,6 +572,46 @@ impl XserialApp {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_detached_plot_windows(&mut self, ctx: &egui::Context) {
|
||||||
|
let mut plot_renders = Vec::new();
|
||||||
|
|
||||||
|
for tab in &mut self.tabs {
|
||||||
|
if !tab.plot_view.detached {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut open = true;
|
||||||
|
let mut output = None;
|
||||||
|
egui::Window::new(format!("Plot - Session {}", tab.id))
|
||||||
|
.open(&mut open)
|
||||||
|
.default_width(720.0)
|
||||||
|
.default_height(480.0)
|
||||||
|
.resizable(true)
|
||||||
|
.vscroll(false)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
let started = Instant::now();
|
||||||
|
let render_output =
|
||||||
|
plot_view::render(ui, &mut tab.plot, tab.id, &mut tab.plot_view);
|
||||||
|
output = Some((started.elapsed(), render_output));
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some((elapsed, render_output)) = output {
|
||||||
|
if render_output.toggle_detached {
|
||||||
|
tab.plot_view.detached = false;
|
||||||
|
}
|
||||||
|
plot_renders.push((elapsed, render_output.stats));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !open {
|
||||||
|
tab.plot_view.detached = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (elapsed, stats) in plot_renders {
|
||||||
|
self.profiler.record_plot_render(elapsed, stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn render_top_bar(&mut self, ui: &mut egui::Ui) {
|
fn render_top_bar(&mut self, ui: &mut egui::Ui) {
|
||||||
Panel::top("top_bar").show_inside(ui, |ui| {
|
Panel::top("top_bar").show_inside(ui, |ui| {
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
@@ -686,6 +843,7 @@ impl eframe::App for XserialApp {
|
|||||||
fn update(&mut self, _ctx: &egui::Context, _frame: &mut eframe::Frame) {}
|
fn update(&mut self, _ctx: &egui::Context, _frame: &mut eframe::Frame) {}
|
||||||
|
|
||||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||||
|
let frame_started = Instant::now();
|
||||||
self.drain_events();
|
self.drain_events();
|
||||||
if self.wants_live_plot_repaint() {
|
if self.wants_live_plot_repaint() {
|
||||||
ui.ctx().request_repaint_after(Duration::from_millis(16));
|
ui.ctx().request_repaint_after(Duration::from_millis(16));
|
||||||
@@ -695,6 +853,9 @@ impl eframe::App for XserialApp {
|
|||||||
self.render_config_window(ui.ctx());
|
self.render_config_window(ui.ctx());
|
||||||
self.render_sidebar(ui);
|
self.render_sidebar(ui);
|
||||||
self.render_main_panel(ui);
|
self.render_main_panel(ui);
|
||||||
|
self.render_detached_plot_windows(ui.ctx());
|
||||||
|
self.profiler
|
||||||
|
.record_frame(frame_started.elapsed(), self.profiler_snapshot());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,46 +863,42 @@ fn render_session_controls(
|
|||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
manager: &SessionManager,
|
manager: &SessionManager,
|
||||||
tab: &mut SessionTab,
|
tab: &mut SessionTab,
|
||||||
) -> (bool, bool) {
|
) -> bool {
|
||||||
let mut configure_clicked = false;
|
|
||||||
let mut auto_reconnect_changed = false;
|
let mut auto_reconnect_changed = false;
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
if ui.button("Connect").clicked() {
|
let connected = matches!(
|
||||||
if let Some(handle) = manager.get(tab.id) {
|
tab.status,
|
||||||
tab.status = ConnectionStatus::Connecting;
|
ConnectionStatus::Connected | ConnectionStatus::Connecting
|
||||||
tokio::spawn(async move {
|
);
|
||||||
let _ = handle.connect().await;
|
let connect_label = if connected { "Disconnect" } else { "Connect" };
|
||||||
});
|
if ui.button(connect_label).clicked() {
|
||||||
} else {
|
|
||||||
tab.status = ConnectionStatus::Error(String::from("session not found"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.button("Disconnect").clicked() {
|
|
||||||
if let Some(handle) = manager.get(tab.id) {
|
if let Some(handle) = manager.get(tab.id) {
|
||||||
|
if connected {
|
||||||
tab.status = ConnectionStatus::Disconnected;
|
tab.status = ConnectionStatus::Disconnected;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _ = handle.disconnect().await;
|
let _ = handle.disconnect().await;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
tab.status = ConnectionStatus::Error(String::from("session not found"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.button("Reconnect").clicked() {
|
|
||||||
if let Some(handle) = manager.get(tab.id) {
|
|
||||||
tab.status = ConnectionStatus::Connecting;
|
tab.status = ConnectionStatus::Connecting;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _ = handle.reconnect().await;
|
let _ = handle.connect().await;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
tab.status = ConnectionStatus::Error(String::from("session not found"));
|
tab.status = ConnectionStatus::Error(String::from("session not found"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.button("Configure").clicked() {
|
// if ui.button("Reconnect").clicked() {
|
||||||
configure_clicked = true;
|
// if let Some(handle) = manager.get(tab.id) {
|
||||||
}
|
// tab.status = ConnectionStatus::Connecting;
|
||||||
|
// tokio::spawn(async move {
|
||||||
|
// let _ = handle.reconnect().await;
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// tab.status = ConnectionStatus::Error(String::from("session not found"));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
if ui.button("Clear").clicked() {
|
if ui.button("Clear").clicked() {
|
||||||
tab.console.clear();
|
tab.console.clear();
|
||||||
@@ -764,7 +921,7 @@ fn render_session_controls(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
(configure_clicked, auto_reconnect_changed)
|
auto_reconnect_changed
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_send_panel(ui: &mut egui::Ui, manager: &SessionManager, tab: &mut SessionTab) {
|
fn render_send_panel(ui: &mut egui::Ui, manager: &SessionManager, tab: &mut SessionTab) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use egui_plot::PlotBounds;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use xserial_client::RingBuffer;
|
use xserial_client::RingBuffer;
|
||||||
@@ -51,8 +52,12 @@ impl TextBuffer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter(&self) -> impl Iterator<Item = &ConsoleLine> {
|
pub fn get(&self, index: usize) -> Option<&ConsoleLine> {
|
||||||
self.lines.iter()
|
self.lines.get(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.lines.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
@@ -135,8 +140,12 @@ impl HexBuffer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter(&self) -> impl Iterator<Item = &HexLine> {
|
pub fn get(&self, index: usize) -> Option<&HexLine> {
|
||||||
self.lines.iter()
|
self.lines.get(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.lines.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
@@ -168,11 +177,17 @@ impl PlotSeries {
|
|||||||
if !sample.is_finite() {
|
if !sample.is_finite() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
self.points.push_back([self.next_x, *sample]);
|
self.push_point([self.next_x, *sample], limit);
|
||||||
self.next_x += 1.0;
|
self.next_x += 1.0;
|
||||||
}
|
}
|
||||||
while self.points.len() > limit {
|
}
|
||||||
self.points.pop_front();
|
|
||||||
|
fn push_point(&mut self, point: [f64; 2], limit: usize) -> Option<[f64; 2]> {
|
||||||
|
self.points.push_back(point);
|
||||||
|
if self.points.len() > limit {
|
||||||
|
self.points.pop_front()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,26 +195,56 @@ impl PlotSeries {
|
|||||||
self.points.iter().copied()
|
self.points.iter().copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_points_time_series(&self, x_min: f64, x_max: f64, max_points: usize) -> Vec<[f64; 2]> {
|
pub fn len(&self) -> usize {
|
||||||
|
self.points.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first_point(&self) -> Option<[f64; 2]> {
|
||||||
|
self.points.front().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn last_point(&self) -> Option<[f64; 2]> {
|
||||||
|
self.points.back().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_points_time_series(
|
||||||
|
&self,
|
||||||
|
x_min: f64,
|
||||||
|
x_max: f64,
|
||||||
|
max_points: usize,
|
||||||
|
) -> Vec<[f64; 2]> {
|
||||||
if self.points.is_empty() || max_points == 0 {
|
if self.points.is_empty() || max_points == 0 {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
let visible: Vec<[f64; 2]> = self
|
let mut exact_visible = Vec::with_capacity(max_points.min(self.points.len()));
|
||||||
.points
|
for point in self.points.iter().copied() {
|
||||||
.iter()
|
if point[0] < x_min {
|
||||||
.copied()
|
continue;
|
||||||
.filter(|point| point[0] >= x_min && point[0] <= x_max)
|
}
|
||||||
.collect();
|
if point[0] > x_max {
|
||||||
if visible.len() <= max_points {
|
break;
|
||||||
return visible;
|
}
|
||||||
|
exact_visible.push(point);
|
||||||
|
if exact_visible.len() > max_points {
|
||||||
|
exact_visible.clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exact_visible.is_empty() {
|
||||||
|
return exact_visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
let bucket_count = (max_points / 2).max(1);
|
let bucket_count = (max_points / 2).max(1);
|
||||||
let width = (x_max - x_min).max(1.0);
|
let width = (x_max - x_min).max(1.0);
|
||||||
let bucket_width = width / bucket_count as f64;
|
let bucket_width = width / bucket_count as f64;
|
||||||
let mut rendered = Vec::with_capacity(bucket_count * 2);
|
let mut rendered = Vec::with_capacity(bucket_count * 2);
|
||||||
let mut start = 0usize;
|
let mut points = self
|
||||||
|
.points
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.skip_while(|point| point[0] < x_min)
|
||||||
|
.peekable();
|
||||||
|
|
||||||
for bucket_index in 0..bucket_count {
|
for bucket_index in 0..bucket_count {
|
||||||
let bucket_start = x_min + bucket_width * bucket_index as f64;
|
let bucket_start = x_min + bucket_width * bucket_index as f64;
|
||||||
@@ -212,8 +257,7 @@ impl PlotSeries {
|
|||||||
let mut min_point: Option<[f64; 2]> = None;
|
let mut min_point: Option<[f64; 2]> = None;
|
||||||
let mut max_point: Option<[f64; 2]> = None;
|
let mut max_point: Option<[f64; 2]> = None;
|
||||||
|
|
||||||
while start < visible.len() {
|
while let Some(point) = points.peek().copied() {
|
||||||
let point = visible[start];
|
|
||||||
if point[0] > bucket_end {
|
if point[0] > bucket_end {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -227,7 +271,7 @@ impl PlotSeries {
|
|||||||
_ => max_point = Some(point),
|
_ => max_point = Some(point),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
start += 1;
|
points.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
match (min_point, max_point) {
|
match (min_point, max_point) {
|
||||||
@@ -274,10 +318,69 @@ pub enum PlotSeriesKind {
|
|||||||
XY,
|
XY,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct BoundsRect {
|
||||||
|
min_x: f64,
|
||||||
|
max_x: f64,
|
||||||
|
min_y: f64,
|
||||||
|
max_y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoundsRect {
|
||||||
|
fn from_point([x, y]: [f64; 2]) -> Option<Self> {
|
||||||
|
if !(x.is_finite() && y.is_finite()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Self {
|
||||||
|
min_x: x,
|
||||||
|
max_x: x,
|
||||||
|
min_y: y,
|
||||||
|
max_y: y,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extend_with_point(&mut self, [x, y]: [f64; 2]) {
|
||||||
|
if !(x.is_finite() && y.is_finite()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.min_x = self.min_x.min(x);
|
||||||
|
self.max_x = self.max_x.max(x);
|
||||||
|
self.min_y = self.min_y.min(y);
|
||||||
|
self.max_y = self.max_y.max(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn touches(&self, [x, y]: [f64; 2]) -> bool {
|
||||||
|
x == self.min_x || x == self.max_x || y == self.min_y || y == self.max_y
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_plot_bounds(self) -> PlotBounds {
|
||||||
|
let mut bounds =
|
||||||
|
PlotBounds::from_min_max([self.min_x, self.min_y], [self.max_x, self.max_y]);
|
||||||
|
|
||||||
|
if bounds.width() <= 0.0 {
|
||||||
|
bounds.set_x_center_width(bounds.center().x, 1.0);
|
||||||
|
} else {
|
||||||
|
bounds.expand_x(bounds.width() * 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
if bounds.height() <= 0.0 {
|
||||||
|
bounds.set_y_center_height(bounds.center().y, 1.0);
|
||||||
|
} else {
|
||||||
|
bounds.expand_y(bounds.height() * 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PlotBuffer {
|
pub struct PlotBuffer {
|
||||||
limit: usize,
|
limit: usize,
|
||||||
kind: PlotSeriesKind,
|
kind: PlotSeriesKind,
|
||||||
series: Vec<PlotSeries>,
|
series: Vec<PlotSeries>,
|
||||||
|
time_series_y_bounds: Option<(f64, f64)>,
|
||||||
|
time_series_y_dirty: bool,
|
||||||
|
xy_bounds: Option<BoundsRect>,
|
||||||
|
xy_bounds_dirty: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlotBuffer {
|
impl PlotBuffer {
|
||||||
@@ -286,6 +389,10 @@ impl PlotBuffer {
|
|||||||
limit,
|
limit,
|
||||||
kind: PlotSeriesKind::TimeSeries,
|
kind: PlotSeriesKind::TimeSeries,
|
||||||
series: Vec::new(),
|
series: Vec::new(),
|
||||||
|
time_series_y_bounds: None,
|
||||||
|
time_series_y_dirty: false,
|
||||||
|
xy_bounds: None,
|
||||||
|
xy_bounds_dirty: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,10 +403,18 @@ impl PlotBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn push_frame(&mut self, pipeline_name: &str, frame: &PlotFrame) {
|
fn push_frame(&mut self, pipeline_name: &str, frame: &PlotFrame) {
|
||||||
self.kind = match frame.format {
|
let next_kind = match frame.format {
|
||||||
PlotFormat::XY => PlotSeriesKind::XY,
|
PlotFormat::XY => PlotSeriesKind::XY,
|
||||||
PlotFormat::Interleaved | PlotFormat::Block => PlotSeriesKind::TimeSeries,
|
PlotFormat::Interleaved | PlotFormat::Block => PlotSeriesKind::TimeSeries,
|
||||||
};
|
};
|
||||||
|
if !matches!(
|
||||||
|
(&self.kind, &next_kind),
|
||||||
|
(PlotSeriesKind::TimeSeries, PlotSeriesKind::TimeSeries)
|
||||||
|
| (PlotSeriesKind::XY, PlotSeriesKind::XY)
|
||||||
|
) {
|
||||||
|
self.invalidate_bounds();
|
||||||
|
}
|
||||||
|
self.kind = next_kind;
|
||||||
|
|
||||||
if matches!(frame.format, PlotFormat::XY) {
|
if matches!(frame.format, PlotFormat::XY) {
|
||||||
self.push_xy_frame(pipeline_name, frame);
|
self.push_xy_frame(pipeline_name, frame);
|
||||||
@@ -313,15 +428,33 @@ impl PlotBuffer {
|
|||||||
format!("{pipeline_name}:ch{}", index + 1)
|
format!("{pipeline_name}:ch{}", index + 1)
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(series) = self
|
if let Some(series_index) = self
|
||||||
.series
|
.series
|
||||||
.iter_mut()
|
.iter()
|
||||||
.find(|series| series.name == series_name)
|
.position(|series| series.name == series_name)
|
||||||
{
|
{
|
||||||
series.push_samples(channel, self.limit);
|
for sample in channel {
|
||||||
|
if !sample.is_finite() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (point, removed) = {
|
||||||
|
let series = &mut self.series[series_index];
|
||||||
|
let point = [series.next_x, *sample];
|
||||||
|
let removed = series.push_point(point, self.limit);
|
||||||
|
series.next_x += 1.0;
|
||||||
|
(point, removed)
|
||||||
|
};
|
||||||
|
if let Some(removed) = removed {
|
||||||
|
self.note_time_series_removed(removed);
|
||||||
|
}
|
||||||
|
self.note_time_series_point(point);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut series = PlotSeries::new(series_name);
|
let mut series = PlotSeries::new(series_name);
|
||||||
series.push_samples(channel, self.limit);
|
series.push_samples(channel, self.limit);
|
||||||
|
for point in series.points() {
|
||||||
|
self.note_time_series_point(point);
|
||||||
|
}
|
||||||
self.series.push(series);
|
self.series.push(series);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,30 +470,36 @@ impl PlotBuffer {
|
|||||||
let len = x.len().min(y.len());
|
let len = x.len().min(y.len());
|
||||||
let series_name = format!("{pipeline_name}:xy");
|
let series_name = format!("{pipeline_name}:xy");
|
||||||
|
|
||||||
let series = if let Some(series) = self
|
let series_index = if let Some(index) = self
|
||||||
.series
|
.series
|
||||||
.iter_mut()
|
.iter()
|
||||||
.find(|series| series.name == series_name)
|
.position(|series| series.name == series_name)
|
||||||
{
|
{
|
||||||
series
|
index
|
||||||
} else {
|
} else {
|
||||||
self.series.push(PlotSeries::new(series_name));
|
self.series.push(PlotSeries::new(series_name));
|
||||||
self.series.last_mut().expect("just pushed")
|
self.series.len() - 1
|
||||||
};
|
};
|
||||||
|
|
||||||
for index in 0..len {
|
for index in 0..len {
|
||||||
if !x[index].is_finite() || !y[index].is_finite() {
|
if !x[index].is_finite() || !y[index].is_finite() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
series.points.push_back([x[index], y[index]]);
|
let point = [x[index], y[index]];
|
||||||
|
let removed = {
|
||||||
|
let series = &mut self.series[series_index];
|
||||||
|
series.push_point(point, self.limit)
|
||||||
|
};
|
||||||
|
if let Some(removed) = removed {
|
||||||
|
self.note_xy_removed(removed);
|
||||||
}
|
}
|
||||||
while series.points.len() > self.limit {
|
self.note_xy_point(point);
|
||||||
series.points.pop_front();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.series.clear();
|
self.series.clear();
|
||||||
|
self.invalidate_bounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_limit(&mut self, limit: usize) {
|
pub fn set_limit(&mut self, limit: usize) {
|
||||||
@@ -370,6 +509,7 @@ impl PlotBuffer {
|
|||||||
series.points.pop_front();
|
series.points.pop_front();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.invalidate_bounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
@@ -383,6 +523,141 @@ impl PlotBuffer {
|
|||||||
pub fn iter(&self) -> impl Iterator<Item = &PlotSeries> {
|
pub fn iter(&self) -> impl Iterator<Item = &PlotSeries> {
|
||||||
self.series.iter()
|
self.series.iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn series_len(&self) -> usize {
|
||||||
|
self.series.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn total_points(&self) -> usize {
|
||||||
|
self.series.iter().map(PlotSeries::len).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn plot_bounds(&mut self) -> Option<PlotBounds> {
|
||||||
|
match self.kind {
|
||||||
|
PlotSeriesKind::TimeSeries => self.time_series_plot_bounds(),
|
||||||
|
PlotSeriesKind::XY => self.xy_plot_bounds(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalidate_bounds(&mut self) {
|
||||||
|
self.time_series_y_bounds = None;
|
||||||
|
self.time_series_y_dirty = false;
|
||||||
|
self.xy_bounds = None;
|
||||||
|
self.xy_bounds_dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note_time_series_point(&mut self, point: [f64; 2]) {
|
||||||
|
if !point[1].is_finite() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match &mut self.time_series_y_bounds {
|
||||||
|
Some((min_y, max_y)) => {
|
||||||
|
*min_y = min_y.min(point[1]);
|
||||||
|
*max_y = max_y.max(point[1]);
|
||||||
|
}
|
||||||
|
None => self.time_series_y_bounds = Some((point[1], point[1])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note_time_series_removed(&mut self, removed: [f64; 2]) {
|
||||||
|
if let Some((min_y, max_y)) = self.time_series_y_bounds
|
||||||
|
&& (removed[1] == min_y || removed[1] == max_y)
|
||||||
|
{
|
||||||
|
self.time_series_y_dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note_xy_point(&mut self, point: [f64; 2]) {
|
||||||
|
match &mut self.xy_bounds {
|
||||||
|
Some(bounds) => bounds.extend_with_point(point),
|
||||||
|
None => self.xy_bounds = BoundsRect::from_point(point),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note_xy_removed(&mut self, removed: [f64; 2]) {
|
||||||
|
if let Some(bounds) = self.xy_bounds
|
||||||
|
&& bounds.touches(removed)
|
||||||
|
{
|
||||||
|
self.xy_bounds_dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn time_series_plot_bounds(&mut self) -> Option<PlotBounds> {
|
||||||
|
let mut min_x = f64::INFINITY;
|
||||||
|
let mut max_x = f64::NEG_INFINITY;
|
||||||
|
|
||||||
|
for series in &self.series {
|
||||||
|
if let Some([x, _]) = series.first_point() {
|
||||||
|
min_x = min_x.min(x);
|
||||||
|
}
|
||||||
|
if let Some([x, _]) = series.last_point() {
|
||||||
|
max_x = max_x.max(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(min_x.is_finite() && max_x.is_finite()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.time_series_y_dirty {
|
||||||
|
self.recompute_time_series_y_bounds();
|
||||||
|
}
|
||||||
|
let (min_y, max_y) = self.time_series_y_bounds?;
|
||||||
|
|
||||||
|
Some(
|
||||||
|
BoundsRect {
|
||||||
|
min_x,
|
||||||
|
max_x,
|
||||||
|
min_y,
|
||||||
|
max_y,
|
||||||
|
}
|
||||||
|
.into_plot_bounds(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xy_plot_bounds(&mut self) -> Option<PlotBounds> {
|
||||||
|
if self.xy_bounds_dirty {
|
||||||
|
self.recompute_xy_bounds();
|
||||||
|
}
|
||||||
|
self.xy_bounds.map(BoundsRect::into_plot_bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recompute_time_series_y_bounds(&mut self) {
|
||||||
|
let mut min_y = f64::INFINITY;
|
||||||
|
let mut max_y = f64::NEG_INFINITY;
|
||||||
|
|
||||||
|
for series in &self.series {
|
||||||
|
for [_, y] in series.points() {
|
||||||
|
if y.is_finite() {
|
||||||
|
min_y = min_y.min(y);
|
||||||
|
max_y = max_y.max(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.time_series_y_bounds = if min_y.is_finite() && max_y.is_finite() {
|
||||||
|
Some((min_y, max_y))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
self.time_series_y_dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recompute_xy_bounds(&mut self) {
|
||||||
|
let mut bounds: Option<BoundsRect> = None;
|
||||||
|
|
||||||
|
for series in &self.series {
|
||||||
|
for point in series.points() {
|
||||||
|
match &mut bounds {
|
||||||
|
Some(existing) => existing.extend_with_point(point),
|
||||||
|
None => bounds = BoundsRect::from_point(point),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.xy_bounds = bounds;
|
||||||
|
self.xy_bounds_dirty = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -459,7 +734,11 @@ mod tests {
|
|||||||
let rendered = series.render_points_time_series(0.0, 99.0, 16);
|
let rendered = series.render_points_time_series(0.0, 99.0, 16);
|
||||||
assert!(!rendered.is_empty());
|
assert!(!rendered.is_empty());
|
||||||
assert!(rendered.len() <= 16);
|
assert!(rendered.len() <= 16);
|
||||||
assert!(rendered.iter().all(|point| point[0] >= 0.0 && point[0] <= 99.0));
|
assert!(
|
||||||
|
rendered
|
||||||
|
.iter()
|
||||||
|
.all(|point| point[0] >= 0.0 && point[0] <= 99.0)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ mod app;
|
|||||||
mod app_state;
|
mod app_state;
|
||||||
mod buffers;
|
mod buffers;
|
||||||
mod panels;
|
mod panels;
|
||||||
|
mod perf;
|
||||||
mod ui_fonts;
|
mod ui_fonts;
|
||||||
|
|
||||||
use xserial_client::SessionManager;
|
use xserial_client::SessionManager;
|
||||||
|
|||||||
@@ -1,10 +1,29 @@
|
|||||||
use crate::app::DisplayOptions;
|
use crate::app::DisplayOptions;
|
||||||
use crate::buffers::{LineDirection, TextBuffer};
|
use crate::buffers::{ConsoleLine, LineDirection, TextBuffer};
|
||||||
use egui::{ScrollArea, Ui};
|
use egui::{Label, RichText, ScrollArea, TextStyle, TextWrapMode, Ui};
|
||||||
|
|
||||||
pub fn render(ui: &mut Ui, buf: &TextBuffer, display: DisplayOptions) {
|
pub fn render(ui: &mut Ui, buf: &TextBuffer, display: DisplayOptions) -> usize {
|
||||||
ScrollArea::vertical().stick_to_bottom(true).show(ui, |ui| {
|
let line_count = buf.len();
|
||||||
for line in buf.iter() {
|
let row_height = ui.text_style_height(&TextStyle::Monospace);
|
||||||
|
ScrollArea::vertical().stick_to_bottom(true).show_rows(
|
||||||
|
ui,
|
||||||
|
row_height,
|
||||||
|
line_count,
|
||||||
|
|ui, row_range| {
|
||||||
|
for row in row_range {
|
||||||
|
if let Some(line) = buf.get(row) {
|
||||||
|
ui.add(
|
||||||
|
Label::new(RichText::new(format_console_line(line, display)).monospace())
|
||||||
|
.wrap_mode(TextWrapMode::Extend),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
line_count
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_console_line(line: &ConsoleLine, display: DisplayOptions) -> String {
|
||||||
let mut prefix = Vec::new();
|
let mut prefix = Vec::new();
|
||||||
if display.show_timestamp {
|
if display.show_timestamp {
|
||||||
let ts = line.elapsed.as_secs_f64();
|
let ts = line.elapsed.as_secs_f64();
|
||||||
@@ -30,7 +49,5 @@ pub fn render(ui: &mut Ui, buf: &TextBuffer, display: DisplayOptions) {
|
|||||||
} else {
|
} else {
|
||||||
format!("{} ", prefix.join(" "))
|
format!("{} ", prefix.join(" "))
|
||||||
};
|
};
|
||||||
ui.monospace(format!("{prefix}{}", line.text));
|
format!("{prefix}{}", line.text)
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,33 @@
|
|||||||
use crate::app::DisplayOptions;
|
use crate::app::DisplayOptions;
|
||||||
use crate::buffers::{HexBuffer, LineDirection};
|
use crate::buffers::{HexBuffer, HexLine, LineDirection};
|
||||||
use egui::{ScrollArea, Ui};
|
use egui::{Label, RichText, ScrollArea, TextStyle, TextWrapMode, Ui};
|
||||||
|
|
||||||
pub fn render(ui: &mut Ui, buf: &HexBuffer, display: DisplayOptions) {
|
pub fn render(ui: &mut Ui, buf: &HexBuffer, display: DisplayOptions) -> usize {
|
||||||
let mut lines = buf.iter().peekable();
|
let line_count = buf.len();
|
||||||
if lines.peek().is_none() {
|
if line_count == 0 {
|
||||||
ui.label("no hex data");
|
ui.label("no hex data");
|
||||||
return;
|
return 0;
|
||||||
}
|
}
|
||||||
ScrollArea::vertical().stick_to_bottom(true).show(ui, |ui| {
|
let row_height = ui.text_style_height(&TextStyle::Monospace);
|
||||||
for line in lines {
|
ScrollArea::vertical().stick_to_bottom(true).show_rows(
|
||||||
|
ui,
|
||||||
|
row_height,
|
||||||
|
line_count,
|
||||||
|
|ui, row_range| {
|
||||||
|
for row in row_range {
|
||||||
|
if let Some(line) = buf.get(row) {
|
||||||
|
ui.add(
|
||||||
|
Label::new(RichText::new(format_hex_line(line, display)).monospace())
|
||||||
|
.wrap_mode(TextWrapMode::Extend),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
line_count
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_hex_line(line: &HexLine, display: DisplayOptions) -> String {
|
||||||
let mut prefix = Vec::new();
|
let mut prefix = Vec::new();
|
||||||
if display.show_timestamp {
|
if display.show_timestamp {
|
||||||
let ts = line.elapsed.as_secs_f64();
|
let ts = line.elapsed.as_secs_f64();
|
||||||
@@ -35,7 +53,5 @@ pub fn render(ui: &mut Ui, buf: &HexBuffer, display: DisplayOptions) {
|
|||||||
} else {
|
} else {
|
||||||
format!("{} ", prefix.join(" "))
|
format!("{} ", prefix.join(" "))
|
||||||
};
|
};
|
||||||
ui.monospace(format!("{prefix}{} |{}|", line.hex, line.ascii));
|
format!("{prefix}{} |{}|", line.hex, line.ascii)
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
use crate::buffers::{PlotBuffer, PlotSeriesKind};
|
use crate::buffers::{PlotBuffer, PlotSeriesKind};
|
||||||
|
use crate::perf::PlotRenderStats;
|
||||||
use egui::{Button, Ui, vec2};
|
use egui::{Button, Ui, vec2};
|
||||||
use egui_plot::{Legend, Line, Plot, PlotBounds, PlotPoints};
|
use egui_plot::{Legend, Line, Plot, PlotBounds, PlotPoints};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum PlotDisplayMode {
|
||||||
|
Embedded,
|
||||||
|
Detached,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PlotViewState {
|
pub struct PlotViewState {
|
||||||
pub lock_x: bool,
|
pub lock_x: bool,
|
||||||
pub lock_y: bool,
|
pub lock_y: bool,
|
||||||
pub box_zoom: bool,
|
pub box_zoom: bool,
|
||||||
pub follow_latest: bool,
|
pub follow_latest: bool,
|
||||||
pub last_bounds: Option<PlotBounds>,
|
pub last_bounds: Option<PlotBounds>,
|
||||||
|
pub detached: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PlotViewState {
|
impl Default for PlotViewState {
|
||||||
@@ -18,14 +26,48 @@ impl Default for PlotViewState {
|
|||||||
box_zoom: false,
|
box_zoom: false,
|
||||||
follow_latest: false,
|
follow_latest: false,
|
||||||
last_bounds: None,
|
last_bounds: None,
|
||||||
|
detached: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(ui: &mut Ui, buf: &PlotBuffer, session_id: u64, state: &mut PlotViewState) {
|
pub struct PlotRenderOutput {
|
||||||
|
pub stats: PlotRenderStats,
|
||||||
|
pub toggle_detached: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(
|
||||||
|
ui: &mut Ui,
|
||||||
|
buf: &mut PlotBuffer,
|
||||||
|
session_id: u64,
|
||||||
|
state: &mut PlotViewState,
|
||||||
|
) -> PlotRenderOutput {
|
||||||
|
let mode = if state.detached {
|
||||||
|
PlotDisplayMode::Detached
|
||||||
|
} else {
|
||||||
|
PlotDisplayMode::Embedded
|
||||||
|
};
|
||||||
|
let mut toggle_detached = false;
|
||||||
|
|
||||||
if buf.is_empty() {
|
if buf.is_empty() {
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
let toggle_label = match mode {
|
||||||
|
PlotDisplayMode::Embedded => "Pop Out",
|
||||||
|
PlotDisplayMode::Detached => "Dock Back",
|
||||||
|
};
|
||||||
|
if ui.button(toggle_label).clicked() {
|
||||||
|
toggle_detached = true;
|
||||||
|
}
|
||||||
ui.label("no plot data");
|
ui.label("no plot data");
|
||||||
return;
|
});
|
||||||
|
return PlotRenderOutput {
|
||||||
|
stats: PlotRenderStats {
|
||||||
|
series_count: 0,
|
||||||
|
stored_points: 0,
|
||||||
|
rendered_points: 0,
|
||||||
|
},
|
||||||
|
toggle_detached,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut auto_fit = false;
|
let mut auto_fit = false;
|
||||||
@@ -40,6 +82,13 @@ pub fn render(ui: &mut Ui, buf: &PlotBuffer, session_id: u64, state: &mut PlotVi
|
|||||||
if ui.button("Reset View").clicked() {
|
if ui.button("Reset View").clicked() {
|
||||||
reset_view = true;
|
reset_view = true;
|
||||||
}
|
}
|
||||||
|
let toggle_label = match mode {
|
||||||
|
PlotDisplayMode::Embedded => "Pop Out",
|
||||||
|
PlotDisplayMode::Detached => "Dock Back",
|
||||||
|
};
|
||||||
|
if ui.button(toggle_label).clicked() {
|
||||||
|
toggle_detached = true;
|
||||||
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.checkbox(&mut state.lock_x, "Lock X");
|
ui.checkbox(&mut state.lock_x, "Lock X");
|
||||||
ui.checkbox(&mut state.lock_y, "Lock Y");
|
ui.checkbox(&mut state.lock_y, "Lock Y");
|
||||||
@@ -92,7 +141,7 @@ pub fn render(ui: &mut Ui, buf: &PlotBuffer, session_id: u64, state: &mut PlotVi
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let data_bounds = plot_bounds(buf);
|
let data_bounds = buf.plot_bounds();
|
||||||
let mut plot = Plot::new(format!("plot_view_{session_id}"))
|
let mut plot = Plot::new(format!("plot_view_{session_id}"))
|
||||||
.legend(Legend::default())
|
.legend(Legend::default())
|
||||||
.allow_boxed_zoom(state.box_zoom)
|
.allow_boxed_zoom(state.box_zoom)
|
||||||
@@ -107,6 +156,7 @@ pub fn render(ui: &mut Ui, buf: &PlotBuffer, session_id: u64, state: &mut PlotVi
|
|||||||
plot = plot.reset();
|
plot = plot.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut rendered_points = 0usize;
|
||||||
plot.show(ui, |plot_ui| {
|
plot.show(ui, |plot_ui| {
|
||||||
let current_bounds = plot_ui.plot_bounds();
|
let current_bounds = plot_ui.plot_bounds();
|
||||||
state.last_bounds = current_bounds.is_finite().then_some(current_bounds);
|
state.last_bounds = current_bounds.is_finite().then_some(current_bounds);
|
||||||
@@ -123,7 +173,10 @@ pub fn render(ui: &mut Ui, buf: &PlotBuffer, session_id: u64, state: &mut PlotVi
|
|||||||
if let Some(bounds) = data_bounds {
|
if let Some(bounds) = data_bounds {
|
||||||
let current_width = current_bounds.width();
|
let current_width = current_bounds.width();
|
||||||
let data_width = bounds.width();
|
let data_width = bounds.width();
|
||||||
let x_range = if current_width.is_finite() && current_width > 0.0 && data_width > current_width {
|
let x_range = if current_width.is_finite()
|
||||||
|
&& current_width > 0.0
|
||||||
|
&& data_width > current_width
|
||||||
|
{
|
||||||
(bounds.max()[0] - current_width)..=bounds.max()[0]
|
(bounds.max()[0] - current_width)..=bounds.max()[0]
|
||||||
} else {
|
} else {
|
||||||
bounds.min()[0]..=bounds.max()[0]
|
bounds.min()[0]..=bounds.max()[0]
|
||||||
@@ -132,7 +185,10 @@ pub fn render(ui: &mut Ui, buf: &PlotBuffer, session_id: u64, state: &mut PlotVi
|
|||||||
plot_ui.set_plot_bounds_x(x_range.clone());
|
plot_ui.set_plot_bounds_x(x_range.clone());
|
||||||
|
|
||||||
let mut followed = current_bounds;
|
let mut followed = current_bounds;
|
||||||
followed.set_x(&PlotBounds::from_min_max([*x_range.start(), current_bounds.min()[1]], [*x_range.end(), current_bounds.max()[1]]));
|
followed.set_x(&PlotBounds::from_min_max(
|
||||||
|
[*x_range.start(), current_bounds.min()[1]],
|
||||||
|
[*x_range.end(), current_bounds.max()[1]],
|
||||||
|
));
|
||||||
state.last_bounds = Some(followed);
|
state.last_bounds = Some(followed);
|
||||||
}
|
}
|
||||||
} else if zoom_x != 1.0 || zoom_y != 1.0 {
|
} else if zoom_x != 1.0 || zoom_y != 1.0 {
|
||||||
@@ -151,49 +207,25 @@ pub fn render(ui: &mut Ui, buf: &PlotBuffer, session_id: u64, state: &mut PlotVi
|
|||||||
|
|
||||||
for series in buf.iter() {
|
for series in buf.iter() {
|
||||||
let sampled_points = match buf.kind() {
|
let sampled_points = match buf.kind() {
|
||||||
PlotSeriesKind::TimeSeries => {
|
PlotSeriesKind::TimeSeries => series.render_points_time_series(
|
||||||
series.render_points_time_series(
|
|
||||||
current_bounds.min()[0],
|
current_bounds.min()[0],
|
||||||
current_bounds.max()[0],
|
current_bounds.max()[0],
|
||||||
max_render_points,
|
max_render_points,
|
||||||
)
|
),
|
||||||
}
|
|
||||||
PlotSeriesKind::XY => series.render_points_xy(max_render_points),
|
PlotSeriesKind::XY => series.render_points_xy(max_render_points),
|
||||||
};
|
};
|
||||||
|
rendered_points += sampled_points.len();
|
||||||
let points = PlotPoints::from_iter(sampled_points);
|
let points = PlotPoints::from_iter(sampled_points);
|
||||||
plot_ui.line(Line::new(series.name.clone(), points));
|
plot_ui.line(Line::new(series.name.clone(), points));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
fn plot_bounds(buf: &PlotBuffer) -> Option<PlotBounds> {
|
PlotRenderOutput {
|
||||||
let mut bounds = PlotBounds::NOTHING;
|
stats: PlotRenderStats {
|
||||||
|
series_count: buf.series_len(),
|
||||||
for series in buf.iter() {
|
stored_points: buf.total_points(),
|
||||||
for [x, y] in series.points() {
|
rendered_points,
|
||||||
if x.is_finite() && y.is_finite() {
|
},
|
||||||
bounds.extend_with(&[x, y].into());
|
toggle_detached,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if !bounds.is_finite() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if bounds.width() <= 0.0 {
|
|
||||||
let center_x = bounds.center().x;
|
|
||||||
bounds.set_x_center_width(center_x, 1.0);
|
|
||||||
} else {
|
|
||||||
bounds.expand_x(bounds.width() * 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
if bounds.height() <= 0.0 {
|
|
||||||
let center_y = bounds.center().y;
|
|
||||||
bounds.set_y_center_height(center_y, 1.0);
|
|
||||||
} else {
|
|
||||||
bounds.expand_y(bounds.height() * 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(bounds)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub fn render(
|
|||||||
sessions: &[SessionListItem],
|
sessions: &[SessionListItem],
|
||||||
active: &mut usize,
|
active: &mut usize,
|
||||||
on_new: &mut bool,
|
on_new: &mut bool,
|
||||||
|
on_edit: &mut Option<usize>,
|
||||||
on_delete: &mut Option<usize>,
|
on_delete: &mut Option<usize>,
|
||||||
) {
|
) {
|
||||||
ui.heading("Sessions");
|
ui.heading("Sessions");
|
||||||
@@ -37,12 +38,23 @@ pub fn render(
|
|||||||
"{} Session {}\n{}",
|
"{} Session {}\n{}",
|
||||||
status_tag, session.id, session.transport_summary
|
status_tag, session.id, session.transport_summary
|
||||||
);
|
);
|
||||||
|
ui.horizontal(|ui| {
|
||||||
if ui
|
if ui
|
||||||
.selectable_label(*active == i, RichText::new(label).color(color))
|
.selectable_label(*active == i, RichText::new(label).color(color))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
*active = i;
|
*active = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.small_button("⚙")
|
||||||
|
.on_hover_text("Edit session config")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
*active = i;
|
||||||
|
*on_edit = Some(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|||||||
257
crates/xserial-gui/src/perf.rs
Normal file
257
crates/xserial-gui/src/perf.rs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct RepaintCounter {
|
||||||
|
inner: Arc<AtomicU64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RepaintCounter {
|
||||||
|
pub fn increment(&self) {
|
||||||
|
self.inner.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take(&self) -> u64 {
|
||||||
|
self.inner.swap(0, Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct DurationStats {
|
||||||
|
count: u64,
|
||||||
|
total: Duration,
|
||||||
|
max: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DurationStats {
|
||||||
|
fn record(&mut self, elapsed: Duration) {
|
||||||
|
self.count += 1;
|
||||||
|
self.total += elapsed;
|
||||||
|
self.max = self.max.max(elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn avg_ms(&self) -> f64 {
|
||||||
|
if self.count == 0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
(self.total.as_secs_f64() * 1000.0) / self.count as f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_ms(&self) -> f64 {
|
||||||
|
self.max.as_secs_f64() * 1000.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct IntervalStats {
|
||||||
|
frames: DurationStats,
|
||||||
|
drains: DurationStats,
|
||||||
|
text_renders: DurationStats,
|
||||||
|
hex_renders: DurationStats,
|
||||||
|
plot_renders: DurationStats,
|
||||||
|
drained_events: u64,
|
||||||
|
drained_data_events: u64,
|
||||||
|
drained_text_events: u64,
|
||||||
|
drained_hex_events: u64,
|
||||||
|
drained_plot_events: u64,
|
||||||
|
max_pending_events: usize,
|
||||||
|
max_text_lines: usize,
|
||||||
|
max_hex_lines: usize,
|
||||||
|
max_plot_series: usize,
|
||||||
|
max_plot_stored_points: usize,
|
||||||
|
max_plot_rendered_points: usize,
|
||||||
|
tabs: usize,
|
||||||
|
active_view: &'static str,
|
||||||
|
active_text_lines: usize,
|
||||||
|
active_hex_lines: usize,
|
||||||
|
active_plot_series: usize,
|
||||||
|
active_plot_points: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DrainStats {
|
||||||
|
pub drained_events: u64,
|
||||||
|
pub drained_data_events: u64,
|
||||||
|
pub drained_text_events: u64,
|
||||||
|
pub drained_hex_events: u64,
|
||||||
|
pub drained_plot_events: u64,
|
||||||
|
pub pending_events: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct GuiSnapshot {
|
||||||
|
pub tabs: usize,
|
||||||
|
pub active_view: &'static str,
|
||||||
|
pub active_text_lines: usize,
|
||||||
|
pub active_hex_lines: usize,
|
||||||
|
pub active_plot_series: usize,
|
||||||
|
pub active_plot_points: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PlotRenderStats {
|
||||||
|
pub series_count: usize,
|
||||||
|
pub stored_points: usize,
|
||||||
|
pub rendered_points: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GuiProfiler {
|
||||||
|
enabled: bool,
|
||||||
|
interval: Duration,
|
||||||
|
last_report_at: Instant,
|
||||||
|
repaint_counter: RepaintCounter,
|
||||||
|
interval_stats: IntervalStats,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GuiProfiler {
|
||||||
|
pub fn from_env() -> Self {
|
||||||
|
let enabled = env_flag("XSERIAL_GUI_PROFILE");
|
||||||
|
let interval_ms = env::var("XSERIAL_GUI_PROFILE_INTERVAL_MS")
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.parse::<u64>().ok())
|
||||||
|
.filter(|value| *value > 0)
|
||||||
|
.unwrap_or(1_000);
|
||||||
|
Self {
|
||||||
|
enabled,
|
||||||
|
interval: Duration::from_millis(interval_ms),
|
||||||
|
last_report_at: Instant::now(),
|
||||||
|
repaint_counter: RepaintCounter::default(),
|
||||||
|
interval_stats: IntervalStats {
|
||||||
|
active_view: "none",
|
||||||
|
..IntervalStats::default()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn repaint_counter(&self) -> RepaintCounter {
|
||||||
|
self.repaint_counter.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_drain(&mut self, elapsed: Duration, stats: DrainStats) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.interval_stats.drains.record(elapsed);
|
||||||
|
self.interval_stats.drained_events += stats.drained_events;
|
||||||
|
self.interval_stats.drained_data_events += stats.drained_data_events;
|
||||||
|
self.interval_stats.drained_text_events += stats.drained_text_events;
|
||||||
|
self.interval_stats.drained_hex_events += stats.drained_hex_events;
|
||||||
|
self.interval_stats.drained_plot_events += stats.drained_plot_events;
|
||||||
|
self.interval_stats.max_pending_events = self
|
||||||
|
.interval_stats
|
||||||
|
.max_pending_events
|
||||||
|
.max(stats.pending_events);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_text_render(&mut self, elapsed: Duration, line_count: usize) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.interval_stats.text_renders.record(elapsed);
|
||||||
|
self.interval_stats.max_text_lines = self.interval_stats.max_text_lines.max(line_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_hex_render(&mut self, elapsed: Duration, line_count: usize) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.interval_stats.hex_renders.record(elapsed);
|
||||||
|
self.interval_stats.max_hex_lines = self.interval_stats.max_hex_lines.max(line_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_plot_render(&mut self, elapsed: Duration, stats: PlotRenderStats) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.interval_stats.plot_renders.record(elapsed);
|
||||||
|
self.interval_stats.max_plot_series =
|
||||||
|
self.interval_stats.max_plot_series.max(stats.series_count);
|
||||||
|
self.interval_stats.max_plot_stored_points = self
|
||||||
|
.interval_stats
|
||||||
|
.max_plot_stored_points
|
||||||
|
.max(stats.stored_points);
|
||||||
|
self.interval_stats.max_plot_rendered_points = self
|
||||||
|
.interval_stats
|
||||||
|
.max_plot_rendered_points
|
||||||
|
.max(stats.rendered_points);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_frame(&mut self, elapsed: Duration, snapshot: GuiSnapshot) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.interval_stats.frames.record(elapsed);
|
||||||
|
self.interval_stats.tabs = snapshot.tabs;
|
||||||
|
self.interval_stats.active_view = snapshot.active_view;
|
||||||
|
self.interval_stats.active_text_lines = snapshot.active_text_lines;
|
||||||
|
self.interval_stats.active_hex_lines = snapshot.active_hex_lines;
|
||||||
|
self.interval_stats.active_plot_series = snapshot.active_plot_series;
|
||||||
|
self.interval_stats.active_plot_points = snapshot.active_plot_points;
|
||||||
|
|
||||||
|
if self.last_report_at.elapsed() < self.interval {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let repaints = self.repaint_counter.take();
|
||||||
|
info!(
|
||||||
|
target: "xserial_gui::perf",
|
||||||
|
frames = self.interval_stats.frames.count,
|
||||||
|
frame_avg_ms = self.interval_stats.frames.avg_ms(),
|
||||||
|
frame_max_ms = self.interval_stats.frames.max_ms(),
|
||||||
|
drains = self.interval_stats.drains.count,
|
||||||
|
drain_avg_ms = self.interval_stats.drains.avg_ms(),
|
||||||
|
drain_max_ms = self.interval_stats.drains.max_ms(),
|
||||||
|
drained_events = self.interval_stats.drained_events,
|
||||||
|
drained_data_events = self.interval_stats.drained_data_events,
|
||||||
|
drained_text_events = self.interval_stats.drained_text_events,
|
||||||
|
drained_hex_events = self.interval_stats.drained_hex_events,
|
||||||
|
drained_plot_events = self.interval_stats.drained_plot_events,
|
||||||
|
repaint_requests = repaints,
|
||||||
|
max_pending_events = self.interval_stats.max_pending_events,
|
||||||
|
text_renders = self.interval_stats.text_renders.count,
|
||||||
|
text_avg_ms = self.interval_stats.text_renders.avg_ms(),
|
||||||
|
text_max_ms = self.interval_stats.text_renders.max_ms(),
|
||||||
|
max_text_lines = self.interval_stats.max_text_lines,
|
||||||
|
hex_renders = self.interval_stats.hex_renders.count,
|
||||||
|
hex_avg_ms = self.interval_stats.hex_renders.avg_ms(),
|
||||||
|
hex_max_ms = self.interval_stats.hex_renders.max_ms(),
|
||||||
|
max_hex_lines = self.interval_stats.max_hex_lines,
|
||||||
|
plot_renders = self.interval_stats.plot_renders.count,
|
||||||
|
plot_avg_ms = self.interval_stats.plot_renders.avg_ms(),
|
||||||
|
plot_max_ms = self.interval_stats.plot_renders.max_ms(),
|
||||||
|
max_plot_series = self.interval_stats.max_plot_series,
|
||||||
|
max_plot_stored_points = self.interval_stats.max_plot_stored_points,
|
||||||
|
max_plot_rendered_points = self.interval_stats.max_plot_rendered_points,
|
||||||
|
tabs = self.interval_stats.tabs,
|
||||||
|
active_view = self.interval_stats.active_view,
|
||||||
|
active_text_lines = self.interval_stats.active_text_lines,
|
||||||
|
active_hex_lines = self.interval_stats.active_hex_lines,
|
||||||
|
active_plot_series = self.interval_stats.active_plot_series,
|
||||||
|
active_plot_points = self.interval_stats.active_plot_points,
|
||||||
|
"gui perf"
|
||||||
|
);
|
||||||
|
|
||||||
|
self.last_report_at = Instant::now();
|
||||||
|
self.interval_stats = IntervalStats {
|
||||||
|
active_view: "none",
|
||||||
|
..IntervalStats::default()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_flag(name: &str) -> bool {
|
||||||
|
env::var(name)
|
||||||
|
.map(|value| {
|
||||||
|
let value = value.trim().to_ascii_lowercase();
|
||||||
|
!value.is_empty() && value != "0" && value != "false" && value != "off"
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
@@ -258,7 +258,7 @@ def main() -> None:
|
|||||||
for index, value in enumerate(values)
|
for index, value in enumerate(values)
|
||||||
)
|
)
|
||||||
line = (
|
line = (
|
||||||
f"time={now - started_at:7.3f}s "
|
f"测试 time={now - started_at:7.3f}s "
|
||||||
f"sample={sample_index:7d} {joined}\n"
|
f"sample={sample_index:7d} {joined}\n"
|
||||||
)
|
)
|
||||||
conn.sendall(line.encode("utf-8"))
|
conn.sendall(line.encode("utf-8"))
|
||||||
|
|||||||
Reference in New Issue
Block a user