Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions core/src/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,8 @@ impl WorkflowStep<LaunchFlowState, ()> for DownloadStep {
let status = Status::State {
step: Step::Downloading {
progress: 0,
bytes_per_second: 0.0,
time_remaining: None,
build_type: mode,
},
};
Expand Down Expand Up @@ -359,8 +361,7 @@ impl WorkflowStep<LaunchFlowState, ()> for InstallStep {
async fn is_complete(&self, state: Arc<Mutex<LaunchFlowState>>) -> Result<bool> {
let guard = state.lock().await;

Ok(guard.recent_download.is_none()
&& installs::explorer_latest_version_path().exists())
Ok(guard.recent_download.is_none() && installs::explorer_latest_version_path().exists())
}

fn start_label(&self) -> Result<Status> {
Expand All @@ -377,7 +378,7 @@ impl WorkflowStep<LaunchFlowState, ()> for InstallStep {
state: Arc<Mutex<LaunchFlowState>>,
) -> StepResult {
let recent_download = Self::recent_download_and_update_state(state).await;

if let Some(download) = recent_download {
let version = download.version.clone();
self.analytics
Expand Down
1 change: 1 addition & 0 deletions core/src/installs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use std::thread;
use std::time::Duration;

pub mod compression;
pub mod download_speed_estimator;
pub mod downloads;

const APP_NAME: &str = "DecentralandLauncherLight";
Expand Down
84 changes: 84 additions & 0 deletions core/src/installs/download_speed_estimator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Generated by Claude.

use core::f64;
use std::time::Duration;

pub struct DownloadSpeedEstimator {
/// Smoothed bytes per second (exponential moving average).
bytes_per_second: f64,
/// EMA smoothing factor in 0..=1. Lower values = smoother.
alpha: f64,
}

impl Default for DownloadSpeedEstimator {
fn default() -> Self {
Self::new(0.1)
}
}

impl DownloadSpeedEstimator {
/// Create a new estimator.
/// `alpha` controls how quickly the estimate reacts to new samples.
/// A value around 0.2–0.3 works well for download progress bars.
const fn new(alpha: f64) -> Self {
Self {
bytes_per_second: 0.0,
alpha: alpha.clamp(0.0, 1.0),
}
}

/// Feed a sample of (`bytes_downloaded`, `time_passed`) for the most recent interval.
/// `time_passed` is in seconds.
fn update(&mut self, bytes_downloaded: u64, time_passed: Duration) -> Result<(), Error> {
if time_passed <= Duration::ZERO {
return Err(Error::TimeIsNotPositive);
}

let bytes_downloaded = u64_to_f64_lossy(bytes_downloaded);
let sample_bps = bytes_downloaded / time_passed.as_secs_f64();

if self.bytes_per_second == 0.0 {
// First sample — seed the estimate directly.
self.bytes_per_second = sample_bps;
} else {
self.bytes_per_second =
sample_bps.mul_add(self.alpha, self.bytes_per_second * (1.0 - self.alpha));
}

Ok(())
}

pub fn try_update(&mut self, bytes_downloaded: u64, time_passed: Duration) {
if let Err(e) = self.update(bytes_downloaded, time_passed) {
log::error!("Cannot update estimator: {:?}", e);
}
}

/// Current smoothed bytes-per-second estimate.
pub const fn bytes_per_second(&self) -> f64 {
Comment thread
NickKhalow marked this conversation as resolved.
self.bytes_per_second
}

/// Estimated milliseconds remaining to download `bytes_remaining`.
pub fn time_remaining(&self, bytes_remaining: u64) -> Option<f64> {
if self.bytes_per_second <= 0.0 {
return None;
}

let bytes_remaining = u64_to_f64_lossy(bytes_remaining);
let seconds_remaining = bytes_remaining / self.bytes_per_second;
Some(seconds_remaining * 1000.0)
}
}

// It's okay here, because we don't need exact estimation
#[allow(clippy::cast_precision_loss)]
#[inline]
const fn u64_to_f64_lossy(x: u64) -> f64 {
x as f64
}

#[derive(Debug)]
pub enum Error {
TimeIsNotPositive,
}
22 changes: 20 additions & 2 deletions core/src/installs/downloads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::time::Duration;
use crate::analytics::Analytics;
use crate::analytics::event::Event;
use crate::channel::EventChannel;
use crate::installs::download_speed_estimator::DownloadSpeedEstimator;
use crate::types::{BuildType, Status, Step};
use anyhow::Context;
use std::sync::Arc;
Expand Down Expand Up @@ -102,7 +103,7 @@ pub async fn download_file<T: EventChannel>(
let client = Client::new();

let res = client.get(url).send().await?;
let total_size =
let total_size: u64 =
res.content_length()
.ok_or_else(|| DownloadFileError::ContentLengthNotFound {
url: url.to_owned(),
Expand All @@ -113,6 +114,7 @@ pub async fn download_file<T: EventChannel>(
let duration = std::time::Duration::from_millis(500);
let mut tasks = Vec::new();

let mut bytes_per_interval: u64 = 0;
let mut downloaded: u64 = 0;
{
let mut file =
Expand All @@ -122,13 +124,21 @@ pub async fn download_file<T: EventChannel>(
})?;
let mut stream = res.bytes_stream();

let mut estimator = DownloadSpeedEstimator::default();

loop {
match timeout(Duration::from_secs(15), stream.next()).await {
Ok(Some(item)) => {
let chunk = item?;

// practially it's safe to convert to u64 on 32 and 64 bits platforms
let chunk_len: u64 =
u64::try_from(chunk.len()).context("cannot convert usize to u64")?;

file.write_all(&chunk)?;

let new = min(downloaded.saturating_add(chunk.len() as u64), total_size);
bytes_per_interval = bytes_per_interval.saturating_add(chunk_len);
let new = min(downloaded.saturating_add(chunk_len), total_size);
downloaded = new;

let should_send = match last_analytics_time {
Expand All @@ -145,6 +155,9 @@ pub async fn download_file<T: EventChannel>(
total_size,
));
tasks.push(task);

estimator.try_update(bytes_per_interval, duration);
bytes_per_interval = 0;
}

#[allow(
Expand All @@ -155,9 +168,14 @@ pub async fn download_file<T: EventChannel>(
)]
let progress: u8 = ((downloaded as f64 / total_size as f64) * 100.0) as u8;

let time_remaining =
estimator.time_remaining(total_size.saturating_sub(downloaded));

let event: Status = Status::State {
step: Step::Downloading {
progress,
bytes_per_second: estimator.bytes_per_second(),
time_remaining,
build_type: build_type.clone(),
},
};
Expand Down
15 changes: 13 additions & 2 deletions core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ pub enum Step {
#[serde(rename_all = "camelCase")]
Fetching,
#[serde(rename_all = "camelCase")]
Downloading { progress: u8, build_type: BuildType },
Downloading {
progress: u8,
bytes_per_second: f64,
/// In milliseconds because that's what JavaScript's `Date` uses.
time_remaining: Option<f64>,
build_type: BuildType,
},
#[serde(rename_all = "camelCase")]
Installing { build_type: BuildType },
#[serde(rename_all = "camelCase")]
Expand All @@ -30,7 +36,12 @@ pub enum Step {
#[serde(rename_all = "camelCase", tag = "event", content = "data")]
pub enum LauncherUpdate {
CheckingForUpdate,
Downloading { progress: Option<u8> },
Downloading {
progress: Option<u8>,
bytes_per_second: f64,
/// In milliseconds because that's what JavaScript's `Date` uses.
time_remaining: Option<f64>,
Comment thread
AnsisMalins marked this conversation as resolved.
},
DownloadFinished,
InstallingUpdate,
RestartingApp,
Expand Down
Loading
Loading