//! 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) -> 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, /// Whether the runtime currently considers the download seeding. pub seeding: bool, /// Share ratio expressed in milli-units when available. pub share_ratio_milli: Option, /// Accumulated share time in seconds when available. pub share_time_secs: Option, /// Accumulated seeding time in seconds when available. pub seeding_time_secs: Option, /// 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); } }