progress.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. from __future__ import annotations
  2. import bisect
  3. import threading
  4. import time
  5. class ProgressCalculator:
  6. # Time to calculate the speed over (seconds)
  7. SAMPLING_WINDOW = 3
  8. # Minimum timeframe before to sample next downloaded bytes (seconds)
  9. SAMPLING_RATE = 0.05
  10. # Time before showing eta (seconds)
  11. GRACE_PERIOD = 1
  12. def __init__(self, initial: int):
  13. self._initial = initial or 0
  14. self.downloaded = self._initial
  15. self.elapsed: float = 0
  16. self.speed = SmoothValue(0, smoothing=0.7)
  17. self.eta = SmoothValue(None, smoothing=0.9)
  18. self._total = 0
  19. self._start_time = time.monotonic()
  20. self._last_update = self._start_time
  21. self._lock = threading.Lock()
  22. self._thread_sizes: dict[int, int] = {}
  23. self._times = [self._start_time]
  24. self._downloaded = [self.downloaded]
  25. @property
  26. def total(self):
  27. return self._total
  28. @total.setter
  29. def total(self, value: int | None):
  30. with self._lock:
  31. if value is not None and value < self.downloaded:
  32. value = self.downloaded
  33. self._total = value
  34. def thread_reset(self):
  35. current_thread = threading.get_ident()
  36. with self._lock:
  37. self._thread_sizes[current_thread] = 0
  38. def update(self, size: int | None):
  39. if not size:
  40. return
  41. current_thread = threading.get_ident()
  42. with self._lock:
  43. last_size = self._thread_sizes.get(current_thread, 0)
  44. self._thread_sizes[current_thread] = size
  45. self._update(size - last_size)
  46. def _update(self, size: int):
  47. current_time = time.monotonic()
  48. self.downloaded += size
  49. self.elapsed = current_time - self._start_time
  50. if self.total is not None and self.downloaded > self.total:
  51. self._total = self.downloaded
  52. if self._last_update + self.SAMPLING_RATE > current_time:
  53. return
  54. self._last_update = current_time
  55. self._times.append(current_time)
  56. self._downloaded.append(self.downloaded)
  57. offset = bisect.bisect_left(self._times, current_time - self.SAMPLING_WINDOW)
  58. del self._times[:offset]
  59. del self._downloaded[:offset]
  60. if len(self._times) < 2:
  61. self.speed.reset()
  62. self.eta.reset()
  63. return
  64. download_time = current_time - self._times[0]
  65. if not download_time:
  66. return
  67. self.speed.set((self.downloaded - self._downloaded[0]) / download_time)
  68. if self.total and self.speed.value and self.elapsed > self.GRACE_PERIOD:
  69. self.eta.set((self.total - self.downloaded) / self.speed.value)
  70. else:
  71. self.eta.reset()
  72. class SmoothValue:
  73. def __init__(self, initial: float | None, smoothing: float):
  74. self.value = self.smooth = self._initial = initial
  75. self._smoothing = smoothing
  76. def set(self, value: float):
  77. self.value = value
  78. if self.smooth is None:
  79. self.smooth = self.value
  80. else:
  81. self.smooth = (1 - self._smoothing) * value + self._smoothing * self.smooth
  82. def reset(self):
  83. self.value = self.smooth = self._initial