From 290f01e55932ad2741dc2b5ae7a0c6913c68120a Mon Sep 17 00:00:00 2001 From: 777genius Date: Sun, 31 May 2026 08:03:31 +0300 Subject: [PATCH] perf(renderer): cache activity render signatures --- .../team/activity/activityRenderCache.ts | 64 +++++++++++++------ .../team/activity/activityRenderCache.test.ts | 43 +++++++++++++ 2 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 test/renderer/components/team/activity/activityRenderCache.test.ts diff --git a/src/renderer/components/team/activity/activityRenderCache.ts b/src/renderer/components/team/activity/activityRenderCache.ts index be9b8ceb..881d04cb 100644 --- a/src/renderer/components/team/activity/activityRenderCache.ts +++ b/src/renderer/components/team/activity/activityRenderCache.ts @@ -6,6 +6,10 @@ const MAX_ACTIVITY_RENDER_CACHE_ENTRIES = 500; type StringCache = Map; +const taskRefsSignatureCache = new WeakMap(); +const stringArraySignatureCache = new WeakMap(); +const stringMapSignatureCache = new WeakMap, string>(); + export function getCachedString(cache: StringCache, key: string, buildValue: () => string): string { const cached = cache.get(key); if (cached !== undefined || cache.has(key)) return cached ?? ''; @@ -20,42 +24,60 @@ export function getCachedString(cache: StringCache, key: string, buildValue: () } export function encodeCacheParts(parts: readonly string[]): string { - const encodedParts: string[] = []; - for (const part of parts) { - pushEncodedCachePart(encodedParts, part); + let encoded = ''; + for (let index = 0; index < parts.length; index += 1) { + if (index > 0) encoded += '|'; + const part = parts[index]; + encoded += `${part.length}:${part}`; } - return encodedParts.join('|'); + return encoded; } export function taskRefsCacheSignature(taskRefs?: readonly TaskRef[]): string { if (!taskRefs || taskRefs.length === 0) return ''; - const encodedParts: string[] = []; - for (const ref of taskRefs) { - pushEncodedCachePart(encodedParts, ref.taskId); - pushEncodedCachePart(encodedParts, ref.displayId); - pushEncodedCachePart(encodedParts, ref.teamName ?? ''); + const cached = taskRefsSignatureCache.get(taskRefs); + if (cached !== undefined) return cached; + + let encoded = ''; + let hasPart = false; + for (let index = 0; index < taskRefs.length; index += 1) { + const ref = taskRefs[index]; + const parts = [ref.taskId, ref.displayId, ref.teamName ?? '']; + for (const part of parts) { + if (hasPart) encoded += '|'; + encoded += `${part.length}:${part}`; + hasPart = true; + } } - return encodedParts.join('|'); + taskRefsSignatureCache.set(taskRefs, encoded); + return encoded; } export function stringArrayCacheSignature(values?: readonly string[]): string { if (!values || values.length === 0) return ''; - return encodeCacheParts(values); + const cached = stringArraySignatureCache.get(values); + if (cached !== undefined) return cached; + const signature = encodeCacheParts(values); + stringArraySignatureCache.set(values, signature); + return signature; } export function stringMapCacheSignature(map?: ReadonlyMap): string { if (!map || map.size === 0) return ''; - const entries = [...map.entries()].sort(([a], [b]) => a.localeCompare(b)); - const encodedParts: string[] = []; - for (const [key, value] of entries) { - pushEncodedCachePart(encodedParts, key); - pushEncodedCachePart(encodedParts, value); - } - return encodedParts.join('|'); -} + const cached = stringMapSignatureCache.get(map); + if (cached !== undefined) return cached; -function pushEncodedCachePart(encodedParts: string[], part: string): void { - encodedParts.push(`${part.length}:${part}`); + const entries = [...map.entries()].sort(([a], [b]) => a.localeCompare(b)); + let encoded = ''; + let hasPart = false; + for (const [key, value] of entries) { + if (hasPart) encoded += '|'; + encoded += `${key.length}:${key}`; + hasPart = true; + encoded += `|${value.length}:${value}`; + } + stringMapSignatureCache.set(map, encoded); + return encoded; } const markdownPlainTextCache: StringCache = new Map(); diff --git a/test/renderer/components/team/activity/activityRenderCache.test.ts b/test/renderer/components/team/activity/activityRenderCache.test.ts new file mode 100644 index 00000000..2d2e9250 --- /dev/null +++ b/test/renderer/components/team/activity/activityRenderCache.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; + +import { + encodeCacheParts, + stringArrayCacheSignature, + stringMapCacheSignature, + taskRefsCacheSignature, +} from '../../../../../src/renderer/components/team/activity/activityRenderCache'; + +import type { TaskRef } from '../../../../../src/shared/types'; + +describe('activityRenderCache', () => { + it('encodes cache parts with length prefixes', () => { + expect(encodeCacheParts(['a', 'bb', ''])).toBe('1:a|2:bb|0:'); + }); + + it('builds stable task reference signatures', () => { + const refs: TaskRef[] = [ + { taskId: 'task-1', displayId: '#1', teamName: 'team-a' }, + { taskId: 'task-2', displayId: '#2' }, + ]; + + expect(taskRefsCacheSignature(refs)).toBe('6:task-1|2:#1|6:team-a|6:task-2|2:#2|0:'); + expect(taskRefsCacheSignature(refs)).toBe(taskRefsCacheSignature(refs)); + }); + + it('builds stable string array signatures', () => { + const values = ['alice', 'bob']; + + expect(stringArrayCacheSignature(values)).toBe('5:alice|3:bob'); + expect(stringArrayCacheSignature(values)).toBe(stringArrayCacheSignature(values)); + }); + + it('sorts string map signatures by key', () => { + const map = new Map([ + ['bob', 'blue'], + ['alice', 'red'], + ]); + + expect(stringMapCacheSignature(map)).toBe('5:alice|3:red|3:bob|4:blue'); + expect(stringMapCacheSignature(map)).toBe(stringMapCacheSignature(map)); + }); +});