diff --git a/src/utils/CircularList.test.ts b/src/utils/CircularList.test.ts index e27e7f7a6..891ba3c50 100644 --- a/src/utils/CircularList.test.ts +++ b/src/utils/CircularList.test.ts @@ -5,6 +5,13 @@ import { assert } from 'chai'; import { CircularList } from './CircularList'; +class TestCircularList extends CircularList { + public get array(): T[] { return this._array; } +} + +const MAX_LENGTH_PADDING = 40; +const MAX_LENGTH_SHRINK_REBUILD_THRESHOLD = 120; + describe('CircularList', () => { describe('push', () => { it('should push values onto the array', () => { @@ -68,6 +75,28 @@ describe('CircularList', () => { list.maxLength = 4; assert.equal(list.maxLength, 4); }); + + it('should resize the backing array\'s size only when it increases beyond the allocated padding', () => { + const initSize = 200; + const list = new TestCircularList(initSize); + const expectedBackingMaxLength = initSize + MAX_LENGTH_PADDING; + assert.equal(list.array.length, expectedBackingMaxLength); + list.maxLength = expectedBackingMaxLength; + assert.equal(list.array.length, expectedBackingMaxLength); + list.maxLength = expectedBackingMaxLength + 1; + assert.equal(list.array.length, expectedBackingMaxLength + 1 + MAX_LENGTH_PADDING); + }); + + it('should resize the backing array\'s size only when it reduces beyond the shrink threshold', () => { + const initSize = 200; + const list = new TestCircularList(initSize); + const expectedThresholdMaxLength = initSize + MAX_LENGTH_PADDING - MAX_LENGTH_SHRINK_REBUILD_THRESHOLD; + assert.equal(list.array.length, initSize + MAX_LENGTH_PADDING); + list.maxLength = expectedThresholdMaxLength; + assert.equal(list.array.length, initSize + MAX_LENGTH_PADDING); + list.maxLength = expectedThresholdMaxLength - 1; + assert.equal(list.array.length, expectedThresholdMaxLength - 1 + MAX_LENGTH_PADDING); + }); }); describe('length', () => { diff --git a/src/utils/CircularList.ts b/src/utils/CircularList.ts index 23d6c3373..ed8c065d7 100644 --- a/src/utils/CircularList.ts +++ b/src/utils/CircularList.ts @@ -8,33 +8,60 @@ import { EventEmitter } from '../EventEmitter'; import { ICircularList } from '../Interfaces'; +/** + * Padding added to the max length of the CircularList, this allows increasing + * the max length of the list without reconstructing the backing array and then + * copying over the values. + */ +const MAX_LENGTH_PADDING = 40; + +const MAX_LENGTH_SHRINK_REBUILD_THRESHOLD = MAX_LENGTH_PADDING * 3; + export class CircularList extends EventEmitter implements ICircularList { - private _array: T[]; + protected _array: T[]; private _startIndex: number; private _length: number; - constructor(maxLength: number) { + constructor( + private _maxLength: number + ) { super(); - this._array = new Array(maxLength); + this._array = new Array(this._maxLength + MAX_LENGTH_PADDING); this._startIndex = 0; this._length = 0; } public get maxLength(): number { - return this._array.length; + return this._maxLength; } public set maxLength(newMaxLength: number) { - if (this.maxLength === newMaxLength) { + // There was no change in maxLength, return early. + if (this._maxLength === newMaxLength) { return; } + + // Ensure the array has padding to expand into. + if (this._array.length >= newMaxLength) { + // Ensure that the array hasn't shrunk significantly, if it has we + // should rebuild the array with a new max length to reclaim some memory. + if (this._array.length - newMaxLength <= MAX_LENGTH_SHRINK_REBUILD_THRESHOLD) { + // Increase the max length and return. + this._maxLength = newMaxLength; + return; + } + } + // Reconstruct array, starting at index 0. Only transfer values from the - // indexes 0 to length. - let newArray = new Array(newMaxLength); + // indexes 0 to length. The new array's size includes some padding that + // allows the array's size to increase a little without rebuilding the + // array. + let newArray = new Array(newMaxLength + MAX_LENGTH_PADDING); for (let i = 0; i < Math.min(newMaxLength, this.length); i++) { newArray[i] = this._array[this._getCyclicIndex(i)]; } this._array = newArray; + this._maxLength = newMaxLength; this._startIndex = 0; } @@ -92,9 +119,9 @@ export class CircularList extends EventEmitter implements ICircularList { */ public push(value: T): void { this._array[this._getCyclicIndex(this._length)] = value; - if (this._length === this.maxLength) { + if (this._length === this._maxLength) { this._startIndex++; - if (this._startIndex === this.maxLength) { + if (this._startIndex === this._maxLength) { this._startIndex = 0; } this.emit('trim', 1);