fix: speed calculation

This commit is contained in:
Fredrik Burmester
2025-10-03 08:52:39 +02:00
parent 7fef2ed5e2
commit d764e5f9d2
3 changed files with 162 additions and 13 deletions

View File

@@ -14,6 +14,7 @@ import {
import { toast } from "sonner-native";
import { Text } from "@/components/common/Text";
import { useDownload } from "@/providers/DownloadProvider";
import { calculateSmoothedETA } from "@/providers/Downloads/hooks/useDownloadSpeedCalculator";
import { JobStatus } from "@/providers/Downloads/types";
import { storage } from "@/utils/mmkv";
import { formatTimeString } from "@/utils/time";
@@ -62,16 +63,23 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
}
};
const eta = (p: JobStatus) => {
if (!p.speed || p.speed <= 0 || !p.estimatedTotalSizeBytes) return null;
const eta = useMemo(() => {
if (!process.estimatedTotalSizeBytes || !process.bytesDownloaded) {
return null;
}
const bytesRemaining = p.estimatedTotalSizeBytes - (p.bytesDownloaded || 0);
if (bytesRemaining <= 0) return null;
const secondsRemaining = calculateSmoothedETA(
process.id,
process.bytesDownloaded,
process.estimatedTotalSizeBytes,
);
const secondsRemaining = bytesRemaining / p.speed;
if (!secondsRemaining || secondsRemaining <= 0) {
return null;
}
return formatTimeString(secondsRemaining, "s");
};
}, [process.id, process.bytesDownloaded, process.estimatedTotalSizeBytes]);
const base64Image = useMemo(() => {
return storage.getString(process.item.Id!);
@@ -161,9 +169,9 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
{bytesToMB(process.speed).toFixed(2)} MB/s
</Text>
)}
{eta(process) && (
{eta && (
<Text className='text-xs'>
{t("home.downloads.eta", { eta: eta(process) })}
{t("home.downloads.eta", { eta: eta })}
</Text>
)}
</View>

View File

@@ -19,6 +19,7 @@ class DownloadSessionDelegate: NSObject, URLSessionDownloadDelegate {
init(module: BackgroundDownloaderModule) {
self.module = module
super.init()
print("[DownloadSessionDelegate] Delegate initialized with module: \(String(describing: module))")
}
func urlSession(
@@ -141,10 +142,73 @@ public class BackgroundDownloaderModule: Module {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
session.getAllTasks { tasks in
if let downloadTask = tasks.first(where: { $0.taskIdentifier == taskId }) {
print("[BackgroundDownloader] Task state after 0.5s: \(self.taskStateString(downloadTask.state))")
print("[BackgroundDownloader] === 0.5s CHECK ===")
print("[BackgroundDownloader] Task state: \(self.taskStateString(downloadTask.state))")
if let response = downloadTask.response as? HTTPURLResponse {
print("[BackgroundDownloader] Response status: \(response.statusCode)")
print("[BackgroundDownloader] Expected content length: \(response.expectedContentLength)")
} else {
print("[BackgroundDownloader] No HTTP response yet after 0.5s")
}
} else {
print("[BackgroundDownloader] Task not found after 0.5s")
}
}
}
// Additional diagnostics at 1s, 2s, and 3s
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
session.getAllTasks { tasks in
if let downloadTask = tasks.first(where: { $0.taskIdentifier == taskId }) {
print("[BackgroundDownloader] === 1s CHECK ===")
print("[BackgroundDownloader] Task state: \(self.taskStateString(downloadTask.state))")
print("[BackgroundDownloader] Task error: \(String(describing: downloadTask.error))")
print("[BackgroundDownloader] Current request URL: \(downloadTask.currentRequest?.url?.absoluteString ?? "nil")")
print("[BackgroundDownloader] Original request URL: \(downloadTask.originalRequest?.url?.absoluteString ?? "nil")")
if let response = downloadTask.response as? HTTPURLResponse {
print("[BackgroundDownloader] HTTP Status: \(response.statusCode)")
print("[BackgroundDownloader] Expected content length: \(response.expectedContentLength)")
print("[BackgroundDownloader] All headers: \(response.allHeaderFields)")
} else {
print("[BackgroundDownloader] ⚠️ STILL NO HTTP RESPONSE after 1s")
}
let countOfBytesReceived = downloadTask.countOfBytesReceived
if countOfBytesReceived > 0 {
print("[BackgroundDownloader] Bytes received: \(countOfBytesReceived)")
} else {
print("[BackgroundDownloader] ⚠️ NO BYTES RECEIVED YET")
}
} else {
print("[BackgroundDownloader] ⚠️ Task disappeared after 1s")
}
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
session.getAllTasks { tasks in
if let downloadTask = tasks.first(where: { $0.taskIdentifier == taskId }) {
print("[BackgroundDownloader] === 2s CHECK ===")
print("[BackgroundDownloader] Task state: \(self.taskStateString(downloadTask.state))")
let countOfBytesReceived = downloadTask.countOfBytesReceived
print("[BackgroundDownloader] Bytes received: \(countOfBytesReceived)")
if downloadTask.error != nil {
print("[BackgroundDownloader] ⚠️ Task has error: \(String(describing: downloadTask.error))")
}
}
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
session.getAllTasks { tasks in
if let downloadTask = tasks.first(where: { $0.taskIdentifier == taskId }) {
print("[BackgroundDownloader] === 3s CHECK ===")
print("[BackgroundDownloader] Task state: \(self.taskStateString(downloadTask.state))")
let countOfBytesReceived = downloadTask.countOfBytesReceived
print("[BackgroundDownloader] Bytes received: \(countOfBytesReceived)")
if downloadTask.error != nil {
print("[BackgroundDownloader] ⚠️ Task has error: \(String(describing: downloadTask.error))")
}
}
}
@@ -173,17 +237,20 @@ public class BackgroundDownloaderModule: Module {
AsyncFunction("getActiveDownloads") { () -> [[String: Any]] in
return try await withCheckedThrowingContinuation { continuation in
let downloadTasks = self.downloadTasks
let taskStateString = self.taskStateString
self.session?.getAllTasks { tasks in
let activeDownloads = tasks.compactMap { task -> [String: Any]? in
guard task is URLSessionDownloadTask,
let info = self.downloadTasks[task.taskIdentifier] else {
let info = downloadTasks[task.taskIdentifier] else {
return nil
}
return [
"taskId": task.taskIdentifier,
"url": info.url,
"state": self.taskStateString(task.state)
"state": taskStateString(task.state)
]
}
continuation.resume(returning: activeDownloads)
@@ -210,6 +277,15 @@ public class BackgroundDownloaderModule: Module {
)
print("[BackgroundDownloader] URLSession initialized with delegate: \(String(describing: self.sessionDelegate))")
print("[BackgroundDownloader] Session identifier: \(config.identifier ?? "nil")")
print("[BackgroundDownloader] Delegate queue: nil (uses default)")
// Verify delegate is connected
if let session = self.session, session.delegate != nil {
print("[BackgroundDownloader] ✅ Delegate successfully attached to session")
} else {
print("[BackgroundDownloader] ⚠️ DELEGATE NOT ATTACHED!")
}
}
private func taskStateString(_ state: URLSessionTask.State) -> String {

View File

@@ -4,11 +4,13 @@ interface SpeedDataPoint {
}
const WINDOW_DURATION = 60000; // 1 minute in ms
const MIN_DATA_POINTS = 3; // Need at least 3 points for accurate speed
const MIN_DATA_POINTS = 5; // Need at least 5 points for accurate speed
const MAX_REASONABLE_SPEED = 1024 * 1024 * 1024; // 1 GB/s sanity check
const EMA_ALPHA = 0.2; // Smoothing factor for EMA (lower = smoother, 0-1 range)
// Private state
const dataPoints = new Map<string, SpeedDataPoint[]>();
const emaSpeed = new Map<string, number>(); // Store EMA speed for each process
function isValidBytes(bytes: number): boolean {
return typeof bytes === "number" && Number.isFinite(bytes) && bytes >= 0;
@@ -161,7 +163,8 @@ export function calculateWeightedSpeed(processId: string): number | undefined {
}
// More recent points get exponentially higher weight
const weight = 2 ** i;
// Using 1.3 instead of 2 for gentler weighting (less sensitive to recent changes)
const weight = 1.3 ** i;
totalWeightedSpeed += speed * weight;
totalWeight += weight;
}
@@ -180,12 +183,74 @@ export function calculateWeightedSpeed(processId: string): number | undefined {
return weightedSpeed;
}
// Calculate ETA in seconds
export function calculateETA(
processId: string,
bytesDownloaded: number,
totalBytes: number,
): number | undefined {
const speed = calculateWeightedSpeed(processId);
if (!speed || speed <= 0 || !totalBytes || totalBytes <= 0) {
return undefined;
}
const bytesRemaining = totalBytes - bytesDownloaded;
if (bytesRemaining <= 0) {
return 0;
}
const secondsRemaining = bytesRemaining / speed;
// Sanity check
if (!Number.isFinite(secondsRemaining) || secondsRemaining < 0) {
return undefined;
}
return secondsRemaining;
}
// Calculate smoothed ETA using Exponential Moving Average (EMA)
// This provides much smoother ETA estimates, reducing jumpy time estimates
const emaETA = new Map<string, number>();
export function calculateSmoothedETA(
processId: string,
bytesDownloaded: number,
totalBytes: number,
): number | undefined {
const currentETA = calculateETA(processId, bytesDownloaded, totalBytes);
if (currentETA === undefined) {
return undefined;
}
const previousEma = emaETA.get(processId);
if (previousEma === undefined) {
// First calculation, initialize with current ETA
emaETA.set(processId, currentETA);
return currentETA;
}
// EMA formula: EMA(t) = α * current + (1 - α) * EMA(t-1)
// Lower alpha = smoother but slower to respond
const smoothed = EMA_ALPHA * currentETA + (1 - EMA_ALPHA) * previousEma;
emaETA.set(processId, smoothed);
return smoothed;
}
export function clearSpeedData(processId: string): void {
dataPoints.delete(processId);
emaSpeed.delete(processId);
emaETA.delete(processId);
}
export function resetAllSpeedData(): void {
dataPoints.clear();
emaSpeed.clear();
emaETA.clear();
}
// Debug function to inspect current state