Skip to content

Commit

Permalink
Add some padding and a shrink threshold to CircularList
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
Tyriar committed Aug 6, 2017
1 parent 109d467 commit 27f7679
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 9 deletions.
29 changes: 29 additions & 0 deletions src/utils/CircularList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
import { assert } from 'chai';
import { CircularList } from './CircularList';

class TestCircularList<T> extends CircularList<T> {
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', () => {
Expand Down Expand Up @@ -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<string>(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<string>(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', () => {
Expand Down
45 changes: 36 additions & 9 deletions src/utils/CircularList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> extends EventEmitter implements ICircularList<T> {
private _array: T[];
protected _array: T[];
private _startIndex: number;
private _length: number;

constructor(maxLength: number) {
constructor(
private _maxLength: number
) {
super();
this._array = new Array<T>(maxLength);
this._array = new Array<T>(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<T>(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<T>(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;
}

Expand Down Expand Up @@ -92,9 +119,9 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
*/
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);
Expand Down

0 comments on commit 27f7679

Please sign in to comment.