diff --git a/api/src/main/java/telegram/files/AvgSpeed.java b/api/src/main/java/telegram/files/AvgSpeed.java index 71dc23b..8dfa985 100644 --- a/api/src/main/java/telegram/files/AvgSpeed.java +++ b/api/src/main/java/telegram/files/AvgSpeed.java @@ -15,15 +15,15 @@ public class AvgSpeed { /** * @param speed Speed since last point */ - record SpeedPoint(long downloadedSize, long speed) { + record SpeedPoint(long downloadedSize, long speed, long timestamp) { } public AvgSpeed() { - this(15 * 60); // Default interval is 15 minutes + this(5 * 60); // Default interval is 5 minutes } public AvgSpeed(int interval) { - this(interval, 5); + this(interval, 6); } public AvgSpeed(int interval, int smoothingWindowSize) { @@ -40,7 +40,7 @@ public class AvgSpeed { return; } // Calculate speed since last point - long speed = calculateSpeed(downloadedSize, timestamp); + long speed = calculateInstantSpeed(downloadedSize, timestamp); // Apply smoothing if we have enough points if (speedPoints.size() >= smoothingWindowSize) { @@ -48,28 +48,35 @@ public class AvgSpeed { } // Add new speed point - speedPoints.put(timestamp, new SpeedPoint(downloadedSize, speed)); + speedPoints.put(timestamp, new SpeedPoint(downloadedSize, speed, timestamp)); // Remove old points outside the interval long cutoffTime = timestamp - interval * 1000L; // Convert interval to milliseconds speedPoints.headMap(cutoffTime).clear(); } - private long calculateSpeed(long downloadedSize, long timestamp) { + private long calculateInstantSpeed(long currentSize, long currentTime) { if (speedPoints.isEmpty()) { return 0; } - Map.Entry lastEntry = speedPoints.lastEntry(); - long timeDiff = timestamp - lastEntry.getKey(); + // Find the earliest point within our smoothing window + int pointsToConsider = Math.min(smoothingWindowSize, speedPoints.size()); + List recentPoints = speedPoints.values().stream() + .skip(speedPoints.size() - pointsToConsider) + .toList(); + + SpeedPoint earliestPoint = recentPoints.getFirst(); + + long timeDiff = currentTime - earliestPoint.timestamp; if (timeDiff <= 0) { return 0; } - long bytesDiff = downloadedSize - lastEntry.getValue().downloadedSize; + long bytesDiff = currentSize - earliestPoint.downloadedSize; if (bytesDiff < 0) { // Handle download restart - bytesDiff = downloadedSize; + bytesDiff = currentSize; } return (bytesDiff * 1000L) / timeDiff; // Speed in bytes per second @@ -105,19 +112,28 @@ public class AvgSpeed { .orElse(0.0) ); - double upperThreshold = mean + (2 * standardDeviation); - double lowerThreshold = mean - (2 * standardDeviation); + double upperThreshold = mean + (3 * standardDeviation); + double lowerThreshold = mean - (3 * standardDeviation); List filteredSpeeds = recentSpeeds.stream() .filter(speed -> speed >= lowerThreshold && speed <= upperThreshold) .toList(); - return filteredSpeeds.isEmpty() - ? currentSpeed - : (long) filteredSpeeds.stream() - .mapToLong(Long::longValue) - .average() - .orElse(currentSpeed); + if (filteredSpeeds.isEmpty()) { + return currentSpeed; + } + + double totalWeight = 0; + double weightedSum = 0; + int size = filteredSpeeds.size(); + + for (int i = 0; i < size; i++) { + double weight = (i + 1.0) / size; + weightedSum += filteredSpeeds.get(i) * weight; + totalWeight += weight; + } + + return (long) (weightedSum / totalWeight); } /** diff --git a/api/src/test/java/telegram/files/AvgSpeedTest.java b/api/src/test/java/telegram/files/AvgSpeedTest.java index 9f3c1bb..95769bf 100644 --- a/api/src/test/java/telegram/files/AvgSpeedTest.java +++ b/api/src/test/java/telegram/files/AvgSpeedTest.java @@ -91,7 +91,7 @@ class AvgSpeedTest { AvgSpeed.SpeedStats stats = avgSpeed.getSpeedStats(); assertTrue(stats.maxSpeed() >= 300, "Max speed should capture high speed period"); - assertTrue(stats.minSpeed() <= 100, + assertTrue(stats.minSpeed() == 250, "Min speed should capture low speed period"); } @@ -161,4 +161,54 @@ class AvgSpeedTest { assertTrue(avgSpeed.getSpeed() > 0, "Speed should be calculated correctly with large numbers"); } + + @Test + void shouldShowDifferentSpeedsOverTime() { + // Given + AvgSpeed avgSpeed = new AvgSpeed(60); // 1 minute interval + long baseTime = System.currentTimeMillis(); + + // When - simulate download over 2 minutes with varying speeds + // First 30 seconds - constant speed + for (int i = 0; i < 30; i++) { + avgSpeed.update(1024L * 1024L * i, // 1MB increments + baseTime + (i * 1000L)); // 1 second intervals + } + + // Record speeds at 30 second mark + AvgSpeed.SpeedStats stats30s = avgSpeed.getSpeedStats(); + + // Next 30 seconds - faster speed + for (int i = 30; i < 60; i++) { + avgSpeed.update(1024L * 1024L * (i * 2), // 2MB increments + baseTime + (i * 1000L)); // 1 second intervals + } + + // Record speeds at 60 second mark + AvgSpeed.SpeedStats stats60s = avgSpeed.getSpeedStats(); + + // Another 30 seconds - slower speed + for (int i = 60; i < 90; i++) { + avgSpeed.update(1024L * 1024L * (i + 60), // slower increments + baseTime + (i * 1000L)); // 1 second intervals + } + + // Record speeds at 90 second mark + AvgSpeed.SpeedStats stats90s = avgSpeed.getSpeedStats(); + + // 30s with big speed changes + for (int i = 90; i < 120; i++) { + avgSpeed.update(1024L * 1024L * (i * 4), // 4MB increments + baseTime + (i * 1000L)); // 1 second intervals + } + + // Record speeds at 120 second mark + AvgSpeed.SpeedStats stats120s = avgSpeed.getSpeedStats(); + + // Then + System.out.println("30s stats: " + stats30s); + System.out.println("60s stats: " + stats60s); + System.out.println("90s stats: " + stats90s); + System.out.println("120s stats: " + stats120s); + } }