perf(renderer): cache activity render signatures

This commit is contained in:
777genius 2026-05-31 08:03:31 +03:00
parent d433df78af
commit 290f01e559
2 changed files with 86 additions and 21 deletions

View file

@ -6,6 +6,10 @@ const MAX_ACTIVITY_RENDER_CACHE_ENTRIES = 500;
type StringCache = Map<string, string>;
const taskRefsSignatureCache = new WeakMap<readonly TaskRef[], string>();
const stringArraySignatureCache = new WeakMap<readonly string[], string>();
const stringMapSignatureCache = new WeakMap<ReadonlyMap<string, string>, 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, string>): 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();

View file

@ -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));
});
});