Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
5 changes: 3 additions & 2 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: 0.0,
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 Down
11 changes: 8 additions & 3 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 Expand Up @@ -413,7 +414,9 @@ pub fn install_explorer(version: &str, downloaded_file_path: Option<PathBuf>) ->

// Rename latest back to its version so that cleanup_versions can do its
// job. InstallStep will rename the new newest build to "latest".
if let Ok(v) = latest_version && latest_path.exists() {
if let Ok(v) = latest_version
&& latest_path.exists()
{
fs::rename(latest_path, explorer_path.join(v))?;
}

Expand Down Expand Up @@ -443,8 +446,10 @@ pub fn rename_explorer_to_latest() -> StepResult {
let version_data = get_version_data()?;
let latest_version = get_latest_version(&version_data)?;

fs::rename(explorer_path().join(latest_version),
explorer_latest_version_path())?;
fs::rename(
explorer_path().join(latest_version),
explorer_latest_version_path(),
)?;

Ok(())
}
Expand Down
60 changes: 60 additions & 0 deletions core/src/installs/download_speed_estimator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// 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 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.
pub 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.
pub fn update(&mut self, bytes_downloaded: usize, time_passed: Duration) {
if time_passed <= Duration::ZERO {
return;
Comment thread
AnsisMalins marked this conversation as resolved.
Outdated
}

#[allow(clippy::cast_precision_loss)]
let bytes_downloaded = bytes_downloaded as f64;
Comment thread
AnsisMalins marked this conversation as resolved.
Outdated

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

/// 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`.ß
/// Returns `f64::INFINITY` if no speed data is available yet.
pub fn time_remaining(&self, bytes_remaining: u64) -> f64 {
if self.bytes_per_second <= 0.0 {
return f64::INFINITY;
Comment thread
AnsisMalins marked this conversation as resolved.
Outdated
}

#[allow(clippy::cast_precision_loss)]
Comment thread
AnsisMalins marked this conversation as resolved.
Outdated
let seconds_remaining = bytes_remaining as f64 / self.bytes_per_second;
seconds_remaining * 1000.0
}
}
46 changes: 46 additions & 0 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 @@ -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: usize = 0;
let mut downloaded: u64 = 0;
{
let mut file =
Expand All @@ -122,12 +124,17 @@ pub async fn download_file<T: EventChannel>(
})?;
let mut stream = res.bytes_stream();

let mut estimator = DownloadSpeedEstimator::new(0.1);

//fake_progress(channel, build_type, download_paused).await?;
Comment thread
AnsisMalins marked this conversation as resolved.
Outdated

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

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

Expand All @@ -145,6 +152,8 @@ pub async fn download_file<T: EventChannel>(
total_size,
));
tasks.push(task);
estimator.update(bytes_per_interval, duration);
bytes_per_interval = 0;
}

#[allow(
Expand All @@ -158,6 +167,9 @@ pub async fn download_file<T: EventChannel>(
let event: Status = Status::State {
step: Step::Downloading {
progress,
bytes_per_second: estimator.bytes_per_second(),
time_remaining: estimator
.time_remaining(total_size.saturating_sub(downloaded)),
build_type: build_type.clone(),
},
};
Expand Down Expand Up @@ -195,3 +207,37 @@ pub async fn download_file<T: EventChannel>(

Ok(())
}

/*async fn fake_progress<T: EventChannel>(
Comment thread
AnsisMalins marked this conversation as resolved.
Outdated
channel: &T,
build_type: &BuildType,
download_paused: &AtomicBool,
) -> DownloadFileResult {
let mut fake_progress: u8 = 0;

while std::hint::black_box(true) {
let download_paused = download_paused.load(Ordering::SeqCst);

let event: Status = Status::State {
step: Step::Downloading {
progress: fake_progress,
build_type: build_type.clone(),
bytes_per_second: 1_000_000.0,
time_remaining: 1_000_000_000.0,
paused: download_paused,
},
};

channel
.send(event)
.context("Cannot send event to channel")?;

tokio::time::sleep(std::time::Duration::from_millis(100)).await;

if !download_paused {
fake_progress = fake_progress.saturating_add(1) % 100;
}
}

Ok(())
}*/
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: 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