perf(backend): ノートのリアクション情報をキャッシュすることでDBへのクエリを削減
This commit is contained in:
parent
4d1d25e02f
commit
1671575d5d
13 changed files with 103 additions and 23 deletions
|
@ -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
|
// 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);
|
this.globalEventService.publishNotesStream(noteObj);
|
||||||
|
|
||||||
|
|
|
@ -187,6 +187,9 @@ export class ReactionService {
|
||||||
await this.notesRepository.createQueryBuilder().update()
|
await this.notesRepository.createQueryBuilder().update()
|
||||||
.set({
|
.set({
|
||||||
reactions: () => sql,
|
reactions: () => sql,
|
||||||
|
...(note.reactionAndUserPairCache.length < 10 ? {
|
||||||
|
reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}:${reaction}')`,
|
||||||
|
} : {}),
|
||||||
})
|
})
|
||||||
.where('id = :id', { id: note.id })
|
.where('id = :id', { id: note.id })
|
||||||
.execute();
|
.execute();
|
||||||
|
@ -293,6 +296,7 @@ export class ReactionService {
|
||||||
await this.notesRepository.createQueryBuilder().update()
|
await this.notesRepository.createQueryBuilder().update()
|
||||||
.set({
|
.set({
|
||||||
reactions: () => sql,
|
reactions: () => sql,
|
||||||
|
reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}:${exist.reaction}')`,
|
||||||
})
|
})
|
||||||
.where('id = :id', { id: note.id })
|
.where('id = :id', { id: note.id })
|
||||||
.execute();
|
.execute();
|
||||||
|
|
|
@ -170,26 +170,37 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async populateMyReaction(noteId: MiNote['id'], meId: MiUser['id'], _hint_?: {
|
public async populateMyReaction(note: { id: MiNote['id']; reactions: MiNote['reactions']; reactionAndUserPairCache?: MiNote['reactionAndUserPairCache']; }, meId: MiUser['id'], _hint_?: {
|
||||||
myReactions: Map<MiNote['id'], MiNoteReaction | null>;
|
myReactions: Map<MiNote['id'], string | null>;
|
||||||
}) {
|
}) {
|
||||||
if (_hint_?.myReactions) {
|
if (_hint_?.myReactions) {
|
||||||
const reaction = _hint_.myReactions.get(noteId);
|
const reaction = _hint_.myReactions.get(note.id);
|
||||||
if (reaction) {
|
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 {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない
|
// パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない
|
||||||
if (this.idService.parse(noteId).date.getTime() + 2000 > Date.now()) {
|
if (this.idService.parse(note.id).date.getTime() + 2000 > Date.now()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reaction = await this.noteReactionsRepository.findOneBy({
|
const reaction = await this.noteReactionsRepository.findOneBy({
|
||||||
userId: meId,
|
userId: meId,
|
||||||
noteId: noteId,
|
noteId: note.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (reaction) {
|
if (reaction) {
|
||||||
|
@ -275,8 +286,9 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
options?: {
|
options?: {
|
||||||
detail?: boolean;
|
detail?: boolean;
|
||||||
skipHide?: boolean;
|
skipHide?: boolean;
|
||||||
|
withReactionAndUserPairCache?: boolean;
|
||||||
_hint_?: {
|
_hint_?: {
|
||||||
myReactions: Map<MiNote['id'], MiNoteReaction | null>;
|
myReactions: Map<MiNote['id'], string | null>;
|
||||||
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
|
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -284,6 +296,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
const opts = Object.assign({
|
const opts = Object.assign({
|
||||||
detail: true,
|
detail: true,
|
||||||
skipHide: false,
|
skipHide: false,
|
||||||
|
withReactionAndUserPairCache: false,
|
||||||
}, options);
|
}, options);
|
||||||
|
|
||||||
const meId = me ? me.id : null;
|
const meId = me ? me.id : null;
|
||||||
|
@ -324,6 +337,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
repliesCount: note.repliesCount,
|
repliesCount: note.repliesCount,
|
||||||
reactions: this.reactionService.convertLegacyReactions(note.reactions),
|
reactions: this.reactionService.convertLegacyReactions(note.reactions),
|
||||||
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
|
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
|
||||||
|
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,
|
||||||
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
|
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
|
||||||
tags: note.tags.length > 0 ? note.tags : undefined,
|
tags: note.tags.length > 0 ? note.tags : undefined,
|
||||||
fileIds: note.fileIds,
|
fileIds: note.fileIds,
|
||||||
|
@ -346,18 +360,20 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
|
|
||||||
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, {
|
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, {
|
||||||
detail: false,
|
detail: false,
|
||||||
|
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
|
||||||
_hint_: options?._hint_,
|
_hint_: options?._hint_,
|
||||||
}) : undefined,
|
}) : undefined,
|
||||||
|
|
||||||
renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, {
|
renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, {
|
||||||
detail: true,
|
detail: true,
|
||||||
|
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
|
||||||
_hint_: options?._hint_,
|
_hint_: options?._hint_,
|
||||||
}) : undefined,
|
}) : undefined,
|
||||||
|
|
||||||
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
|
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
|
||||||
|
|
||||||
...(meId && Object.keys(note.reactions).length > 0 ? {
|
...(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 [];
|
if (notes.length === 0) return [];
|
||||||
|
|
||||||
const meId = me ? me.id : null;
|
const meId = me ? me.id : null;
|
||||||
const myReactionsMap = new Map<MiNote['id'], MiNoteReaction | null>();
|
const myReactionsMap = new Map<MiNote['id'], string | null>();
|
||||||
if (meId) {
|
if (meId) {
|
||||||
const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
|
const idsNeedFetchMyReaction = new Set<MiNote['id']>();
|
||||||
|
|
||||||
// パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない
|
// パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない
|
||||||
const oldId = this.idService.gen(Date.now() - 2000);
|
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,
|
userId: meId,
|
||||||
noteId: In(targets),
|
noteId: In(Array.from(idsNeedFetchMyReaction)),
|
||||||
}) : [];
|
}) : [];
|
||||||
|
|
||||||
for (const target of targets) {
|
for (const id of idsNeedFetchMyReaction) {
|
||||||
myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null);
|
myReactionsMap.set(id, myReactions.find(reaction => reaction.noteId === id)?.reaction ?? null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -164,6 +164,11 @@ export class MiNote {
|
||||||
})
|
})
|
||||||
public mentionedRemoteUsers: string;
|
public mentionedRemoteUsers: string;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024, array: true, default: '{}',
|
||||||
|
})
|
||||||
|
public reactionAndUserPairCache: string[];
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 128, array: true, default: '{}',
|
length: 128, array: true, default: '{}',
|
||||||
})
|
})
|
||||||
|
|
|
@ -174,6 +174,14 @@ export const packedNoteSchema = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
},
|
},
|
||||||
|
reactionAndUserPairCache: {
|
||||||
|
type: 'array',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
myReaction: {
|
myReaction: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
|
|
@ -47,7 +47,7 @@ class ChannelChannel extends Channel {
|
||||||
|
|
||||||
if (this.user && note.renoteId && !note.text) {
|
if (this.user && note.renoteId && !note.text) {
|
||||||
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
|
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;
|
note.renote.myReaction = myRenoteReaction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ class GlobalTimelineChannel extends Channel {
|
||||||
|
|
||||||
if (this.user && note.renoteId && !note.text) {
|
if (this.user && note.renoteId && !note.text) {
|
||||||
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
|
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;
|
note.renote.myReaction = myRenoteReaction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ class HashtagChannel extends Channel {
|
||||||
|
|
||||||
if (this.user && note.renoteId && !note.text) {
|
if (this.user && note.renoteId && !note.text) {
|
||||||
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
|
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;
|
note.renote.myReaction = myRenoteReaction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ class HomeTimelineChannel extends Channel {
|
||||||
|
|
||||||
if (this.user && note.renoteId && !note.text) {
|
if (this.user && note.renoteId && !note.text) {
|
||||||
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
|
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;
|
note.renote.myReaction = myRenoteReaction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,8 @@ class HybridTimelineChannel extends Channel {
|
||||||
|
|
||||||
if (this.user && note.renoteId && !note.text) {
|
if (this.user && note.renoteId && !note.text) {
|
||||||
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
|
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;
|
note.renote.myReaction = myRenoteReaction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ class LocalTimelineChannel extends Channel {
|
||||||
|
|
||||||
if (this.user && note.renoteId && !note.text) {
|
if (this.user && note.renoteId && !note.text) {
|
||||||
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
|
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;
|
note.renote.myReaction = myRenoteReaction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,7 +104,7 @@ class UserListChannel extends Channel {
|
||||||
|
|
||||||
if (this.user && note.renoteId && !note.text) {
|
if (this.user && note.renoteId && !note.text) {
|
||||||
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
|
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;
|
note.renote.myReaction = myRenoteReaction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue