Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calculate upload rate #1029

Merged
merged 12 commits into from
Dec 1, 2023
Prev Previous commit
Next Next commit
feat(rate calculation): implement a weighted moving average
  • Loading branch information
gilest committed Dec 1, 2023
commit efae27c1642168a66d6cec7b0cedc341b39c560b
10 changes: 9 additions & 1 deletion ember-file-upload/src/internal.ts
Original file line number Diff line number Diff line change
@@ -2,13 +2,21 @@ import DataTransferWrapper from './system/data-transfer-wrapper.ts';
import HTTPRequest from './system/http-request.ts';
import UploadFileReader from './system/upload-file-reader.ts';
import { onloadstart, onprogress, onloadend } from './system/upload.ts';
import {
estimatedRate,
generateWeights,
proportionForPosition,
} from './system/rate.ts';

export {
// Non-public classes imported by the test app
// Non-public modules imported by the test app
DataTransferWrapper,
HTTPRequest,
UploadFileReader,
onloadstart,
onprogress,
onloadend,
estimatedRate,
generateWeights,
proportionForPosition,
};
46 changes: 46 additions & 0 deletions ember-file-upload/src/system/rate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Calculate from the last x rate recordings
const CALCULATE_FROM_LAST = 30;

// Weigh recent rates more highly
const THRESHOLDS = [
{ threshold: 10, proportion: 3 },
{ threshold: 20, proportion: 2 },
{ threshold: 30, proportion: 1 },
];
const DEFAULT_PROPORTION = 1;

export function estimatedRate(allRates: number[]): number {
if (!allRates.length) return 0;

const rates = allRates.slice(CALCULATE_FROM_LAST * -1).reverse();
const weights = generateWeights(rates.length);

return rates.reduce((acc, rate, index) => {
const weight = weights[index] as number;
return acc + rate * weight;
}, 0);
}

export function generateWeights(totalRates: number): number[] {
const proportions: number[] = [];

Array.from({ length: totalRates }).forEach((_, index) => {
proportions.push(proportionForPosition(index + 1));
});

const proportionTotal = proportions.reduce((acc, value) => acc + value, 0);
const realWeights = proportions.map(
(proportion) => proportion / proportionTotal,
);

return realWeights;
}

