notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 16b20568da95ad37847525569a398dbea8201a89
parent e1187c372f02d0c3c8303ba89264295df381a8f2
Author: William Casarin <jb55@jb55.com>
Date:   Sat,  4 Jan 2025 13:54:29 -0800

Merge relay debug view

Fix a few conflicts

Diffstat:
Mcrates/enostr/src/client/message.rs | 28+++++++++++++++-------------
Mcrates/enostr/src/lib.rs | 1+
Mcrates/enostr/src/relay/message.rs | 6++++++
Mcrates/enostr/src/relay/mod.rs | 3++-
Mcrates/enostr/src/relay/pool.rs | 40++++++++++++++++++++++++++++++++++++----
Acrates/enostr/src/relay/subs_debug.rs | 267+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/notedeck/src/args.rs | 4++++
Mcrates/notedeck_chrome/src/app.rs | 11+++++++++++
Mcrates/notedeck_columns/src/ui/mod.rs | 1+
Mcrates/notedeck_columns/src/ui/note/post.rs | 2+-
Mcrates/notedeck_columns/src/ui/profile/edit.rs | 2+-
Acrates/notedeck_columns/src/ui/relay_debug.rs | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 518 insertions(+), 20 deletions(-)

diff --git a/crates/enostr/src/client/message.rs b/crates/enostr/src/client/message.rs @@ -2,21 +2,21 @@ use crate::Error; use nostrdb::{Filter, Note}; use serde_json::json; -#[derive(Debug)] -pub struct EventClientMessage<'a> { - note: Note<'a>, +#[derive(Debug, Clone)] +pub struct EventClientMessage { + pub note_json: String, } -impl EventClientMessage<'_> { - pub fn to_json(&self) -> Result<String, Error> { - Ok(format!("[\"EVENT\", {}]", self.note.json()?)) +impl EventClientMessage { + pub fn to_json(&self) -> String { + format!("[\"EVENT\", {}]", self.note_json) } } /// Messages sent by clients, received by relays -#[derive(Debug)] -pub enum ClientMessage<'a> { - Event(EventClientMessage<'a>), +#[derive(Debug, Clone)] +pub enum ClientMessage { + Event(EventClientMessage), Req { sub_id: String, filters: Vec<Filter>, @@ -27,9 +27,11 @@ pub enum ClientMessage<'a> { Raw(String), } -impl<'a> ClientMessage<'a> { - pub fn event(note: Note<'a>) -> Self { - ClientMessage::Event(EventClientMessage { note }) +impl ClientMessage { + pub fn event(note: Note) -> Result<Self, Error> { + Ok(ClientMessage::Event(EventClientMessage { + note_json: note.json()?, + })) } pub fn raw(raw: String) -> Self { @@ -46,7 +48,7 @@ impl<'a> ClientMessage<'a> { pub fn to_json(&self) -> Result<String, Error> { Ok(match self { - Self::Event(ecm) => ecm.to_json()?, + Self::Event(ecm) => ecm.to_json(), Self::Raw(raw) => raw.clone(), Self::Req { sub_id, filters } => { if filters.is_empty() { diff --git a/crates/enostr/src/lib.rs b/crates/enostr/src/lib.rs @@ -18,6 +18,7 @@ pub use profile::Profile; pub use pubkey::Pubkey; pub use relay::message::{RelayEvent, RelayMessage}; pub use relay::pool::{PoolEvent, PoolRelay, RelayPool}; +pub use relay::subs_debug::{OwnedRelayEvent, RelayLogEvent, SubsDebug, TransferStats}; pub use relay::{Relay, RelayStatus}; pub type Result<T> = std::result::Result<T, error::Error>; diff --git a/crates/enostr/src/relay/message.rs b/crates/enostr/src/relay/message.rs @@ -8,6 +8,12 @@ pub struct CommandResult<'a> { message: &'a str, } +pub fn calculate_command_result_size(result: &CommandResult) -> usize { + std::mem::size_of_val(result) + + result.event_id.as_bytes().len() + + result.message.as_bytes().len() +} + #[derive(Debug, Eq, PartialEq)] pub enum RelayMessage<'a> { OK(CommandResult<'a>), diff --git a/crates/enostr/src/relay/mod.rs b/crates/enostr/src/relay/mod.rs @@ -13,6 +13,7 @@ use tracing::{debug, error}; pub mod message; pub mod pool; +pub mod subs_debug; #[derive(Debug, Copy, Clone)] pub enum RelayStatus { @@ -91,7 +92,7 @@ impl MulticastRelay { } pub fn send(&self, msg: &EventClientMessage) -> Result<()> { - let json = msg.to_json()?; + let json = msg.to_json(); let len = json.len(); debug!("writing to multicast relay"); diff --git a/crates/enostr/src/relay/pool.rs b/crates/enostr/src/relay/pool.rs @@ -13,6 +13,8 @@ use ewebsock::{WsEvent, WsMessage}; #[cfg(not(target_arch = "wasm32"))] use tracing::{debug, error}; +use super::subs_debug::SubsDebug; + #[derive(Debug)] pub struct PoolEvent<'a> { pub relay: &'a str, @@ -124,6 +126,7 @@ impl WebsocketRelay { pub struct RelayPool { pub relays: Vec<PoolRelay>, pub ping_rate: Duration, + pub debug: Option<SubsDebug>, } impl Default for RelayPool { @@ -138,6 +141,7 @@ impl RelayPool { RelayPool { relays: vec![], ping_rate: Duration::from_secs(25), + debug: None, } } @@ -150,6 +154,10 @@ impl RelayPool { Ok(()) } + pub fn use_debug(&mut self) { + self.debug = Some(SubsDebug::default()); + } + pub fn ping_rate(&mut self, duration: Duration) -> &mut Self { self.ping_rate = duration; self @@ -174,6 +182,9 @@ impl RelayPool { pub fn send(&mut self, cmd: &ClientMessage) { for relay in &mut self.relays { + if let Some(debug) = &mut self.debug { + debug.send_cmd(relay.url().to_owned(), cmd); + } if let Err(err) = relay.send(cmd) { error!("error sending {:?} to {}: {err}", cmd, relay.url()); } @@ -182,7 +193,11 @@ impl RelayPool { pub fn unsubscribe(&mut self, subid: String) { for relay in &mut self.relays { - if let Err(err) = relay.send(&ClientMessage::close(subid.clone())) { + let cmd = ClientMessage::close(subid.clone()); + if let Some(debug) = &mut self.debug { + debug.send_cmd(relay.url().to_owned(), &cmd); + } + if let Err(err) = relay.send(&cmd) { error!( "error unsubscribing from {} on {}: {err}", &subid, @@ -194,6 +209,13 @@ impl RelayPool { pub fn subscribe(&mut self, subid: String, filter: Vec<Filter>) { for relay in &mut self.relays { + if let Some(debug) = &mut self.debug { + debug.send_cmd( + relay.url().to_owned(), + &ClientMessage::req(subid.clone(), filter.clone()), + ); + } + if let Err(err) = relay.send(&ClientMessage::req(subid.clone(), filter.clone())) { error!("error subscribing to {}: {err}", relay.url()); } @@ -255,8 +277,11 @@ impl RelayPool { pub fn send_to(&mut self, cmd: &ClientMessage, relay_url: &str) { for relay in &mut self.relays { if relay.url() == relay_url { + if let Some(debug) = &mut self.debug { + debug.send_cmd(relay.url().to_owned(), cmd); + } if let Err(err) = relay.send(cmd) { - error!("error sending {:?} to {}: {err}", cmd, relay_url); + error!("send_to err: {err}"); } return; } @@ -350,10 +375,17 @@ impl RelayPool { } } } - return Some(PoolEvent { + + if let Some(debug) = &mut self.debug { + debug.receive_cmd(relay.url().to_owned(), (&event).into()); + } + + let pool_event = PoolEvent { event, relay: relay.url(), - }); + }; + + return Some(pool_event); } } diff --git a/crates/enostr/src/relay/subs_debug.rs b/crates/enostr/src/relay/subs_debug.rs @@ -0,0 +1,267 @@ +use std::{collections::HashMap, mem, time::SystemTime}; + +use ewebsock::WsMessage; +use nostrdb::Filter; + +use crate::{ClientMessage, Error, RelayEvent, RelayMessage}; + +use super::message::calculate_command_result_size; + +type RelayId = String; +type SubId = String; + +pub struct SubsDebug { + data: HashMap<RelayId, RelayStats>, + time_incd: SystemTime, + pub relay_events_selection: Option<RelayId>, +} + +#[derive(Default)] +pub struct RelayStats { + pub count: TransferStats, + pub events: Vec<RelayLogEvent>, + pub sub_data: HashMap<SubId, SubStats>, +} + +#[derive(Clone)] +pub enum RelayLogEvent { + Send(ClientMessage), + Recieve(OwnedRelayEvent), +} + +#[derive(Clone)] +pub enum OwnedRelayEvent { + Opened, + Closed, + Other(String), + Error(String), + Message(String), +} + +impl From<RelayEvent<'_>> for OwnedRelayEvent { + fn from(value: RelayEvent<'_>) -> Self { + match value { + RelayEvent::Opened => OwnedRelayEvent::Opened, + RelayEvent::Closed => OwnedRelayEvent::Closed, + RelayEvent::Other(ws_message) => { + let ws_str = match ws_message { + WsMessage::Binary(_) => "Binary".to_owned(), + WsMessage::Text(t) => format!("Text:{}", t), + WsMessage::Unknown(u) => format!("Unknown:{}", u), + WsMessage::Ping(_) => "Ping".to_owned(), + WsMessage::Pong(_) => "Pong".to_owned(), + }; + OwnedRelayEvent::Other(ws_str) + } + RelayEvent::Error(error) => OwnedRelayEvent::Error(error.to_string()), + RelayEvent::Message(relay_message) => { + let relay_msg = match relay_message { + RelayMessage::OK(_) => "OK".to_owned(), + RelayMessage::Eose(s) => format!("EOSE:{}", s), + RelayMessage::Event(_, s) => format!("EVENT:{}", s), + RelayMessage::Notice(s) => format!("NOTICE:{}", s), + }; + OwnedRelayEvent::Message(relay_msg) + } + } + } +} + +#[derive(PartialEq, Eq, Hash, Clone)] +pub struct RelaySub { + pub(crate) subid: String, + pub(crate) filter: String, +} + +#[derive(Default)] +pub struct SubStats { + pub filter: String, + pub count: TransferStats, +} + +#[derive(Default)] +pub struct TransferStats { + pub up_total: usize, + pub down_total: usize, + + // 1 sec < last tick < 2 sec + pub up_sec_prior: usize, + pub down_sec_prior: usize, + + // < 1 sec since last tick + up_sec_cur: usize, + down_sec_cur: usize, +} + +impl Default for SubsDebug { + fn default() -> Self { + Self { + data: Default::default(), + time_incd: SystemTime::now(), + relay_events_selection: None, + } + } +} + +impl SubsDebug { + pub fn get_data(&self) -> &HashMap<RelayId, RelayStats> { + &self.data + } + + pub(crate) fn send_cmd(&mut self, relay: String, cmd: &ClientMessage) { + let data = self.data.entry(relay).or_default(); + let msg_num_bytes = calculate_client_message_size(cmd); + match cmd { + ClientMessage::Req { sub_id, filters } => { + data.sub_data.insert( + sub_id.to_string(), + SubStats { + filter: filters_to_string(filters), + count: Default::default(), + }, + ); + } + + ClientMessage::Close { sub_id } => { + data.sub_data.remove(sub_id); + } + + _ => {} + } + + data.count.up_sec_cur += msg_num_bytes; + + data.events.push(RelayLogEvent::Send(cmd.clone())); + } + + pub(crate) fn receive_cmd(&mut self, relay: String, cmd: RelayEvent) { + let data = self.data.entry(relay).or_default(); + let msg_num_bytes = calculate_relay_event_size(&cmd); + if let RelayEvent::Message(RelayMessage::Event(sid, _)) = cmd { + if let Some(sub_data) = data.sub_data.get_mut(sid) { + let c = &mut sub_data.count; + c.down_sec_cur += msg_num_bytes; + } + }; + + data.count.down_sec_cur += msg_num_bytes; + + data.events.push(RelayLogEvent::Recieve(cmd.into())); + } + + pub fn try_increment_stats(&mut self) { + let cur_time = SystemTime::now(); + if let Ok(dur) = cur_time.duration_since(self.time_incd) { + if dur.as_secs() >= 1 { + self.time_incd = cur_time; + self.internal_inc_stats(); + } + } + } + + fn internal_inc_stats(&mut self) { + for relay_data in self.data.values_mut() { + let c = &mut relay_data.count; + inc_data_count(c); + + for sub in relay_data.sub_data.values_mut() { + inc_data_count(&mut sub.count); + } + } + } +} + +fn inc_data_count(c: &mut TransferStats) { + c.up_total += c.up_sec_cur; + c.up_sec_prior = c.up_sec_cur; + + c.down_total += c.down_sec_cur; + c.down_sec_prior = c.down_sec_cur; + + c.up_sec_cur = 0; + c.down_sec_cur = 0; +} + +fn calculate_client_message_size(message: &ClientMessage) -> usize { + match message { + ClientMessage::Event(note) => note.note_json.len() + 10, // 10 is ["EVENT",] + ClientMessage::Req { sub_id, filters } => { + mem::size_of_val(message) + + mem::size_of_val(sub_id) + + sub_id.as_bytes().len() + + filters.iter().map(mem::size_of_val).sum::<usize>() + } + ClientMessage::Close { sub_id } => { + mem::size_of_val(message) + mem::size_of_val(sub_id) + sub_id.as_bytes().len() + } + ClientMessage::Raw(data) => mem::size_of_val(message) + data.as_bytes().len(), + } +} + +fn calculate_relay_event_size(event: &RelayEvent<'_>) -> usize { + let base_size = mem::size_of_val(event); // Size of the enum on the stack + + let variant_size = match event { + RelayEvent::Opened | RelayEvent::Closed => 0, // No additional data + RelayEvent::Other(ws_message) => calculate_ws_message_size(ws_message), + RelayEvent::Error(error) => calculate_error_size(error), + RelayEvent::Message(message) => calculate_relay_message_size(message), + }; + + base_size + variant_size +} + +fn calculate_ws_message_size(message: &WsMessage) -> usize { + match message { + WsMessage::Binary(vec) | WsMessage::Ping(vec) | WsMessage::Pong(vec) => { + mem::size_of_val(message) + vec.len() + } + WsMessage::Text(string) | WsMessage::Unknown(string) => { + mem::size_of_val(message) + string.as_bytes().len() + } + } +} + +fn calculate_error_size(error: &Error) -> usize { + match error { + Error::Empty + | Error::DecodeFailed + | Error::HexDecodeFailed + | Error::InvalidBech32 + | Error::InvalidByteSize + | Error::InvalidSignature + | Error::Io(_) + | Error::InvalidPublicKey => mem::size_of_val(error), // No heap usage + + Error::Json(json_err) => mem::size_of_val(error) + json_err.to_string().as_bytes().len(), + + Error::Nostrdb(nostrdb_err) => { + mem::size_of_val(error) + nostrdb_err.to_string().as_bytes().len() + } + + Error::Generic(string) => mem::size_of_val(error) + string.as_bytes().len(), + } +} + +fn calculate_relay_message_size(message: &RelayMessage) -> usize { + match message { + RelayMessage::OK(result) => calculate_command_result_size(result), + RelayMessage::Eose(str_ref) + | RelayMessage::Event(str_ref, _) + | RelayMessage::Notice(str_ref) => mem::size_of_val(message) + str_ref.as_bytes().len(), + } +} + +fn filters_to_string(f: &Vec<Filter>) -> String { + let mut cur_str = String::new(); + for filter in f { + if let Ok(json) = filter.json() { + if !cur_str.is_empty() { + cur_str.push_str(", "); + } + cur_str.push_str(&json); + } + } + + cur_str +} diff --git a/crates/notedeck/src/args.rs b/crates/notedeck/src/args.rs @@ -7,6 +7,7 @@ pub struct Args { pub keys: Vec<Keypair>, pub light: bool, pub debug: bool, + pub relay_debug: bool, /// Enable when running tests so we don't panic on app startup pub tests: bool, @@ -24,6 +25,7 @@ impl Args { keys: vec![], light: false, debug: false, + relay_debug: false, tests: false, use_keystore: true, dbpath: None, @@ -108,6 +110,8 @@ impl Args { res.relays.push(relay.clone()); } else if arg == "--no-keystore" { res.use_keystore = false; + } else if arg == "--relay-debug" { + res.relay_debug = true; } i += 1; diff --git a/crates/notedeck_chrome/src/app.rs b/crates/notedeck_chrome/src/app.rs @@ -7,6 +7,7 @@ use notedeck::{ use enostr::RelayPool; use nostrdb::{Config, Ndb, Transaction}; +use notedeck_columns::ui::relay_debug::RelayDebugView; use std::cell::RefCell; use std::path::Path; use std::rc::Rc; @@ -80,6 +81,16 @@ impl eframe::App for Notedeck { self.app_rect_handler.try_save_app_size(ctx); + if self.args.relay_debug { + if self.pool.debug.is_none() { + self.pool.use_debug(); + } + + if let Some(debug) = &mut self.pool.debug { + RelayDebugView::window(ctx, debug); + } + } + #[cfg(feature = "profiling")] puffin_egui::profiler_window(ctx); } diff --git a/crates/notedeck_columns/src/ui/mod.rs b/crates/notedeck_columns/src/ui/mod.rs @@ -10,6 +10,7 @@ pub mod note; pub mod preview; pub mod profile; pub mod relay; +pub mod relay_debug; pub mod side_panel; pub mod support; pub mod thread; diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs @@ -61,7 +61,7 @@ impl PostAction { } }; - pool.send(&enostr::ClientMessage::event(note)); + pool.send(&enostr::ClientMessage::event(note)?); drafts.get_from_post_type(&self.post_type).clear(); Ok(()) diff --git a/crates/notedeck_columns/src/ui/profile/edit.rs b/crates/notedeck_columns/src/ui/profile/edit.rs @@ -195,7 +195,7 @@ mod preview { } } - impl<'a> Preview for EditProfileView<'a> { + impl Preview for EditProfileView<'_> { type Prev = EditProfilePreivew; fn preview(_cfg: PreviewConfig) -> Self::Prev { diff --git a/crates/notedeck_columns/src/ui/relay_debug.rs b/crates/notedeck_columns/src/ui/relay_debug.rs @@ -0,0 +1,173 @@ +use egui::ScrollArea; +use enostr::{RelayLogEvent, SubsDebug}; + +pub struct RelayDebugView<'a> { + debug: &'a mut SubsDebug, +} + +impl<'a> RelayDebugView<'a> { + pub fn new(debug: &'a mut SubsDebug) -> Self { + Self { debug } + } +} + +impl RelayDebugView<'_> { + pub fn ui(&mut self, ui: &mut egui::Ui) { + ScrollArea::vertical() + .id_salt(ui.id().with("relays_debug")) + .max_height(ui.max_rect().height() / 2.0) + .show(ui, |ui| { + ui.label("Active Relays:"); + for (relay_str, data) in self.debug.get_data() { + egui::CollapsingHeader::new(format!( + "{} {} {}", + relay_str, + format_total(&data.count), + format_sec(&data.count) + )) + .default_open(true) + .show(ui, |ui| { + ui.horizontal_wrapped(|ui| { + for (i, sub_data) in data.sub_data.values().enumerate() { + ui.label(format!( + "Filter {} ({})", + i + 1, + format_sec(&sub_data.count) + )) + .on_hover_cursor(egui::CursorIcon::Help) + .on_hover_text(sub_data.filter.to_string()); + } + }) + }); + } + }); + + ui.separator(); + egui::ComboBox::from_label("Show events from relay") + .selected_text( + self.debug + .relay_events_selection + .as_ref() + .map_or(String::new(), |s| s.clone()), + ) + .show_ui(ui, |ui| { + let mut make_selection = None; + for relay in self.debug.get_data().keys() { + if ui + .selectable_label( + if let Some(s) = &self.debug.relay_events_selection { + *s == *relay + } else { + false + }, + relay, + ) + .clicked() + { + make_selection = Some(relay.clone()); + } + } + if make_selection.is_some() { + self.debug.relay_events_selection = make_selection + } + }); + let show_relay_evs = + |ui: &mut egui::Ui, relay: Option<String>, events: Vec<RelayLogEvent>| { + for ev in events { + ui.horizontal_wrapped(|ui| { + if let Some(r) = &relay { + ui.label("relay").on_hover_text(r.clone()); + } + match ev { + RelayLogEvent::Send(client_message) => { + ui.label("SEND: "); + let msg = &match client_message { + enostr::ClientMessage::Event { .. } => "Event", + enostr::ClientMessage::Req { .. } => "Req", + enostr::ClientMessage::Close { .. } => "Close", + enostr::ClientMessage::Raw(_) => "Raw", + }; + + if let Ok(json) = client_message.to_json() { + ui.label(*msg).on_hover_text(json) + } else { + ui.label(*msg) + } + } + RelayLogEvent::Recieve(e) => { + ui.label("RECIEVE: "); + match e { + enostr::OwnedRelayEvent::Opened => ui.label("Opened"), + enostr::OwnedRelayEvent::Closed => ui.label("Closed"), + enostr::OwnedRelayEvent::Other(s) => { + ui.label("Other").on_hover_text(s) + } + enostr::OwnedRelayEvent::Error(s) => { + ui.label("Error").on_hover_text(s) + } + enostr::OwnedRelayEvent::Message(s) => { + ui.label("Message").on_hover_text(s) + } + } + } + } + }); + } + }; + + ScrollArea::vertical() + .id_salt(ui.id().with("events")) + .show(ui, |ui| { + if let Some(relay) = &self.debug.relay_events_selection { + if let Some(data) = self.debug.get_data().get(relay) { + show_relay_evs(ui, None, data.events.clone()); + } + } else { + for (relay, data) in self.debug.get_data() { + show_relay_evs(ui, Some(relay.clone()), data.events.clone()); + } + } + }); + + self.debug.try_increment_stats(); + } + + pub fn window(ctx: &egui::Context, debug: &mut SubsDebug) { + let mut open = true; + egui::Window::new("Relay Debugger") + .open(&mut open) + .show(ctx, |ui| { + RelayDebugView::new(debug).ui(ui); + }); + } +} + +fn format_sec(c: &enostr::TransferStats) -> String { + format!( + "⬇{} ⬆️{}", + byte_to_string(c.down_sec_prior), + byte_to_string(c.up_sec_prior) + ) +} + +fn format_total(c: &enostr::TransferStats) -> String { + format!( + "total: ⬇{} ⬆️{}", + byte_to_string(c.down_total), + byte_to_string(c.up_total) + ) +} + +const MB: usize = 1_048_576; +const KB: usize = 1024; +fn byte_to_string(b: usize) -> String { + if b >= MB { + let mbs = b as f32 / MB as f32; + format!("{:.2} MB", mbs) + } else if b >= KB { + let kbs = b as f32 / KB as f32; + format!("{:.2} KB", kbs) + } else { + format!("{} B", b) + } +}