Files
aria2-rust-pro/crates/aria2-rust-pro-core/src/progress.rs
T

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);
}
}