Skip to content

Commit

Permalink
Display upload speed in Web UI
Browse files Browse the repository at this point in the history
  • Loading branch information
ikatson committed Dec 5, 2023
1 parent 4784f3f commit 80df2c1
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 55 deletions.
25 changes: 18 additions & 7 deletions crates/librqbit/src/torrent_state/live/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ pub struct TorrentStateLive {
cancel_tx: tokio::sync::watch::Sender<()>,
cancel_rx: tokio::sync::watch::Receiver<()>,

speed_estimator: SpeedEstimator,
down_speed_estimator: SpeedEstimator,
up_speed_estimator: SpeedEstimator,
}

impl TorrentStateLive {
Expand All @@ -198,7 +199,8 @@ impl TorrentStateLive {
) -> Arc<Self> {
let (peer_queue_tx, peer_queue_rx) = unbounded_channel();

let speed_estimator = SpeedEstimator::new(5);
let down_speed_estimator = SpeedEstimator::new(5);
let up_speed_estimator = SpeedEstimator::new(5);

let have_bytes = paused.have_bytes;
let needed_bytes = paused.info.lengths.total_length() - have_bytes;
Expand All @@ -225,7 +227,8 @@ impl TorrentStateLive {
peer_semaphore: Semaphore::new(128),
peer_queue_tx,
finished_notify: Notify::new(),
speed_estimator,
down_speed_estimator,
up_speed_estimator,
cancel_rx,
cancel_tx,
});
Expand All @@ -249,6 +252,7 @@ impl TorrentStateLive {
Some(state) => state,
None => return Ok(()),
};
let now = Instant::now();
let stats = state.stats_snapshot();
let fetched = stats.fetched_bytes;
let needed = state.initially_needed();
Expand All @@ -257,8 +261,11 @@ impl TorrentStateLive {
.wrapping_sub(fetched)
.min(needed - stats.downloaded_and_checked_bytes);
state
.speed_estimator
.add_snapshot(fetched, remaining, Instant::now());
.down_speed_estimator
.add_snapshot(fetched, Some(remaining), now);
state
.up_speed_estimator
.add_snapshot(stats.uploaded_bytes, None, now);
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
Expand Down Expand Up @@ -291,8 +298,12 @@ impl TorrentStateLive {
});
}

pub fn speed_estimator(&self) -> &SpeedEstimator {
&self.speed_estimator
pub fn down_speed_estimator(&self) -> &SpeedEstimator {
&self.down_speed_estimator
}

pub fn up_speed_estimator(&self) -> &SpeedEstimator {
&self.up_speed_estimator
}

async fn tracker_one_request(&self, tracker_url: Url) -> anyhow::Result<u64> {
Expand Down
14 changes: 10 additions & 4 deletions crates/librqbit/src/torrent_state/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,35 @@ pub struct LiveStats {
pub snapshot: StatsSnapshot,
pub average_piece_download_time: Option<Duration>,
pub download_speed: Speed,
pub upload_speed: Speed,
pub time_remaining: Option<DurationWithHumanReadable>,
}

impl std::fmt::Display for LiveStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "down speed: {}", self.download_speed)?;
if let Some(time_remaining) = &self.time_remaining {
write!(f, " eta: {time_remaining}")?;
write!(f, ", eta: {time_remaining}")?;
}
write!(f, ", up speed: {}", self.upload_speed)?;
Ok(())
}
}

impl From<&TorrentStateLive> for LiveStats {
fn from(live: &TorrentStateLive) -> Self {
let snapshot = live.stats_snapshot();
let estimator = live.speed_estimator();
let down_estimator = live.down_speed_estimator();
let up_estimator = live.up_speed_estimator();

Self {
average_piece_download_time: snapshot.average_piece_download_time(),
snapshot,
download_speed: estimator.download_mbps().into(),
time_remaining: estimator.time_remaining().map(DurationWithHumanReadable),
download_speed: down_estimator.mbps().into(),
upload_speed: up_estimator.mbps().into(),
time_remaining: down_estimator
.time_remaining()
.map(DurationWithHumanReadable),
}
}
}
Expand Down
18 changes: 9 additions & 9 deletions crates/librqbit/webui/dist/assets/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/librqbit/webui/dist/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"src": "assets/logo.svg"
},
"index.html": {
"file": "assets/index-6d4556f3.js",
"file": "assets/index-713a95fc.js",
"isEntry": true,
"src": "index.html"
}
Expand Down
11 changes: 7 additions & 4 deletions crates/librqbit/webui/src/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export interface ListTorrentsResponse {
torrents: Array<TorrentId>;
}

export interface Speed {
mbps: number;
human_readable: string;
}

