@@ -33,7 +33,7 @@ use std::collections::{HashMap, HashSet};
3333use std:: path:: PathBuf ;
3434use std:: sync:: Arc ;
3535use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
36- use std:: time:: Instant ;
36+ use std:: time:: { Duration , Instant } ;
3737use tokio:: sync:: Notify ;
3838use tokio_util:: sync:: CancellationToken ;
3939use tracing:: { debug, error, info, warn} ;
@@ -42,6 +42,11 @@ fn native_task_log_fields(task_id: &TaskId) -> [(&'static str, String); 1] {
4242 [ ( "task_id" , task_id. to_string ( ) ) ]
4343}
4444
45+ fn speed_from_delta ( bytes : u64 , elapsed : Duration ) -> u64 {
46+ let nanos = elapsed. as_nanos ( ) . max ( 1 ) ;
47+ ( ( bytes as u128 ) . saturating_mul ( 1_000_000_000 ) / nanos) as u64
48+ }
49+
4550/// Input for planning a native segmented range download.
4651#[ derive( Debug , Clone , Copy ) ]
4752pub struct NativeSegmentPlanningInput < ' a > {
@@ -155,6 +160,8 @@ pub struct Engine {
155160 global_upload_limit : AtomicU64 ,
156161 /// Per-job limiter handles layered on top of the global limiter.
157162 job_rate_limiters : Mutex < HashMap < Gid , Arc < SharedRateLimiter > > > ,
163+ /// Per-job progress sampling state used to project native range speed.
164+ job_speed_samples : Mutex < HashMap < Gid , SpeedSample > > ,
158165 /// Unique session identifier (random hex, persisted for lifetime of process).
159166 pub session_id : String ,
160167 store : Option < Arc < Store > > ,
@@ -164,6 +171,12 @@ pub struct Engine {
164171 started_at : Instant ,
165172}
166173
174+ #[ derive( Debug , Clone , Copy ) ]
175+ struct SpeedSample {
176+ downloaded : u64 ,
177+ sampled_at : Instant ,
178+ }
179+
167180impl Engine {
168181 /// Create a new Engine with the given configuration (no persistence).
169182 pub fn new ( config : GlobalConfig ) -> Self {
@@ -180,6 +193,7 @@ impl Engine {
180193 global_rate_limiter,
181194 global_upload_limit : AtomicU64 :: new ( global_upload_limit) ,
182195 job_rate_limiters : Mutex :: new ( HashMap :: new ( ) ) ,
196+ job_speed_samples : Mutex :: new ( HashMap :: new ( ) ) ,
183197 session_id : format ! ( "{:016x}" , rand:: random:: <u64 >( ) ) ,
184198 store : None ,
185199 shutdown : CancellationToken :: new ( ) ,
@@ -203,6 +217,7 @@ impl Engine {
203217 global_rate_limiter,
204218 global_upload_limit : AtomicU64 :: new ( global_upload_limit) ,
205219 job_rate_limiters : Mutex :: new ( HashMap :: new ( ) ) ,
220+ job_speed_samples : Mutex :: new ( HashMap :: new ( ) ) ,
206221 session_id : format ! ( "{:016x}" , rand:: random:: <u64 >( ) ) ,
207222 store : Some ( store) ,
208223 shutdown : CancellationToken :: new ( ) ,
@@ -778,6 +793,7 @@ impl Engine {
778793 self . cancel_registry . cancel ( task_id) ;
779794 self . scheduler . dequeue_task ( task_id) ;
780795 self . clear_job_rate_limiter ( gid) ;
796+ self . clear_job_speed_sample ( gid) ;
781797 self . registry
782798 . update ( gid, |job| {
783799 job. status = Status :: Removed ;
@@ -805,6 +821,7 @@ impl Engine {
805821 let gid = self
806822 . gid_for_task_id ( task_id)
807823 . context ( "native task not found" ) ?;
824+ self . clear_job_speed_sample ( gid) ;
808825 self . registry
809826 . update ( gid, |job| {
810827 job. status = Status :: Waiting ;
@@ -1065,6 +1082,7 @@ impl Engine {
10651082 [ ( "gid" , gid. to_string ( ) ) ] ,
10661083 ) ;
10671084 self . clear_job_rate_limiter ( gid) ;
1085+ self . clear_job_speed_sample ( gid) ;
10681086 Ok ( ( ) )
10691087 }
10701088
@@ -1155,6 +1173,7 @@ impl Engine {
11551173 [ ( "gid" , gid. to_string ( ) ) ] ,
11561174 ) ;
11571175 self . clear_job_rate_limiter ( gid) ;
1176+ self . clear_job_speed_sample ( gid) ;
11581177 self . work_notify . notify_one ( ) ;
11591178 Ok ( ( ) )
11601179 }
@@ -1196,6 +1215,7 @@ impl Engine {
11961215 [ ( "gid" , gid. to_string ( ) ) , ( "error" , error_msg. to_string ( ) ) ] ,
11971216 ) ;
11981217 self . clear_job_rate_limiter ( gid) ;
1218+ self . clear_job_speed_sample ( gid) ;
11991219 self . work_notify . notify_one ( ) ;
12001220 Ok ( ( ) )
12011221 }
@@ -1374,10 +1394,45 @@ impl Engine {
13741394 self . job_rate_limiters . lock ( ) . remove ( & gid) ;
13751395 }
13761396
1397+ fn clear_job_speed_sample ( & self , gid : Gid ) {
1398+ self . job_speed_samples . lock ( ) . remove ( & gid) ;
1399+ }
1400+
13771401 /// Update download progress for a job.
13781402 pub fn update_progress ( & self , gid : Gid , bytes : u64 ) {
13791403 let _ = self . registry . update ( gid, |job| {
1380- job. downloaded += bytes;
1404+ job. downloaded = job. downloaded . saturating_add ( bytes) ;
1405+ let downloaded = job. downloaded ;
1406+ let now = Instant :: now ( ) ;
1407+ let mut samples = self . job_speed_samples . lock ( ) ;
1408+ let speed = if let Some ( sample) = samples. get ( & gid) . copied ( ) {
1409+ let elapsed = now. duration_since ( sample. sampled_at ) ;
1410+ if !elapsed. is_zero ( ) && downloaded >= sample. downloaded {
1411+ let delta = downloaded - sample. downloaded ;
1412+ let bps = speed_from_delta ( delta, elapsed) ;
1413+ samples. insert (
1414+ gid,
1415+ SpeedSample {
1416+ downloaded,
1417+ sampled_at : now,
1418+ } ,
1419+ ) ;
1420+ bps
1421+ } else {
1422+ job. download_speed
1423+ }
1424+ } else {
1425+ let speed = job. download_speed ;
1426+ samples. insert (
1427+ gid,
1428+ SpeedSample {
1429+ downloaded,
1430+ sampled_at : now,
1431+ } ,
1432+ ) ;
1433+ speed
1434+ } ;
1435+ job. download_speed = speed;
13811436 } ) ;
13821437 }
13831438
@@ -1705,6 +1760,7 @@ impl Engine {
17051760 job. connections = 0 ;
17061761 } )
17071762 . context ( "native task not found" ) ?;
1763+ self . clear_job_speed_sample ( gid) ;
17081764 Ok ( ( ) )
17091765 }
17101766
@@ -1948,6 +2004,7 @@ impl Engine {
19482004 self . scheduler . dequeue_task ( & task_id) ;
19492005 }
19502006 self . clear_job_rate_limiter ( gid) ;
2007+ self . clear_job_speed_sample ( gid) ;
19512008
19522009 // Force transition to Removed regardless of current state.
19532010 self . registry
@@ -1975,6 +2032,7 @@ impl Engine {
19752032 Status :: Complete | Status :: Error | Status :: Removed => {
19762033 self . registry . remove ( gid) ;
19772034 self . clear_job_rate_limiter ( gid) ;
2035+ self . clear_job_speed_sample ( gid) ;
19782036 debug ! ( %gid, "download result removed" ) ;
19792037 Ok ( ( ) )
19802038 }
@@ -1993,6 +2051,7 @@ impl Engine {
19932051 Status :: Complete | Status :: Error | Status :: Removed => {
19942052 self . registry . remove ( job. gid ) ;
19952053 self . clear_job_rate_limiter ( job. gid ) ;
2054+ self . clear_job_speed_sample ( job. gid ) ;
19962055 purged += 1 ;
19972056 }
19982057 _ => { }
@@ -3089,6 +3148,19 @@ mod tests {
30893148 assert_eq ! ( job. downloaded, 3000 ) ;
30903149 }
30913150
3151+ #[ test]
3152+ fn update_progress_projects_speed_after_delta ( ) {
3153+ let engine = Engine :: new ( default_config ( ) ) ;
3154+ let handle = engine. add_uri ( & default_spec ( ) ) . unwrap ( ) ;
3155+
3156+ engine. update_progress ( handle. gid , 1000 ) ;
3157+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 1 ) ) ;
3158+ engine. update_progress ( handle. gid , 1000 ) ;
3159+
3160+ let job = engine. registry . get ( handle. gid ) . unwrap ( ) ;
3161+ assert ! ( job. download_speed > 0 ) ;
3162+ }
3163+
30923164 #[ test]
30933165 fn completion_frees_slot ( ) {
30943166 let engine = Engine :: new ( GlobalConfig {
0 commit comments