提交 35e5c9af 编写于 作者: B Benjamin Pasero

Merge pull request #6031 from Microsoft/ben/cache

LinkedMap for bounded caches (#5621)
......@@ -5,6 +5,7 @@
'use strict';
import strings = require('vs/base/common/strings');
import {LinkedMap} from 'vs/base/common/map';
export interface IFilter {
// Returns null if word doesn't match.
......@@ -298,7 +299,7 @@ export enum SubstringMatching {
export const fuzzyContiguousFilter = or(matchesPrefix, matchesCamelCase, matchesContiguousSubString);
const fuzzySeparateFilter = or(matchesPrefix, matchesCamelCase, matchesSubString);
const fuzzyRegExpCache: { [key: string]: RegExp; } = {};
const fuzzyRegExpCache = new LinkedMap<RegExp>(10000); // bounded to 10000 elements
export function matchesFuzzy(word: string, wordToMatchAgainst: string, enableSeparateSubstringMatching = false): IMatch[] {
if (typeof word !== 'string' || typeof wordToMatchAgainst !== 'string') {
......@@ -306,10 +307,10 @@ export function matchesFuzzy(word: string, wordToMatchAgainst: string, enableSep
}
// Form RegExp for wildcard matches
let regexp = fuzzyRegExpCache[word];
let regexp = fuzzyRegExpCache.get(word);
if (!regexp) {
regexp = new RegExp(strings.convertSimple2RegExpPattern(word), 'i');
fuzzyRegExpCache[word] = regexp;
fuzzyRegExpCache.set(word, regexp);
}
// RegExp Filter
......
......@@ -6,9 +6,7 @@
import strings = require('vs/base/common/strings');
import paths = require('vs/base/common/paths');
const CACHE: { [glob: string]: RegExp } = Object.create(null);
const MAX_CACHED = 10000;
import {LinkedMap} from 'vs/base/common/map';
export interface IExpression {
[pattern: string]: boolean | SiblingClause | any;
......@@ -212,7 +210,25 @@ function parseRegExp(pattern: string): string {
return regEx;
}
function globToRegExp(pattern: string): RegExp {
// regexes to check for trival glob patterns that just check for String#endsWith
const T1 = /^\*\*\/\*\.[\w\.-]+$/; // **/*.something
const T2 = /^\*\*\/[\w\.-]+$/; // **/something
const T3 = /^{\*\*\/\*\.[\w\.-]+(,\*\*\/\*\.[\w\.-]+)*}$/; // {**/*.something,**/*.else}
enum Trivia {
T1, // **/*.something
T2, // **/something
T3 // {**/*.something,**/*.else}
}
interface IParsedPattern {
regexp?: RegExp;
trivia?: Trivia;
}
const CACHE = new LinkedMap<IParsedPattern>(10000); // bounded to 10000 elements
function parsePattern(pattern: string): IParsedPattern {
if (!pattern) {
return null;
}
......@@ -221,27 +237,35 @@ function globToRegExp(pattern: string): RegExp {
pattern = pattern.trim();
// Check cache
if (CACHE[pattern]) {
let cached = CACHE[pattern];
cached.lastIndex = 0; // reset RegExp to its initial state to reuse it!
let parsedPattern = CACHE.get(pattern);
if (parsedPattern) {
if (parsedPattern.regexp) {
parsedPattern.regexp.lastIndex = 0; // reset RegExp to its initial state to reuse it!
}
return cached;
return parsedPattern;
}
let regEx = parseRegExp(pattern);
// Wrap it
regEx = '^' + regEx + '$';
parsedPattern = Object.create(null);
// Convert to regexp and be ready for errors
let result = toRegExp(regEx);
// Check for Trivias
if (T1.test(pattern)) {
parsedPattern.trivia = Trivia.T1;
} else if (T2.test(pattern)) {
parsedPattern.trivia = Trivia.T2;
} else if (T3.test(pattern)) {
parsedPattern.trivia = Trivia.T3;
}
// Make sure to cache (bounded)
if (Object.getOwnPropertyNames(CACHE).length < MAX_CACHED) {
CACHE[pattern] = result;
// Otherwise convert to pattern
else {
parsedPattern.regexp = toRegExp(`^${parseRegExp(pattern)}$`);
}
return result;
// Cache
CACHE.set(pattern, parsedPattern);
return parsedPattern;
}
function toRegExp(regEx: string): RegExp {
......@@ -252,28 +276,6 @@ function toRegExp(regEx: string): RegExp {
}
}
function testWithCache(glob: string, pattern: RegExp, cache: { [glob: string]: boolean }): boolean {
let res = cache[glob];
if (typeof res !== 'boolean') {
res = pattern.test(glob);
// Make sure to cache (bounded)
if (Object.getOwnPropertyNames(cache).length < MAX_CACHED) {
cache[glob] = res;
}
}
return res;
}
// regexes to check for trival glob patterns that just check for String#endsWith
const trivia1 = /^\*\*\/\*\.[\w\.-]+$/; // **/*.something
const trivia2 = /^\*\*\/[\w\.-]+$/; // **/something
const trivia3 = /^{\*\*\/\*\.[\w\.-]+(,\*\*\/\*\.[\w\.-]+)*}$/; // {**/*.something,**/*.else}
const T1_CACHE: { [glob: string]: boolean } = Object.create(null);
const T2_CACHE: { [glob: string]: boolean } = Object.create(null);
const T3_CACHE: { [glob: string]: boolean } = Object.create(null);
/**
* Simplified glob matching. Supports a subset of glob patterns:
* - * matches anything inside a path segment
......@@ -291,27 +293,29 @@ export function match(arg1: string | IExpression, path: string, siblings?: strin
// Glob with String
if (typeof arg1 === 'string') {
const parsedPattern = parsePattern(arg1);
if (!parsedPattern) {
return false;
}
// common pattern: **/*.txt just need endsWith check
if (testWithCache(arg1, trivia1, T1_CACHE)) {
if (parsedPattern.trivia === Trivia.T1) {
return strings.endsWith(path, arg1.substr(4)); // '**/*'.length === 4
}
// common pattern: **/some.txt just need basename check
if (testWithCache(arg1, trivia2, T2_CACHE)) {
if (parsedPattern.trivia === Trivia.T2) {
const base = arg1.substr(3); // '**/'.length === 3
return path === base || strings.endsWith(path, `/${base}`) || strings.endsWith(path, `\\${base}`);
}
// repetition of common patterns (see above) {**/*.txt,**/*.png}
if (testWithCache(arg1, trivia3, T3_CACHE)) {
if (parsedPattern.trivia === Trivia.T3) {
return arg1.slice(1, -1).split(',').some(pattern => match(pattern, path));
}
const regExp = globToRegExp(arg1);
return regExp && regExp.test(path);
return parsedPattern.regexp.test(path);
}
// Glob with Expression
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
export interface Entry<T> {
next?: Entry<T>;
prev?: Entry<T>;
key: string;
value: 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 LinkedMap<T> {
protected map: { [key: string]: Entry<T> };
private head: Entry<T>;
private tail: Entry<T>;
private _size: number;
private ratio: number;
constructor(private limit = Number.MAX_VALUE, ratio = 1) {
this.map = Object.create(null);
this._size = 0;
this.ratio = limit * ratio;
}
public get size(): number {
return this._size;
}
public set(key: string, value: T): boolean {
if (this.map[key]) {
return false; // already present!
}
const entry: Entry<T> = { key, value };
this.push(entry);
if (this._size > this.limit) {
this.trim();
}
return true;
}
public get(key: string): T {
const entry = this.map[key];
return entry ? entry.value : null;
}
public delete(key: string): T {
const entry = this.map[key];
if (entry) {
this.map[key] = void 0;
this._size--;
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[key];
}
public clear(): void {
this.map = Object.create(null);
this._size = 0;
this.head = null;
this.tail = null;
}
protected push(entry: Entry<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[entry.key] = entry;
this._size++;
}
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[current.key] = void 0;
this._size--;
// 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[this.tail.key] = void 0;
this._size--;
// [x]-[B] = [B]
this.tail = this.tail.next;
this.tail.prev = null;
}
}
}
}
/**
* A subclass of Map<T> that makes an entry the MRU entry as soon
* as it is being accessed. In combination with the limit for the
* maximum number of elements in the cache, it helps to remove those
* entries from the cache that are LRU.
*/
export class LRUCache<T> extends LinkedMap<T> {
constructor(limit: number) {
super(limit);
}
public get(key: string): T {
// Upon access of an entry, make it the head of
// the linked map so that it is the MRU element
const entry = this.map[key];
if (entry) {
this.delete(key);
this.push(entry);
return entry.value;
}
return null;
}
}
\ No newline at end of file
......@@ -4,6 +4,8 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import {LinkedMap} from 'vs/base/common/map';
/**
* The empty string.
*/
......@@ -247,14 +249,13 @@ export function regExpLeadsToEndlessLoop(regexp: RegExp): boolean {
*/
export let canNormalize = typeof ((<any>'').normalize) === 'function';
const nonAsciiCharactersPattern = /[^\u0000-\u0080]/;
const normalizedCache = Object.create(null);
let cacheCounter = 0;
const normalizedCache = new LinkedMap<string>(10000); // bounded to 10000 elements
export function normalizeNFC(str: string): string {
if (!canNormalize || !str) {
return str;
}
const cached = normalizedCache[str];
const cached = normalizedCache.get(str);
if (cached) {
return cached;
}
......@@ -266,11 +267,8 @@ export function normalizeNFC(str: string): string {
res = str;
}
// Use the cache for fast lookup but do not let it grow unbounded
if (cacheCounter < 10000) {
normalizedCache[str] = res;
cacheCounter++;
}
// Use the cache for fast lookup
normalizedCache.set(str, res);
return res;
}
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {LinkedMap, LRUCache} from 'vs/base/common/map';
import * as assert from 'assert';
suite('Map', () => {
test('LinkedMap - basics', function () {
const map = new LinkedMap<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'));
});
test('LinkedMap - bounded', function () {
const map = new LinkedMap<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('LinkedMap - bounded with ratio', function () {
const map = new LinkedMap<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('LRUCache', function () {
const cache = new LRUCache<number>(3);
assert.equal(0, cache.size);
cache.set('1', 1);
cache.set('2', 2);
cache.set('3', 3);
assert.equal(3, cache.size);
assert.equal(cache.get('1'), 1);
assert.equal(cache.get('2'), 2);
assert.equal(cache.get('3'), 3);
cache.set('4', 4);
assert.equal(3, cache.size);
assert.equal(cache.get('4'), 4); // this changes MRU order
assert.equal(cache.get('3'), 3);
assert.equal(cache.get('2'), 2);
cache.set('5', 5);
cache.set('6', 6);
assert.equal(3, cache.size);
assert.equal(cache.get('2'), 2);
assert.equal(cache.get('5'), 5);
assert.equal(cache.get('6'), 6);
assert.ok(!cache.has('3'));
assert.ok(!cache.has('4'));
});
});
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册