From 7ec07d5fd2e28d523c85d160944916b2a5ee04a1 Mon Sep 17 00:00:00 2001 From: tamaina <tamaina@hotmail.co.jp> Date: Sat, 8 Jul 2023 21:18:16 +0900 Subject: [PATCH] perf(backend): Reduce memory usage of MemoryKVCache (#11076) * perf(backend): Reduce memory usage of MemoryKVCache * fix --- packages/backend/src/core/CacheService.ts | 47 +++++++++++++++++++---- packages/backend/src/misc/cache.ts | 34 +++++++++++----- 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index 2b7f9a48d..cd6b68e72 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -11,10 +11,10 @@ import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class CacheService implements OnApplicationShutdown { - public userByIdCache: MemoryKVCache<User>; - public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null>; + public userByIdCache: MemoryKVCache<User, User | string>; + public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null, string | null>; public localUserByIdCache: MemoryKVCache<LocalUser>; - public uriPersonCache: MemoryKVCache<User | null>; + public uriPersonCache: MemoryKVCache<User | null, string | null>; public userProfileCache: RedisKVCache<UserProfile>; public userMutingsCache: RedisKVCache<Set<string>>; public userBlockingCache: RedisKVCache<Set<string>>; @@ -55,10 +55,41 @@ export class CacheService implements OnApplicationShutdown { ) { //this.onMessage = this.onMessage.bind(this); - this.userByIdCache = new MemoryKVCache<User>(Infinity); - this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null>(Infinity); - this.localUserByIdCache = new MemoryKVCache<LocalUser>(Infinity); - this.uriPersonCache = new MemoryKVCache<User | null>(Infinity); + const localUserByIdCache = new MemoryKVCache<LocalUser>(1000 * 60 * 60 * 6 /* 6h */); + this.localUserByIdCache = localUserByIdCache; + + // ローカルユーザーならlocalUserByIdCacheにデータを追加し、こちらにはid(文字列)だけを追加する + const userByIdCache = new MemoryKVCache<User, User | string>(1000 * 60 * 60 * 6 /* 6h */, { + toMapConverter: user => { + if (user.host === null) { + localUserByIdCache.set(user.id, user as LocalUser); + return user.id; + } + + return user; + }, + fromMapConverter: userOrId => typeof userOrId === 'string' ? localUserByIdCache.get(userOrId) : userOrId, + }); + this.userByIdCache = userByIdCache; + + this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null, string | null>(Infinity, { + toMapConverter: user => { + if (user === null) return null; + + localUserByIdCache.set(user.id, user); + return user.id; + }, + fromMapConverter: id => id === null ? null : localUserByIdCache.get(id), + }); + this.uriPersonCache = new MemoryKVCache<User | null, string | null>(Infinity, { + toMapConverter: user => { + if (user === null) return null; + + userByIdCache.set(user.id, user); + return user.id; + }, + fromMapConverter: id => id === null ? null : userByIdCache.get(id), + }); this.userProfileCache = new RedisKVCache<UserProfile>(this.redisClient, 'userProfile', { lifetime: 1000 * 60 * 30, // 30m @@ -131,7 +162,7 @@ export class CacheService implements OnApplicationShutdown { const user = await this.usersRepository.findOneByOrFail({ id: body.id }); this.userByIdCache.set(user.id, user); for (const [k, v] of this.uriPersonCache.cache.entries()) { - if (v.value?.id === user.id) { + if (v.value === user.id) { this.uriPersonCache.set(k, user); } } diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index f130a7db8..e825d5137 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -181,14 +181,28 @@ export class RedisSingleCache<T> { // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? -export class MemoryKVCache<T> { - public cache: Map<string, { date: number; value: T; }>; +function nothingToDo<T, V = T>(value: T): V { + return value as unknown as V; +} + +export class MemoryKVCache<T, V = T> { + public cache: Map<string, { date: number; value: V; }>; private lifetime: number; private gcIntervalHandle: NodeJS.Timer; + private toMapConverter: (value: T) => V; + private fromMapConverter: (cached: V) => T | undefined; - constructor(lifetime: MemoryKVCache<never>['lifetime']) { + constructor(lifetime: MemoryKVCache<never>['lifetime'], options: { + toMapConverter: (value: T) => V; + fromMapConverter: (cached: V) => T | undefined; + } = { + toMapConverter: nothingToDo, + fromMapConverter: nothingToDo, + }) { this.cache = new Map(); this.lifetime = lifetime; + this.toMapConverter = options.toMapConverter; + this.fromMapConverter = options.fromMapConverter; this.gcIntervalHandle = setInterval(() => { this.gc(); @@ -199,7 +213,7 @@ export class MemoryKVCache<T> { public set(key: string, value: T): void { this.cache.set(key, { date: Date.now(), - value, + value: this.toMapConverter(value), }); } @@ -211,7 +225,7 @@ export class MemoryKVCache<T> { this.cache.delete(key); return undefined; } - return cached.value; + return this.fromMapConverter(cached.value); } @bindThis @@ -222,9 +236,10 @@ export class MemoryKVCache<T> { /** * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + * fetcherの引数はcacheに保存されている値があれば渡されます */ @bindThis - public async fetch(key: string, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> { + public async fetch(key: string, fetcher: (value: V | undefined) => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> { const cachedValue = this.get(key); if (cachedValue !== undefined) { if (validator) { @@ -239,7 +254,7 @@ export class MemoryKVCache<T> { } // Cache MISS - const value = await fetcher(); + const value = await fetcher(this.cache.get(key)?.value); this.set(key, value); return value; } @@ -247,9 +262,10 @@ export class MemoryKVCache<T> { /** * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + * fetcherの引数はcacheに保存されている値があれば渡されます */ @bindThis - public async fetchMaybe(key: string, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> { + public async fetchMaybe(key: string, fetcher: (value: V | undefined) => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> { const cachedValue = this.get(key); if (cachedValue !== undefined) { if (validator) { @@ -264,7 +280,7 @@ export class MemoryKVCache<T> { } // Cache MISS - const value = await fetcher(); + const value = await fetcher(this.cache.get(key)?.value); if (value !== undefined) { this.set(key, value); }