Merge branch 'feat-memoryParseCacheRelease' into feat-memoryParseCacheRelease2

This commit is contained in:
SiynMa 2024-08-13 23:21:14 +08:00
commit a8b269bb94
5 changed files with 283 additions and 6 deletions

View File

@ -0,0 +1,107 @@
import VisitedCache from '../../src/utils/visitedCache';
describe('VisitedCache', () => {
let cache: VisitedCache<string, number>;
beforeEach(() => {
cache = new VisitedCache<string, number>(5);
});
it('should set and get values correctly', () => {
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
expect(cache.get('a')).toBe(1);
expect(cache.get('b')).toBe(2);
expect(cache.get('c')).toBe(3);
});
it('should evict the least visited entry when cache is full', () => {
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
cache.set('d', 4);
cache.set('e', 5);
// Access 'a' to increase its visit count
cache.get('a');
cache.set('f', 6);
expect(cache.get('a')).toBe(1);
expect(cache.get('b')).toBeUndefined();
});
it('should handle cache misses correctly', () => {
expect(cache.get('non-existent-key')).toBeUndefined();
});
it('should handle multiple entries with the same visit count', () => {
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
cache.set('d', 4);
cache.set('e', 5);
// Access 'a', 'b', 'c' to make them have the same visit count
cache.get('a');
cache.get('b');
cache.get('c');
cache.set('f', 6);
// 动态清理的个数为1清理掉最少访问最旧的d项
expect(cache.get('a')).toBe(1);
expect(cache.get('b')).toBe(2);
expect(cache.get('c')).toBe(3);
expect(cache.get('d')).toBeUndefined();
expect(cache.get('e')).toBe(5);
expect(cache.get('f')).toBe(6);
});
it('should handle dynamic release count', () => {
cache = new VisitedCache<string, number>(10, 1 / 5);
// 释放数量为 10 * 1/5 >> 0 || 1 = 2
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
cache.set('d', 4);
cache.set('e', 5);
cache.set('f', 6);
cache.set('g', 7);
cache.set('h', 8);
cache.set('i', 9);
cache.set('j', 10);
// 提升除了a之外的访问次数
cache.get('b');
cache.get('c');
cache.get('d');
cache.get('e');
cache.get('f');
cache.get('g');
cache.get('h');
cache.get('i');
cache.get('j');
// 再次提升高b的访问次数
cache.get('b');
// 此时应该清理掉两项,a和c
cache.set('k', 11);
expect(cache.get('a')).toBeUndefined();
expect(cache.get('b')).toBe(2);
expect(cache.get('c')).toBeUndefined();
expect(cache.get('d')).toBe(4);
expect(cache.get('e')).toBe(5);
expect(cache.get('f')).toBe(6);
expect(cache.get('g')).toBe(7);
expect(cache.get('h')).toBe(8);
expect(cache.get('i')).toBe(9);
expect(cache.get('j')).toBe(10);
expect(cache.get('k')).toBe(11);
});
});

View File

@ -5,7 +5,7 @@ export function isPureVariable(path?: any): path is string {
try {
const ast = memoryParse(path);
// 只有一个成员说明是纯表达式模式
return ast.body.length === 1 && ast.body[0].type === 'script';
return ast?.body.length === 1 && ast.body[0].type === 'script';
} catch (err) {
return false;
}

View File

@ -1,6 +1,10 @@
import {parse} from 'amis-formula';
import type {ASTNode} from 'amis-formula';
import VisitedCache from './visitedCache';
// NOTE 缓存前40条表达式
const cache = new VisitedCache<string, ASTNode>(40);
const cache = <any>{};
export function memoryParse(
input: string,
options: {
@ -26,17 +30,22 @@ export function memoryParse(
evalMode: false
}
) {
// @todo 优化内存缓存释放,比如只缓存最高频的模版
// 优化内存缓存释放,比如只缓存最高频的模版
if (typeof input !== 'string') {
return;
}
const key = input + JSON.stringify(options);
if (cache[key]) {
return cache[key];
// get cache result
if (cache.has(key)) {
return cache.get(key);
}
// run parse function and cache
const ast = parse(input, options);
cache[key] = ast;
cache.set(key, ast);
return ast;
}

View File

@ -0,0 +1,159 @@
/**
* @description
*/
export type CacheEntry<V> = {
value: V;
visitCount: number;
};
/**
* 访key的Map
*
* @class VisitedCache
* @template K - key的类型
* @template V - value的类型
*/
export default class VisitedCache<K, V> {
private capacity: number;
private cache: Map<K, CacheEntry<V>>;
private visitCountOrder: number[] = [];
private keyOrderMatrixForByCount: Record<number, K[]> = {};
private releaseCount: number;
/**
*
* @param capacity
* @param releasePercent
*/
constructor(capacity: number, releasePercent?: number) {
this.capacity = capacity;
this.cache = new Map();
this.releaseCount = (this.capacity * (releasePercent || 1 / 8)) >> 0 || 1;
}
/**
*
* @param key
* @param entry
*/
private updateCacheEntryOrder(
key: K,
entry: CacheEntry<V>,
nextVisitCount: number
) {
const {visitCount: oldVisitCount} = entry;
const oldKeyOrder = this.keyOrderMatrixForByCount[oldVisitCount];
if (Array.isArray(oldKeyOrder)) {
// 从key为旧访问次数的顺序数组中删除
const oldKeyIndex = oldKeyOrder.indexOf(key);
if (oldKeyIndex !== -1) {
oldKeyOrder.splice(oldKeyIndex, 1);
}
}
// 相同访问量,更新访问顺序
this.keyOrderMatrixForByCount[nextVisitCount] = Array.isArray(
this.keyOrderMatrixForByCount[nextVisitCount]
)
? [...this.keyOrderMatrixForByCount[nextVisitCount], key]
: [key];
entry.visitCount = nextVisitCount;
// 重新按照访问量排序
this.visitCountOrder = [
...new Set([...this.visitCountOrder, nextVisitCount])
].sort((a, b) => a - b);
}
/**
*
* @param {K} key
* @returns {(V | undefined)}
*/
get(key: K): V | undefined {
if (this.cache.has(key)) {
const entry = this.cache.get(key);
if (entry !== undefined) {
const {visitCount} = entry;
// 更新访问顺序
this.updateCacheEntryOrder(key, entry, visitCount + 1);
return entry.value;
}
}
return undefined;
}
/**
*
* @param {K} key
* @param {V} value
*/
set(key: K, value: V): void {
// 更新现存项的值和访问次数及新鲜度
if (this.cache.has(key)) {
const entry = this.cache.get(key);
if (entry !== undefined) {
const {visitCount} = entry;
this.updateCacheEntryOrder(key, entry, visitCount + 1);
entry.value = value;
}
} else {
// 先进行清理
if (this.cache.size === this.capacity) {
// TODO 依据最大容量的1/10进行释放,试试效果
const dynamicReleaseCount = this.releaseCount;
// const dynamicReleaseCount =
// (this.keyOrderMatrixForByCount[this.visitCountOrder[this.visitCountOrder.length - 1]].length / (this.capacity)) >>
// 0 || 1
let findIndex = 0;
let released = 0;
while (
released < dynamicReleaseCount &&
findIndex <= this.visitCountOrder.length - 1
) {
// 最少访问的次数
const targetCount = this.visitCountOrder[findIndex];
// 查看其中有没有项
const targetKeyOrder = this.keyOrderMatrixForByCount[targetCount];
if (!targetKeyOrder.length) {
findIndex++;
} else {
while (
targetKeyOrder.length > 0 &&
released < dynamicReleaseCount
) {
this.cache.delete(targetKeyOrder.shift()!);
released++;
}
}
}
}
const newEntry: CacheEntry<V> = {
visitCount: 1,
value
};
this.cache.set(key, newEntry);
this.updateCacheEntryOrder(key, newEntry, 1);
}
}
has(key: K): boolean {
return this.cache.has(key);
}
}

View File

@ -25,6 +25,8 @@ export {
extendsFilters
};
export * from './types';
export function evaluate(
astOrString: string | ASTNode,
data: any,