diff --git a/docs/uploading.md b/docs/uploading.md index 07b3ade2..fb42d438 100644 --- a/docs/uploading.md +++ b/docs/uploading.md @@ -49,6 +49,7 @@ In addition to the file list, the queue tracks properties that indicate the prog | queue.size | `number` – Total size of all files currently being uploaded in bytes. | | queue.loaded | `number` – Number of bytes that have been uploaded to the server. | | queue.progress | `number` – Current progress of all uploads, as a percentage in the range of 0 to 100. | +| queue.rate | `number` – Current time in ms it is taking to upload 1 byte. | ```hbs {{#if queue.files.length}} diff --git a/ember-file-upload/src/queue.ts b/ember-file-upload/src/queue.ts index 2d31608d..ac1b836a 100644 --- a/ember-file-upload/src/queue.ts +++ b/ember-file-upload/src/queue.ts @@ -70,6 +70,17 @@ export class Queue { return [...this.#distinctFiles.values()]; } + /** + * The current time in ms it is taking to upload 1 byte. + * + * @defaultValue 0 + */ + get rate(): number { + return this.files.reduce((acc, { rate }) => { + return acc + rate; + }, 0); + } + /** * The total size of all files currently being uploaded in bytes. * diff --git a/ember-file-upload/src/services/file-queue.ts b/ember-file-upload/src/services/file-queue.ts index 8f5602c7..3b07d3f4 100644 --- a/ember-file-upload/src/services/file-queue.ts +++ b/ember-file-upload/src/services/file-queue.ts @@ -88,6 +88,17 @@ export default class FileQueueService extends Service { }, []); } + /** + * The current time in ms it is taking to upload 1 byte. + * + * @defaultValue 0 + */ + get rate(): number { + return this.files.reduce((acc, { rate }) => { + return acc + rate; + }, 0); + } + /** * The total size of all files currently being uploaded in bytes. * diff --git a/ember-file-upload/src/system/upload.ts b/ember-file-upload/src/system/upload.ts index 8ee95289..60b431c4 100644 --- a/ember-file-upload/src/system/upload.ts +++ b/ember-file-upload/src/system/upload.ts @@ -1,4 +1,5 @@ import { assert } from '@ember/debug'; +import { next } from '@ember/runloop'; import HTTPRequest from './http-request.ts'; import RSVP from 'rsvp'; import { waitForPromise } from '@ember/test-waiters'; @@ -65,6 +66,8 @@ export function onloadstart( // The correct should be returned while progress file.size = Math.max(file.size, event.total); file.progress = (file.loaded / file.size) * 100; + file.bytesWhenProgressLastUpdated = event.loaded; + file.timestampWhenProgressLastUpdated = new Date().getTime(); } export function onprogress( @@ -88,6 +91,13 @@ export function onprogress( } file.loaded = Math.max(loaded, file.loaded); file.progress = (file.loaded / file.size) * 100; + + const updatedTime = new Date().getTime(); + + next(() => { + file.bytesWhenProgressLastUpdated = file.loaded; + file.timestampWhenProgressLastUpdated = updatedTime; + }); } export function onloadend( @@ -100,6 +110,8 @@ export function onloadend( file.loaded = file.size; file.progress = (file.loaded / file.size) * 100; file.isUploadComplete = true; + file.bytesWhenProgressLastUpdated = 0; + file.timestampWhenProgressLastUpdated = 0; } export function upload( @@ -142,10 +154,14 @@ export function upload( request.onloadend = (event) => onloadend(file, event); request.ontimeout = () => { + file.bytesWhenProgressLastUpdated = 0; + file.timestampWhenProgressLastUpdated = 0; file.state = FileState.TimedOut; file.queue?.flush(); }; request.onabort = () => { + file.bytesWhenProgressLastUpdated = 0; + file.timestampWhenProgressLastUpdated = 0; file.state = FileState.Aborted; file.queue?.flush(); }; diff --git a/ember-file-upload/src/upload-file.ts b/ember-file-upload/src/upload-file.ts index 0d225f2a..620d3ba6 100644 --- a/ember-file-upload/src/upload-file.ts +++ b/ember-file-upload/src/upload-file.ts @@ -51,6 +51,19 @@ export class UploadFile { this.#name = value; } + /** The current speed in ms that it takes to upload one byte */ + get rate() { + const updatedTime = new Date().getTime(); + const timeElapsedSinceLastUpdate = + updatedTime - this.timestampWhenProgressLastUpdated; + + const bytesTransferredSinceLastUpdate = + this.loaded - this.bytesWhenProgressLastUpdated; + + // Divide by elapsed time to get bytes per millisecond + return bytesTransferredSinceLastUpdate / timeElapsedSinceLastUpdate; + } + #size = 0; /** The size of the file in bytes. */ @@ -79,6 +92,11 @@ export class UploadFile { return this.type.split('/').slice(-1)[0] ?? ''; } + /** + * Tracks the number of bytes that had been uploaded when progress values last changed. + */ + @tracked bytesWhenProgressLastUpdated = 0; + /** The number of bytes that have been uploaded to the server */ @tracked loaded = 0; @@ -138,6 +156,12 @@ export class UploadFile { // */ // source?: FileSource; + /** + * The timestamp of when the progress last updated in milliseconds. Used to calculate the time + * that has elapsed. + */ + @tracked timestampWhenProgressLastUpdated = 0; + /** * Upload file with `application/octet-stream` content type. * diff --git a/test-app/tests/integration/progress-test.ts b/test-app/tests/integration/progress-test.ts index dd950ac2..2202cb7a 100644 --- a/test-app/tests/integration/progress-test.ts +++ b/test-app/tests/integration/progress-test.ts @@ -3,7 +3,10 @@ import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { selectFiles } from 'ember-file-upload/test-support'; -import { type MirageTestContext, setupMirage } from 'ember-cli-mirage/test-support'; +import { + type MirageTestContext, + setupMirage, +} from 'ember-cli-mirage/test-support'; import { TrackedArray } from 'tracked-built-ins'; import { type Asset } from 'test-app/components/demo-upload'; import type Owner from '@ember/owner'; @@ -66,6 +69,8 @@ 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( @@ -73,6 +78,7 @@ module('Integration | progress', function (hooks) { 33, 'first file uploaded - queue progress 33%', ); + assert.true(queue.rate > 0, 'rate should be > 0 when uploading'); assert.strictEqual( queue.files.length, 3, @@ -86,6 +92,7 @@ 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, @@ -99,6 +106,11 @@ 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, diff --git a/test-app/tests/unit/upload-file-test.ts b/test-app/tests/unit/upload-file-test.ts index 42f1cd3d..5d958746 100644 --- a/test-app/tests/unit/upload-file-test.ts +++ b/test-app/tests/unit/upload-file-test.ts @@ -3,7 +3,10 @@ import { setupTest } from 'ember-qunit'; import { uploadHandler } from 'ember-file-upload'; import { UploadFile, FileSource } from 'ember-file-upload'; -import { type MirageTestContext, setupMirage } from 'ember-cli-mirage/test-support'; +import { + type MirageTestContext, + setupMirage, +} from 'ember-cli-mirage/test-support'; module('Unit | UploadFile', function (hooks) { setupTest(hooks); @@ -85,4 +88,14 @@ 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); + }); });