Remove BoundedMap

This commit is contained in:
Dirk Baeumer
2017-11-20 10:11:49 +01:00
parent ac41cd118e
commit 07501c2c64
7 changed files with 63 additions and 518 deletions
@@ -13,7 +13,7 @@ import paths = require('vs/base/common/paths');
import { Builder, $ } from 'vs/base/browser/builder';
import DOM = require('vs/base/browser/dom');
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { BoundedMap } from 'vs/base/common/map';
import { LRUCache } from 'vs/base/common/map';
import { Schemas } from 'vs/base/common/network';
interface MapExtToMediaMimes {
@@ -82,7 +82,7 @@ export interface IResourceDescriptor {
// we need to bypass the cache or not. We could always bypass the cache everytime we show the image
// however that has very bad impact on memory consumption because each time the image gets shown,
// memory grows (see also https://github.com/electron/electron/issues/6275)
const IMAGE_RESOURCE_ETAG_CACHE = new BoundedMap<{ etag: string, src: string }>(100);
const IMAGE_RESOURCE_ETAG_CACHE = new LRUCache<string, { etag: string, src: string }>(100);
function imageSrc(descriptor: IResourceDescriptor): string {
if (descriptor.resource.scheme === Schemas.data) {
return descriptor.resource.toString(true /* skip encoding */);
+2 -2
View File
@@ -5,7 +5,7 @@
'use strict';
import strings = require('vs/base/common/strings');
import { BoundedMap } from 'vs/base/common/map';
import { LRUCache } from 'vs/base/common/map';
import { CharCode } from 'vs/base/common/charCode';
export interface IFilter {
@@ -317,7 +317,7 @@ function nextWord(word: string, start: number): number {
export const fuzzyContiguousFilter = or(matchesPrefix, matchesCamelCase, matchesContiguousSubString);
const fuzzySeparateFilter = or(matchesPrefix, matchesCamelCase, matchesSubString);
const fuzzyRegExpCache = new BoundedMap<RegExp>(10000); // bounded to 10000 elements
const fuzzyRegExpCache = new LRUCache<string, RegExp>(10000); // bounded to 10000 elements
export function matchesFuzzy(word: string, wordToMatchAgainst: string, enableSeparateSubstringMatching = false): IMatch[] {
if (typeof word !== 'string' || typeof wordToMatchAgainst !== 'string') {
+2 -2
View File
@@ -7,7 +7,7 @@
import arrays = require('vs/base/common/arrays');
import strings = require('vs/base/common/strings');
import paths = require('vs/base/common/paths');
import { BoundedMap } from 'vs/base/common/map';
import { LRUCache } from 'vs/base/common/map';
import { CharCode } from 'vs/base/common/charCode';
import { TPromise } from 'vs/base/common/winjs.base';
@@ -267,7 +267,7 @@ interface ParsedExpressionPattern {
allPaths?: string[];
}
const CACHE = new BoundedMap<ParsedStringPattern>(10000); // bounded to 10000 elements
const CACHE = new LRUCache<string, ParsedStringPattern>(10000); // bounded to 10000 elements
const FALSE = function () {
return false;
+4 -186
View File
@@ -7,11 +7,6 @@
import URI from 'vs/base/common/uri';
export interface Entry<K, T> {
key: K;
value: T;
}
export function values<K, V>(map: Map<K, V>): V[] {
const result: V[] = [];
map.forEach(value => result.push(value));
@@ -36,187 +31,6 @@ export function getOrSet<K, V>(map: Map<K, V>, key: K, value: V): V {
return result;
}
export interface ISerializedBoundedLinkedMap<T> {
version?: string;
entries: { key: string; value: T }[];
}
interface LinkedEntry<K, T> extends Entry<K, T> {
next?: LinkedEntry<K, T>;
prev?: LinkedEntry<K, T>;
}
/**
* A simple Map<T> that optionally allows to set a limit of entries to store. Once the limit is hit,
* the cache will remove the entry that was last recently added. Or, if a ratio is provided below 1,
* all elements will be removed until the ratio is full filled (e.g. 0.75 to remove 25% of old elements).
*/
export class BoundedMap<T> {
private map: Map<string, LinkedEntry<string, T>>;
private head: LinkedEntry<string, T>;
private tail: LinkedEntry<string, T>;
private ratio: number;
constructor(private limit = Number.MAX_VALUE, ratio = 1, value?: ISerializedBoundedLinkedMap<T>) {
this.map = new Map<string, LinkedEntry<string, T>>();
this.ratio = limit * ratio;
if (value) {
value.entries.forEach(entry => {
this.set(entry.key, entry.value);
});
}
}
public setLimit(limit: number): void {
if (limit < 0) {
return; // invalid limit
}
this.limit = limit;
while (this.map.size > this.limit) {
this.trim();
}
}
public serialize(): ISerializedBoundedLinkedMap<T> {
const serialized: ISerializedBoundedLinkedMap<T> = { entries: [] };
this.map.forEach(entry => {
serialized.entries.push({ key: entry.key, value: entry.value });
});
return serialized;
}
public get size(): number {
return this.map.size;
}
public set(key: string, value: T): boolean {
if (this.map.has(key)) {
return false; // already present!
}
const entry: LinkedEntry<string, T> = { key, value };
this.push(entry);
if (this.size > this.limit) {
this.trim();
}
return true;
}
public get(key: string): T {
const entry = this.map.get(key);
return entry ? entry.value : null;
}
public getOrSet(k: string, t: T): T {
const res = this.get(k);
if (res) {
return res;
}
this.set(k, t);
return t;
}
public delete(key: string): T {
const entry = this.map.get(key);
if (entry) {
this.map.delete(key);
if (entry.next) {
entry.next.prev = entry.prev; // [A]<-[x]<-[C] = [A]<-[C]
} else {
this.head = entry.prev; // [A]-[x] = [A]
}
if (entry.prev) {
entry.prev.next = entry.next; // [A]->[x]->[C] = [A]->[C]
} else {
this.tail = entry.next; // [x]-[A] = [A]
}
return entry.value;
}
return null;
}
public has(key: string): boolean {
return this.map.has(key);
}
public clear(): void {
this.map.clear();
this.head = null;
this.tail = null;
}
private push(entry: LinkedEntry<string, T>): void {
if (this.head) {
// [A]-[B] = [A]-[B]->[X]
entry.prev = this.head;
this.head.next = entry;
}
if (!this.tail) {
this.tail = entry;
}
this.head = entry;
this.map.set(entry.key, entry);
}
private trim(): void {
if (this.tail) {
// Remove all elements until ratio is reached
if (this.ratio < this.limit) {
let index = 0;
let current = this.tail;
while (current.next) {
// Remove the entry
this.map.delete(current.key);
// if we reached the element that overflows our ratio condition
// make its next element the new tail of the Map and adjust the size
if (index === this.ratio) {
this.tail = current.next;
this.tail.prev = null;
break;
}
// Move on
current = current.next;
index++;
}
}
// Just remove the tail element
else {
this.map.delete(this.tail.key);
// [x]-[B] = [B]
this.tail = this.tail.next;
if (this.tail) {
this.tail.prev = null;
}
}
}
}
}
export interface IKeyIterator {
reset(key: string): this;
next(): this;
@@ -975,6 +789,10 @@ export class LRUCache<K, V> extends LinkedMap<K, V> {
return super.get(key, Touch.AsNew);
}
public peek(key: K): V | undefined {
return super.get(key, Touch.None);
}
public set(key: K, value: V): void {
super.set(key, value, Touch.AsNew);
this.checkTrim();
+4 -4
View File
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import { BoundedMap } from 'vs/base/common/map';
import { LRUCache } from 'vs/base/common/map';
import { CharCode } from 'vs/base/common/charCode';
/**
@@ -243,18 +243,18 @@ export function regExpContainsBackreference(regexpValue: string): boolean {
*/
export const canNormalize = typeof ((<any>'').normalize) === 'function';
const nfcCache = new BoundedMap<string>(10000); // bounded to 10000 elements
const nfcCache = new LRUCache<string, string>(10000); // bounded to 10000 elements
export function normalizeNFC(str: string): string {
return normalize(str, 'NFC', nfcCache);
}
const nfdCache = new BoundedMap<string>(10000); // bounded to 10000 elements
const nfdCache = new LRUCache<string, string>(10000); // bounded to 10000 elements
export function normalizeNFD(str: string): string {
return normalize(str, 'NFD', nfdCache);
}
const nonAsciiCharactersPattern = /[^\u0000-\u0080]/;
function normalize(str: string, form: string, normalizedCache: BoundedMap<string>): string {
function normalize(str: string, form: string, normalizedCache: LRUCache<string, string>): string {
if (!canNormalize || !str) {
return str;
}
+18 -303
View File
@@ -5,8 +5,7 @@
'use strict';
import { BoundedMap, ResourceMap, TernarySearchTree, PathIterator, StringIterator, LinkedMap, Touch, LRUCache } from 'vs/base/common/map';
import { ResourceMap, TernarySearchTree, PathIterator, StringIterator, LinkedMap, Touch, LRUCache } from 'vs/base/common/map';
import * as assert from 'assert';
import URI from 'vs/base/common/uri';
@@ -130,7 +129,7 @@ suite('Map', () => {
assert.ok(!map.has('1'));
});
test('LinkedMap - LRUCache simple', () => {
test('LinkedMap - LRU Cache simple', () => {
const cache = new LRUCache<number, number>(5);
[1, 2, 3, 4, 5].forEach(value => cache.set(value, value));
@@ -146,6 +145,21 @@ suite('Map', () => {
assert.deepStrictEqual(values, [3, 4, 5, 6, 7]);
});
test('LinkedMap - LRU Cache get', () => {
const cache = new LRUCache<number, number>(5);
[1, 2, 3, 4, 5].forEach(value => cache.set(value, value));
assert.strictEqual(cache.size, 5);
assert.deepStrictEqual(cache.keys(), [1, 2, 3, 4, 5]);
cache.get(3);
assert.deepStrictEqual(cache.keys(), [1, 2, 4, 5, 3]);
cache.peek(4);
assert.deepStrictEqual(cache.keys(), [1, 2, 4, 5, 3]);
let values: number[] = [];
[1, 2, 3, 4, 5].forEach(key => values.push(cache.get(key)));
assert.deepStrictEqual(values, [1, 2, 3, 4, 5]);
});
test('LinkedMap - LRU Cache limit', () => {
const cache = new LRUCache<number, number>(10);
@@ -170,7 +184,7 @@ suite('Map', () => {
assert.deepStrictEqual(cache.values(), values);
});
test('LinkedMap - LRU Cache limit with ration', () => {
test('LinkedMap - LRU Cache limit with ratio', () => {
const cache = new LRUCache<number, number>(10, 0.5);
for (let i = 1; i <= 10; i++) {
@@ -186,305 +200,6 @@ suite('Map', () => {
assert.deepStrictEqual(cache.values(), values);
});
test('BoundedMap - basics', function () {
const map = new BoundedMap<any>();
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'), 1);
assert.equal(map.delete('2'), '2');
assert.equal(map.delete('3'), true);
assert.equal(map.delete('4'), obj);
assert.equal(map.delete('5'), date);
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');
assert.ok(map.set('3', true)); // adding an element returns true
assert.ok(!map.set('3', true)); // adding it again returns false
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'));
const res = map.getOrSet('foo', 'bar');
assert.equal(map.get('foo'), res);
assert.equal(res, 'bar');
});
test('BoundedMap - serialization', function () {
const map = new BoundedMap<any>(5);
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);
const mapClone = new BoundedMap<any>(5, 1, map.serialize());
assert.deepEqual(map.serialize(), mapClone.serialize());
assert.equal(mapClone.size, 5);
assert.equal(mapClone.get('1'), 1);
assert.equal(mapClone.get('2'), '2');
assert.equal(mapClone.get('3'), true);
assert.equal(mapClone.get('4'), obj);
assert.equal(mapClone.get('5'), date);
assert.ok(!mapClone.get('6'));
mapClone.set('6', '6');
assert.equal(mapClone.size, 5);
assert.ok(!mapClone.get('1'));
});
test('BoundedMap - setLimit', function () {
const map = new BoundedMap<any>(5);
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.setLimit(3);
assert.equal(map.size, 3);
assert.ok(!map.get('1'));
assert.ok(!map.get('2'));
assert.equal(map.get('3'), true);
assert.equal(map.get('4'), obj);
assert.equal(map.get('5'), date);
map.setLimit(0);
assert.equal(map.size, 0);
assert.ok(!map.get('3'));
assert.ok(!map.get('4'));
assert.ok(!map.get('5'));
map.set('6', 6);
assert.equal(map.size, 0);
assert.ok(!map.get('6'));
map.setLimit(100);
map.set('1', 1);
map.set('2', '2');
map.set('3', true);
map.set('4', obj);
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);
});
test('BoundedMap - bounded', function () {
const map = new BoundedMap<number>(5);
assert.equal(0, map.size);
map.set('1', 1);
map.set('2', 2);
map.set('3', 3);
map.set('4', 4);
map.set('5', 5);
assert.equal(5, map.size);
assert.equal(map.get('1'), 1);
assert.equal(map.get('2'), 2);
assert.equal(map.get('3'), 3);
assert.equal(map.get('4'), 4);
assert.equal(map.get('5'), 5);
map.set('6', 6);
assert.equal(5, map.size);
assert.ok(!map.get('1'));
assert.equal(map.get('2'), 2);
assert.equal(map.get('3'), 3);
assert.equal(map.get('4'), 4);
assert.equal(map.get('5'), 5);
assert.equal(map.get('6'), 6);
map.set('7', 7);
map.set('8', 8);
map.set('9', 9);
assert.equal(5, map.size);
assert.ok(!map.get('1'));
assert.ok(!map.get('2'));
assert.ok(!map.get('3'));
assert.ok(!map.get('4'));
assert.equal(map.get('5'), 5);
assert.equal(map.get('6'), 6);
assert.equal(map.get('7'), 7);
assert.equal(map.get('8'), 8);
assert.equal(map.get('9'), 9);
map.delete('5');
map.delete('7');
assert.equal(3, map.size);
assert.ok(!map.get('5'));
assert.ok(!map.get('7'));
assert.equal(map.get('6'), 6);
assert.equal(map.get('8'), 8);
assert.equal(map.get('9'), 9);
map.set('10', 10);
map.set('11', 11);
map.set('12', 12);
map.set('13', 13);
map.set('14', 14);
assert.equal(5, map.size);
assert.equal(map.get('10'), 10);
assert.equal(map.get('11'), 11);
assert.equal(map.get('12'), 12);
assert.equal(map.get('13'), 13);
assert.equal(map.get('14'), 14);
});
test('BoundedMap - bounded with ratio', function () {
const map = new BoundedMap<number>(6, 0.5);
assert.equal(0, map.size);
map.set('1', 1);
map.set('2', 2);
map.set('3', 3);
map.set('4', 4);
map.set('5', 5);
map.set('6', 6);
assert.equal(6, map.size);
map.set('7', 7);
assert.equal(3, map.size);
assert.ok(!map.has('1'));
assert.ok(!map.has('2'));
assert.ok(!map.has('3'));
assert.ok(!map.has('4'));
assert.equal(map.get('5'), 5);
assert.equal(map.get('6'), 6);
assert.equal(map.get('7'), 7);
map.set('8', 8);
map.set('9', 9);
map.set('10', 10);
assert.equal(6, map.size);
assert.equal(map.get('5'), 5);
assert.equal(map.get('6'), 6);
assert.equal(map.get('7'), 7);
assert.equal(map.get('8'), 8);
assert.equal(map.get('9'), 9);
assert.equal(map.get('10'), 10);
});
test('BoundedMap - MRU order', function () {
const map = new BoundedMap<number>(3);
function peek(key) {
const res = map.get(key);
if (res) {
map.delete(key);
map.set(key, res);
}
return res;
}
assert.equal(0, map.size);
map.set('1', 1);
map.set('2', 2);
map.set('3', 3);
assert.equal(3, map.size);
assert.equal(map.get('1'), 1);
assert.equal(map.get('2'), 2);
assert.equal(map.get('3'), 3);
map.set('4', 4);
assert.equal(3, map.size);
assert.equal(peek('4'), 4); // this changes MRU order
assert.equal(peek('3'), 3);
assert.equal(peek('2'), 2);
map.set('5', 5);
map.set('6', 6);
assert.equal(3, map.size);
assert.equal(peek('2'), 2);
assert.equal(peek('5'), 5);
assert.equal(peek('6'), 6);
assert.ok(!map.has('3'));
assert.ok(!map.has('4'));
});
test('PathIterator', function () {
const iter = new PathIterator();
iter.reset('file:///usr/bin/file.txt');
@@ -29,7 +29,7 @@ import { registerEditorAction, EditorAction } from 'vs/editor/browser/editorExte
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { once } from 'vs/base/common/event';
import { BoundedMap, ISerializedBoundedLinkedMap } from 'vs/base/common/map';
import { LRUCache } from 'vs/base/common/map';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
@@ -39,9 +39,14 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
export const ALL_COMMANDS_PREFIX = '>';
let lastCommandPaletteInput: string;
let commandHistory: BoundedMap<number>;
let commandHistory: LRUCache<string, number>;
let commandCounter = 1;
interface ISerializedCommandHistory {
usesLRU?: boolean;
entries: { key: string; value: number }[];
}
function resolveCommandHistory(configurationService: IConfigurationService): number {
const config = <IWorkbenchQuickOpenConfiguration>configurationService.getValue();
@@ -77,22 +82,31 @@ class CommandsHistory {
this.commandHistoryLength = resolveCommandHistory(this.configurationService);
if (commandHistory) {
commandHistory.setLimit(this.commandHistoryLength);
commandHistory.limit = this.commandHistoryLength;
}
}
private load(): void {
const raw = this.storageService.get(CommandsHistory.PREF_KEY_CACHE);
let deserializedCache: ISerializedBoundedLinkedMap<number>;
let serializedCache: ISerializedCommandHistory;
if (raw) {
try {
deserializedCache = JSON.parse(raw);
serializedCache = JSON.parse(raw);
} catch (error) {
// invalid data
}
}
commandHistory = new BoundedMap<number>(this.commandHistoryLength, 1, deserializedCache);
commandHistory = new LRUCache<string, number>(this.commandHistoryLength, 1);
if (serializedCache) {
let entries: { key: string; value: number }[];
if (serializedCache.usesLRU) {
entries = serializedCache.entries;
} else {
entries = serializedCache.entries.sort((a, b) => a.value - b.value);
}
entries.forEach(entry => commandHistory.set(entry.key, entry.value));
}
commandCounter = this.storageService.getInteger(CommandsHistory.PREF_KEY_COUNTER, void 0, commandCounter);
}
@@ -102,21 +116,19 @@ class CommandsHistory {
}
private save(): void {
this.storageService.store(CommandsHistory.PREF_KEY_CACHE, JSON.stringify(commandHistory.serialize()));
let serializedCache: ISerializedCommandHistory = { usesLRU: true, entries: [] };
commandHistory.forEach((value, key) => serializedCache.entries.push({ key, value }));
this.storageService.store(CommandsHistory.PREF_KEY_CACHE, JSON.stringify(serializedCache));
this.storageService.store(CommandsHistory.PREF_KEY_COUNTER, commandCounter);
}
public push(commandId: string): void {
// make MRU by deleting it first
commandHistory.delete(commandId);
// set counter to command
commandHistory.set(commandId, commandCounter++);
}
public get(commandId: string): number {
return commandHistory.get(commandId);
public peek(commandId: string): number {
return commandHistory.peek(commandId);
}
}
@@ -166,7 +178,7 @@ export class ClearCommandHistoryAction extends Action {
public run(context?: any): TPromise<void> {
const commandHistoryLength = resolveCommandHistory(this.configurationService);
if (commandHistoryLength > 0) {
commandHistory = new BoundedMap<number>(commandHistoryLength);
commandHistory = new LRUCache<string, number>(commandHistoryLength);
commandCounter = 1;
}
@@ -435,8 +447,8 @@ export class CommandsHandler extends QuickOpenHandler {
// Sort by MRU order and fallback to name otherwie
entries = entries.sort((elementA, elementB) => {
const counterA = this.commandsHistory.get(elementA.getCommandId());
const counterB = this.commandsHistory.get(elementB.getCommandId());
const counterA = this.commandsHistory.peek(elementA.getCommandId());
const counterB = this.commandsHistory.peek(elementB.getCommandId());
if (counterA && counterB) {
return counterA > counterB ? -1 : 1; // use more recently used command before older
@@ -457,11 +469,11 @@ export class CommandsHandler extends QuickOpenHandler {
// Introduce group marker border between recently used and others
// only if we have recently used commands in the result set
const firstEntry = entries[0];
if (firstEntry && this.commandsHistory.get(firstEntry.getCommandId())) {
if (firstEntry && this.commandsHistory.peek(firstEntry.getCommandId())) {
firstEntry.setGroupLabel(nls.localize('recentlyUsed', "recently used"));
for (let i = 1; i < entries.length; i++) {
const entry = entries[i];
if (!this.commandsHistory.get(entry.getCommandId())) {
if (!this.commandsHistory.peek(entry.getCommandId())) {
entry.setShowBorder(true);
entry.setGroupLabel(nls.localize('morecCommands', "other commands"));
break;
@@ -546,7 +558,7 @@ export class CommandsHandler extends QuickOpenHandler {
if (autoFocusPrefixMatch && this.commandHistoryEnabled) {
const firstEntry = context.model && context.model.entries[0];
if (firstEntry instanceof BaseCommandEntry && this.commandsHistory.get(firstEntry.getCommandId())) {
if (firstEntry instanceof BaseCommandEntry && this.commandsHistory.peek(firstEntry.getCommandId())) {
autoFocusPrefixMatch = void 0; // keep focus on MRU element if we have history elements
}
}