diff --git a/Cargo.toml b/Cargo.toml index 72b3d225..3331010b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "sorock", + "sorock-check", "tests/*", ] diff --git a/README.md b/README.md index 5853141a..295fb0f8 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,6 @@ A Multi-Raft implementation in Rust language. ## Related Projects -- [sorock-monitor](https://github.com/akiradeveloper/sorock-monitor): Monitoring tool to watch the log state in a cluster. Implementing using [ratatui](https://github.com/ratatui/ratatui). - [phi-detector](https://github.com/akiradeveloper/phi-detector): Implementation of Phi Accrual Failure Detector in Rust. ## Author diff --git a/sorock-check/Cargo.toml b/sorock-check/Cargo.toml new file mode 100644 index 00000000..b19a23a8 --- /dev/null +++ b/sorock-check/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "sorock-check" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow.workspace = true +async-stream = "0.3.6" +async-trait.workspace = true +clap = { version = "4.5.20", features = ["derive"] } +crossbeam = "0.8.4" +futures.workspace = true +prost.workspace = true +rand.workspace = true +ratatui = "0.28.1" +spin.workspace = true +tokio = { workspace = true, features = ["full"] } +tonic.workspace = true +tonic-prost.workspace = true +tui-widget-list = "0.12.2" + +[build-dependencies] +tonic-prost-build.workspace = true diff --git a/sorock-check/README.md b/sorock-check/README.md new file mode 100644 index 00000000..cb04f58a --- /dev/null +++ b/sorock-check/README.md @@ -0,0 +1,12 @@ +# sorock-check + +A lightweight tool to troubleshoot Raft clusters by visualizing the cluster and the log progress. + +https://github.com/user-attachments/assets/9aff6794-778b-48fa-bfbd-838e63b3e5c8 + +## Usage + +`sorock-check connect $URL $SHARD_ID`. (e.g. `sorock-check connect http://node5:50051 34`) + +Once connected to any node in a cluster, +the program will automatically connect to all nodes in the cluster. diff --git a/sorock-check/build.rs b/sorock-check/build.rs new file mode 100644 index 00000000..289d944c --- /dev/null +++ b/sorock-check/build.rs @@ -0,0 +1,5 @@ +fn main() { + tonic_prost_build::configure() + .compile_protos(&["sorock.proto"], &["../sorock/proto"]) + .unwrap(); +} diff --git a/sorock-check/src/main.rs b/sorock-check/src/main.rs new file mode 100644 index 00000000..f90544af --- /dev/null +++ b/sorock-check/src/main.rs @@ -0,0 +1,167 @@ +use anyhow::Result; +use spin::RwLock; +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::sync::Arc; +use std::{io, vec}; + +use clap::Parser; +use futures::Stream; +use futures::StreamExt; +use ratatui::prelude::*; +use ratatui::widgets::{Block, Gauge}; +use ratatui::{ + crossterm::event::{self, KeyCode, KeyEventKind}, + widgets::{Axis, Borders, Chart, Dataset, GraphType, StatefulWidget, Widget}, + DefaultTerminal, +}; +use std::pin::Pin; +use std::time::{Duration, Instant}; +use tonic::transport::{Channel, Endpoint, Uri}; + +mod mock; +mod model; +mod real; +mod ui; + +mod proto { + tonic::include_proto!("sorock"); +} + +#[derive(Parser)] +enum Sub { + #[clap(about = "Start monitoring a cluster by connecting to a node.")] + Monitor { addr: Uri, shard_id: u32 }, + #[clap(about = "Embedded test. 0 -> Static data, 1 -> Mock servers")] + TestMonitor { number: u8 }, +} + +#[derive(Parser)] +struct Args { + #[clap(subcommand)] + sub: Sub, +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(); + + let model = match args.sub { + Sub::Monitor { addr, shard_id } => { + let node = real::connect_real_node(addr, shard_id); + model::Model::new(node).await + } + Sub::TestMonitor { number: 0 } => model::Model::test(), + Sub::TestMonitor { number: 1 } => { + let mock = mock::connect_mock_node(); + model::Model::new(mock).await + } + _ => unreachable!(), + }; + + let mut terminal = ratatui::init(); + let app_result = App::new(model).run(&mut terminal)?; + terminal.clear()?; + ratatui::restore(); + + Ok(app_result) +} + +struct App { + model: model::Model, +} +impl App { + pub fn new(model: model::Model) -> Self { + Self { model } + } + + fn run(self, terminal: &mut DefaultTerminal) -> io::Result<()> { + let mut app_state = AppState::default(); + loop { + terminal.draw(|frame| { + frame.render_stateful_widget(&self, frame.area(), &mut app_state); + })?; + + if !event::poll(Duration::from_millis(100))? { + continue; + } + + if let event::Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Char('q') => return Ok(()), + KeyCode::Up | KeyCode::Char('k') => app_state.list_state.previous(), + KeyCode::Down | KeyCode::Char('j') => app_state.list_state.next(), + _ => {} + } + } + } + } + } +} + +#[derive(Default)] +struct AppState { + list_state: tui_widget_list::ListState, +} +impl StatefulWidget for &App { + type State = AppState; + fn render( + self, + area: ratatui::prelude::Rect, + buf: &mut ratatui::prelude::Buffer, + state: &mut Self::State, + ) where + Self: Sized, + { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([Constraint::Length(15), Constraint::Fill(1)].as_ref()) + .split(area); + + let progress_chart = { + let end = Instant::now(); + let start = end - Duration::from_secs(120); + let data = self.model.progress_log.read().get_range(start, end); + ui::progress_chart::ProgressChart::new(data, start, end) + }; + Widget::render(progress_chart, chunks[0], buf); + + let nodes_list = { + let mut nodes = vec![]; + let reader = &self.model.nodes.read(); + + let min_index = reader + .nodes + .values() + .map(|node_state| node_state.log_state.head_index) + .min() + .unwrap_or(0); + let max_index = reader + .nodes + .values() + .map(|node_state| node_state.log_state.last_index) + .max() + .unwrap_or(0); + + for (uri, node_state) in &reader.nodes { + let log_state = &node_state.log_state; + nodes.push(ui::node_list::Node { + name: uri.to_string(), + head_index: log_state.head_index, + snapshot_index: log_state.snapshot_index, + app_index: log_state.app_index, + commit_index: log_state.commit_index, + last_index: log_state.last_index, + min_max: ui::node_list::IndexRange { + min_index, + max_index, + }, + }); + } + nodes.sort_by_key(|node| node.name.clone()); + ui::node_list::NodeList::new(nodes) + }; + StatefulWidget::render(nodes_list, chunks[1], buf, &mut state.list_state); + } +} diff --git a/sorock-check/src/mock.rs b/sorock-check/src/mock.rs new file mode 100644 index 00000000..db271428 --- /dev/null +++ b/sorock-check/src/mock.rs @@ -0,0 +1,69 @@ +use super::*; + +use futures::stream::Stream; +use std::time::Instant; +use tonic::transport::Uri; + +pub struct MockNode { + start_time: Instant, +} +impl MockNode { + pub fn new() -> Self { + Self { + start_time: Instant::now(), + } + } +} + +#[async_trait::async_trait] +impl model::stream::Node for MockNode { + async fn watch_membership(&self) -> Pin + Send>> { + let out = proto::Membership { + members: vec![ + "http://n1:4000".to_string(), + "http://n2:4000".to_string(), + "http://n3:4000".to_string(), + "http://n4:4000".to_string(), + "http://n5:4000".to_string(), + "http://n6:4000".to_string(), + "http://n7:4000".to_string(), + "http://n8:4000".to_string(), + ], + }; + Box::pin(futures::stream::once(async move { out })) + } + + async fn watch_log_metrics( + &self, + _: Uri, + ) -> Pin + Send>> { + let start_time = self.start_time; + let st = async_stream::stream! { + loop { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + let x = Instant::now().duration_since(start_time).as_secs(); + // f(x) = x^2 * log(x) + // is a monotonically increasing function + let f = |x: u64| { + let a = f64::powf(x as f64, 2.); + let b = f64::log(10.0, x as f64); + (a * b) as u64 + }; + let metrics = proto::LogMetrics { + head_index: f(x), + snap_index: f(x+1), + app_index: f(x+2), + commit_index: f(x+3), + last_index: f(x+4), + }; + yield metrics + } + }; + Box::pin(st) + } +} + +pub fn connect_mock_node() -> impl model::stream::Node { + let app = MockNode::new(); + app +} diff --git a/sorock-check/src/model/mod.rs b/sorock-check/src/model/mod.rs new file mode 100644 index 00000000..3bc1b61b --- /dev/null +++ b/sorock-check/src/model/mod.rs @@ -0,0 +1,61 @@ +use super::*; + +mod nodes; +mod progress_log; +pub mod stream; +pub use nodes::*; +pub use progress_log::*; + +pub struct Model { + pub nodes: Arc>, + pub progress_log: Arc>, +} +impl Model { + pub async fn new(node: impl stream::Node + 'static) -> Self { + let node = Arc::new(node); + let nodes = Arc::new(RwLock::new(Nodes::default())); + let progress_log = Arc::new(RwLock::new(ProgressLog::new())); + + tokio::spawn({ + let node = node.watch_membership().await; + let nodes = nodes.clone(); + async move { + stream::CopyMembership::copy(node, nodes).await; + } + }); + + tokio::spawn({ + let node = node.clone(); + let nodes = nodes.clone(); + async move { + loop { + tokio::time::sleep(Duration::from_secs(1)).await; + nodes::start_copying(node.clone(), nodes.clone()); + } + } + }); + + tokio::spawn({ + let nodes = nodes.clone(); + let progress_log = progress_log.clone(); + async move { + loop { + progress_log::copy(nodes.clone(), progress_log.clone()); + tokio::time::sleep(Duration::from_secs(1)).await; + } + } + }); + + Self { + nodes, + progress_log, + } + } + + pub fn test() -> Self { + Self { + nodes: Arc::new(RwLock::new(Nodes::test())), + progress_log: Arc::new(RwLock::new(ProgressLog::test())), + } + } +} diff --git a/sorock-check/src/model/nodes.rs b/sorock-check/src/model/nodes.rs new file mode 100644 index 00000000..7778085a --- /dev/null +++ b/sorock-check/src/model/nodes.rs @@ -0,0 +1,110 @@ +use super::*; + +use tokio::task::AbortHandle; +struct DropHandle(AbortHandle); +impl Drop for DropHandle { + fn drop(&mut self) { + self.0.abort(); + } +} + +#[derive(Default)] +pub struct LogState { + pub head_index: u64, + pub snapshot_index: u64, + pub app_index: u64, + pub commit_index: u64, + pub last_index: u64, +} + +#[derive(Default)] +pub struct NodeState { + pub log_state: LogState, + drop_log_metrics_stream: Option, +} + +#[derive(Default)] +pub struct Nodes { + pub nodes: HashMap, +} +impl Nodes { + pub async fn update_membership(&mut self, new_membership: HashSet) { + let mut del_list = vec![]; + for (uri, _) in self.nodes.iter() { + if !new_membership.contains(uri) { + del_list.push(uri.clone()); + } + } + for uri in del_list { + self.nodes.remove(&uri); + } + for uri in new_membership { + self.nodes.entry(uri.clone()).or_default(); + } + } + + pub fn test() -> Self { + let mut out = HashMap::new(); + out.insert( + Uri::from_static("http://n1:3000"), + NodeState { + log_state: model::LogState { + head_index: 100, + snapshot_index: 110, + app_index: 140, + commit_index: 160, + last_index: 165, + }, + drop_log_metrics_stream: None, + }, + ); + out.insert( + Uri::from_static("http://n2:3000"), + NodeState { + log_state: model::LogState { + head_index: 125, + snapshot_index: 130, + app_index: 140, + commit_index: 165, + last_index: 180, + }, + drop_log_metrics_stream: None, + }, + ); + out.insert( + Uri::from_static("http://n3:3000"), + NodeState { + log_state: model::LogState { + head_index: 168, + snapshot_index: 168, + app_index: 168, + commit_index: 168, + last_index: 168, + }, + drop_log_metrics_stream: None, + }, + ); + Self { nodes: out } + } +} + +/// Start data fetching for each node. +pub fn start_copying(node: Arc, nodes: Arc>) { + let mut data = nodes.write(); + for (url, state) in &mut data.nodes { + if state.drop_log_metrics_stream.is_none() { + let hdl = tokio::spawn({ + let url = url.clone(); + let node = node.clone(); + let data = nodes.clone(); + async move { + stream::CopyLogMetrics { url: url.clone() } + .copy(node.watch_log_metrics(url).await, data) + .await; + } + }) + .abort_handle(); + state.drop_log_metrics_stream = Some(DropHandle(hdl)); + } + } +} diff --git a/sorock-check/src/model/progress_log.rs b/sorock-check/src/model/progress_log.rs new file mode 100644 index 00000000..1606aa08 --- /dev/null +++ b/sorock-check/src/model/progress_log.rs @@ -0,0 +1,35 @@ +use super::*; + +pub struct ProgressLog { + log: BTreeMap, +} +impl ProgressLog { + pub fn new() -> Self { + Self { + log: BTreeMap::new(), + } + } + pub fn get_range(&self, start: Instant, end: Instant) -> BTreeMap { + self.log.range(start..=end).map(|(&k, &v)| (k, v)).collect() + } + pub fn test() -> Self { + let mut log = BTreeMap::new(); + let now = Instant::now(); + for i in 0..100000 { + log.insert(now + Duration::from_secs(i), i * i); + } + Self { log } + } +} + +pub fn copy(nodes: Arc>, progress_log: Arc>) { + let nodes = nodes.read(); + let mut progress_log = progress_log.write(); + let max_value = nodes + .nodes + .values() + .map(|node_state| node_state.log_state.commit_index) + .max() + .unwrap_or(0); + progress_log.log.insert(Instant::now(), max_value); +} diff --git a/sorock-check/src/model/stream/log_metrics.rs b/sorock-check/src/model/stream/log_metrics.rs new file mode 100644 index 00000000..900eafe5 --- /dev/null +++ b/sorock-check/src/model/stream/log_metrics.rs @@ -0,0 +1,26 @@ +use super::*; + +pub struct CopyLogMetrics { + pub url: Uri, +} +impl CopyLogMetrics { + pub async fn copy( + &mut self, + st: impl Stream, + data: Arc>, + ) { + let mut st = Box::pin(st); + while let Some(metric) = st.next().await { + if let Some(state) = data.write().nodes.get_mut(&self.url) { + let new_state = LogState { + head_index: metric.head_index, + snapshot_index: metric.snap_index, + app_index: metric.app_index, + commit_index: metric.commit_index, + last_index: metric.last_index, + }; + state.log_state = new_state; + } + } + } +} diff --git a/sorock-check/src/model/stream/membership.rs b/sorock-check/src/model/stream/membership.rs new file mode 100644 index 00000000..a24628a9 --- /dev/null +++ b/sorock-check/src/model/stream/membership.rs @@ -0,0 +1,22 @@ +use super::*; + +pub struct CopyMembership; + +impl CopyMembership { + pub async fn copy(st: impl Stream, nodes: Arc>) { + let mut st = Box::pin(st); + while let Some(membership) = st.next().await { + let new_membership = { + let mut out = HashSet::new(); + for mem in membership.members { + let url = Uri::from_maybe_shared(mem).unwrap(); + out.insert(url); + } + out + }; + + let mut nodes = nodes.write(); + nodes.update_membership(new_membership).await; + } + } +} diff --git a/sorock-check/src/model/stream/mod.rs b/sorock-check/src/model/stream/mod.rs new file mode 100644 index 00000000..8ba00d90 --- /dev/null +++ b/sorock-check/src/model/stream/mod.rs @@ -0,0 +1,16 @@ +use super::*; + +pub mod log_metrics; +mod membership; + +pub use log_metrics::CopyLogMetrics; +pub use membership::CopyMembership; + +#[async_trait::async_trait] +pub trait Node: Send + Sync { + async fn watch_membership(&self) -> Pin + Send>>; + async fn watch_log_metrics( + &self, + url: Uri, + ) -> Pin + Send>>; +} diff --git a/sorock-check/src/real.rs b/sorock-check/src/real.rs new file mode 100644 index 00000000..fb3b78c0 --- /dev/null +++ b/sorock-check/src/real.rs @@ -0,0 +1,68 @@ +use super::*; + +use futures::StreamExt; +use std::{pin::Pin, time::Duration}; +use tonic::transport::Uri; + +pub fn connect_real_node(uri: Uri, shard_id: u32) -> impl model::stream::Node { + let chan = Endpoint::from(uri).connect_lazy(); + let client = proto::raft_client::RaftClient::new(chan); + RealNode { client, shard_id } +} + +struct RealNode { + client: proto::raft_client::RaftClient, + shard_id: u32, +} + +#[async_trait::async_trait] +impl model::stream::Node for RealNode { + async fn watch_membership(&self) -> Pin + Send>> { + let shard = proto::Shard { id: self.shard_id }; + let mut client = self.client.clone(); + let st = async_stream::stream! { + loop { + tokio::time::sleep(Duration::from_secs(5)).await; + match client.get_membership(shard).await { + Ok(response) => { + let membership = response.into_inner(); + yield membership + } + Err(e) => { + eprintln!("Failed to get membership: {}", e); + continue; + } + } + } + }; + Box::pin(st) + } + + async fn watch_log_metrics( + &self, + _: Uri, + ) -> Pin + Send>> { + let shard = proto::Shard { id: self.shard_id }; + let mut client = self.client.clone(); + let st = async_stream::stream! { + match client.watch_log_metrics(shard).await { + Ok(response) => { + let mut st = response.into_inner(); + while let Some(result) = st.next().await { + match result { + Ok(metrics) => yield metrics, + Err(e) => { + eprintln!("Error in log metrics stream: {}", e); + break; + } + } + } + } + Err(e) => { + eprintln!("Failed to watch log metrics: {}", e); + } + } + }; + Box::pin(st) + } +} diff --git a/sorock-check/src/ui/mod.rs b/sorock-check/src/ui/mod.rs new file mode 100644 index 00000000..cfec5958 --- /dev/null +++ b/sorock-check/src/ui/mod.rs @@ -0,0 +1,4 @@ +use super::*; + +pub mod node_list; +pub mod progress_chart; diff --git a/sorock-check/src/ui/node_list.rs b/sorock-check/src/ui/node_list.rs new file mode 100644 index 00000000..61a9c3e5 --- /dev/null +++ b/sorock-check/src/ui/node_list.rs @@ -0,0 +1,169 @@ +use super::*; + +#[derive(Clone, Copy)] +pub struct IndexRange { + pub min_index: u64, + pub max_index: u64, +} + +struct LogStripe { + min_to_head: u16, + head_to_snap: u16, + snap_to_app: u16, + app_to_commit: u16, + commit_to_last: u16, + last_to_max: u16, +} +impl LogStripe { + pub fn from(node: &Node) -> Self { + let width = node.min_max.max_index - node.min_max.min_index; + + let min_to_head = (node.head_index - node.min_max.min_index) as f64 / width as f64; + let min_to_snap = (node.snapshot_index - node.min_max.min_index) as f64 / width as f64; + let min_to_app = (node.app_index - node.min_max.min_index) as f64 / width as f64; + let min_to_commit = (node.commit_index - node.min_max.min_index) as f64 / width as f64; + let min_to_last = (node.last_index - node.min_max.min_index) as f64 / width as f64; + + let k = 10000.0; + + Self { + min_to_head: (min_to_head * k) as u16, + head_to_snap: ((min_to_snap - min_to_head) * k) as u16, + snap_to_app: ((min_to_app - min_to_snap) * k) as u16, + app_to_commit: ((min_to_commit - min_to_app) * k) as u16, + commit_to_last: ((min_to_last - min_to_commit) * k) as u16, + last_to_max: ((1.0 - min_to_last) * k) as u16, + } + } +} + +#[derive(Clone)] +pub struct Node { + pub name: String, + + pub head_index: u64, + pub snapshot_index: u64, + pub app_index: u64, + pub commit_index: u64, + pub last_index: u64, + + pub min_max: IndexRange, +} +impl Widget for Node { + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) + where + Self: Sized, + { + let stripe = LogStripe::from(&self); + + let outer_block = Block::default().borders(Borders::ALL).title(self.name); + + let inner_area = outer_block.inner(area); + + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Fill(stripe.min_to_head), + Constraint::Fill(stripe.head_to_snap), + Constraint::Fill(stripe.snap_to_app), + Constraint::Fill(stripe.app_to_commit), + Constraint::Fill(stripe.commit_to_last), + Constraint::Fill(stripe.last_to_max), + ] + .as_ref(), + ) + .split(inner_area); + + Gauge::default() + .gauge_style( + Style::default() + .fg(Color::Gray) + .bg(Color::Black) + .add_modifier(Modifier::ITALIC), + ) + .label("gc") + .ratio(1.0) + .render(chunks[1], buf); + + Gauge::default() + .gauge_style( + Style::default() + .fg(Color::Green) + .bg(Color::Black) + .add_modifier(Modifier::ITALIC), + ) + .label("applied") + .ratio(1.0) + .render(chunks[2], buf); + + Gauge::default() + .gauge_style( + Style::default() + .fg(Color::Yellow) + .bg(Color::Black) + .add_modifier(Modifier::ITALIC), + ) + .label("commit") + .ratio(1.0) + .render(chunks[3], buf); + + Gauge::default() + .gauge_style( + Style::default() + .fg(Color::Red) + .bg(Color::Black) + .add_modifier(Modifier::ITALIC), + ) + .label("uncommit") + .ratio(1.0) + .render(chunks[4], buf); + + outer_block.render(area, buf); + } +} + +pub struct NodeList { + nodes: Vec, +} +impl NodeList { + pub fn new(nodes: Vec) -> Self { + Self { nodes } + } +} +impl StatefulWidget for NodeList { + type State = tui_widget_list::ListState; + fn render( + self, + area: ratatui::prelude::Rect, + buf: &mut ratatui::prelude::Buffer, + state: &mut Self::State, + ) where + Self: Sized, + { + let n = self.nodes.len(); + let builder = tui_widget_list::ListBuilder::new(move |ctx| { + let selected = ctx.is_selected; + let idx = ctx.index; + let mut node = self.nodes[idx].clone(); + let name = format!( + "{} [{}/{}/{}/{}/{}]", + node.name, + node.head_index, + node.snapshot_index, + node.app_index, + node.commit_index, + node.last_index + ); + node.name = if selected { + format!("> {}", name) + } else { + name + }; + (node, 3) + }); + let view = tui_widget_list::ListView::new(builder, n); + + StatefulWidget::render(view, area, buf, state); + } +} diff --git a/sorock-check/src/ui/progress_chart.rs b/sorock-check/src/ui/progress_chart.rs new file mode 100644 index 00000000..bdb1bfd3 --- /dev/null +++ b/sorock-check/src/ui/progress_chart.rs @@ -0,0 +1,77 @@ +use super::*; + +pub struct ProgressChart { + data: BTreeMap, + start: Instant, + end: Instant, +} +impl ProgressChart { + pub fn new(data: BTreeMap, start: Instant, end: Instant) -> Self { + Self { data, start, end } + } + fn to_relative_time(&self, t: Instant) -> f64 { + let duration = t - self.start; + duration.as_millis() as f64 / 1_000. + } +} +impl Widget for ProgressChart { + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) + where + Self: Sized, + { + let (dataseq, hi_v) = { + let mut data = vec![]; + for (&t, &v) in &self.data { + let x = self.to_relative_time(t); + let y = v as f64; + data.push((x, y)); + } + + let n = data.len(); + let mut hi_v = 0.; + let mut out = vec![]; + for j in 1..n { + let i = j - 1; + let (ti, xi) = data[i]; + let (tj, xj) = data[j]; + let v = if tj == ti { 0. } else { (xj - xi) / (tj - ti) }; + if v > hi_v { + hi_v = v; + } + out.push((tj, v)); + } + (out, hi_v) + }; + + let dataset = Dataset::default() + // .marker(Marker::Braille) + .marker(symbols::Marker::HalfBlock) + .style(Style::default().fg(Color::Yellow)) + .graph_type(GraphType::Bar) + .data(&dataseq); + + let x_axis = { + let lo = self.to_relative_time(self.start); + let hi = self.to_relative_time(self.end); + Axis::default() + .style(Style::default().fg(Color::Gray)) + .title("Time") + .bounds([lo as f64, hi as f64]) + .labels(["-60s", "0s"]) + }; + let y_axis = Axis::default() + .style(Style::default().fg(Color::Gray)) + .title("Commit/Sec") + .bounds([0., hi_v]) + .labels(["0".to_string(), format!("{hi_v:.2}")]); + Chart::new(vec![dataset]) + .block( + Block::default() + .title("Commit Progress") + .borders(Borders::ALL), + ) + .x_axis(x_axis) + .y_axis(y_axis) + .render(area, buf); + } +} diff --git a/sorock/build.rs b/sorock/build.rs index 40567928..ff8065f4 100644 --- a/sorock/build.rs +++ b/sorock/build.rs @@ -12,9 +12,6 @@ fn main() { .bytes(".sorock.ReplicationStreamEntry.command") .bytes(".sorock.SnapshotChunk.data") .file_descriptor_set_path(out_dir.join("sorock_descriptor.bin")) - .compile_protos( - &["proto/sorock.proto"], - &["proto"], - ) + .compile_protos(&["proto/sorock.proto"], &["proto"]) .unwrap(); }