diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index cfbbfd63588..20afbbf6bb9 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -37,6 +37,7 @@ export function getOrSet(map: Map, key: K, value: V): V { } export interface ISerializedBoundedLinkedMap { + version?: string; entries: { key: string; value: T }[]; } @@ -621,14 +622,12 @@ interface Item { value: V; } -export namespace Touch { - export const None: 0 = 0; - export const First: 1 = 1; - export const Last: 2 = 2; +export enum Touch { + None = 0, + AsOld = 1, + AsNew = 2 } -export type Touch = 0 | 1 | 2; - export class LinkedMap { private _map: Map>; @@ -662,11 +661,14 @@ export class LinkedMap { return this._map.has(key); } - public get(key: K): V | undefined { + public get(key: K, touch: Touch = Touch.None): V | undefined { const item = this._map.get(key); if (!item) { return undefined; } + if (touch !== Touch.None) { + this.touch(item, touch); + } return item.value; } @@ -683,10 +685,10 @@ export class LinkedMap { case Touch.None: this.addItemLast(item); break; - case Touch.First: + case Touch.AsOld: this.addItemFirst(item); break; - case Touch.Last: + case Touch.AsNew: this.addItemLast(item); break; default: @@ -811,6 +813,26 @@ export class LinkedMap { } */ + protected trimOld(newSize: number) { + if (newSize >= this.size) { + return; + } + if (newSize === 0) { + this.clear(); + return; + } + let current = this._head; + let currentSize = this.size; + while (current && currentSize > newSize) { + this._map.delete(current.key); + current = current.next; + currentSize--; + } + this._head = current; + this._size = currentSize; + current.previous = void 0; + } + private addItemFirst(item: Item): void { // First time Insert if (!this._head && !this._tail) { @@ -839,8 +861,8 @@ export class LinkedMap { private removeItem(item: Item): void { if (item === this._head && item === this._tail) { - this._head = undefined; - this._tail = undefined; + this._head = void 0; + this._tail = void 0; } else if (item === this._head) { this._head = item.next; @@ -863,11 +885,11 @@ export class LinkedMap { if (!this._head || !this._tail) { throw new Error('Invalid list'); } - if ((touch !== Touch.First && touch !== Touch.Last)) { + if ((touch !== Touch.AsOld && touch !== Touch.AsNew)) { return; } - if (touch === Touch.First) { + if (touch === Touch.AsOld) { if (item === this._head) { return; } @@ -879,7 +901,7 @@ export class LinkedMap { if (item === this._tail) { // previous must be defined since item was not head but is tail // So there are more than on item in the map - previous!.next = undefined; + previous!.next = void 0; this._tail = previous; } else { @@ -889,11 +911,11 @@ export class LinkedMap { } // Insert the node at head - item.previous = undefined; + item.previous = void 0; item.next = this._head; this._head.previous = item; this._head = item; - } else if (touch === Touch.Last) { + } else if (touch === Touch.AsNew) { if (item === this._tail) { return; } @@ -905,17 +927,62 @@ export class LinkedMap { if (item === this._head) { // next must be defined since item was not tail but is head // So there are more than on item in the map - next!.previous = undefined; + next!.previous = void 0; this._head = next; } else { // Both next and previous are not undefined since item was neither head nor tail. next!.previous = previous; previous!.next = next; } - item.next = undefined; + item.next = void 0; item.previous = this._tail; this._tail.next = item; this._tail = item; } } } + +export class LRUCache extends LinkedMap { + + private _limit: number; + private _ratio: number; + + constructor(limit: number, ratio: number = 1) { + super(); + this._limit = limit; + this._ratio = Math.min(Math.max(0, ratio), 1); + } + + public get limit(): number { + return this._limit; + } + + public set limit(limit: number) { + this._limit = limit; + this.checkTrim(); + } + + public get ratio(): number { + return this._ratio; + } + + public set ratio(ratio: number) { + this._ratio = Math.min(Math.max(0, ratio), 1); + this.checkTrim(); + } + + public get(key: K): V | undefined { + return super.get(key, Touch.AsNew); + } + + public set(key: K, value: V): void { + super.set(key, value, Touch.AsNew); + this.checkTrim(); + } + + private checkTrim() { + if (this.size > this._limit) { + this.trimOld(Math.round(this._limit * this._ratio)); + } + } +} diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index 830f6592004..627437ec6fc 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -6,12 +6,186 @@ 'use strict'; -import { BoundedMap, ResourceMap, TernarySearchTree, PathIterator, StringIterator } from 'vs/base/common/map'; +import { BoundedMap, ResourceMap, TernarySearchTree, PathIterator, StringIterator, LinkedMap, Touch, LRUCache } from 'vs/base/common/map'; import * as assert from 'assert'; import URI from 'vs/base/common/uri'; suite('Map', () => { + test('LinkedMap - Simple', () => { + let map = new LinkedMap(); + map.set('ak', 'av'); + map.set('bk', 'bv'); + assert.deepStrictEqual(map.keys(), ['ak', 'bk']); + assert.deepStrictEqual(map.values(), ['av', 'bv']); + }); + + test('LinkedMap - Touch Old one', () => { + let map = new LinkedMap(); + map.set('ak', 'av'); + map.set('ak', 'av', Touch.AsOld); + assert.deepStrictEqual(map.keys(), ['ak']); + assert.deepStrictEqual(map.values(), ['av']); + }); + + test('LinkedMap - Touch New one', () => { + let map = new LinkedMap(); + map.set('ak', 'av'); + map.set('ak', 'av', Touch.AsNew); + assert.deepStrictEqual(map.keys(), ['ak']); + assert.deepStrictEqual(map.values(), ['av']); + }); + + test('LinkedMap - Touch Old two', () => { + let map = new LinkedMap(); + map.set('ak', 'av'); + map.set('bk', 'bv'); + map.set('bk', 'bv', Touch.AsOld); + assert.deepStrictEqual(map.keys(), ['bk', 'ak']); + assert.deepStrictEqual(map.values(), ['bv', 'av']); + }); + + test('LinkedMap - Touch New two', () => { + let map = new LinkedMap(); + map.set('ak', 'av'); + map.set('bk', 'bv'); + map.set('ak', 'av', Touch.AsNew); + assert.deepStrictEqual(map.keys(), ['bk', 'ak']); + assert.deepStrictEqual(map.values(), ['bv', 'av']); + }); + + test('LinkedMap - Touch Old from middle', () => { + let map = new LinkedMap(); + map.set('ak', 'av'); + map.set('bk', 'bv'); + map.set('ck', 'cv'); + map.set('bk', 'bv', Touch.AsOld); + assert.deepStrictEqual(map.keys(), ['bk', 'ak', 'ck']); + assert.deepStrictEqual(map.values(), ['bv', 'av', 'cv']); + }); + + test('LinkedMap - Touch New from middle', () => { + let map = new LinkedMap(); + map.set('ak', 'av'); + map.set('bk', 'bv'); + map.set('ck', 'cv'); + map.set('bk', 'bv', Touch.AsNew); + assert.deepStrictEqual(map.keys(), ['ak', 'ck', 'bk']); + assert.deepStrictEqual(map.values(), ['av', 'cv', 'bv']); + }); + + test('LinkedMap - basics', function () { + const map = new LinkedMap(); + + assert.equal(map.size, 0); + + map.set('1', 1); + map.set('2', '2'); + map.set('3', true); + + const obj = Object.create(null); + map.set('4', obj); + + const date = Date.now(); + map.set('5', date); + + assert.equal(map.size, 5); + assert.equal(map.get('1'), 1); + assert.equal(map.get('2'), '2'); + assert.equal(map.get('3'), true); + assert.equal(map.get('4'), obj); + assert.equal(map.get('5'), date); + assert.ok(!map.get('6')); + + map.delete('6'); + assert.equal(map.size, 5); + assert.equal(map.delete('1'), true); + assert.equal(map.delete('2'), true); + assert.equal(map.delete('3'), true); + assert.equal(map.delete('4'), true); + assert.equal(map.delete('5'), true); + + assert.equal(map.size, 0); + assert.ok(!map.get('5')); + assert.ok(!map.get('4')); + assert.ok(!map.get('3')); + assert.ok(!map.get('2')); + assert.ok(!map.get('1')); + + map.set('1', 1); + map.set('2', '2'); + map.set('3', true); + + assert.ok(map.has('1')); + assert.equal(map.get('1'), 1); + assert.equal(map.get('2'), '2'); + assert.equal(map.get('3'), true); + + map.clear(); + + assert.equal(map.size, 0); + assert.ok(!map.get('1')); + assert.ok(!map.get('2')); + assert.ok(!map.get('3')); + assert.ok(!map.has('1')); + }); + + test('LinkedMap - LRUCache simple', () => { + const cache = new LRUCache(5); + + [1, 2, 3, 4, 5].forEach(value => cache.set(value, value)); + assert.strictEqual(cache.size, 5); + cache.set(6, 6); + assert.strictEqual(cache.size, 5); + assert.deepStrictEqual(cache.keys(), [2, 3, 4, 5, 6]); + cache.set(7, 7); + assert.strictEqual(cache.size, 5); + assert.deepStrictEqual(cache.keys(), [3, 4, 5, 6, 7]); + let values: number[] = []; + [3, 4, 5, 6, 7].forEach(key => values.push(cache.get(key))); + assert.deepStrictEqual(values, [3, 4, 5, 6, 7]); + }); + + test('LinkedMap - LRU Cache limit', () => { + const cache = new LRUCache(10); + + for (let i = 1; i <= 10; i++) { + cache.set(i, i); + } + assert.strictEqual(cache.size, 10); + cache.limit = 5; + assert.strictEqual(cache.size, 5); + assert.deepStrictEqual(cache.keys(), [6, 7, 8, 9, 10]); + cache.limit = 20; + assert.strictEqual(cache.size, 5); + for (let i = 11; i <= 20; i++) { + cache.set(i, i); + } + assert.deepEqual(cache.size, 15); + let values: number[] = []; + for (let i = 6; i <= 20; i++) { + values.push(cache.get(i)); + assert.strictEqual(cache.get(i), i); + } + assert.deepStrictEqual(cache.values(), values); + }); + + test('LinkedMap - LRU Cache limit with ration', () => { + const cache = new LRUCache(10, 0.5); + + for (let i = 1; i <= 10; i++) { + cache.set(i, i); + } + assert.strictEqual(cache.size, 10); + cache.set(11, 11); + assert.strictEqual(cache.size, 5); + assert.deepStrictEqual(cache.keys(), [7, 8, 9, 10, 11]); + let values: number[] = []; + cache.keys().forEach(key => values.push(cache.get(key))); + assert.deepStrictEqual(values, [7, 8, 9, 10, 11]); + assert.deepStrictEqual(cache.values(), values); + }); + test('BoundedMap - basics', function () { const map = new BoundedMap(); diff --git a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts index 02096c0abd6..3f7532cd32e 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts @@ -1188,7 +1188,7 @@ class TaskService implements ITaskService { let executeResult = this.getTaskSystem().run(task, resolver); let key = Task.getRecentlyUsedKey(task); if (key) { - this.getRecentlyUsedTasks().set(key, key, Touch.First); + this.getRecentlyUsedTasks().set(key, key, Touch.AsOld); } if (executeResult.kind === TaskExecuteKind.Active) { let active = executeResult.active; diff --git a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts index 074ee5b0e5b..55f852198c0 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts @@ -280,7 +280,7 @@ export class TerminalTaskSystem implements ITaskSystem { this.sameTaskTerminals[key] = terminal.id.toString(); break; case PanelKind.Shared: - this.idleTaskTerminals.set(key, terminal.id.toString(), Touch.First); + this.idleTaskTerminals.set(key, terminal.id.toString(), Touch.AsOld); break; } watchingProblemMatcher.done(); @@ -322,7 +322,7 @@ export class TerminalTaskSystem implements ITaskSystem { this.sameTaskTerminals[key] = terminal.id.toString(); break; case PanelKind.Shared: - this.idleTaskTerminals.set(key, terminal.id.toString(), Touch.First); + this.idleTaskTerminals.set(key, terminal.id.toString(), Touch.AsOld); break; } startStopProblemMatcher.done();