Skip to content

Commit

Permalink
perf(dynamic-chunk): increase chunks time limits (#342)
Browse files Browse the repository at this point in the history
Changed the chunk time range from 2 - 8 sec to 8 - 24 sec.
This noticeably reduces server and client overhead and decreases upload time.
  • Loading branch information
kukhariev authored Nov 11, 2021
1 parent 6ee281e commit f415d2f
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 72 deletions.
2 changes: 1 addition & 1 deletion src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ export class AppComponent implements DoCheck {
}

ngDoCheck(): void {
console.log('change-detection ', this.changes++);
console.debug('change-detection count: ', this.changes++);
}
}
16 changes: 16 additions & 0 deletions src/uploadx/lib/dynamic-chunk.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DynamicChunk } from './dynamic-chunk';

describe('DynamicChunk', () => {
const init = DynamicChunk.size;
it('scale', () => {
expect(DynamicChunk.scale(0)).toEqual(init / 2);
expect(DynamicChunk.scale(0)).toEqual(init / 4);
expect(DynamicChunk.scale(Number.MAX_SAFE_INTEGER)).toEqual(init / 2);
expect(DynamicChunk.scale(Number.MAX_SAFE_INTEGER)).toEqual(init);
expect(DynamicChunk.scale(Number.MAX_SAFE_INTEGER)).toEqual(init * 2);
expect(DynamicChunk.scale(undefined as any)).toEqual(init * 2);
});
afterEach(() => {
DynamicChunk.size = init;
});
});
25 changes: 25 additions & 0 deletions src/uploadx/lib/dynamic-chunk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const KiB = 1024;
/**
* Adaptive chunk size
*/
export class DynamicChunk {
/** Maximum chunk size in bytes */
static maxSize = Number.MAX_SAFE_INTEGER;
/** Minimum chunk size in bytes */
static minSize = 256 * KiB;
/** Initial chunk size in bytes */
static size = 4 * (256 * KiB);
static minChunkTime = 8;
static maxChunkTime = 24;

static scale(throughput: number): number {
const elapsedTime = DynamicChunk.size / throughput;
if (elapsedTime < DynamicChunk.minChunkTime) {
DynamicChunk.size = Math.min(DynamicChunk.maxSize, DynamicChunk.size * 2);
}
if (elapsedTime > DynamicChunk.maxChunkTime) {
DynamicChunk.size = Math.max(DynamicChunk.minSize, DynamicChunk.size / 2);
}
return DynamicChunk.size;
}
}
62 changes: 32 additions & 30 deletions src/uploadx/lib/uploader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Ajax, AjaxRequestConfig } from './ajax';
import { Canceler } from './canceler';
import { DynamicChunk } from './dynamic-chunk';
import {
AuthorizeRequest,
Metadata,
Expand All @@ -16,7 +17,7 @@ import {
} from './interfaces';
import { ErrorType, RetryHandler } from './retry-handler';
import { store } from './store';
import { DynamicChunk, isNumber, unfunc } from './utils';
import { isNumber, unfunc } from './utils';

const actionToStatusMap: { [K in UploadAction]: UploadStatus } = {
pause: 'paused',
Expand Down Expand Up @@ -58,6 +59,29 @@ export abstract class Uploader implements UploadState {
private readonly _authorize: AuthorizeRequest;
private readonly _prerequest: PreRequest;
private _token!: string;

constructor(
readonly file: File,
readonly options: Readonly<UploaderOptions>,
readonly stateChange: (evt: UploadState) => void,
readonly ajax: Ajax
) {
this.retry = new RetryHandler(options.retryConfig);
this.name = file.name;
this.size = file.size;
this.metadata = {
name: file.name,
mimeType: file.type || 'application/octet-stream',
size: file.size,
lastModified:
file.lastModified || (file as File & { lastModifiedDate: Date }).lastModifiedDate.getTime()
};
options.maxChunkSize && (DynamicChunk.maxSize = options.maxChunkSize);
this._prerequest = options.prerequest || (req => req);
this._authorize = options.authorize || (req => req);
this.configure(options);
}

private _url = '';

get url(): string {
Expand Down Expand Up @@ -88,28 +112,6 @@ export abstract class Uploader implements UploadState {
}
}

constructor(
readonly file: File,
readonly options: Readonly<UploaderOptions>,
readonly stateChange: (evt: UploadState) => void,
readonly ajax: Ajax
) {
this.retry = new RetryHandler(options.retryConfig);
this.name = file.name;
this.size = file.size;
this.metadata = {
name: file.name,
mimeType: file.type || 'application/octet-stream',
size: file.size,
lastModified:
file.lastModified || (file as File & { lastModifiedDate: Date }).lastModifiedDate.getTime()
};
options.maxChunkSize && (DynamicChunk.maxSize = options.maxChunkSize);
this._prerequest = options.prerequest || (req => req);
this._authorize = options.authorize || (req => req);
this.configure(options);
}

/**
* Configure uploader
*/
Expand Down Expand Up @@ -170,10 +172,6 @@ export abstract class Uploader implements UploadState {
}
}

private getRetryAfterFromBackend(): number {
return Number(this.getValueFromResponse('retry-after')) * 1000;
}

/**
* Performs http requests
*/
Expand Down Expand Up @@ -264,20 +262,24 @@ export abstract class Uploader implements UploadState {
return { start, end, body };
}

private getRetryAfterFromBackend(): number {
return Number(this.getValueFromResponse('retry-after')) * 1000;
}

private cleanup = () => store.delete(this.uploadId);

private onProgress(): (evt: ProgressEvent) => void {
let throttle: ReturnType<typeof setTimeout> | undefined;
const startTime = Date.now();
const startTime = new Date().getTime();
return ({ loaded }) => {
const elapsedTime = (Date.now() - startTime) / 1000;
const elapsedTime = (new Date().getTime() - startTime) / 1000;
this.speed = Math.round(
(this.speed * this._progressEventCount + loaded / elapsedTime) / ++this._progressEventCount
);
DynamicChunk.scale(this.speed);
if (!throttle) {
throttle = setTimeout(() => (throttle = undefined), 500);
const uploaded = (this.offset || 0) + loaded;
const uploaded = (this.offset as number) + loaded;
this.progress = +((uploaded / this.size) * 100).toFixed(2);
this.remaining = Math.ceil((this.size - uploaded) / this.speed);
this.stateChange(this);
Expand Down
17 changes: 1 addition & 16 deletions src/uploadx/lib/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { b64, DynamicChunk, isNumber, resolveUrl, unfunc } from './utils';
import { b64, isNumber, resolveUrl, unfunc } from './utils';

/* eslint-disable @typescript-eslint/no-explicit-any */

Expand Down Expand Up @@ -47,18 +47,3 @@ describe('primitives', () => {
expect(unfunc((x: number) => 10 * x, 2)).toBe(20);
});
});

describe('DynamicChunk', () => {
const init = DynamicChunk.size;
it('scale', () => {
expect(DynamicChunk.scale(0)).toEqual(init / 2);
expect(DynamicChunk.scale(0)).toEqual(init / 4);
expect(DynamicChunk.scale(Number.MAX_SAFE_INTEGER)).toEqual(init / 2);
expect(DynamicChunk.scale(Number.MAX_SAFE_INTEGER)).toEqual(init);
expect(DynamicChunk.scale(Number.MAX_SAFE_INTEGER)).toEqual(init * 2);
expect(DynamicChunk.scale(undefined as any)).toEqual(init * 2);
});
afterEach(() => {
DynamicChunk.size = init;
});
});
25 changes: 0 additions & 25 deletions src/uploadx/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,31 +75,6 @@ export const b64 = {
}
};

/**
* Adaptive chunk size
*/
export class DynamicChunk {
/** Maximum chunk size in bytes */
static maxSize = Number.MAX_SAFE_INTEGER;
/** Minimum chunk size in bytes */
static minSize = 1024 * 256;
/** Initial chunk size in bytes */
static size = 4096 * 256;
static minChunkTime = 2;
static maxChunkTime = 8;

static scale(throughput: number): number {
const elapsedTime = DynamicChunk.size / throughput;
if (elapsedTime < DynamicChunk.minChunkTime) {
DynamicChunk.size = Math.min(DynamicChunk.maxSize, DynamicChunk.size * 2);
}
if (elapsedTime > DynamicChunk.maxChunkTime) {
DynamicChunk.size = Math.max(DynamicChunk.minSize, DynamicChunk.size / 2);
}
return DynamicChunk.size;
}
}

export function isIOS(): boolean {
return /iPad|iPhone|iPod/.test(navigator.platform)
? true
Expand Down

0 comments on commit f415d2f

Please sign in to comment.