Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(launchpad): read stats from metrics endpoint #1821

Merged
merged 8 commits into from
Jun 4, 2024
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions node-launchpad/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,12 @@ libc = "0.2.148"
log = "0.4.20"
nix = { version = "0.28.0", features = ["user"] }
pretty_assertions = "1.4.0"
prometheus-parse = "0.2.5"
rand = "0.8.5"
ratatui = { version = "0.26.0", features = ["serde", "macros", "unstable-widget-ref"] }
reqwest = { version = "0.12.2", default-features = false, features = [
"rustls-tls-manual-roots",
] }
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.107"
signal-hook = "0.3.17"
Expand Down
10 changes: 5 additions & 5 deletions node-launchpad/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
// KIND, either express or implied. Please review the Licences for the specific language governing
// permissions and limitations relating to use of the SAFE Network Software.

use crate::mode::{InputMode, Scene};
use crate::{
mode::{InputMode, Scene},
node_stats::NodeStats,
};
use serde::{Deserialize, Serialize};
use strum::Display;

Expand Down Expand Up @@ -41,10 +44,7 @@ pub enum HomeActions {
SuccessfullyDetectedNatStatus,
ErrorWhileRunningNatDetection,

NodesStatsObtained {
wallet_balance: u64,
space_used: u64,
},
NodesStatsObtained(NodeStats),

TriggerBetaProgramme,
TriggerManageNodes,
Expand Down
216 changes: 54 additions & 162 deletions node-launchpad/src/components/home.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,32 @@
// KIND, either express or implied. Please review the Licences for the specific language governing
// permissions and limitations relating to use of the SAFE Network Software.

use super::{
manage_nodes::{GB, MB},
utils::centered_rect_fixed,
Component, Frame,
};
use super::{manage_nodes::GB_PER_NODE, utils::centered_rect_fixed, Component, Frame};
use crate::{
action::{Action, HomeActions},
config::Config,
mode::{InputMode, Scene},
style::{clear_area, COOL_GREY, EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VERY_LIGHT_AZURE},
node_stats::NodeStats,
style::{
clear_area, COOL_GREY, EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VERY_LIGHT_AZURE,
VIVID_SKY_BLUE,
},
};
use color_eyre::eyre::{OptionExt, Result};
use fs_extra::dir::get_size;
use futures::StreamExt;
use rand::seq::SliceRandom;
use ratatui::{prelude::*, widgets::*};
use sn_node_manager::{config::get_node_registry_path, VerbosityLevel};
use sn_peers_acquisition::{get_bootstrap_peers_from_url, PeersArgs};
use sn_service_management::{
rpc::{RpcActions, RpcClient},
NodeRegistry, NodeServiceData, ServiceStatus,
};
use sn_service_management::{NodeRegistry, NodeServiceData, ServiceStatus};
use std::{
net::SocketAddr,
path::PathBuf,
time::{Duration, Instant},
vec,
};
use tokio::sync::mpsc::UnboundedSender;

const NODE_START_INTERVAL: usize = 10;
const NODE_STAT_UPDATE_INTERVAL: Duration = Duration::from_secs(15);
const NODE_STAT_UPDATE_INTERVAL: Duration = Duration::from_secs(5);
const NAT_DETECTION_SERVERS_LIST_URL: &str =
"https://sn-testnet.s3.eu-west-2.amazonaws.com/nat-detection-servers";
/// If nat detection fails for more than 3 times, we don't want to waste time running during every node start.
Expand All @@ -51,7 +46,8 @@ pub struct Home {
node_services: Vec<NodeServiceData>,
is_nat_status_determined: bool,
error_while_running_nat_detection: usize,
node_stats: NodesStats,
node_stats: NodeStats,
node_stats_last_update: Instant,
node_table_state: TableState,
nodes_to_start: usize,
discord_username: String,
Expand Down Expand Up @@ -85,7 +81,8 @@ impl Home {
node_services: Default::default(),
is_nat_status_determined: false,
error_while_running_nat_detection: 0,
node_stats: NodesStats::new(),
node_stats: NodeStats::default(),
node_stats_last_update: Instant::now(),
nodes_to_start: allocated_disk_space,
node_table_state: Default::default(),
lock_registry: None,
Expand All @@ -100,10 +97,10 @@ impl Home {
/// Tries to trigger the update of node stats if the last update was more than `NODE_STAT_UPDATE_INTERVAL` ago.
/// The result is sent via the HomeActions::NodesStatsObtained action.
fn try_update_node_stats(&mut self, force_update: bool) -> Result<()> {
if self.node_stats.last_update.elapsed() > NODE_STAT_UPDATE_INTERVAL || force_update {
self.node_stats.last_update = Instant::now();
if self.node_stats_last_update.elapsed() > NODE_STAT_UPDATE_INTERVAL || force_update {
self.node_stats_last_update = Instant::now();

NodesStats::fetch_all_node_stats(&self.node_services, self.get_actions_sender()?);
NodeStats::fetch_all_node_stats(&self.node_services, self.get_actions_sender()?);
}
Ok(())
}
Expand Down Expand Up @@ -282,12 +279,8 @@ impl Component for Home {
Action::Tick => {
self.try_update_node_stats(false)?;
}
Action::HomeActions(HomeActions::NodesStatsObtained {
wallet_balance,
space_used,
}) => {
self.node_stats.wallet_balance = wallet_balance;
self.node_stats.space_used = space_used;
Action::HomeActions(HomeActions::NodesStatsObtained(stats)) => {
self.node_stats = stats;
}
Action::HomeActions(HomeActions::StartNodesCompleted)
| Action::HomeActions(HomeActions::StopNodesCompleted) => {
Expand Down Expand Up @@ -407,54 +400,45 @@ impl Component for Home {
);
} else {
// display stats as a table
let (space_used_value, space_used_header) = {
// if space used within 1GB, display in mb
if self.node_stats.space_used as f64 / (MB as f64) < (MB as f64) {
(
format!("{:.2}", self.node_stats.space_used as f64 / MB as f64),
"Space Used (MB)".to_string(),
)
} else {
// else display in gb
(
format!("{:.2}", self.node_stats.space_used as f64 / GB as f64),
"Space Used (GB)".to_string(),
)
}

let storage_allocated_row = Row::new(vec![
Cell::new("Storage Allocated".to_string()).fg(GHOST_WHITE),
Cell::new(format!("{} GB", self.nodes_to_start * GB_PER_NODE)).fg(GHOST_WHITE),
]);
let memory_use_val = if self.node_stats.memory_usage_mb as f64 / 1024 as f64 > 1.0 {
format!(
"{:.2} GB",
self.node_stats.memory_usage_mb as f64 / 1024 as f64
)
} else {
format!("{} MB", self.node_stats.memory_usage_mb)
};
let stats_rows = vec![Row::new(vec![
self.node_stats.wallet_balance.to_string(),
space_used_value,
// self.node_stats.memory_usage.to_string(),
// self.node_stats.network_usage.to_string(),
])];
let stats_width = [
Constraint::Min(15),
Constraint::Min(10),
// Constraint::Min(10),
// Constraint::Min(10),

let memory_use_row = Row::new(vec![
Cell::new("Memory Use".to_string()).fg(GHOST_WHITE),
Cell::new(memory_use_val).fg(GHOST_WHITE),
]);
let total_nanos_earned_row = Row::new(vec![
Cell::new("Total Nanos Earned".to_string()).fg(VIVID_SKY_BLUE),
Cell::new(self.node_stats.forwarded_rewards.to_string())
.fg(VIVID_SKY_BLUE)
.bold(),
]);
let stats_rows = vec![
storage_allocated_row,
memory_use_row.bottom_margin(2),
total_nanos_earned_row,
];
let stats_table = Table::new(stats_rows, stats_width)
.column_spacing(2)
.header(
Row::new(vec![
"Wallet Balance",
space_used_header.as_str(),
// "Memory usage",
// "Network Usage",
])
.style(Style::new().bold().fg(GHOST_WHITE)),
)
.block(
Block::default()
.title("Device Status")
.title_style(Style::default().fg(GHOST_WHITE))
.borders(Borders::ALL)
.padding(Padding::uniform(1))
.style(Style::default().fg(VERY_LIGHT_AZURE)),
);
let stats_width = [Constraint::Max(25), Constraint::Min(5)];
let stats_table = Table::new(stats_rows, stats_width).block(
Block::default()
.title("Device Status")
.title_style(Style::default().fg(GHOST_WHITE))
.borders(Borders::ALL)
.padding(Padding::uniform(1))
.style(Style::default().fg(VERY_LIGHT_AZURE)),
);
f.render_widget(stats_table, layer_zero[1]);
// "todo: display a table".to_string()
};

// ==== Node Status =====
Expand Down Expand Up @@ -671,95 +655,3 @@ fn reset_nodes(action_sender: UnboundedSender<Action>) {
}
});
}

/// The stats of all the running nodes
/// todo: certain stats like wallet balance, space used can be calculated even if the node is offline.
struct NodesStats {
pub wallet_balance: u64,
pub space_used: u64,
// pub memory_usage: usize,
// pub network_usage: usize,
pub last_update: Instant,
}

impl NodesStats {
pub fn new() -> Self {
Self {
wallet_balance: 0,
space_used: 0,
// memory_usage: 0,
// network_usage: 0,
last_update: Instant::now(),
}
}

pub fn fetch_all_node_stats(nodes: &[NodeServiceData], action_sender: UnboundedSender<Action>) {
let node_details = nodes
.iter()
.filter_map(|node| {
if node.status == ServiceStatus::Running {
Some((
node.service_name.clone(),
node.rpc_socket_addr,
node.data_dir_path.clone(),
))
} else {
None
}
})
.collect::<Vec<_>>();

tokio::task::spawn_local(async move {
Self::fetch_all_node_stats_inner(node_details, action_sender).await;
});
}

async fn fetch_all_node_stats_inner(
node_details: Vec<(String, SocketAddr, PathBuf)>,
action_sender: UnboundedSender<Action>,
) {
let mut stream = futures::stream::iter(node_details)
.map(|(service_name, rpc_addr, data_dir)| async move {
(
Self::fetch_stat_per_node(rpc_addr, data_dir).await,
service_name,
)
})
.buffer_unordered(5);

let mut all_wallet_balance = 0;
let mut all_space_used = 0;

while let Some((result, service_name)) = stream.next().await {
match result {
Ok((wallet_balance, space_used)) => {
info!("Wallet balance: {wallet_balance}, Space used: {space_used}");
all_wallet_balance += wallet_balance;
all_space_used += space_used;
}
Err(err) => {
error!("Error while fetching stats from {service_name:?}: {err:?}");
}
}
}

if let Err(err) = action_sender.send(Action::HomeActions(HomeActions::NodesStatsObtained {
wallet_balance: all_wallet_balance,
space_used: all_space_used,
})) {
error!("Error while sending action: {err:?}");
}
}

// todo: get all the stats
async fn fetch_stat_per_node(rpc_addr: SocketAddr, data_dir: PathBuf) -> Result<(u64, u64)> {
let now = Instant::now();
let rpc_client = RpcClient::from_socket_addr(rpc_addr);
let wallet_balance = rpc_client.node_info().await?.wallet_balance;

let space_used = get_size(data_dir)?;

debug!("Fetched stats from {rpc_addr:?} in {:?}", now.elapsed());
Ok((wallet_balance, space_used))
}
}
1 change: 1 addition & 0 deletions node-launchpad/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod app;
pub mod components;
pub mod config;
pub mod mode;
pub mod node_stats;
pub mod style;
pub mod tui;
pub mod utils;
Expand Down
Loading
Loading