// Interface for the Torrent Stats API response
export interface LiveTorrentStats {
snapshot: {
Expand All @@ -52,10 +57,8 @@ export interface LiveTorrentStats {
secs: number;
nanos: number;
};
download_speed: {
mbps: number;
human_readable: string;
};
download_speed: Speed;
upload_speed: Speed;
all_time_download_speed: {
mbps: number;
human_readable: string;
Expand Down
38 changes: 24 additions & 14 deletions crates/librqbit/webui/src/rqbit-web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,27 @@ const TorrentActions: React.FC<{
</Row>
}

const Speed: React.FC<{ statsResponse: TorrentStats }> = ({ statsResponse }) => {
switch (statsResponse.state) {
case STATE_PAUSED: return 'Paused';
case STATE_INITIALIZING: return 'Checking files';
case STATE_ERROR: return 'Error';
}
// Unknown state
if (statsResponse.state != 'live' || statsResponse.live === null) {
return statsResponse.state;
}

return <>
{!statsResponse.finished && <p>{statsResponse.live.download_speed.human_readable}</p>}
<p>{statsResponse.live.upload_speed.human_readable}</p>
</>

if (statsResponse.finished) {
return <span>Completed</span>;
}
}

const TorrentRow: React.FC<{
id: number,
detailsResponse: TorrentDetails | null,
Expand All @@ -208,19 +229,6 @@ const TorrentRow: React.FC<{
return `${peer_stats.live} / ${peer_stats.seen}`;
}

const formatDownloadSpeed = () => {
if (finished) {
return 'Completed';
}
switch (state) {
case STATE_PAUSED: return 'Paused';
case STATE_INITIALIZING: return 'Checking files';
case STATE_ERROR: return 'Error';
}

return statsResponse?.live?.download_speed.human_readable ?? "N/A";
}

let classNames = [];

if (error) {
Expand Down Expand Up @@ -253,7 +261,9 @@ const TorrentRow: React.FC<{
animated={isAnimated}
variant={progressBarVariant} />
</Column>
<Column size={2} label="Down Speed">{formatDownloadSpeed()}</Column>
<Column size={2} label="Speed">
<Speed statsResponse={statsResponse} />
</Column>
<Column label="ETA">{getCompletionETA(statsResponse)}</Column>
<Column size={2} label="Peers">{formatPeersString()}</Column >
<Column label="Actions">
Expand Down
32 changes: 18 additions & 14 deletions crates/librqbit_core/src/speed_estimator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ use parking_lot::Mutex;

#[derive(Clone, Copy)]
struct ProgressSnapshot {
downloaded_bytes: u64,
progress_bytes: u64,
instant: Instant,
}

/// Estimates download speed in a sliding time window.
/// Estimates download/upload speed in a sliding time window.
pub struct SpeedEstimator {
latest_per_second_snapshots: Mutex<VecDeque<ProgressSnapshot>>,
download_bytes_per_second: AtomicU64,
bytes_per_second: AtomicU64,
time_remaining_millis: AtomicU64,
}

Expand All @@ -24,7 +24,7 @@ impl SpeedEstimator {
assert!(window_seconds > 1);
Self {
latest_per_second_snapshots: Mutex::new(VecDeque::with_capacity(window_seconds)),
download_bytes_per_second: Default::default(),
bytes_per_second: Default::default(),
time_remaining_millis: Default::default(),
}
}
Expand All @@ -37,20 +37,25 @@ impl SpeedEstimator {
Some(Duration::from_millis(tr))
}

pub fn download_bps(&self) -> u64 {
self.download_bytes_per_second.load(Ordering::Relaxed)
pub fn bps(&self) -> u64 {
self.bytes_per_second.load(Ordering::Relaxed)
}

pub fn download_mbps(&self) -> f64 {
self.download_bps() as f64 / 1024f64 / 1024f64
pub fn mbps(&self) -> f64 {
self.bps() as f64 / 1024f64 / 1024f64
}

pub fn add_snapshot(&self, downloaded_bytes: u64, remaining_bytes: u64, instant: Instant) {
pub fn add_snapshot(
&self,
progress_bytes: u64,
remaining_bytes: Option<u64>,
instant: Instant,
) {
let first = {
let mut g = self.latest_per_second_snapshots.lock();

let current = ProgressSnapshot {
downloaded_bytes,
progress_bytes,
instant,
};

Expand All @@ -67,19 +72,18 @@ impl SpeedEstimator {
}
};

let downloaded_bytes_diff = downloaded_bytes - first.downloaded_bytes;
let downloaded_bytes_diff = progress_bytes - first.progress_bytes;
let elapsed = instant - first.instant;
let bps = downloaded_bytes_diff as f64 / elapsed.as_secs_f64();

let time_remaining_millis_rounded: u64 = if downloaded_bytes_diff > 0 {
let time_remaining_secs = remaining_bytes as f64 / bps;
let time_remaining_secs = remaining_bytes.unwrap_or_default() as f64 / bps;
(time_remaining_secs * 1000f64) as u64
} else {
0
};
self.time_remaining_millis
.store(time_remaining_millis_rounded, Ordering::Relaxed);
self.download_bytes_per_second
.store(bps as u64, Ordering::Relaxed);
self.bytes_per_second.store(bps as u64, Ordering::Relaxed);
}
}
4 changes: 2 additions & 2 deletions crates/rqbit/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ async fn async_main(opts: Opts) -> anyhow::Result<()> {
None => continue
};
let stats = handle.stats_snapshot();
let speed = handle.speed_estimator();
let speed = handle.down_speed_estimator();
let total = stats.total_bytes;
let progress = stats.total_bytes - stats.remaining_bytes;
let downloaded_pct = if stats.remaining_bytes == 0 {
Expand All @@ -390,7 +390,7 @@ async fn async_main(opts: Opts) -> anyhow::Result<()> {
idx,
downloaded_pct,
SF::new(progress),
speed.download_mbps(),
speed.mbps(),
SF::new(stats.fetched_bytes),
SF::new(stats.remaining_bytes),
SF::new(total),
Expand Down

0 comments on commit 80df2c1

Please sign in to comment.