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:
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)
+ }
+}