From f82a4019f876a39112ea190988077971a047516a Mon Sep 17 00:00:00 2001 From: SiynMa Date: Tue, 13 Aug 2024 17:25:50 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=A1=A8?= =?UTF-8?q?=E8=BE=BE=E5=BC=8F=E7=BC=93=E5=AD=98=E9=87=8A=E6=94=BE=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/utils/visitedCache.test.ts | 54 +++++++++++ .../amis-core/src/utils/isPureVariable.ts | 2 +- packages/amis-core/src/utils/memoryParse.ts | 19 ++-- packages/amis-core/src/utils/visitedCache.ts | 90 +++++++++++++++++++ packages/amis-formula/src/index.ts | 2 + 5 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 packages/amis-core/__tests__/utils/visitedCache.test.ts create mode 100644 packages/amis-core/src/utils/visitedCache.ts diff --git a/packages/amis-core/__tests__/utils/visitedCache.test.ts b/packages/amis-core/__tests__/utils/visitedCache.test.ts new file mode 100644 index 000000000..0f2a2f135 --- /dev/null +++ b/packages/amis-core/__tests__/utils/visitedCache.test.ts @@ -0,0 +1,54 @@ +import VisitedCache from '../../src/utils/visitedCache'; + +describe('测试访问次数缓存store', () => { + let cache: VisitedCache; + + beforeEach(() => { + cache = new VisitedCache(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 update the least visited entry when an existing entry is set', () => { + cache.set('a', 1); + cache.set('b', 2); + cache.set('c', 3); + + // Access 'b' to increase its visit count + cache.get('b'); + + cache.set('a', 10); + + expect(cache.get('a')).toBe(10); + expect(cache.get('b')).toBe(2); + expect(cache.get('c')).toBeUndefined(); + }); + + it('should handle cache misses correctly', () => { + expect(cache.get('non-existent-key')).toBeUndefined(); + }); +}); diff --git a/packages/amis-core/src/utils/isPureVariable.ts b/packages/amis-core/src/utils/isPureVariable.ts index a779bc681..b0b9d81e4 100644 --- a/packages/amis-core/src/utils/isPureVariable.ts +++ b/packages/amis-core/src/utils/isPureVariable.ts @@ -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; } diff --git a/packages/amis-core/src/utils/memoryParse.ts b/packages/amis-core/src/utils/memoryParse.ts index c159059b0..033229420 100644 --- a/packages/amis-core/src/utils/memoryParse.ts +++ b/packages/amis-core/src/utils/memoryParse.ts @@ -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(40); -const cache = {}; 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; } diff --git a/packages/amis-core/src/utils/visitedCache.ts b/packages/amis-core/src/utils/visitedCache.ts new file mode 100644 index 000000000..93b6a81ff --- /dev/null +++ b/packages/amis-core/src/utils/visitedCache.ts @@ -0,0 +1,90 @@ +/** + * @description 缓存值的单元 + */ +export type CacheEntry = { + value: V; + visitCount: number; +}; + +/** + * 自动清理访问次数最少key的Map + * //TODO 考虑上次访问时间? + * @class CombinedCache + * @template K - 缓存key的类型 + * @template V - 缓存value的类型 + */ +export default class CombinedCache { + private capacity: number; + private cache: Map>; + private leastVisitedKey: K | undefined; + private leastVisitedCount: number = Infinity; + + /** + * Creates an instance of CombinedCache + * @param {number} capacity - 最大缓存容量 + */ + constructor(capacity: number) { + this.capacity = capacity; + this.cache = new Map(); + } + + /** + * 从Map中获取value,并更新访问次数与最少访问项 + * @param {K} key + * @returns {(V | undefined)} + */ + get(key: K): V | undefined { + if (this.cache.has(key)) { + const entry = this.cache.get(key)!; + this.cache.delete(key); + this.cache.set(key, entry); + entry.visitCount++; + + if (entry.visitCount < this.leastVisitedCount) { + this.leastVisitedKey = key; + this.leastVisitedCount = entry.visitCount; + } + + 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)!; + this.cache.delete(key); + entry.value = value; + entry.visitCount++; + + // Update the least visited entry + if (entry.visitCount < this.leastVisitedCount) { + this.leastVisitedKey = key; + this.leastVisitedCount = entry.visitCount; + } + + this.cache.set(key, entry); + } else { + if (this.cache.size === this.capacity) { + // 删除最少访问项 + if (this.leastVisitedKey) { + this.cache.delete(this.leastVisitedKey); + } + } + + this.cache.set(key, {value, visitCount: 1}); + + // 更新最少访问的key及次数 + this.leastVisitedKey = key; + this.leastVisitedCount = 1; + } + } + + has(key: K): boolean { + return this.cache.has(key); + } +} diff --git a/packages/amis-formula/src/index.ts b/packages/amis-formula/src/index.ts index 446566d75..a332bc7f8 100644 --- a/packages/amis-formula/src/index.ts +++ b/packages/amis-formula/src/index.ts @@ -25,6 +25,8 @@ export { extendsFilters }; +export * from './types'; + export function evaluate( astOrString: string | ASTNode, data: any, From a057a7fc4b1a28aadac7625edb0da0a901ca74b6 Mon Sep 17 00:00:00 2001 From: SiynMa Date: Tue, 13 Aug 2024 23:15:56 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E8=A1=A8?= =?UTF-8?q?=E8=BE=BE=E5=BC=8F=E7=BC=93=E5=AD=98=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/utils/visitedCache.test.ts | 85 +++++++++-- packages/amis-core/src/utils/visitedCache.ts | 141 +++++++++++++----- 2 files changed, 174 insertions(+), 52 deletions(-) diff --git a/packages/amis-core/__tests__/utils/visitedCache.test.ts b/packages/amis-core/__tests__/utils/visitedCache.test.ts index 0f2a2f135..c6de98e15 100644 --- a/packages/amis-core/__tests__/utils/visitedCache.test.ts +++ b/packages/amis-core/__tests__/utils/visitedCache.test.ts @@ -1,6 +1,6 @@ import VisitedCache from '../../src/utils/visitedCache'; -describe('测试访问次数缓存store', () => { +describe('VisitedCache', () => { let cache: VisitedCache; beforeEach(() => { @@ -33,22 +33,75 @@ describe('测试访问次数缓存store', () => { expect(cache.get('b')).toBeUndefined(); }); - it('should update the least visited entry when an existing entry is set', () => { - cache.set('a', 1); - cache.set('b', 2); - cache.set('c', 3); - - // Access 'b' to increase its visit count - cache.get('b'); - - cache.set('a', 10); - - expect(cache.get('a')).toBe(10); - expect(cache.get('b')).toBe(2); - expect(cache.get('c')).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(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); + }); }); diff --git a/packages/amis-core/src/utils/visitedCache.ts b/packages/amis-core/src/utils/visitedCache.ts index 93b6a81ff..f9d4b8fc4 100644 --- a/packages/amis-core/src/utils/visitedCache.ts +++ b/packages/amis-core/src/utils/visitedCache.ts @@ -7,80 +7,149 @@ export type CacheEntry = { }; /** - * 自动清理访问次数最少key的Map - * //TODO 考虑上次访问时间? - * @class CombinedCache + * 自动清理访问次数最少key的Map,次数相同时优先淘汰旧项 + * 每次触发清理的计数基于容量的百分比 + * @class VisitedCache * @template K - 缓存key的类型 * @template V - 缓存value的类型 */ -export default class CombinedCache { +export default class VisitedCache { private capacity: number; private cache: Map>; - private leastVisitedKey: K | undefined; - private leastVisitedCount: number = Infinity; + private visitCountOrder: number[] = []; + private keyOrderMatrixForByCount: Record = {}; + private releaseCount: number; /** - * Creates an instance of CombinedCache - * @param {number} capacity - 最大缓存容量 + * + * @param capacity 容量 + * @param releasePercent 清理数量占容量百分比 */ - constructor(capacity: number) { + constructor(capacity: number, releasePercent?: number) { this.capacity = capacity; this.cache = new Map(); + this.releaseCount = (this.capacity * (releasePercent || 1 / 8)) >> 0 || 1; } /** - * 从Map中获取value,并更新访问次数与最少访问项 + * 更新现存项目的缓存顺序 + * @param key + * @param entry + */ + private updateCacheEntryOrder( + key: K, + entry: CacheEntry, + 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)!; - this.cache.delete(key); - this.cache.set(key, entry); - entry.visitCount++; + const entry = this.cache.get(key); - if (entry.visitCount < this.leastVisitedCount) { - this.leastVisitedKey = key; - this.leastVisitedCount = entry.visitCount; + if (entry !== undefined) { + const {visitCount} = entry; + // 更新访问顺序 + this.updateCacheEntryOrder(key, entry, visitCount + 1); + + return entry.value; } - - 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)!; - this.cache.delete(key); - entry.value = value; - entry.visitCount++; + const entry = this.cache.get(key); - // Update the least visited entry - if (entry.visitCount < this.leastVisitedCount) { - this.leastVisitedKey = key; - this.leastVisitedCount = entry.visitCount; + if (entry !== undefined) { + const {visitCount} = entry; + + this.updateCacheEntryOrder(key, entry, visitCount + 1); + + entry.value = value; } - - this.cache.set(key, entry); } else { + // 先进行清理 if (this.cache.size === this.capacity) { - // 删除最少访问项 - if (this.leastVisitedKey) { - this.cache.delete(this.leastVisitedKey); + // 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++; + } + } } } - this.cache.set(key, {value, visitCount: 1}); + const newEntry: CacheEntry = { + visitCount: 1, + value + }; - // 更新最少访问的key及次数 - this.leastVisitedKey = key; - this.leastVisitedCount = 1; + this.cache.set(key, newEntry); + + this.updateCacheEntryOrder(key, newEntry, 1); } }