mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Add a new LRUMap class, remove uses of Map
LRUMap implements the parts of an ordered map that we need to efficiently implement DynamicCharAtlas. It's more code, but there's a few advantages of this approach: - Map isn't available on some older browsers, so this removes the need for a polyfill. - Moving an item to the end of the map's iteration order now only requires unlinking and linking a linked-list node, whereas before we had to delete and re-insert our value. - Peeking at the oldest entry in the map no longer requires allocating and destroying an iterator. - We can preallocate the linked-list nodes we want to improve cache locality. Similarly, we can recycle linked-list nodes to reduce allocations and the GC pauses those allocations may cause. - LRUMap seems to give slightly better results in Chrome's profiler than Map did. We now spend about 5% of our time on map operations instead of about 10%. - In my (limited) testing, it doesn't look like LRUMap is slowing down over time. Map appeared to get slightly slower the longer I ran the terminal for, either due to memory fragmentation or some sort of leak. I still need to write some tests for LRUMap, but I've been using this implementation for the last hour without problems.
This commit is contained in:
@@ -7,40 +7,26 @@ import { DIM_OPACITY, IGlyphIdentifier, INVERTED_DEFAULT_COLOR } from './Types';
|
||||
import { ICharAtlasConfig } from '../../shared/atlas/Types';
|
||||
import BaseCharAtlas from './BaseCharAtlas';
|
||||
import { clearColor } from '../../shared/atlas/CharAtlasGenerator';
|
||||
import LRUMap from './LRUMap';
|
||||
|
||||
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
|
||||
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
|
||||
const TEXTURE_WIDTH = 1024;
|
||||
const TEXTURE_HEIGHT = 1024;
|
||||
|
||||
type GlyphCacheKey = string;
|
||||
|
||||
interface IGlyphCacheValue {
|
||||
index: number;
|
||||
isEmpty: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the oldest element in a map.
|
||||
*/
|
||||
function mapShift<K, V>(map: Map<K, V>): [K, V] {
|
||||
// Map guarantees insertion-order iteration.
|
||||
const entry = map.entries().next().value;
|
||||
if (entry === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
map.delete(entry[0]);
|
||||
return entry;
|
||||
}
|
||||
|
||||
function getGlyphCacheKey(glyph: IGlyphIdentifier): GlyphCacheKey {
|
||||
function getGlyphCacheKey(glyph: IGlyphIdentifier): string {
|
||||
return `${glyph.bg}_${glyph.fg}_${glyph.bold ? 0 : 1}${glyph.dim ? 0 : 1}${glyph.char}`;
|
||||
}
|
||||
|
||||
export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
// An ordered map that we're using to keep track of where each glyph is in the atlas texture.
|
||||
// It's ordered so that we can determine when to remove the old entries.
|
||||
private _cacheMap: Map<GlyphCacheKey, IGlyphCacheValue> = new Map();
|
||||
private _cacheMap: LRUMap<IGlyphCacheValue>;
|
||||
|
||||
// The texture that the atlas is drawn to
|
||||
private _cacheCanvas: HTMLCanvasElement;
|
||||
@@ -51,7 +37,6 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
private _tmpCtx: CanvasRenderingContext2D;
|
||||
|
||||
// The number of characters stored in the atlas by width/height
|
||||
private _capacity: number;
|
||||
private _width: number;
|
||||
private _height: number;
|
||||
|
||||
@@ -70,7 +55,9 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
|
||||
this._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth);
|
||||
this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight);
|
||||
this._capacity = this._width * this._height;
|
||||
const capacity = this._width * this._height;
|
||||
this._cacheMap = new LRUMap(capacity);
|
||||
this._cacheMap.prealloc(capacity);
|
||||
|
||||
// This is useful for debugging
|
||||
// document.body.appendChild(this._cacheCanvas);
|
||||
@@ -85,17 +72,15 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
const glyphKey = getGlyphCacheKey(glyph);
|
||||
const cacheValue = this._cacheMap.get(glyphKey);
|
||||
if (cacheValue != null) {
|
||||
// move to end of insertion order, so this can behave like an LRU cache
|
||||
this._cacheMap.delete(glyphKey);
|
||||
this._cacheMap.set(glyphKey, cacheValue);
|
||||
this._drawFromCache(ctx, cacheValue, x, y);
|
||||
return true;
|
||||
} else if (this._canCache(glyph)) {
|
||||
let index;
|
||||
if (this._cacheMap.size < this._capacity) {
|
||||
if (this._cacheMap.size < this._cacheMap.capacity) {
|
||||
index = this._cacheMap.size;
|
||||
} else {
|
||||
index = mapShift(this._cacheMap)[1].index;
|
||||
// we're out of space, so our call to set will delete this item
|
||||
index = this._cacheMap.peek().index;
|
||||
}
|
||||
const cacheValue = this._drawToCache(glyph, index);
|
||||
this._cacheMap.set(glyphKey, cacheValue);
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
interface ILinkedListNode<T> {
|
||||
prev: ILinkedListNode<T>,
|
||||
next: ILinkedListNode<T>,
|
||||
key: string,
|
||||
value: T,
|
||||
}
|
||||
|
||||
export default class LRUMap<T> {
|
||||
private _map = {};
|
||||
private _head: ILinkedListNode<T> = null;
|
||||
private _tail: ILinkedListNode<T> = 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 {
|
||||
node.prev = this._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) {
|
||||
const nodePool = this._nodePool;
|
||||
for (let i = 0; i < count; i++) {
|
||||
nodePool.push({
|
||||
prev: null,
|
||||
next: null,
|
||||
key: null,
|
||||
value: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get(key: string): 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;
|
||||
}
|
||||
|
||||
public peek(): T | null {
|
||||
const head = this._head;
|
||||
return head === null ? null : head.value;
|
||||
}
|
||||
|
||||
public set(key: string, 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);
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -6,9 +6,7 @@
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ScriptHost",
|
||||
"ES2015.Promise",
|
||||
"ES2015.Collection",
|
||||
"ES2015.Iterable"
|
||||
"ES2015.Promise"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"outDir": "lib",
|
||||
|
||||
Reference in New Issue
Block a user