iuna

iuna

iuna - experimental devnet protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

metrics.rs (8586B)


      1 use crate::{
      2     adapters::ui_data_store::BlockMetricRow,
      3     app::{PeerDirection, PeerInfo},
      4     domain::Amount,
      5 };
      6 
      7 use super::{
      8     PEER_STALE_AFTER_MS, now_ms,
      9     types::{
     10         MempoolCounts, MetricsChart, MetricsLeaderboards, MetricsPoint, MetricsResponse,
     11         MetricsValueKind, NetworkHealthLocalState, NetworkHealthResponse,
     12     },
     13 };
     14 
     15 pub(super) fn network_health(
     16     local: NetworkHealthLocalState,
     17     peers: &[PeerInfo],
     18     mempool: MempoolCounts,
     19 ) -> NetworkHealthResponse {
     20     network_health_at(local, peers, mempool, now_ms())
     21 }
     22 
     23 pub(super) fn metrics_response(
     24     enabled: bool,
     25     rows: Vec<BlockMetricRow>,
     26     leaderboards: MetricsLeaderboards,
     27 ) -> MetricsResponse {
     28     let latest = rows.last().cloned();
     29     MetricsResponse {
     30         enabled,
     31         latest,
     32         leaderboards,
     33         charts: vec![
     34             metrics_chart(
     35                 "block-time",
     36                 "Time per block",
     37                 "s",
     38                 MetricsValueKind::Seconds,
     39                 &rows,
     40                 |row| {
     41                     row.block_time_ms
     42                         .filter(|_| row.height > 1)
     43                         .map(|ms| ms as f64 / 1_000.0)
     44                 },
     45             ),
     46             metrics_chart(
     47                 "difficulty",
     48                 "Difficulty",
     49                 "bits",
     50                 MetricsValueKind::Number,
     51                 &rows,
     52                 |row| Some(row.mine_difficulty_bits as f64),
     53             ),
     54             metrics_chart(
     55                 "supply",
     56                 "IUNA in circulation",
     57                 "IUNA",
     58                 MetricsValueKind::Iuna,
     59                 &rows,
     60                 |row| Some(micro_iuna_as_iuna(row.circulating_supply)),
     61             ),
     62             metrics_chart(
     63                 "known-wallet-addresses",
     64                 "Known wallet addresses",
     65                 "addresses",
     66                 MetricsValueKind::Number,
     67                 &rows,
     68                 |row| Some(row.known_wallet_addresses as f64),
     69             ),
     70             metrics_chart(
     71                 "transactions",
     72                 "Transactions",
     73                 "tx",
     74                 MetricsValueKind::Number,
     75                 &rows,
     76                 |row| Some(row.transaction_count as f64),
     77             ),
     78             metrics_chart(
     79                 "burn-count",
     80                 "Burn transactions",
     81                 "burns",
     82                 MetricsValueKind::Number,
     83                 &rows,
     84                 |row| Some(row.burn_count as f64),
     85             ),
     86             metrics_chart(
     87                 "burn-amount",
     88                 "Burn amount",
     89                 "IUNA",
     90                 MetricsValueKind::Iuna,
     91                 &rows,
     92                 |row| Some(micro_iuna_as_iuna(row.burned_amount)),
     93             ),
     94             metrics_chart(
     95                 "total-burn",
     96                 "Total burn",
     97                 "IUNA",
     98                 MetricsValueKind::Iuna,
     99                 &rows,
    100                 |row| Some(micro_iuna_as_iuna(row.total_burned_amount)),
    101             ),
    102             metrics_chart(
    103                 "fees",
    104                 "Fees",
    105                 "IUNA",
    106                 MetricsValueKind::Iuna,
    107                 &rows,
    108                 |row| Some(micro_iuna_as_iuna(row.fees_amount)),
    109             ),
    110             metrics_chart(
    111                 "mine-actions",
    112                 "Mine actions",
    113                 "mine",
    114                 MetricsValueKind::Number,
    115                 &rows,
    116                 |row| Some(row.mine_count as f64),
    117             ),
    118             metrics_chart(
    119                 "vdf-rounds",
    120                 "VDF rounds",
    121                 "rounds",
    122                 MetricsValueKind::Number,
    123                 &rows,
    124                 |row| (row.vdf_rounds > 0).then_some(row.vdf_rounds as f64),
    125             ),
    126         ],
    127     }
    128 }
    129 
    130 fn metrics_chart(
    131     id: &'static str,
    132     title: &'static str,
    133     unit: &'static str,
    134     value_kind: MetricsValueKind,
    135     rows: &[BlockMetricRow],
    136     value: impl Fn(&BlockMetricRow) -> Option<f64>,
    137 ) -> MetricsChart {
    138     MetricsChart {
    139         id,
    140         title,
    141         unit,
    142         value_kind,
    143         points: rows
    144             .iter()
    145             .filter_map(|row| {
    146                 value(row).map(|value| MetricsPoint {
    147                     height: row.height,
    148                     value,
    149                 })
    150             })
    151             .collect(),
    152     }
    153 }
    154 
    155 fn micro_iuna_as_iuna(amount: Amount) -> f64 {
    156     amount as f64 / 1_000_000.0
    157 }
    158 
    159 pub(super) fn network_health_at(
    160     local: NetworkHealthLocalState,
    161     peers: &[PeerInfo],
    162     mempool: MempoolCounts,
    163     now_ms: u64,
    164 ) -> NetworkHealthResponse {
    165     let local_height = local.height;
    166     let remote_best_height = peers.iter().filter_map(|peer| peer.last_known_height).max();
    167     let best_known_height = remote_best_height.unwrap_or(local_height).max(local_height);
    168     let healthy_heights = peers
    169         .iter()
    170         .filter(|peer| peer.last_error.is_none())
    171         .filter_map(|peer| peer.last_known_height)
    172         .collect::<Vec<_>>();
    173     let shared_height = healthy_heights
    174         .iter()
    175         .copied()
    176         .min()
    177         .unwrap_or(local_height)
    178         .min(local_height);
    179     let outbound_peers = peers
    180         .iter()
    181         .filter(|peer| peer.direction != PeerDirection::Inbound)
    182         .count();
    183     let inbound_peers = peers
    184         .iter()
    185         .filter(|peer| peer.direction == PeerDirection::Inbound)
    186         .count();
    187     let healthy_peers = peers
    188         .iter()
    189         .filter(|peer| peer.last_error.is_none() && peer.last_known_height.is_some())
    190         .count();
    191     let failed_peers = peers
    192         .iter()
    193         .filter(|peer| peer.last_error.is_some())
    194         .count();
    195     let stale_peers = peers
    196         .iter()
    197         .filter(|peer| {
    198             peer.last_success_ms.is_some_and(|last_success| {
    199                 now_ms.saturating_sub(last_success) > PEER_STALE_AFTER_MS
    200             })
    201         })
    202         .count();
    203     let banned_peers = peers
    204         .iter()
    205         .filter(|peer| peer.is_banned_at(now_ms))
    206         .count();
    207     let network_time_offset_ms = median_peer_clock_offset(peers, now_ms);
    208     let bad_clock_peers = peers
    209         .iter()
    210         .filter(|peer| {
    211             peer.last_clock_observed_ms.is_some_and(|observed_ms| {
    212                 now_ms.saturating_sub(observed_ms) <= PEER_STALE_AFTER_MS
    213             })
    214         })
    215         .filter(|peer| peer.last_clock_offset_accepted == Some(false))
    216         .count();
    217     let lag_blocks = best_known_height.saturating_sub(local_height);
    218     let last_error = peers.iter().rev().find_map(|peer| {
    219         peer.last_error
    220             .as_ref()
    221             .map(|error| format!("{}: {error}", peer.address))
    222     });
    223 
    224     let state = if peers.is_empty() {
    225         "isolated"
    226     } else if banned_peers > 0 && healthy_peers == 0 {
    227         "banned"
    228     } else if lag_blocks > 0 {
    229         "syncing"
    230     } else if failed_peers > 0 && healthy_peers == 0 {
    231         "peer errors"
    232     } else if stale_peers > 0 && healthy_peers == stale_peers {
    233         "stale"
    234     } else if remote_best_height.is_some_and(|height| local_height > height) {
    235         "ahead of peers"
    236     } else {
    237         "healthy"
    238     }
    239     .to_string();
    240 
    241     NetworkHealthResponse {
    242         ok: !peers.is_empty() && lag_blocks == 0 && healthy_peers > stale_peers,
    243         state,
    244         local_height,
    245         best_known_height,
    246         shared_height,
    247         lag_blocks,
    248         outbound_peers,
    249         inbound_peers,
    250         healthy_peers,
    251         failed_peers,
    252         stale_peers,
    253         banned_peers,
    254         pending_transactions: local.pending_transactions,
    255         pending_plain_transactions: mempool.plain_transactions,
    256         pending_blinded_transactions: mempool.blinded_transactions,
    257         pending_blinded_reveals: mempool.blinded_reveals,
    258         network_time_offset_ms,
    259         bad_clock_peers,
    260         last_error,
    261     }
    262 }
    263 
    264 fn median_peer_clock_offset(peers: &[PeerInfo], now_ms: u64) -> Option<i64> {
    265     let mut offsets = peers
    266         .iter()
    267         .filter(|peer| peer.last_error.is_none())
    268         .filter(|peer| !peer.is_banned_at(now_ms))
    269         .filter(|peer| peer.last_clock_offset_accepted == Some(true))
    270         .filter(|peer| {
    271             peer.last_clock_observed_ms.is_some_and(|observed_ms| {
    272                 now_ms.saturating_sub(observed_ms) <= PEER_STALE_AFTER_MS
    273             })
    274         })
    275         .filter_map(|peer| peer.last_clock_offset_ms)
    276         .collect::<Vec<_>>();
    277     if offsets.is_empty() {
    278         return None;
    279     }
    280     offsets.sort_unstable();
    281     Some(offsets[offsets.len() / 2])
    282 }