smoldot/
informant.rs

1// Smoldot
2// Copyright (C) 2019-2022  Parity Technologies (UK) Ltd.
3// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
4
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13// GNU General Public License for more details.
14
15// You should have received a copy of the GNU General Public License
16// along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18//! Information string printed out.
19//!
20//! The so-called informant is a line of text typically printed at a regular interval on stdout.
21//!
22//! You can make the informant overwrite itself by printing a `\r` at the end of it.
23//!
24//! This code intentionally doesn't perform any printing and only provides some helping code to
25//! make the printing straight-forward.
26//!
27//! # Usage
28//!
29//! The [`InformantLine`] struct implements the [`core::fmt::Display`] trait.
30//!
31//! ```
32//! use smoldot::informant::InformantLine;
33//! eprint!("{}\r", InformantLine {
34//!     enable_colors: true,
35//!     chain_name: "My chain",
36//!     relay_chain: None,
37//!     max_line_width: 80,
38//!     num_peers: 8,
39//!     num_network_connections: 12,
40//!     best_number: 220,
41//!     finalized_number: 217,
42//!     best_hash: &[0x12, 0x34, 0x56, 0x76],
43//!     finalized_hash: &[0xaa, 0xbb, 0xcc, 0xdd],
44//!     network_known_best: Some(224),
45//! });
46//! ```
47
48use alloc::format;
49use core::{cmp, fmt};
50
51/// Values used to build the informant line. Implements the [`core::fmt::Display`] trait.
52// TODO: some fields here aren't printed; remove them once what is printed is final
53#[derive(Debug)]
54pub struct InformantLine<'a> {
55    /// If true, ANSI escape characters will be written out.
56    ///
57    /// > **Note**: Despite its name, this controls *all* styling escape characters, not just
58    /// >           colors.
59    pub enable_colors: bool,
60    /// Name of the chain.
61    pub chain_name: &'a str,
62    /// Extra fields related to the relay chain.
63    pub relay_chain: Option<RelayChain<'a>>,
64    /// Maximum number of characters of the informant line.
65    pub max_line_width: u32,
66    /// Number of gossiping substreams open with nodes of the same chain.
67    pub num_peers: u64,
68    /// Number of network connections we are having with the rest of the peer-to-peer network.
69    pub num_network_connections: u64,
70    /// Best block currently being propagated on the peer-to-peer. `None` if unknown.
71    pub network_known_best: Option<u64>,
72    /// Number of the best block that we have locally.
73    pub best_number: u64,
74    /// Hash of the best block that we have locally.
75    pub best_hash: &'a [u8],
76    /// Number of the latest finalized block we have locally.
77    pub finalized_number: u64,
78    /// Hash of the latest finalized block we have locally.
79    pub finalized_hash: &'a [u8],
80}
81
82/// Extra fields if a relay chain exists.
83#[derive(Debug)]
84pub struct RelayChain<'a> {
85    /// Name of the chain.
86    pub chain_name: &'a str,
87    /// Number of the best block that we have locally.
88    pub best_number: u64,
89}
90
91impl<'a> fmt::Display for InformantLine<'a> {
92    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
93        // TODO: lots of allocations in here
94        // TODO: this bar is actually harder to implement than expected; clean up code
95
96        // Define the escape sequences used below for colouring purposes.
97        let cyan = if self.enable_colors { "\x1b[36m" } else { "" };
98        let white_bold = if self.enable_colors { "\x1b[1;37m" } else { "" };
99        let light_gray = if self.enable_colors { "\x1b[90m" } else { "" };
100        let reset = if self.enable_colors { "\x1b[0m" } else { "" };
101
102        let (header, header_len) = if let Some(relay_chain) = &self.relay_chain {
103            let header = format!(
104                "    {cyan}{chain_name}{reset}   {white_bold}{local_best:<10}{reset} {light_gray}({relay_chain_name} {relay_best}){reset} [",
105                cyan = cyan,
106                reset = reset,
107                white_bold = white_bold,
108                light_gray = light_gray,
109                chain_name = self.chain_name,
110                relay_chain_name = relay_chain.chain_name,
111                local_best = BlockNumberDisplay(self.best_number),
112                relay_best = BlockNumberDisplay(relay_chain.best_number),
113            );
114
115            let header_len = self.chain_name.chars().count() + relay_chain.chain_name.len() + 29; // TODO: ? it's easier to do that than deal with unicode
116            (header, header_len)
117        } else {
118            let header = format!(
119                "    {cyan}{chain_name}{reset}   {white_bold}{local_best:<10}{reset} [",
120                cyan = cyan,
121                reset = reset,
122                white_bold = white_bold,
123                chain_name = self.chain_name,
124                local_best = BlockNumberDisplay(self.best_number),
125            );
126
127            let header_len = self.chain_name.chars().count() + 19; // TODO: ? it's easier to do that than deal with unicode
128            (header, header_len)
129        };
130
131        // TODO: it's a bit of a clusterfuck to properly align because the emoji eats a whitespace
132        let trailer = format!(
133            "] {white_bold}{network_best}{reset} (🔗{white_bold}{peers:>3}{reset}) (🌐{white_bold}{connec:>4}{reset})   ",
134            network_best = self
135                .network_known_best
136                .map(BlockNumberDisplay)
137                .map_or(either::Right("?"), either::Left),
138            peers = self.num_network_connections,
139            connec = self.num_network_connections,
140            white_bold = white_bold,
141            reset = reset,
142        );
143        let trailer_len = format!(
144            "] {network_best} (  {peers:>3}) (  {connec:>4})   ",
145            network_best = self
146                .network_known_best
147                .map(BlockNumberDisplay)
148                .map(either::Left)
149                .unwrap_or(either::Right("?")),
150            peers = self.num_network_connections,
151            connec = self.num_network_connections,
152        )
153        .len();
154
155        let bar_width = self
156            .max_line_width
157            .saturating_sub(u32::try_from(header_len).unwrap())
158            .saturating_sub(u32::try_from(trailer_len).unwrap());
159
160        let actual_network_best = cmp::max(self.network_known_best.unwrap_or(0), self.best_number);
161        assert!(self.best_number <= actual_network_best);
162        let bar_done_width = u128::from(self.best_number)
163            .checked_mul(u128::from(bar_width))
164            .unwrap()
165            .checked_div(u128::from(actual_network_best))
166            .unwrap_or(0); // TODO: hack to not panic
167        let bar_done_width = u32::try_from(bar_done_width).unwrap();
168
169        let done_bar1 = "=".repeat(usize::try_from(bar_done_width.saturating_sub(1)).unwrap());
170        let done_bar2 = if bar_done_width == bar_width {
171            '='
172        } else {
173            '>'
174        };
175        let todo_bar = " ".repeat(
176            usize::try_from(
177                bar_width
178                    .checked_sub(bar_done_width.saturating_sub(1).saturating_add(1))
179                    .unwrap(),
180            )
181            .unwrap(),
182        );
183        assert_eq!(
184            done_bar1.len() + 1 + todo_bar.len(),
185            usize::try_from(bar_width).unwrap()
186        );
187
188        write!(f, "{header}{done_bar1}{done_bar2}{todo_bar}{trailer}")
189    }
190}
191
192/// Implements `fmt::Display` and displays hashes in a nice way.
193pub struct HashDisplay<'a>(pub &'a [u8]);
194
195impl<'a> fmt::Display for HashDisplay<'a> {
196    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
197        write!(f, "0x")?;
198        if self.0.len() >= 2 {
199            let val = u16::from_be_bytes(<[u8; 2]>::try_from(&self.0[..2]).unwrap());
200            write!(f, "{val:04x}")?;
201        }
202        if self.0.len() >= 5 {
203            write!(f, "…")?;
204        }
205        if self.0.len() >= 4 {
206            let len = self.0.len();
207            let val = u16::from_be_bytes(<[u8; 2]>::try_from(&self.0[len - 2..]).unwrap());
208            write!(f, "{val:04x}")?;
209        }
210        Ok(())
211    }
212}
213
214/// Implements `fmt::Display` and displays a number of bytes in a nice way.
215pub struct BytesDisplay(pub u64);
216
217impl fmt::Display for BytesDisplay {
218    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
219        let mut value = self.0 as f64;
220
221        if value < 1000.0 {
222            return write!(f, "{value} B");
223        }
224        value /= 1024.0;
225
226        if value < 100.0 {
227            return write!(f, "{value:.1} kiB");
228        }
229        if value < 1000.0 {
230            return write!(f, "{value:.0} kiB");
231        }
232        value /= 1024.0;
233
234        if value < 100.0 {
235            return write!(f, "{value:.1} MiB");
236        }
237        if value < 1000.0 {
238            return write!(f, "{value:.0} MiB");
239        }
240        value /= 1024.0;
241
242        if value < 100.0 {
243            return write!(f, "{value:.1} GiB");
244        }
245        if value < 1000.0 {
246            return write!(f, "{value:.0} GiB");
247        }
248        value /= 1024.0;
249
250        if value < 100.0 {
251            return write!(f, "{value:.1} TiB");
252        }
253        if value < 1000.0 {
254            return write!(f, "{value:.0} TiB");
255        }
256        value /= 1024.0;
257
258        write!(f, "{value:.1} PiB")
259
260        // Hopefully we never have to go above petabytes.
261    }
262}
263
264/// Implements `fmt::Display` and displays a block number with a `#` in front.
265struct BlockNumberDisplay(u64);
266
267impl fmt::Display for BlockNumberDisplay {
268    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
269        write!(f, "#{}", self.0)?;
270        Ok(())
271    }
272}