use std::collections::VecDeque;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use parking_lot::{Mutex, RwLock};
use serde::{Deserialize, Serialize};
use super::nat::UplinkNatState;
use crate::crypto::NoiseSession;
use crate::metrics::QualityMetrics;
use crate::transport::{Transport, TransportProtocol};
use crate::types::{
Bandwidth, ConnectionState, InterfaceType, Latency, TrafficStats, UplinkHealth, UplinkId,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UplinkConfig {
pub id: UplinkId,
pub interface: Option<String>,
pub local_addr: Option<SocketAddr>,
pub remote_addr: SocketAddr,
#[serde(default)]
pub protocol: TransportProtocol,
#[serde(default)]
pub interface_type: InterfaceType,
#[serde(default = "default_weight")]
pub weight: u32,
#[serde(default)]
pub max_bandwidth_mbps: u32,
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(default)]
pub priority_override: u32,
}
fn default_weight() -> u32 {
100
}
fn default_enabled() -> bool {
true
}
impl Default for UplinkConfig {
fn default() -> Self {
Self {
id: UplinkId::new("uplink-0"),
interface: None,
local_addr: None,
remote_addr: SocketAddr::from(([0, 0, 0, 0], 0)),
protocol: TransportProtocol::default(),
interface_type: InterfaceType::Unknown,
weight: 100,
max_bandwidth_mbps: 0,
enabled: true,
priority_override: 0,
}
}
}
#[derive(Debug)]
pub struct UplinkState {
pub connection_state: ConnectionState,
pub health: UplinkHealth,
pub last_activity: Instant,
pub last_send: Option<Instant>,
pub last_recv: Option<Instant>,
pub consecutive_failures: u32,
pub last_failure: Option<Instant>,
pub last_error: Option<String>,
}
impl Default for UplinkState {
fn default() -> Self {
Self {
connection_state: ConnectionState::Disconnected,
health: UplinkHealth::Unknown,
last_activity: Instant::now(),
last_send: None,
last_recv: None,
consecutive_failures: 0,
last_failure: None,
last_error: None,
}
}
}
#[derive(Debug, Clone)]
pub struct ConnectionParams {
pub uplink_id: u16,
pub interface_type: InterfaceType,
pub protocol: TransportProtocol,
pub rtt: Duration,
pub rtt_variance: Duration,
pub rtt_min: Duration,
pub jitter: Duration,
pub bandwidth_up: Bandwidth,
pub bandwidth_down: Bandwidth,
pub effective_throughput: Bandwidth,
pub goodput: Bandwidth,
pub max_bandwidth_mbps: u32,
pub bdp: u64,
pub cwnd: u32,
pub in_flight: u32,
pub packet_loss: f64,
pub retransmission_rate: f64,
pub health: UplinkHealth,
pub consecutive_failures: u32,
pub is_usable: bool,
pub is_natted: bool,
pub nat_type: super::nat::NatType,
pub time_for_1mb: Duration,
pub priority_score: u32,
pub bytes_sent: u64,
pub bytes_received: u64,
pub packets_sent: u64,
pub packets_received: u64,
pub packets_dropped: u64,
pub packets_retransmitted: u64,
}
impl ConnectionParams {
pub fn is_low_latency(&self) -> bool {
self.rtt < Duration::from_millis(50) && self.jitter < Duration::from_millis(10)
}
pub fn is_high_bandwidth(&self) -> bool {
self.effective_throughput.bytes_per_sec > 10_000_000.0 }
pub fn has_low_loss(&self) -> bool {
self.packet_loss < 0.01 }
pub fn quality_score(&self) -> f64 {
let rtt_score = 1.0 / (1.0 + self.rtt.as_secs_f64() * 10.0);
let loss_score = 1.0 - self.packet_loss.min(1.0);
let jitter_score = 1.0 / (1.0 + self.jitter.as_secs_f64() * 100.0);
let health_score = self.health.priority_modifier();
(rtt_score + loss_score + jitter_score + health_score) / 4.0
}
pub fn transfer_time(&self, bytes: u64) -> Duration {
if self.effective_throughput.bytes_per_sec <= 0.0 {
return Duration::MAX;
}
let transfer_secs = bytes as f64 / self.effective_throughput.bytes_per_sec;
let total_secs = (transfer_secs + self.rtt.as_secs_f64()).min(3600.0);
Duration::from_secs_f64(total_secs)
}
pub fn faster_than(&self, other: &Self, bytes: u64) -> bool {
self.transfer_time(bytes) < other.transfer_time(bytes)
}
}
#[derive(Debug)]
struct RttTracker {
samples: VecDeque<Duration>,
max_samples: usize,
smoothed_rtt: Duration,
rtt_var: Duration,
}
impl RttTracker {
fn new(max_samples: usize) -> Self {
Self {
samples: VecDeque::with_capacity(max_samples),
max_samples,
smoothed_rtt: Duration::ZERO,
rtt_var: Duration::ZERO,
}
}
fn add_sample(&mut self, rtt: Duration) {
if self.samples.len() >= self.max_samples {
self.samples.pop_front();
}
self.samples.push_back(rtt);
if self.smoothed_rtt == Duration::ZERO {
self.smoothed_rtt = rtt;
self.rtt_var = rtt / 2;
} else {
let rtt_f = rtt.as_secs_f64();
let srtt_f = self.smoothed_rtt.as_secs_f64();
let rttvar_f = self.rtt_var.as_secs_f64();
let delta = (rtt_f - srtt_f).abs();
self.rtt_var = Duration::from_secs_f64(rttvar_f * 0.75 + delta * 0.25);
self.smoothed_rtt = Duration::from_secs_f64(srtt_f * 0.875 + rtt_f * 0.125);
}
}
fn get_latency(&self) -> Option<Latency> {
if self.samples.is_empty() {
return None;
}
let samples: Vec<_> = self.samples.iter().copied().collect();
Latency::from_samples(&samples)
}
fn smoothed(&self) -> Duration {
self.smoothed_rtt
}
fn variance(&self) -> Duration {
self.rtt_var
}
}
#[derive(Debug)]
struct BandwidthEstimator {
bytes_sent: AtomicU64,
bytes_recv: AtomicU64,
last_update: RwLock<Instant>,
send_rate: RwLock<f64>,
recv_rate: RwLock<f64>,
}
impl BandwidthEstimator {
fn new() -> Self {
Self {
bytes_sent: AtomicU64::new(0),
bytes_recv: AtomicU64::new(0),
last_update: RwLock::new(Instant::now()),
send_rate: RwLock::new(0.0),
recv_rate: RwLock::new(0.0),
}
}
fn record_sent(&self, bytes: u64) {
self.bytes_sent.fetch_add(bytes, Ordering::Relaxed);
}
fn record_recv(&self, bytes: u64) {
self.bytes_recv.fetch_add(bytes, Ordering::Relaxed);
}
fn update(&self) {
let now = Instant::now();
let mut last = self.last_update.write();
let elapsed = now.duration_since(*last).as_secs_f64();
if elapsed < 0.1 {
return; }
let sent = self.bytes_sent.swap(0, Ordering::Relaxed) as f64;
let recv = self.bytes_recv.swap(0, Ordering::Relaxed) as f64;
let new_send_rate = sent / elapsed;
let new_recv_rate = recv / elapsed;
let alpha = super::EMA_ALPHA;
let mut send_rate = self.send_rate.write();
let mut recv_rate = self.recv_rate.write();
*send_rate = alpha * new_send_rate + (1.0 - alpha) * *send_rate;
*recv_rate = alpha * new_recv_rate + (1.0 - alpha) * *recv_rate;
*last = now;
}
fn send_bandwidth(&self) -> Bandwidth {
Bandwidth::from_bps(*self.send_rate.read())
}
fn recv_bandwidth(&self) -> Bandwidth {
Bandwidth::from_bps(*self.recv_rate.read())
}
}
#[derive(Debug)]
struct LossTracker {
packets_sent: AtomicU64,
packets_acked: AtomicU64,
packets_lost: AtomicU64,
window: RwLock<VecDeque<(u64, u64, u64)>>, window_size: usize,
}
impl LossTracker {
fn new(window_size: usize) -> Self {
Self {
packets_sent: AtomicU64::new(0),
packets_acked: AtomicU64::new(0),
packets_lost: AtomicU64::new(0),
window: RwLock::new(VecDeque::with_capacity(window_size)),
window_size,
}
}
fn record_sent(&self) {
self.packets_sent.fetch_add(1, Ordering::Relaxed);
}
fn record_acked(&self) {
self.packets_acked.fetch_add(1, Ordering::Relaxed);
}
fn record_lost(&self) {
self.packets_lost.fetch_add(1, Ordering::Relaxed);
}
fn snapshot_window(&self) {
let sent = self.packets_sent.swap(0, Ordering::Relaxed);
let acked = self.packets_acked.swap(0, Ordering::Relaxed);
let lost = self.packets_lost.swap(0, Ordering::Relaxed);
let mut window = self.window.write();
if window.len() >= self.window_size {
window.pop_front();
}
window.push_back((sent, acked, lost));
}
fn loss_ratio(&self) -> f64 {
let window = self.window.read();
let total_sent: u64 = window.iter().map(|(s, _, _)| s).sum();
let total_lost: u64 = window.iter().map(|(_, _, l)| l).sum();
if total_sent == 0 {
0.0
} else {
total_lost as f64 / total_sent as f64
}
}
}
pub struct Uplink {
config: UplinkConfig,
numeric_id: u16,
state: RwLock<UplinkState>,
rtt: RwLock<RttTracker>,
bandwidth: BandwidthEstimator,
loss: LossTracker,
stats: RwLock<TrafficStats>,
transport: RwLock<Option<Arc<dyn Transport>>>,
noise_session: Mutex<Option<NoiseSession>>,
priority_score: AtomicU32,
in_flight: AtomicU32,
cwnd: AtomicU32,
nat_state: RwLock<UplinkNatState>,
}
impl Uplink {
pub fn new(config: UplinkConfig, numeric_id: u16) -> Self {
Self {
config,
numeric_id,
state: RwLock::new(UplinkState::default()),
rtt: RwLock::new(RttTracker::new(100)),
bandwidth: BandwidthEstimator::new(),
loss: LossTracker::new(10),
stats: RwLock::new(TrafficStats::default()),
transport: RwLock::new(None),
noise_session: Mutex::new(None),
priority_score: AtomicU32::new(0),
in_flight: AtomicU32::new(0),
cwnd: AtomicU32::new(10), nat_state: RwLock::new(UplinkNatState::default()),
}
}
pub fn id(&self) -> &UplinkId {
&self.config.id
}
pub fn numeric_id(&self) -> u16 {
self.numeric_id
}
pub fn config(&self) -> &UplinkConfig {
&self.config
}
pub fn state(&self) -> UplinkState {
let state = self.state.read();
UplinkState {
connection_state: state.connection_state,
health: state.health,
last_activity: state.last_activity,
last_send: state.last_send,
last_recv: state.last_recv,
consecutive_failures: state.consecutive_failures,
last_failure: state.last_failure,
last_error: state.last_error.clone(),
}
}
pub fn health(&self) -> UplinkHealth {
self.state.read().health
}
pub fn is_usable(&self) -> bool {
let state = self.state.read();
self.config.enabled
&& state.connection_state == ConnectionState::Connected
&& state.health.is_usable()
}
pub fn priority_score(&self) -> u32 {
self.priority_score.load(Ordering::Relaxed)
}
pub fn rtt(&self) -> Duration {
self.rtt.read().smoothed()
}
pub fn bandwidth(&self) -> Bandwidth {
self.bandwidth.recv_bandwidth()
}
pub fn loss_ratio(&self) -> f64 {
self.loss.loss_ratio()
}
pub fn stats(&self) -> TrafficStats {
*self.stats.read()
}
pub fn cwnd(&self) -> u32 {
self.cwnd.load(Ordering::Relaxed)
}
pub fn in_flight(&self) -> u32 {
self.in_flight.load(Ordering::Relaxed)
}
pub fn can_send(&self) -> bool {
self.in_flight.load(Ordering::Relaxed) < self.cwnd.load(Ordering::Relaxed)
}
pub fn set_transport(&self, transport: Arc<dyn Transport>) {
*self.transport.write() = Some(transport);
}
pub fn get_transport(&self) -> Option<Arc<dyn Transport>> {
self.transport.read().clone()
}
pub fn set_noise_session(&self, session: NoiseSession) {
*self.noise_session.lock() = Some(session);
}
pub async fn send_raw(&self, data: &[u8]) -> crate::error::Result<usize> {
let transport = self
.get_transport()
.ok_or_else(|| crate::error::TransportError::SendFailed("no transport".into()))?;
let len = transport.send(data).await?;
self.record_send(len);
Ok(len)
}
pub async fn send_encrypted(&self, data: &[u8]) -> crate::error::Result<usize> {
let ciphertext = {
let mut noise = self.noise_session.lock();
let session = noise.as_mut().ok_or_else(|| {
crate::error::CryptoError::NoiseProtocol("no noise session".into())
})?;
if !session.is_transport() {
return Err(crate::error::CryptoError::NoiseProtocol(
"handshake not complete".into(),
)
.into());
}
session.encrypt(data)?
};
let transport = self
.get_transport()
.ok_or_else(|| crate::error::TransportError::SendFailed("no transport".into()))?;
transport.send(&ciphertext).await?;
self.record_send(ciphertext.len());
Ok(data.len())
}
pub async fn recv(&self, buf: &mut [u8]) -> crate::error::Result<usize> {
let transport = self
.get_transport()
.ok_or_else(|| crate::error::TransportError::ReceiveFailed("no transport".into()))?;
let len = transport.recv(buf).await?;
self.record_recv(len);
Ok(len)
}
pub async fn recv_from(
&self,
buf: &mut [u8],
) -> crate::error::Result<(usize, std::net::SocketAddr)> {
let transport = self
.get_transport()
.ok_or_else(|| crate::error::TransportError::ReceiveFailed("no transport".into()))?;
let (len, addr) = transport.recv_from(buf).await?;
self.record_recv(len);
Ok((len, addr))
}
pub fn decrypt(&self, ciphertext: &[u8]) -> crate::error::Result<Vec<u8>> {
let mut noise = self.noise_session.lock();
let session = noise
.as_mut()
.ok_or_else(|| crate::error::CryptoError::NoiseProtocol("no noise session".into()))?;
if !session.is_transport() {
return Err(
crate::error::CryptoError::NoiseProtocol("handshake not complete".into()).into(),
);
}
Ok(session.decrypt(ciphertext)?)
}
pub fn is_noise_ready(&self) -> bool {
self.noise_session
.lock()
.as_ref()
.is_some_and(crate::crypto::NoiseSession::is_transport)
}
pub fn encrypt(&self, data: &[u8]) -> crate::error::Result<Vec<u8>> {
let mut noise = self.noise_session.lock();
let session = noise
.as_mut()
.ok_or_else(|| crate::error::CryptoError::NoiseProtocol("no noise session".into()))?;
if !session.is_transport() {
return Err(
crate::error::CryptoError::NoiseProtocol("handshake not complete".into()).into(),
);
}
Ok(session.encrypt(data)?)
}
pub fn write_handshake(&self, payload: &[u8]) -> crate::error::Result<Vec<u8>> {
let mut noise = self.noise_session.lock();
let session = noise
.as_mut()
.ok_or_else(|| crate::error::CryptoError::NoiseProtocol("no noise session".into()))?;
Ok(session.write_handshake(payload)?)
}
pub fn read_handshake(&self, message: &[u8]) -> crate::error::Result<Vec<u8>> {
let mut noise = self.noise_session.lock();
let session = noise
.as_mut()
.ok_or_else(|| crate::error::CryptoError::NoiseProtocol("no noise session".into()))?;
Ok(session.read_handshake(message)?)
}
pub fn set_connection_state(&self, state: ConnectionState) {
let mut s = self.state.write();
s.connection_state = state;
s.last_activity = Instant::now();
if state == ConnectionState::Connected {
s.consecutive_failures = 0;
if s.health == UplinkHealth::Down || s.health == UplinkHealth::Unknown {
s.health = UplinkHealth::Healthy;
}
} else if state == ConnectionState::Failed {
s.health = UplinkHealth::Down;
}
}
pub fn record_send(&self, bytes: usize) {
{
let mut uplink_state = self.state.write();
uplink_state.last_send = Some(Instant::now());
uplink_state.last_activity = Instant::now();
}
self.bandwidth.record_sent(bytes as u64);
self.loss.record_sent();
self.in_flight.fetch_add(1, Ordering::Relaxed);
let mut traffic = self.stats.write();
traffic.bytes_sent += bytes as u64;
traffic.packets_sent += 1;
}
pub fn record_recv(&self, bytes: usize) {
{
let mut uplink_state = self.state.write();
uplink_state.last_recv = Some(Instant::now());
uplink_state.last_activity = Instant::now();
}
self.bandwidth.record_recv(bytes as u64);
let mut traffic = self.stats.write();
traffic.bytes_received += bytes as u64;
traffic.packets_received += 1;
}
pub fn record_rtt(&self, rtt: Duration) {
self.rtt.write().add_sample(rtt);
self.loss.record_acked();
self.in_flight.fetch_sub(1, Ordering::Relaxed);
self.update_priority();
}
pub fn record_loss(&self) {
self.loss.record_lost();
self.in_flight.fetch_sub(1, Ordering::Relaxed);
let mut stats = self.stats.write();
stats.packets_dropped += 1;
let cwnd = self.cwnd.load(Ordering::Relaxed);
self.cwnd.store((cwnd / 2).max(2), Ordering::Relaxed);
self.update_health();
}
pub fn record_failure(&self, error: &str) {
let mut state = self.state.write();
state.consecutive_failures += 1;
state.last_failure = Some(Instant::now());
state.last_error = Some(error.to_string());
state.health = match state.consecutive_failures {
0..=2 => UplinkHealth::Healthy,
3..=5 => UplinkHealth::Degraded,
6..=10 => UplinkHealth::Unhealthy,
_ => UplinkHealth::Down,
};
}
pub fn record_success(&self) {
let mut state = self.state.write();
state.consecutive_failures = 0;
state.last_activity = Instant::now();
let cwnd = self.cwnd.load(Ordering::Relaxed);
self.cwnd.store((cwnd + 1).min(1000), Ordering::Relaxed);
if state.health == UplinkHealth::Degraded || state.health == UplinkHealth::Unhealthy {
state.health = UplinkHealth::Healthy;
}
}
fn update_priority(&self) {
let base = if self.config.priority_override > 0 {
self.config.priority_override
} else {
self.config.interface_type.base_priority()
};
let health_mod = self.state.read().health.priority_modifier();
let rtt_score = self.rtt_score();
let loss_score = self.loss_score();
let score = (f64::from(base) * health_mod * rtt_score * loss_score) as u32;
self.priority_score.store(score, Ordering::Relaxed);
}
fn rtt_score(&self) -> f64 {
let rtt = self.rtt.read().smoothed();
if rtt == Duration::ZERO {
return 1.0;
}
let rtt_ms = rtt.as_secs_f64() * 1000.0;
1.0 / (1.0 + rtt_ms / 100.0)
}
fn loss_score(&self) -> f64 {
let loss = self.loss.loss_ratio();
1.0 - loss.min(1.0)
}
fn update_health(&self) {
let loss = self.loss.loss_ratio();
let rtt = self.rtt.read().smoothed();
let mut state = self.state.write();
let new_health = if loss > 0.3 || rtt > Duration::from_secs(5) {
UplinkHealth::Unhealthy
} else if loss > 0.1 || rtt > Duration::from_secs(1) {
UplinkHealth::Degraded
} else if state.consecutive_failures > 5 {
UplinkHealth::Unhealthy
} else {
UplinkHealth::Healthy
};
state.health = new_health;
}
pub fn periodic_update(&self) {
self.bandwidth.update();
self.loss.snapshot_window();
self.update_priority();
self.update_health();
let state = self.state.read();
if state.connection_state == ConnectionState::Connected {
if let Some(last_recv) = state.last_recv {
if last_recv.elapsed() > super::DEFAULT_UPLINK_TIMEOUT {
drop(state);
self.record_failure("receive timeout");
}
}
}
}
pub fn quality_metrics(&self) -> QualityMetrics {
let rtt = self.rtt.read();
QualityMetrics {
rtt: rtt.smoothed(),
rtt_variance: rtt.variance(),
latency: rtt.get_latency(),
bandwidth_up: self.bandwidth.send_bandwidth(),
bandwidth_down: self.bandwidth.recv_bandwidth(),
packet_loss: self.loss.loss_ratio(),
jitter: rtt.variance(),
health: self.state.read().health,
}
}
pub fn connection_params(&self) -> ConnectionParams {
let rtt = self.rtt.read();
let state = self.state.read();
let stats = *self.stats.read();
let bandwidth_up = self.bandwidth.send_bandwidth();
let bandwidth_down = self.bandwidth.recv_bandwidth();
let smoothed_rtt = rtt.smoothed();
let loss = self.loss.loss_ratio();
let bdp = if smoothed_rtt > Duration::ZERO {
(bandwidth_down.bytes_per_sec * smoothed_rtt.as_secs_f64()) as u64
} else {
0
};
let loss_factor = (1.0 - loss).max(0.01);
let effective_throughput = bandwidth_down.bytes_per_sec * loss_factor;
let time_for_1mb = if effective_throughput > 0.0 {
let secs =
((1024.0 * 1024.0) / effective_throughput + smoothed_rtt.as_secs_f64()).min(3600.0);
Duration::from_secs_f64(secs)
} else {
Duration::from_secs(3600) };
let total_sent = stats.bytes_sent.max(1);
let retransmission_rate =
stats.packets_retransmitted as f64 / stats.packets_sent.max(1) as f64;
let goodput = bandwidth_down.bytes_per_sec * (1.0 - retransmission_rate);
ConnectionParams {
uplink_id: self.numeric_id,
interface_type: self.config.interface_type,
protocol: self.config.protocol,
rtt: smoothed_rtt,
rtt_variance: rtt.variance(),
rtt_min: rtt.samples.front().copied().unwrap_or(Duration::ZERO),
jitter: rtt.variance(),
bandwidth_up,
bandwidth_down,
effective_throughput: Bandwidth::from_bps(effective_throughput),
goodput: Bandwidth::from_bps(goodput),
max_bandwidth_mbps: self.config.max_bandwidth_mbps,
bdp,
cwnd: self.cwnd.load(Ordering::Relaxed),
in_flight: self.in_flight.load(Ordering::Relaxed),
packet_loss: loss,
retransmission_rate,
health: state.health,
consecutive_failures: state.consecutive_failures,
is_usable: self.config.enabled
&& state.connection_state == ConnectionState::Connected
&& state.health.is_usable(),
is_natted: self.nat_state.read().is_natted(),
nat_type: self.nat_state.read().nat_type(),
time_for_1mb,
priority_score: self.priority_score.load(Ordering::Relaxed),
bytes_sent: stats.bytes_sent,
bytes_received: stats.bytes_received,
packets_sent: stats.packets_sent,
packets_received: stats.packets_received,
packets_dropped: stats.packets_dropped,
packets_retransmitted: stats.packets_retransmitted,
}
}
pub fn send_bandwidth(&self) -> Bandwidth {
self.bandwidth.send_bandwidth()
}
pub fn recv_bandwidth(&self) -> Bandwidth {
self.bandwidth.recv_bandwidth()
}
pub fn rtt_variance(&self) -> Duration {
self.rtt.read().variance()
}
pub fn is_natted(&self) -> bool {
self.nat_state.read().is_natted()
}
pub fn nat_type(&self) -> super::nat::NatType {
self.nat_state.read().nat_type()
}
pub fn external_addr(&self) -> Option<std::net::SocketAddr> {
self.nat_state.read().external_addr()
}
pub fn nat_detection_state(&self) -> super::nat::NatDetectionState {
self.nat_state.read().detection_state().clone()
}
pub fn update_nat_state<F, R>(&self, f: F) -> R
where
F: FnOnce(&mut UplinkNatState) -> R,
{
f(&mut self.nat_state.write())
}
pub fn reset_nat_state(&self) {
self.nat_state.write().reset();
}
}
#[allow(clippy::missing_fields_in_debug)]
impl std::fmt::Debug for Uplink {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let state = self.state.read();
f.debug_struct("Uplink")
.field("id", &self.config.id)
.field("numeric_id", &self.numeric_id)
.field("state", &state.connection_state)
.field("health", &state.health)
.field("rtt", &self.rtt())
.field("loss", &format!("{:.1}%", self.loss_ratio() * 100.0))
.field("priority", &self.priority_score())
.finish()
}
}