Compare commits

...

6 Commits

Author SHA1 Message Date
Simon Sawicki
8fc1d89d9e
Simplifications 2023-10-01 18:32:58 +02:00
Simon Sawicki
8e9aa95c3b
Use exponential moving average 2023-10-01 18:17:33 +02:00
Simon Sawicki
3e2ac33181
More fixes and typings 2023-10-01 18:12:41 +02:00
Simon Sawicki
3227e7e610
Some tweaks 2023-10-01 16:50:48 +02:00
Simon Sawicki
b1bf57584d
Implement linear moving average, cleanup 2023-10-01 15:49:14 +02:00
Simon Sawicki
8c8420df32
Use downloaded bytes as measurement for speed 2023-10-01 13:36:15 +02:00
2 changed files with 40 additions and 53 deletions

View File

@ -257,6 +257,7 @@ class FragmentFD(FileDownloader):
frag_total_bytes = s.get('total_bytes') or 0 frag_total_bytes = s.get('total_bytes') or 0
s['fragment_info_dict'] = s.pop('info_dict', {}) s['fragment_info_dict'] = s.pop('info_dict', {})
# XXX: Fragment resume is not accounted for here
if not ctx['live']: if not ctx['live']:
estimated_size = ( estimated_size = (
(ctx['complete_frags_downloaded_bytes'] + frag_total_bytes) (ctx['complete_frags_downloaded_bytes'] + frag_total_bytes)
@ -269,13 +270,11 @@ class FragmentFD(FileDownloader):
if s['status'] == 'finished': if s['status'] == 'finished':
state['fragment_index'] += 1 state['fragment_index'] += 1
progress.thread_reset()
ctx['fragment_index'] = state['fragment_index'] ctx['fragment_index'] = state['fragment_index']
progress.thread_reset()
state['downloaded_bytes'] = ctx['complete_frags_downloaded_bytes'] = progress.downloaded state['downloaded_bytes'] = ctx['complete_frags_downloaded_bytes'] = progress.downloaded
ctx['speed'] = state['speed'] = progress.speed ctx['speed'] = state['speed'] = progress.smooth_speed
else:
state['downloaded_bytes'] = progress.downloaded
ctx['speed'] = state['speed'] = progress.speed
state['eta'] = progress.eta state['eta'] = progress.eta
self._hook_progress(state, info_dict) self._hook_progress(state, info_dict)

View File

@ -4,44 +4,38 @@ import bisect
import threading import threading
import time import time
# FIXME: monotonic has terrible resolution on Windows
TIME_PROVIDER = time.perf_counter
# XXX: Fragment resume is not accounted for here
class ProgressCalculator: class ProgressCalculator:
# Time to calculate the average over (in seconds) # Time to calculate the speed over (in nanoseconds)
WINDOW_SIZE = 2.0 SAMPLING_WINDOW = 1_000_000_000
# Minimum time before we add another datapoint (in seconds) # Factor for the exponential moving average (from 0 = prev to 1 = current)
# This is *NOT* the same as the time between a progress change SMOOTHING_FACTOR = 0.3
UPDATE_TIME = 0.1
def __init__(self, initial): def __init__(self, initial: int):
self.downloaded = initial if initial else 0 self.downloaded = initial or 0
self.elapsed: float = 0
self.speed: float = 0
self.smooth_speed: int = 0
self.eta: float | None = None
self.elapsed = 0
self.speed = None
self.eta = None
self._total = None self._total = None
self._start_time = time.monotonic_ns()
self._lock = threading.Lock() self._lock = threading.Lock()
self._start_time = TIME_PROVIDER()
self._last_update = self._start_time
self._times: list[float] = []
self._speeds: list[int] = []
self._thread_updates: dict[int, float] = {}
self._thread_sizes: dict[int, int] = {} self._thread_sizes: dict[int, int] = {}
self._times = [self._start_time]
self._downloaded = [self.downloaded]
@property @property
def total(self): def total(self):
return self._total return self._total
@total.setter @total.setter
def total(self, value): def total(self, value: int | None):
with self._lock: with self._lock:
if not value or value <= 0.01: if not value:
value = None value = None
elif value < self.downloaded: elif value < self.downloaded:
value = self.downloaded value = self.downloaded
@ -52,7 +46,6 @@ class ProgressCalculator:
current_thread = threading.get_ident() current_thread = threading.get_ident()
with self._lock: with self._lock:
self._thread_sizes[current_thread] = 0 self._thread_sizes[current_thread] = 0
self._thread_updates[current_thread] = TIME_PROVIDER()
def update(self, size: int | None): def update(self, size: int | None):
if not size: if not size:
@ -61,39 +54,34 @@ class ProgressCalculator:
current_thread = threading.get_ident() current_thread = threading.get_ident()
with self._lock: with self._lock:
current_time = TIME_PROVIDER() last_size = self._thread_sizes.get(current_thread, 0)
last_size = self._thread_sizes.get(current_thread) or 0
last_update = self._thread_updates.get(current_thread) or self._start_time
chunk = size - last_size
print(f' [{threading.get_ident()}] {last_update} -> {current_time} ({chunk}B)')
update_time = self._update(chunk, current_time, last_update)
if update_time:
self._thread_updates[current_thread] = current_time
self._thread_sizes[current_thread] = size self._thread_sizes[current_thread] = size
self._update(size - last_size)
def _update(self, size: int):
current_time = time.monotonic_ns()
def _update(self, size, current_time, last_update):
self.downloaded += size self.downloaded += size
self.elapsed = current_time - self._start_time self.elapsed = (current_time - self._start_time) / 1_000_000_000
if self.total is not None and self.downloaded > self.total: if self.total is not None and self.downloaded > self.total:
self._total = self.downloaded self._total = self.downloaded
self._times.append(current_time) self._times.append(current_time)
self._speeds.append(size / (current_time - last_update)) self._downloaded.append(self.downloaded)
if current_time - self.UPDATE_TIME <= last_update: offset = bisect.bisect_left(self._times, current_time - self.SAMPLING_WINDOW)
return
self._last_update = current_time
offset = bisect.bisect_left(self._times, current_time - self.WINDOW_SIZE)
del self._times[:offset] del self._times[:offset]
del self._speeds[:offset] del self._downloaded[:offset]
weights = tuple(1 + (point - current_time) / self.WINDOW_SIZE for point in self._times) download_time = current_time - self._times[0]
# Same as `statistics.fmean(self.data_points, weights)`, but weights is >=3.11 if not download_time:
self.speed = sum(map(float.__mul__, self._speeds, weights)) / sum(weights) return
downloaded_bytes = self.downloaded - self._downloaded[0]
self.speed = downloaded_bytes * 1_000_000_000 / download_time
self.smooth_speed = int(self.SMOOTHING_FACTOR * self.speed + (1 - self.SMOOTHING_FACTOR) * self.smooth_speed)
if not self.total: if not self.total:
self.eta = None self.eta = None
else: elif self.speed:
self.eta = (self.total - self.downloaded) / self.speed self.eta = (self.total - self.downloaded) / self.speed