333 lines
11 KiB
Rust
333 lines
11 KiB
Rust
//! Progress snapshots and aggregate statistics exposed by the core runtime.
|
|
|
|
use crate::request::{DownloadId, DownloadStatus};
|
|
|
|
/// High-level work state used for coarse progress reporting.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum WorkState {
|
|
/// Work is planned but not yet implemented.
|
|
Planned,
|
|
/// Work has been implemented.
|
|
Implemented,
|
|
/// Work has been verified.
|
|
Verified,
|
|
}
|
|
|
|
/// Coarse progress information for a multi-phase goal.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct GoalProgress {
|
|
/// Overall completion percent across the whole goal.
|
|
overall_percent: u8,
|
|
/// Human-readable name of the current phase.
|
|
phase_name: String,
|
|
/// Completion percent within the current phase.
|
|
phase_percent: u8,
|
|
/// Coarse progress state for the current phase.
|
|
state: WorkState,
|
|
}
|
|
|
|
impl GoalProgress {
|
|
/// Creates a new progress tracker for the given phase.
|
|
#[must_use]
|
|
pub fn new(phase_name: impl Into<String>) -> Self {
|
|
Self {
|
|
overall_percent: 1,
|
|
phase_name: phase_name.into(),
|
|
phase_percent: 20,
|
|
state: WorkState::Planned,
|
|
}
|
|
}
|
|
|
|
/// Returns the overall completion percentage.
|
|
#[must_use]
|
|
pub const fn overall_percent(&self) -> u8 {
|
|
self.overall_percent
|
|
}
|
|
|
|
/// Returns the current phase name.
|
|
#[must_use]
|
|
pub fn phase_name(&self) -> &str {
|
|
&self.phase_name
|
|
}
|
|
|
|
/// Returns the current phase completion percentage.
|
|
#[must_use]
|
|
pub const fn phase_percent(&self) -> u8 {
|
|
self.phase_percent
|
|
}
|
|
|
|
/// Returns the coarse work state.
|
|
#[must_use]
|
|
pub const fn state(&self) -> WorkState {
|
|
self.state
|
|
}
|
|
}
|
|
|
|
/// Aggregated global transfer statistics.
|
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
pub struct GlobalStat {
|
|
/// Aggregate download throughput in bytes per second.
|
|
pub download_speed: u64,
|
|
/// Aggregate upload throughput in bytes per second.
|
|
pub upload_speed: u64,
|
|
/// Number of downloads currently active.
|
|
pub num_active: u32,
|
|
/// Number of downloads queued and waiting.
|
|
pub num_waiting: u32,
|
|
/// Number of downloads stopped without error.
|
|
pub num_stopped: u32,
|
|
/// Number of downloads currently in an error state.
|
|
pub num_error: u32,
|
|
/// Number of downloads completed successfully.
|
|
pub num_complete: u32,
|
|
/// Total tracked payload length across downloads.
|
|
pub total_length: u64,
|
|
/// Total completed payload length across downloads.
|
|
pub completed_length: u64,
|
|
}
|
|
|
|
impl GlobalStat {
|
|
/// Creates an empty statistics snapshot.
|
|
#[must_use]
|
|
pub const fn new() -> Self {
|
|
Self {
|
|
download_speed: 0,
|
|
upload_speed: 0,
|
|
num_active: 0,
|
|
num_waiting: 0,
|
|
num_stopped: 0,
|
|
num_error: 0,
|
|
num_complete: 0,
|
|
total_length: 0,
|
|
completed_length: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Detailed progress snapshot for a single download.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct ProgressSnapshot {
|
|
/// Download id associated with this snapshot.
|
|
pub gid: DownloadId,
|
|
/// Current lifecycle status of the download.
|
|
pub status: DownloadStatus,
|
|
/// Total payload length in bytes.
|
|
pub total_length: u64,
|
|
/// Completed payload length in bytes.
|
|
pub completed_length: u64,
|
|
/// Uploaded payload length in bytes.
|
|
pub upload_length: u64,
|
|
/// Current upload throughput in bytes per second.
|
|
pub upload_speed: u64,
|
|
/// Current download throughput in bytes per second.
|
|
pub download_speed: u64,
|
|
/// Number of active connections assigned to the download.
|
|
pub num_connections: u32,
|
|
/// Estimated seconds remaining when known.
|
|
pub eta_seconds: Option<u64>,
|
|
/// Whether the runtime currently considers the download seeding.
|
|
pub seeding: bool,
|
|
/// Share ratio expressed in milli-units when available.
|
|
pub share_ratio_milli: Option<u64>,
|
|
/// Accumulated share time in seconds when available.
|
|
pub share_time_secs: Option<u64>,
|
|
/// Accumulated seeding time in seconds when available.
|
|
pub seeding_time_secs: Option<u64>,
|
|
/// Total selected `BitTorrent` payload length in bytes.
|
|
pub bt_selected_payload_length: u64,
|
|
/// Remaining selected `BitTorrent` payload length in bytes.
|
|
pub bt_remaining_payload_length: u64,
|
|
/// Whether the torrent has completed selected work and is truly seeding.
|
|
pub bt_true_seeding: bool,
|
|
/// Number of peers in the current swarm snapshot.
|
|
pub bt_total_peers: u32,
|
|
/// Number of peers currently identified as seeders.
|
|
pub bt_seeders: u32,
|
|
/// Number of peers currently identified as leechers.
|
|
pub bt_leechers: u32,
|
|
/// Number of pieces with non-zero availability.
|
|
pub bt_available_pieces: u32,
|
|
/// Number of verified pieces.
|
|
pub bt_verified_pieces: u32,
|
|
/// Number of actively downloading pieces.
|
|
pub bt_downloading_pieces: u32,
|
|
/// Number of queued pieces.
|
|
pub bt_queued_pieces: u32,
|
|
/// Number of missing pieces.
|
|
pub bt_missing_pieces: u32,
|
|
}
|
|
|
|
impl ProgressSnapshot {
|
|
/// Creates an empty progress snapshot for the given download id and status.
|
|
#[must_use]
|
|
pub fn new(gid: DownloadId, status: DownloadStatus) -> Self {
|
|
Self {
|
|
gid,
|
|
status,
|
|
total_length: 0,
|
|
completed_length: 0,
|
|
upload_length: 0,
|
|
upload_speed: 0,
|
|
download_speed: 0,
|
|
num_connections: 0,
|
|
eta_seconds: None,
|
|
seeding: false,
|
|
share_ratio_milli: None,
|
|
share_time_secs: None,
|
|
seeding_time_secs: None,
|
|
bt_selected_payload_length: 0,
|
|
bt_remaining_payload_length: 0,
|
|
bt_true_seeding: false,
|
|
bt_total_peers: 0,
|
|
bt_seeders: 0,
|
|
bt_leechers: 0,
|
|
bt_available_pieces: 0,
|
|
bt_verified_pieces: 0,
|
|
bt_downloading_pieces: 0,
|
|
bt_queued_pieces: 0,
|
|
bt_missing_pieces: 0,
|
|
}
|
|
}
|
|
|
|
/// Returns whether the snapshot has a non-zero payload length.
|
|
#[must_use]
|
|
pub const fn has_payload_length(&self) -> bool {
|
|
self.total_length > 0
|
|
}
|
|
|
|
/// Returns the remaining payload length in bytes.
|
|
#[must_use]
|
|
pub fn remaining_length(&self) -> u64 {
|
|
self.total_length
|
|
.saturating_sub(self.completed_length.min(self.total_length))
|
|
}
|
|
|
|
/// Returns whether the payload transfer is complete.
|
|
#[must_use]
|
|
pub fn transfer_complete(&self) -> bool {
|
|
self.has_payload_length() && self.remaining_length() == 0
|
|
}
|
|
|
|
/// Returns whether the transfer is complete or actively seeding.
|
|
#[must_use]
|
|
pub fn bt_transfer_complete_or_seeding(&self) -> bool {
|
|
self.transfer_complete() || self.seeding
|
|
}
|
|
|
|
/// Returns whether any `BitTorrent` share-runtime data is present.
|
|
#[must_use]
|
|
pub const fn bt_has_share_runtime(&self) -> bool {
|
|
self.share_time_secs.is_some() || self.share_ratio_milli.is_some()
|
|
}
|
|
|
|
/// Returns whether peer or piece-availability activity exists.
|
|
#[must_use]
|
|
pub const fn bt_has_swarm_activity(&self) -> bool {
|
|
self.bt_total_peers > 0 || self.bt_available_pieces > 0
|
|
}
|
|
|
|
/// Returns the total number of active `BitTorrent` pieces.
|
|
#[must_use]
|
|
pub const fn bt_active_piece_count(&self) -> u32 {
|
|
self.bt_downloading_pieces
|
|
.saturating_add(self.bt_queued_pieces)
|
|
}
|
|
|
|
/// Returns whether the selected `BitTorrent` payload is complete.
|
|
#[must_use]
|
|
pub const fn bt_payload_complete(&self) -> bool {
|
|
self.bt_selected_payload_length > 0
|
|
&& self.bt_remaining_payload_length == 0
|
|
&& self.completed_length >= self.bt_selected_payload_length
|
|
}
|
|
|
|
/// Returns completion percent in milli-units.
|
|
#[must_use]
|
|
pub fn completion_percent_milli(&self) -> u64 {
|
|
if self.total_length == 0 {
|
|
return 0;
|
|
}
|
|
self.completed_length
|
|
.min(self.total_length)
|
|
.saturating_mul(1000)
|
|
.checked_div(self.total_length)
|
|
.unwrap_or(0)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn progress_snapshot_new_initializes_bt_share_fields() {
|
|
let snapshot = ProgressSnapshot::new(DownloadId::new(0x42), DownloadStatus::Waiting);
|
|
assert!(!snapshot.seeding);
|
|
assert_eq!(snapshot.share_ratio_milli, None);
|
|
assert_eq!(snapshot.upload_speed, 0);
|
|
assert_eq!(snapshot.share_time_secs, None);
|
|
assert!(!snapshot.bt_has_swarm_activity());
|
|
assert!(!snapshot.bt_has_share_runtime());
|
|
}
|
|
|
|
#[test]
|
|
fn progress_snapshot_bt_completion_semantics_avoid_false_completion() {
|
|
let mut snapshot = ProgressSnapshot::new(DownloadId::new(0x43), DownloadStatus::Active);
|
|
snapshot.total_length = 10_000;
|
|
snapshot.completed_length = 9_000;
|
|
assert_eq!(snapshot.remaining_length(), 1_000);
|
|
assert!(!snapshot.transfer_complete());
|
|
assert!(!snapshot.bt_transfer_complete_or_seeding());
|
|
|
|
snapshot.seeding = true;
|
|
assert!(snapshot.bt_transfer_complete_or_seeding());
|
|
assert!(!snapshot.transfer_complete());
|
|
}
|
|
|
|
#[test]
|
|
fn progress_snapshot_completion_percent_milli_caps_completed_length() {
|
|
let mut snapshot = ProgressSnapshot::new(DownloadId::new(0x44), DownloadStatus::Active);
|
|
snapshot.total_length = 2_000;
|
|
snapshot.completed_length = 2_500;
|
|
assert_eq!(snapshot.remaining_length(), 0);
|
|
assert_eq!(snapshot.completion_percent_milli(), 1000);
|
|
|
|
snapshot.total_length = 0;
|
|
assert_eq!(snapshot.completion_percent_milli(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn progress_snapshot_bt_runtime_metrics_report_activity() {
|
|
let mut snapshot = ProgressSnapshot::new(DownloadId::new(0x45), DownloadStatus::Active);
|
|
snapshot.bt_total_peers = 2;
|
|
snapshot.bt_seeders = 1;
|
|
snapshot.bt_leechers = 1;
|
|
snapshot.bt_available_pieces = 3;
|
|
snapshot.bt_downloading_pieces = 2;
|
|
snapshot.bt_queued_pieces = 1;
|
|
snapshot.bt_missing_pieces = 4;
|
|
|
|
assert!(snapshot.bt_has_swarm_activity());
|
|
assert_eq!(snapshot.bt_active_piece_count(), 3);
|
|
assert_eq!(snapshot.bt_seeders + snapshot.bt_leechers, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn progress_snapshot_bt_share_runtime_helpers_report_true_seeding() {
|
|
let mut snapshot = ProgressSnapshot::new(DownloadId::new(0x46), DownloadStatus::Complete);
|
|
snapshot.completed_length = 4_096;
|
|
snapshot.share_ratio_milli = Some(1250);
|
|
snapshot.share_time_secs = Some(120);
|
|
snapshot.seeding_time_secs = Some(90);
|
|
snapshot.bt_selected_payload_length = 4_096;
|
|
snapshot.bt_remaining_payload_length = 0;
|
|
snapshot.bt_true_seeding = true;
|
|
snapshot.seeding = true;
|
|
|
|
assert!(snapshot.bt_has_share_runtime());
|
|
assert!(snapshot.bt_payload_complete());
|
|
assert!(snapshot.bt_transfer_complete_or_seeding());
|
|
assert!(snapshot.bt_true_seeding);
|
|
}
|
|
}
|