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