export function proportionForPosition(position: number) {
for (const { threshold, proportion } of THRESHOLDS) {
if (position <= threshold) {
return proportion;
}
}
return DEFAULT_PROPORTION;
}
2 changes: 1 addition & 1 deletion ember-file-upload/src/system/upload.ts
Original file line number Diff line number Diff line change
@@ -57,7 +57,7 @@ function updateRate(file: UploadFile) {

// If there's a previous recording, we can calculate a rate from that
if (file.timestampWhenProgressLastUpdated) {
const timeElapsedSinceLastUpdate =
const timeElapsedSinceLastUpdate =
updatedTime - file.timestampWhenProgressLastUpdated;

const bytesTransferredSinceLastUpdate =
5 changes: 2 additions & 3 deletions ember-file-upload/src/upload-file.ts
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import type { Queue } from './queue.ts';
import { guidFor } from '@ember/object/internals';
import RSVP from 'rsvp';
import { FileSource, FileState, type UploadOptions } from './interfaces.ts';
import { estimatedRate } from './system/rate.ts';

/**
* Files provide a uniform interface for interacting
@@ -53,9 +54,7 @@ export class UploadFile {

/** The current speed in ms that it takes to upload one byte */
get rate() {
if (!this.rates.length) return 0;

return this.rates.reduce((a, b) => a + b, 0) / this.rates.length;
return estimatedRate(this.rates);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Work done in plain functions for unit-testing purposes.

}

#size = 0;
9 changes: 0 additions & 9 deletions test-app/tests/integration/progress-test.ts
Original file line number Diff line number Diff line change
@@ -69,16 +69,13 @@ module('Integration | progress', function (hooks) {
selectFiles('#upload-photo', file, file, file);
}

assert.strictEqual(queue.rate, 0, 'rate should be 0 before uploads begin');

await firstFile.promise;

assert.strictEqual(
queue.progress,
33,
'first file uploaded - queue progress 33%',
);
assert.true(queue.rate > 0, 'rate should be > 0 when uploading');
assert.strictEqual(
queue.files.length,
3,
@@ -92,7 +89,6 @@ module('Integration | progress', function (hooks) {
66,
'second file uploaded - queue progress 66%',
);
assert.true(queue.rate > 0, 'rate should be > 0 when uploading');
assert.strictEqual(
queue.files.length,
3,
@@ -106,11 +102,6 @@ module('Integration | progress', function (hooks) {
0,
'third file uploaded - progress is back to 0% since the queue has been flushed',
);
assert.strictEqual(
queue.rate,
0,
'rate should be 0 after all uploads finish',
);
assert.strictEqual(
queue.files.length,
0,
77 changes: 77 additions & 0 deletions test-app/tests/unit/system/rate-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import {
estimatedRate,
generateWeights,
proportionForPosition,
} from 'ember-file-upload/internal';

module('Unit | rate', function (hooks) {
setupTest(hooks);

test('estimatedRate', function (assert) {
assert.strictEqual(
estimatedRate([80, 80, 80, 80, 80, 80, 80, 80, 80, 80]),
80,
'straight average up to first threshold',
);
assert.strictEqual(
estimatedRate([
80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 50, 50, 50, 50, 50, 50, 50, 50,
50, 50,
]),
62.00000000000003,
'slowing down, recent values have more affect',
);
assert.strictEqual(
estimatedRate([
50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 80, 80, 80, 80, 80, 80, 80, 80,
80, 80,
]),
68,
'speeding up, recent values have more affect',
);
});

test('generateWeights', function (assert) {
assert.deepEqual(generateWeights(1), [1], 'single weight is 1');
assert.deepEqual(
generateWeights(10),
[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1],
'first 10 weights evenly divided',
);

assert.deepEqual(
generateWeights(20),
[
0.06, 0.06, 0.06, 0.06, 0.06, 0.06, 0.06, 0.06, 0.06, 0.06, 0.04, 0.04,
0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04,
],
'proportionally even weights in each threshold',
);

assert.deepEqual(
generateWeights(30),
[
0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
0.03333333333333333, 0.03333333333333333, 0.03333333333333333,
0.03333333333333333, 0.03333333333333333, 0.03333333333333333,
0.03333333333333333, 0.03333333333333333, 0.03333333333333333,
0.03333333333333333, 0.016666666666666666, 0.016666666666666666,
0.016666666666666666, 0.016666666666666666, 0.016666666666666666,
0.016666666666666666, 0.016666666666666666, 0.016666666666666666,
0.016666666666666666, 0.016666666666666666,
],
'proportionally even weights in each threshold',
);
});

test('proportionForPosition', function (assert) {
assert.strictEqual(proportionForPosition(1), 3, 'rate 1 high proportion');
assert.strictEqual(proportionForPosition(10), 3, 'rate 10 high proportion');
assert.strictEqual(proportionForPosition(11), 2, 'rate 11 med proportion');
assert.strictEqual(proportionForPosition(20), 2, 'rate 20 med proportion');
assert.strictEqual(proportionForPosition(21), 1, 'rate 21 low proportion');
assert.strictEqual(proportionForPosition(30), 1, 'rate 30 low proportion');
});
});
10 changes: 0 additions & 10 deletions test-app/tests/unit/upload-file-test.ts
Original file line number Diff line number Diff line change
@@ -88,14 +88,4 @@ module('Unit | UploadFile', function (hooks) {
assert.strictEqual(file.size, 13);
assert.strictEqual(file.file.size, 9);
});

test('it correctly calculates rate', function (assert) {
const file = UploadFile.fromBlob(new Blob(['test text'], { type: 'text' }));
const twoSecondsAgo = new Date().getTime() - 2000;
file.timestampWhenProgressLastUpdated = twoSecondsAgo;
file.bytesWhenProgressLastUpdated = 5000;
file.loaded = 20000;
file.size = 500000;
assert.strictEqual(Math.ceil(file.rate), 8);
});
});