From 27f7679494620e6a2467370c792c58894bd450a5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 6 Aug 2017 14:17:27 -0700 Subject: [PATCH] Add some padding and a shrink threshold to CircularList This is needed so that resize is still performant. Now the list size is only rebuild (O(n)) when it goes beyond 40 entries of the initial max length. The List is also rebuilt when the size shrinks below 120 of the actual backing max length (init_max_length + 40 - 120). --- src/utils/CircularList.test.ts | 29 ++++++++++++++++++++++ src/utils/CircularList.ts | 45 +++++++++++++++++++++++++++------- 2 files changed, 65 insertions(+), 9 deletions(-) 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);