mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-04 07:24:13 +00:00 
			
		
		
		
	perf(backend): Reduce memory usage of MemoryKVCache (#11076)
* perf(backend): Reduce memory usage of MemoryKVCache * fix
This commit is contained in:
		
							parent
							
								
									5b8fa25a12
								
							
						
					
					
						commit
						7ec07d5fd2
					
				
					 2 changed files with 64 additions and 17 deletions
				
			
		| 
						 | 
					@ -11,10 +11,10 @@ import type { OnApplicationShutdown } from '@nestjs/common';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class CacheService implements OnApplicationShutdown {
 | 
					export class CacheService implements OnApplicationShutdown {
 | 
				
			||||||
	public userByIdCache: MemoryKVCache<User>;
 | 
						public userByIdCache: MemoryKVCache<User, User | string>;
 | 
				
			||||||
	public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null>;
 | 
						public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null, string | null>;
 | 
				
			||||||
	public localUserByIdCache: MemoryKVCache<LocalUser>;
 | 
						public localUserByIdCache: MemoryKVCache<LocalUser>;
 | 
				
			||||||
	public uriPersonCache: MemoryKVCache<User | null>;
 | 
						public uriPersonCache: MemoryKVCache<User | null, string | null>;
 | 
				
			||||||
	public userProfileCache: RedisKVCache<UserProfile>;
 | 
						public userProfileCache: RedisKVCache<UserProfile>;
 | 
				
			||||||
	public userMutingsCache: RedisKVCache<Set<string>>;
 | 
						public userMutingsCache: RedisKVCache<Set<string>>;
 | 
				
			||||||
	public userBlockingCache: RedisKVCache<Set<string>>;
 | 
						public userBlockingCache: RedisKVCache<Set<string>>;
 | 
				
			||||||
| 
						 | 
					@ -55,10 +55,41 @@ export class CacheService implements OnApplicationShutdown {
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
		//this.onMessage = this.onMessage.bind(this);
 | 
							//this.onMessage = this.onMessage.bind(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.userByIdCache = new MemoryKVCache<User>(Infinity);
 | 
							const localUserByIdCache = new MemoryKVCache<LocalUser>(1000 * 60 * 60 * 6 /* 6h */);
 | 
				
			||||||
		this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null>(Infinity);
 | 
							this.localUserByIdCache	= localUserByIdCache;
 | 
				
			||||||
		this.localUserByIdCache = new MemoryKVCache<LocalUser>(Infinity);
 | 
					
 | 
				
			||||||
		this.uriPersonCache = new MemoryKVCache<User | null>(Infinity);
 | 
							// ローカルユーザーなら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', {
 | 
							this.userProfileCache = new RedisKVCache<UserProfile>(this.redisClient, 'userProfile', {
 | 
				
			||||||
			lifetime: 1000 * 60 * 30, // 30m
 | 
								lifetime: 1000 * 60 * 30, // 30m
 | 
				
			||||||
| 
						 | 
					@ -131,7 +162,7 @@ export class CacheService implements OnApplicationShutdown {
 | 
				
			||||||
					const user = await this.usersRepository.findOneByOrFail({ id: body.id });
 | 
										const user = await this.usersRepository.findOneByOrFail({ id: body.id });
 | 
				
			||||||
					this.userByIdCache.set(user.id, user);
 | 
										this.userByIdCache.set(user.id, user);
 | 
				
			||||||
					for (const [k, v] of this.uriPersonCache.cache.entries()) {
 | 
										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);
 | 
												this.uriPersonCache.set(k, user);
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -181,14 +181,28 @@ export class RedisSingleCache<T> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
 | 
					// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class MemoryKVCache<T> {
 | 
					function nothingToDo<T, V = T>(value: T): V {
 | 
				
			||||||
	public cache: Map<string, { date: number; value: T; }>;
 | 
						return value as unknown as V;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class MemoryKVCache<T, V = T> {
 | 
				
			||||||
 | 
						public cache: Map<string, { date: number; value: V; }>;
 | 
				
			||||||
	private lifetime: number;
 | 
						private lifetime: number;
 | 
				
			||||||
	private gcIntervalHandle: NodeJS.Timer;
 | 
						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.cache = new Map();
 | 
				
			||||||
		this.lifetime = lifetime;
 | 
							this.lifetime = lifetime;
 | 
				
			||||||
 | 
							this.toMapConverter = options.toMapConverter;
 | 
				
			||||||
 | 
							this.fromMapConverter = options.fromMapConverter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.gcIntervalHandle = setInterval(() => {
 | 
							this.gcIntervalHandle = setInterval(() => {
 | 
				
			||||||
			this.gc();
 | 
								this.gc();
 | 
				
			||||||
| 
						 | 
					@ -199,7 +213,7 @@ export class MemoryKVCache<T> {
 | 
				
			||||||
	public set(key: string, value: T): void {
 | 
						public set(key: string, value: T): void {
 | 
				
			||||||
		this.cache.set(key, {
 | 
							this.cache.set(key, {
 | 
				
			||||||
			date: Date.now(),
 | 
								date: Date.now(),
 | 
				
			||||||
			value,
 | 
								value: this.toMapConverter(value),
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -211,7 +225,7 @@ export class MemoryKVCache<T> {
 | 
				
			||||||
			this.cache.delete(key);
 | 
								this.cache.delete(key);
 | 
				
			||||||
			return undefined;
 | 
								return undefined;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return cached.value;
 | 
							return this.fromMapConverter(cached.value);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@bindThis
 | 
						@bindThis
 | 
				
			||||||
| 
						 | 
					@ -222,9 +236,10 @@ export class MemoryKVCache<T> {
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
 | 
						 * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
 | 
				
			||||||
	 * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
 | 
						 * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
 | 
				
			||||||
 | 
						 * fetcherの引数はcacheに保存されている値があれば渡されます
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	@bindThis
 | 
						@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);
 | 
							const cachedValue = this.get(key);
 | 
				
			||||||
		if (cachedValue !== undefined) {
 | 
							if (cachedValue !== undefined) {
 | 
				
			||||||
			if (validator) {
 | 
								if (validator) {
 | 
				
			||||||
| 
						 | 
					@ -239,7 +254,7 @@ export class MemoryKVCache<T> {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Cache MISS
 | 
							// Cache MISS
 | 
				
			||||||
		const value = await fetcher();
 | 
							const value = await fetcher(this.cache.get(key)?.value);
 | 
				
			||||||
		this.set(key, value);
 | 
							this.set(key, value);
 | 
				
			||||||
		return value;
 | 
							return value;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -247,9 +262,10 @@ export class MemoryKVCache<T> {
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
 | 
						 * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
 | 
				
			||||||
	 * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
 | 
						 * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
 | 
				
			||||||
 | 
						 * fetcherの引数はcacheに保存されている値があれば渡されます
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	@bindThis
 | 
						@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);
 | 
							const cachedValue = this.get(key);
 | 
				
			||||||
		if (cachedValue !== undefined) {
 | 
							if (cachedValue !== undefined) {
 | 
				
			||||||
			if (validator) {
 | 
								if (validator) {
 | 
				
			||||||
| 
						 | 
					@ -264,7 +280,7 @@ export class MemoryKVCache<T> {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Cache MISS
 | 
							// Cache MISS
 | 
				
			||||||
		const value = await fetcher();
 | 
							const value = await fetcher(this.cache.get(key)?.value);
 | 
				
			||||||
		if (value !== undefined) {
 | 
							if (value !== undefined) {
 | 
				
			||||||
			this.set(key, value);
 | 
								this.set(key, value);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue