mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +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 {
|
try {
|
||||||
const ast = memoryParse(path);
|
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) {
|
} catch (err) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import {parse} from 'amis-formula';
|
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(
|
export function memoryParse(
|
||||||
input: string,
|
input: string,
|
||||||
options: {
|
options: {
|
||||||
@ -26,17 +30,22 @@ export function memoryParse(
|
|||||||
evalMode: false
|
evalMode: false
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
// @todo 优化内存缓存释放,比如只缓存最高频的模版
|
// 优化内存缓存释放,比如只缓存最高频的模版
|
||||||
if (typeof input !== 'string') {
|
if (typeof input !== 'string') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = input + JSON.stringify(options);
|
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);
|
const ast = parse(input, options);
|
||||||
cache[key] = ast;
|
|
||||||
|
cache.set(key, ast);
|
||||||
|
|
||||||
return 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
|
extendsFilters
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export * from './types';
|
||||||
|
|
||||||
export function evaluate(
|
export function evaluate(
|
||||||
astOrString: string | ASTNode,
|
astOrString: string | ASTNode,
|
||||||
data: any,
|
data: any,
|
||||||
|
Loading…
Reference in New Issue
Block a user