Move LRUMap to browser
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { LRUMap } from 'browser/renderer/atlas/LRUMap';
|
||||
|
||||
describe('LRUMap', () => {
|
||||
it('can be used to store and retrieve values', () => {
|
||||
const map = new LRUMap(10);
|
||||
map.set(1, 'valuea');
|
||||
map.set(2, 'valueb');
|
||||
map.set(3, 'valuec');
|
||||
assert.strictEqual(map.get(1), 'valuea');
|
||||
assert.strictEqual(map.get(2), 'valueb');
|
||||
assert.strictEqual(map.get(3), 'valuec');
|
||||
});
|
||||
|
||||
it('maintains a size from insertions', () => {
|
||||
const map = new LRUMap(10);
|
||||
assert.strictEqual(map.size, 0);
|
||||
map.set(1, 'value');
|
||||
assert.strictEqual(map.size, 1);
|
||||
map.set(2, 'value');
|
||||
assert.strictEqual(map.size, 2);
|
||||
});
|
||||
|
||||
it('deletes the oldest entry when the capacity is exceeded', () => {
|
||||
const map = new LRUMap(4);
|
||||
map.set(1, 'value');
|
||||
map.set(2, 'value');
|
||||
map.set(3, 'value');
|
||||
map.set(4, 'value');
|
||||
map.set(5, 'value');
|
||||
assert.isNull(map.get(1));
|
||||
assert.isNotNull(map.get(2));
|
||||
assert.isNotNull(map.get(3));
|
||||
assert.isNotNull(map.get(4));
|
||||
assert.isNotNull(map.get(5));
|
||||
assert.strictEqual(map.size, 4);
|
||||
});
|
||||
|
||||
it('prevents a recently accessed entry from getting deleted', () => {
|
||||
const map = new LRUMap(2);
|
||||
map.set(1, 'value');
|
||||
map.set(2, 'value');
|
||||
map.get(1);
|
||||
// a would normally get deleted here, except that we called get()
|
||||
map.set(3, 'value');
|
||||
assert.isNotNull(map.get(1));
|
||||
// b got deleted instead of a
|
||||
assert.isNull(map.get(2));
|
||||
assert.isNotNull(map.get(3));
|
||||
});
|
||||
|
||||
it('supports mutation', () => {
|
||||
const map = new LRUMap(10);
|
||||
map.set(1, 'oldvalue');
|
||||
map.set(1, 'newvalue');
|
||||
// mutation doesn't change the size
|
||||
assert.strictEqual(map.size, 1);
|
||||
assert.strictEqual(map.get(1), 'newvalue');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
interface ILinkedListNode<T> {
|
||||
prev: ILinkedListNode<T> | null;
|
||||
next: ILinkedListNode<T> | null;
|
||||
key: number | null;
|
||||
value: T | null;
|
||||
}
|
||||
|
||||
export class LRUMap<T> {
|
||||
private _map: { [key: number]: ILinkedListNode<T> } = {};
|
||||
private _head: ILinkedListNode<T> | null = null;
|
||||
private _tail: ILinkedListNode<T> | null = null;
|
||||
private _nodePool: ILinkedListNode<T>[] = [];
|
||||
public size: number = 0;
|
||||
|
||||
constructor(public capacity: number) { }
|
||||
|
||||
private _unlinkNode(node: ILinkedListNode<T>): void {
|
||||
const prev = node.prev;
|
||||
const next = node.next;
|
||||
if (node === this._head) {
|
||||
this._head = next;
|
||||
}
|
||||
if (node === this._tail) {
|
||||
this._tail = prev;
|
||||
}
|
||||
if (prev !== null) {
|
||||
prev.next = next;
|
||||
}
|
||||
if (next !== null) {
|
||||
next.prev = prev;
|
||||
}
|
||||
}
|
||||
|
||||
private _appendNode(node: ILinkedListNode<T>): void {
|
||||
const tail = this._tail;
|
||||
if (tail !== null) {
|
||||
tail.next = node;
|
||||
}
|
||||
node.prev = tail;
|
||||
node.next = null;
|
||||
this._tail = node;
|
||||
if (this._head === null) {
|
||||
this._head = node;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preallocate a bunch of linked-list nodes. Allocating these nodes ahead of time means that
|
||||
* they're more likely to live next to each other in memory, which seems to improve performance.
|
||||
*
|
||||
* Each empty object only consumes about 60 bytes of memory, so this is pretty cheap, even for
|
||||
* large maps.
|
||||
*/
|
||||
public prealloc(count: number): void {
|
||||
const nodePool = this._nodePool;
|
||||
for (let i = 0; i < count; i++) {
|
||||
nodePool.push({
|
||||
prev: null,
|
||||
next: null,
|
||||
key: null,
|
||||
value: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get(key: number): T | null {
|
||||
// This is unsafe: We're assuming our keyspace doesn't overlap with Object.prototype. However,
|
||||
// it's faster than calling hasOwnProperty, and in our case, it would never overlap.
|
||||
const node = this._map[key];
|
||||
if (node !== undefined) {
|
||||
this._unlinkNode(node);
|
||||
this._appendNode(node);
|
||||
return node.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a value from a key without marking it as the most recently used item.
|
||||
*/
|
||||
public peekValue(key: number): T | null {
|
||||
const node = this._map[key];
|
||||
if (node !== undefined) {
|
||||
return node.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public peek(): T | null {
|
||||
const head = this._head;
|
||||
return head === null ? null : head.value;
|
||||
}
|
||||
|
||||
public set(key: number, value: T): void {
|
||||
// This is unsafe: See note above.
|
||||
let node = this._map[key];
|
||||
if (node !== undefined) {
|
||||
// already exists, we just need to mutate it and move it to the end of the list
|
||||
node = this._map[key];
|
||||
this._unlinkNode(node);
|
||||
node.value = value;
|
||||
} else if (this.size >= this.capacity) {
|
||||
// we're out of space: recycle the head node, move it to the tail
|
||||
node = this._head!;
|
||||
this._unlinkNode(node);
|
||||
delete this._map[node.key!];
|
||||
node.key = key;
|
||||
node.value = value;
|
||||
this._map[key] = node;
|
||||
} else {
|
||||
// make a new element
|
||||
const nodePool = this._nodePool;
|
||||
if (nodePool.length > 0) {
|
||||
// use a preallocated node if we can
|
||||
node = nodePool.pop()!;
|
||||
node.key = key;
|
||||
node.value = value;
|
||||
} else {
|
||||
node = {
|
||||
prev: null,
|
||||
next: null,
|
||||
key,
|
||||
value
|
||||
};
|
||||
}
|
||||
this._map[key] = node;
|
||||
this.size++;
|
||||
}
|
||||
this._appendNode(node);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user