perf(backend): ノートのリアクション情報をキャッシュすることでDBへのクエリを削減
This commit is contained in:
		
							parent
							
								
									4d1d25e02f
								
							
						
					
					
						commit
						1671575d5d
					
				|  | @ -0,0 +1,17 @@ | |||
| /* | ||||
|  * SPDX-FileCopyrightText: syuilo and other misskey contributors | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| 
 | ||||
| export class NoteReactionAndUserPairCache1697673894459 { | ||||
|     name = 'NoteReactionAndUserPairCache1697673894459' | ||||
| 
 | ||||
|     async up(queryRunner) { | ||||
|         await queryRunner.query(`ALTER TABLE "note" ADD "reactionAndUserPairCache" character varying(1024) array NOT NULL DEFAULT '{}'`); | ||||
|     } | ||||
| 
 | ||||
|     async down(queryRunner) { | ||||
|         await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "reactionAndUserPairCache"`); | ||||
|     } | ||||
| } | ||||
|  | @ -584,7 +584,7 @@ export class NoteCreateService implements OnApplicationShutdown { | |||
| 			} | ||||
| 
 | ||||
| 			// Pack the note
 | ||||
| 			const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true }); | ||||
| 			const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true }); | ||||
| 
 | ||||
| 			this.globalEventService.publishNotesStream(noteObj); | ||||
| 
 | ||||
|  |  | |||
|  | @ -187,6 +187,9 @@ export class ReactionService { | |||
| 		await this.notesRepository.createQueryBuilder().update() | ||||
| 			.set({ | ||||
| 				reactions: () => sql, | ||||
| 				...(note.reactionAndUserPairCache.length < 10 ? { | ||||
| 					reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}:${reaction}')`, | ||||
| 				} : {}), | ||||
| 			}) | ||||
| 			.where('id = :id', { id: note.id }) | ||||
| 			.execute(); | ||||
|  | @ -293,6 +296,7 @@ export class ReactionService { | |||
| 		await this.notesRepository.createQueryBuilder().update() | ||||
| 			.set({ | ||||
| 				reactions: () => sql, | ||||
| 				reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}:${exist.reaction}')`, | ||||
| 			}) | ||||
| 			.where('id = :id', { id: note.id }) | ||||
| 			.execute(); | ||||
|  |  | |||
|  | @ -170,26 +170,37 @@ export class NoteEntityService implements OnModuleInit { | |||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public async populateMyReaction(noteId: MiNote['id'], meId: MiUser['id'], _hint_?: { | ||||
| 		myReactions: Map<MiNote['id'], MiNoteReaction | null>; | ||||
| 	public async populateMyReaction(note: { id: MiNote['id']; reactions: MiNote['reactions']; reactionAndUserPairCache?: MiNote['reactionAndUserPairCache']; }, meId: MiUser['id'], _hint_?: { | ||||
| 		myReactions: Map<MiNote['id'], string | null>; | ||||
| 	}) { | ||||
| 		if (_hint_?.myReactions) { | ||||
| 			const reaction = _hint_.myReactions.get(noteId); | ||||
| 			const reaction = _hint_.myReactions.get(note.id); | ||||
| 			if (reaction) { | ||||
| 				return this.reactionService.convertLegacyReaction(reaction.reaction); | ||||
| 				return this.reactionService.convertLegacyReaction(reaction); | ||||
| 			} else { | ||||
| 				return undefined; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0); | ||||
| 		if (reactionsCount === 0) return undefined; | ||||
| 		if (note.reactionAndUserPairCache && reactionsCount <= note.reactionAndUserPairCache.length) { | ||||
| 			const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId)); | ||||
| 			if (pair) { | ||||
| 				return this.reactionService.convertLegacyReaction(pair.split(':')[1]); | ||||
| 			} else { | ||||
| 				return undefined; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		// パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない
 | ||||
| 		if (this.idService.parse(noteId).date.getTime() + 2000 > Date.now()) { | ||||
| 		if (this.idService.parse(note.id).date.getTime() + 2000 > Date.now()) { | ||||
| 			return undefined; | ||||
| 		} | ||||
| 
 | ||||
| 		const reaction = await this.noteReactionsRepository.findOneBy({ | ||||
| 			userId: meId, | ||||
| 			noteId: noteId, | ||||
| 			noteId: note.id, | ||||
| 		}); | ||||
| 
 | ||||
| 		if (reaction) { | ||||
|  | @ -275,8 +286,9 @@ export class NoteEntityService implements OnModuleInit { | |||
| 		options?: { | ||||
| 			detail?: boolean; | ||||
| 			skipHide?: boolean; | ||||
| 			withReactionAndUserPairCache?: boolean; | ||||
| 			_hint_?: { | ||||
| 				myReactions: Map<MiNote['id'], MiNoteReaction | null>; | ||||
| 				myReactions: Map<MiNote['id'], string | null>; | ||||
| 				packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>; | ||||
| 			}; | ||||
| 		}, | ||||
|  | @ -284,6 +296,7 @@ export class NoteEntityService implements OnModuleInit { | |||
| 		const opts = Object.assign({ | ||||
| 			detail: true, | ||||
| 			skipHide: false, | ||||
| 			withReactionAndUserPairCache: false, | ||||
| 		}, options); | ||||
| 
 | ||||
| 		const meId = me ? me.id : null; | ||||
|  | @ -324,6 +337,7 @@ export class NoteEntityService implements OnModuleInit { | |||
| 			repliesCount: note.repliesCount, | ||||
| 			reactions: this.reactionService.convertLegacyReactions(note.reactions), | ||||
| 			reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host), | ||||
| 			reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined, | ||||
| 			emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined, | ||||
| 			tags: note.tags.length > 0 ? note.tags : undefined, | ||||
| 			fileIds: note.fileIds, | ||||
|  | @ -346,18 +360,20 @@ export class NoteEntityService implements OnModuleInit { | |||
| 
 | ||||
| 				reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, { | ||||
| 					detail: false, | ||||
| 					withReactionAndUserPairCache: opts.withReactionAndUserPairCache, | ||||
| 					_hint_: options?._hint_, | ||||
| 				}) : undefined, | ||||
| 
 | ||||
| 				renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, { | ||||
| 					detail: true, | ||||
| 					withReactionAndUserPairCache: opts.withReactionAndUserPairCache, | ||||
| 					_hint_: options?._hint_, | ||||
| 				}) : undefined, | ||||
| 
 | ||||
| 				poll: note.hasPoll ? this.populatePoll(note, meId) : undefined, | ||||
| 
 | ||||
| 				...(meId && Object.keys(note.reactions).length > 0 ? { | ||||
| 					myReaction: this.populateMyReaction(note.id, meId, options?._hint_), | ||||
| 					myReaction: this.populateMyReaction(note, meId, options?._hint_), | ||||
| 				} : {}), | ||||
| 			} : {}), | ||||
| 		}); | ||||
|  | @ -381,19 +397,48 @@ export class NoteEntityService implements OnModuleInit { | |||
| 		if (notes.length === 0) return []; | ||||
| 
 | ||||
| 		const meId = me ? me.id : null; | ||||
| 		const myReactionsMap = new Map<MiNote['id'], MiNoteReaction | null>(); | ||||
| 		const myReactionsMap = new Map<MiNote['id'], string | null>(); | ||||
| 		if (meId) { | ||||
| 			const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!); | ||||
| 			const idsNeedFetchMyReaction = new Set<MiNote['id']>(); | ||||
| 
 | ||||
| 			// パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない
 | ||||
| 			const oldId = this.idService.gen(Date.now() - 2000); | ||||
| 			const targets = [...notes.filter(n => (n.id < oldId) && (Object.keys(n.reactions).length > 0)).map(n => n.id), ...renoteIds]; | ||||
| 			const myReactions = targets.length > 0 ? await this.noteReactionsRepository.findBy({ | ||||
| 
 | ||||
| 			for (const note of notes) { | ||||
| 				if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
 | ||||
| 					const reactionsCount = Object.values(note.renote.reactions).reduce((a, b) => a + b, 0); | ||||
| 					if (reactionsCount === 0) { | ||||
| 						myReactionsMap.set(note.renote.id, null); | ||||
| 					} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length) { | ||||
| 						const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId)); | ||||
| 						myReactionsMap.set(note.renote.id, pair ? pair.split(':')[1] : null); | ||||
| 					} else { | ||||
| 						idsNeedFetchMyReaction.add(note.renote.id); | ||||
| 					} | ||||
| 				} else { | ||||
| 					if (note.id < oldId) { | ||||
| 						const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0); | ||||
| 						if (reactionsCount === 0) { | ||||
| 							myReactionsMap.set(note.id, null); | ||||
| 						} else if (reactionsCount <= note.reactionAndUserPairCache.length) { | ||||
| 							const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId)); | ||||
| 							myReactionsMap.set(note.id, pair ? pair.split(':')[1] : null); | ||||
| 						} else { | ||||
| 							idsNeedFetchMyReaction.add(note.id); | ||||
| 						} | ||||
| 					} else { | ||||
| 						myReactionsMap.set(note.id, null); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			const myReactions = idsNeedFetchMyReaction.size > 0 ? await this.noteReactionsRepository.findBy({ | ||||
| 				userId: meId, | ||||
| 				noteId: In(targets), | ||||
| 				noteId: In(Array.from(idsNeedFetchMyReaction)), | ||||
| 			}) : []; | ||||
| 
 | ||||
| 			for (const target of targets) { | ||||
| 				myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null); | ||||
| 			for (const id of idsNeedFetchMyReaction) { | ||||
| 				myReactionsMap.set(id, myReactions.find(reaction => reaction.noteId === id)?.reaction ?? null); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -164,6 +164,11 @@ export class MiNote { | |||
| 	}) | ||||
| 	public mentionedRemoteUsers: string; | ||||
| 
 | ||||
| 	@Column('varchar', { | ||||
| 		length: 1024, array: true, default: '{}', | ||||
| 	}) | ||||
| 	public reactionAndUserPairCache: string[]; | ||||
| 
 | ||||
| 	@Column('varchar', { | ||||
| 		length: 128, array: true, default: '{}', | ||||
| 	}) | ||||
|  |  | |||
|  | @ -174,6 +174,14 @@ export const packedNoteSchema = { | |||
| 			type: 'string', | ||||
| 			optional: true, nullable: false, | ||||
| 		}, | ||||
| 		reactionAndUserPairCache: { | ||||
| 			type: 'array', | ||||
| 			optional: true, nullable: false, | ||||
| 			items: { | ||||
| 				type: 'string', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 		}, | ||||
| 
 | ||||
| 		myReaction: { | ||||
| 			type: 'object', | ||||
|  |  | |||
|  | @ -47,7 +47,7 @@ class ChannelChannel extends Channel { | |||
| 
 | ||||
| 		if (this.user && note.renoteId && !note.text) { | ||||
| 			if (note.renote && Object.keys(note.renote.reactions).length > 0) { | ||||
| 				const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); | ||||
| 				const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); | ||||
| 				note.renote.myReaction = myRenoteReaction; | ||||
| 			} | ||||
| 		} | ||||
|  |  | |||
|  | @ -73,7 +73,7 @@ class GlobalTimelineChannel extends Channel { | |||
| 
 | ||||
| 		if (this.user && note.renoteId && !note.text) { | ||||
| 			if (note.renote && Object.keys(note.renote.reactions).length > 0) { | ||||
| 				const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); | ||||
| 				const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); | ||||
| 				note.renote.myReaction = myRenoteReaction; | ||||
| 			} | ||||
| 		} | ||||
|  |  | |||
|  | @ -52,7 +52,7 @@ class HashtagChannel extends Channel { | |||
| 
 | ||||
| 		if (this.user && note.renoteId && !note.text) { | ||||
| 			if (note.renote && Object.keys(note.renote.reactions).length > 0) { | ||||
| 				const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); | ||||
| 				const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); | ||||
| 				note.renote.myReaction = myRenoteReaction; | ||||
| 			} | ||||
| 		} | ||||
|  |  | |||
|  | @ -75,7 +75,7 @@ class HomeTimelineChannel extends Channel { | |||
| 
 | ||||
| 		if (this.user && note.renoteId && !note.text) { | ||||
| 			if (note.renote && Object.keys(note.renote.reactions).length > 0) { | ||||
| 				const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); | ||||
| 				const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); | ||||
| 				note.renote.myReaction = myRenoteReaction; | ||||
| 			} | ||||
| 		} | ||||
|  |  | |||
|  | @ -89,7 +89,8 @@ class HybridTimelineChannel extends Channel { | |||
| 
 | ||||
| 		if (this.user && note.renoteId && !note.text) { | ||||
| 			if (note.renote && Object.keys(note.renote.reactions).length > 0) { | ||||
| 				const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); | ||||
| 				console.log(note.renote.reactionAndUserPairCache); | ||||
| 				const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); | ||||
| 				note.renote.myReaction = myRenoteReaction; | ||||
| 			} | ||||
| 		} | ||||
|  |  | |||
|  | @ -72,7 +72,7 @@ class LocalTimelineChannel extends Channel { | |||
| 
 | ||||
| 		if (this.user && note.renoteId && !note.text) { | ||||
| 			if (note.renote && Object.keys(note.renote.reactions).length > 0) { | ||||
| 				const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); | ||||
| 				const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); | ||||
| 				note.renote.myReaction = myRenoteReaction; | ||||
| 			} | ||||
| 		} | ||||
|  |  | |||
|  | @ -104,7 +104,7 @@ class UserListChannel extends Channel { | |||
| 
 | ||||
| 		if (this.user && note.renoteId && !note.text) { | ||||
| 			if (note.renote && Object.keys(note.renote.reactions).length > 0) { | ||||
| 				const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); | ||||
| 				const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); | ||||
| 				note.renote.myReaction = myRenoteReaction; | ||||
| 			} | ||||
| 		} | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue