mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-01 19:47:56 +08:00
Merge branch 'feat-memoryParseCacheRelease' into feat-memoryParseCacheRelease2
This commit is contained in:
commit
a8b269bb94
107
packages/amis-core/__tests__/utils/visitedCache.test.ts
Normal file
107
packages/amis-core/__tests__/utils/visitedCache.test.ts
Normal 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);
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
159
packages/amis-core/src/utils/visitedCache.ts
Normal file
159
packages/amis-core/src/utils/visitedCache.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -25,6 +25,8 @@ export {
|
||||
extendsFilters
|
||||
};
|
||||
|
||||
export * from './types';
|
||||
|
||||
export function evaluate(
|
||||
astOrString: string | ASTNode,
|
||||
data: any,
|
||||
|
Loading…
Reference in New Issue
Block a user