💩 fix: Improve AvgSpeed calculation and add new test for speed variations over time.

This commit is contained in:
jarvis2f 2024-12-31 23:05:41 +08:00
parent a33b73b5c4
commit 27e1d6c46f
2 changed files with 85 additions and 19 deletions

View file

@ -15,15 +15,15 @@ public class AvgSpeed {
/** /**
* @param speed Speed since last point * @param speed Speed since last point
*/ */
record SpeedPoint(long downloadedSize, long speed) { record SpeedPoint(long downloadedSize, long speed, long timestamp) {
} }
public AvgSpeed() { public AvgSpeed() {
this(15 * 60); // Default interval is 15 minutes this(5 * 60); // Default interval is 5 minutes
} }
public AvgSpeed(int interval) { public AvgSpeed(int interval) {
this(interval, 5); this(interval, 6);
} }
public AvgSpeed(int interval, int smoothingWindowSize) { public AvgSpeed(int interval, int smoothingWindowSize) {
@ -40,7 +40,7 @@ public class AvgSpeed {
return; return;
} }
// Calculate speed since last point // Calculate speed since last point
long speed = calculateSpeed(downloadedSize, timestamp); long speed = calculateInstantSpeed(downloadedSize, timestamp);
// Apply smoothing if we have enough points // Apply smoothing if we have enough points
if (speedPoints.size() >= smoothingWindowSize) { if (speedPoints.size() >= smoothingWindowSize) {
@ -48,28 +48,35 @@ public class AvgSpeed {
} }
// Add new speed point // 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 // Remove old points outside the interval
long cutoffTime = timestamp - interval * 1000L; // Convert interval to milliseconds long cutoffTime = timestamp - interval * 1000L; // Convert interval to milliseconds
speedPoints.headMap(cutoffTime).clear(); speedPoints.headMap(cutoffTime).clear();
} }
private long calculateSpeed(long downloadedSize, long timestamp) { private long calculateInstantSpeed(long currentSize, long currentTime) {
if (speedPoints.isEmpty()) { if (speedPoints.isEmpty()) {
return 0; return 0;
} }
Map.Entry<Long, SpeedPoint> lastEntry = speedPoints.lastEntry(); // Find the earliest point within our smoothing window
long timeDiff = timestamp - lastEntry.getKey(); int pointsToConsider = Math.min(smoothingWindowSize, speedPoints.size());
List<SpeedPoint> recentPoints = speedPoints.values().stream()
.skip(speedPoints.size() - pointsToConsider)
.toList();
SpeedPoint earliestPoint = recentPoints.getFirst();
long timeDiff = currentTime - earliestPoint.timestamp;
if (timeDiff <= 0) { if (timeDiff <= 0) {
return 0; return 0;
} }
long bytesDiff = downloadedSize - lastEntry.getValue().downloadedSize; long bytesDiff = currentSize - earliestPoint.downloadedSize;
if (bytesDiff < 0) { if (bytesDiff < 0) {
// Handle download restart // Handle download restart
bytesDiff = downloadedSize; bytesDiff = currentSize;
} }
return (bytesDiff * 1000L) / timeDiff; // Speed in bytes per second return (bytesDiff * 1000L) / timeDiff; // Speed in bytes per second
@ -105,19 +112,28 @@ public class AvgSpeed {
.orElse(0.0) .orElse(0.0)
); );
double upperThreshold = mean + (2 * standardDeviation); double upperThreshold = mean + (3 * standardDeviation);
double lowerThreshold = mean - (2 * standardDeviation); double lowerThreshold = mean - (3 * standardDeviation);
List<Long> filteredSpeeds = recentSpeeds.stream() List<Long> filteredSpeeds = recentSpeeds.stream()
.filter(speed -> speed >= lowerThreshold && speed <= upperThreshold) .filter(speed -> speed >= lowerThreshold && speed <= upperThreshold)
.toList(); .toList();
return filteredSpeeds.isEmpty() if (filteredSpeeds.isEmpty()) {
? currentSpeed return currentSpeed;
: (long) filteredSpeeds.stream() }
.mapToLong(Long::longValue)
.average() double totalWeight = 0;
.orElse(currentSpeed); 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);
} }
/** /**

View file

@ -91,7 +91,7 @@ class AvgSpeedTest {
AvgSpeed.SpeedStats stats = avgSpeed.getSpeedStats(); AvgSpeed.SpeedStats stats = avgSpeed.getSpeedStats();
assertTrue(stats.maxSpeed() >= 300, assertTrue(stats.maxSpeed() >= 300,
"Max speed should capture high speed period"); "Max speed should capture high speed period");
assertTrue(stats.minSpeed() <= 100, assertTrue(stats.minSpeed() == 250,
"Min speed should capture low speed period"); "Min speed should capture low speed period");
} }
@ -161,4 +161,54 @@ class AvgSpeedTest {
assertTrue(avgSpeed.getSpeed() > 0, assertTrue(avgSpeed.getSpeed() > 0,
"Speed should be calculated correctly with large numbers"); "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);
}
} }