iuna

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

metrics.rs (8493B)


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