chore: initial sanitized public snapshot
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user