Skip to content

Commit 5c1d86b

Browse files
authored
refactor(backend): UserEntityService.packMany()の高速化 (misskey-dev#13550)
* refactor(backend): UserEntityService.packMany()の高速化 * 修正
1 parent 6d9c234 commit 5c1d86b

File tree

3 files changed

+729
-36
lines changed

3 files changed

+729
-36
lines changed

packages/backend/src/core/entities/UserEntityService.ts

+198-31
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,38 @@ import { Inject, Injectable } from '@nestjs/common';
77
import * as Redis from 'ioredis';
88
import _Ajv from 'ajv';
99
import { ModuleRef } from '@nestjs/core';
10+
import { In } from 'typeorm';
1011
import { DI } from '@/di-symbols.js';
1112
import type { Config } from '@/config.js';
1213
import type { Packed } from '@/misc/json-schema.js';
1314
import type { Promiseable } from '@/misc/prelude/await-all.js';
1415
import { awaitAll } from '@/misc/prelude/await-all.js';
1516
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
1617
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
17-
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/User.js';
18-
import { MiNotification } from '@/models/Notification.js';
19-
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, MiUserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/_.js';
18+
import {
19+
birthdaySchema,
20+
descriptionSchema,
21+
localUsernameSchema,
22+
locationSchema,
23+
nameSchema,
24+
passwordSchema,
25+
} from '@/models/User.js';
26+
import type {
27+
BlockingsRepository,
28+
FollowingsRepository,
29+
FollowRequestsRepository,
30+
MiFollowing,
31+
MiUserNotePining,
32+
MiUserProfile,
33+
MutingsRepository,
34+
NoteUnreadsRepository,
35+
RenoteMutingsRepository,
36+
UserMemoRepository,
37+
UserNotePiningsRepository,
38+
UserProfilesRepository,
39+
UserSecurityKeysRepository,
40+
UsersRepository,
41+
} from '@/models/_.js';
2042
import { bindThis } from '@/decorators.js';
2143
import { RoleService } from '@/core/RoleService.js';
2244
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
@@ -46,11 +68,23 @@ function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean {
4668
return !isLocalUser(user);
4769
}
4870

71+
export type UserRelation = {
72+
id: MiUser['id']
73+
following: MiFollowing | null,
74+
isFollowing: boolean
75+
isFollowed: boolean
76+
hasPendingFollowRequestFromYou: boolean
77+
hasPendingFollowRequestToYou: boolean
78+
isBlocking: boolean
79+
isBlocked: boolean
80+
isMuted: boolean
81+
isRenoteMuted: boolean
82+
}
83+
4984
@Injectable()
5085
export class UserEntityService implements OnModuleInit {
5186
private apPersonService: ApPersonService;
5287
private noteEntityService: NoteEntityService;
53-
private driveFileEntityService: DriveFileEntityService;
5488
private pageEntityService: PageEntityService;
5589
private customEmojiService: CustomEmojiService;
5690
private announcementService: AnnouncementService;
@@ -89,9 +123,6 @@ export class UserEntityService implements OnModuleInit {
89123
@Inject(DI.renoteMutingsRepository)
90124
private renoteMutingsRepository: RenoteMutingsRepository,
91125

92-
@Inject(DI.driveFilesRepository)
93-
private driveFilesRepository: DriveFilesRepository,
94-
95126
@Inject(DI.noteUnreadsRepository)
96127
private noteUnreadsRepository: NoteUnreadsRepository,
97128

@@ -101,12 +132,6 @@ export class UserEntityService implements OnModuleInit {
101132
@Inject(DI.userProfilesRepository)
102133
private userProfilesRepository: UserProfilesRepository,
103134

104-
@Inject(DI.announcementReadsRepository)
105-
private announcementReadsRepository: AnnouncementReadsRepository,
106-
107-
@Inject(DI.announcementsRepository)
108-
private announcementsRepository: AnnouncementsRepository,
109-
110135
@Inject(DI.userMemosRepository)
111136
private userMemosRepository: UserMemoRepository,
112137
) {
@@ -115,7 +140,6 @@ export class UserEntityService implements OnModuleInit {
115140
onModuleInit() {
116141
this.apPersonService = this.moduleRef.get('ApPersonService');
117142
this.noteEntityService = this.moduleRef.get('NoteEntityService');
118-
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
119143
this.pageEntityService = this.moduleRef.get('PageEntityService');
120144
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
121145
this.announcementService = this.moduleRef.get('AnnouncementService');
@@ -138,7 +162,7 @@ export class UserEntityService implements OnModuleInit {
138162
public isRemoteUser = isRemoteUser;
139163

140164
@bindThis
141-
public async getRelation(me: MiUser['id'], target: MiUser['id']) {
165+
public async getRelation(me: MiUser['id'], target: MiUser['id']): Promise<UserRelation> {
142166
const [
143167
following,
144168
isFollowed,
@@ -211,6 +235,59 @@ export class UserEntityService implements OnModuleInit {
211235
};
212236
}
213237

238+
@bindThis
239+
public async getRelations(me: MiUser['id'], targets: MiUser['id'][]): Promise<Map<MiUser['id'], UserRelation>> {
240+
const [
241+
followers,
242+
followees,
243+
followersRequests,
244+
followeesRequests,
245+
blockers,
246+
blockees,
247+
muters,
248+
renoteMuters,
249+
] = await Promise.all([
250+
this.followingsRepository.findBy({ followerId: me })
251+
.then(f => new Map(f.map(it => [it.followeeId, it]))),
252+
this.followingsRepository.findBy({ followeeId: me })
253+
.then(it => it.map(it => it.followerId)),
254+
this.followRequestsRepository.findBy({ followerId: me })
255+
.then(it => it.map(it => it.followeeId)),
256+
this.followRequestsRepository.findBy({ followeeId: me })
257+
.then(it => it.map(it => it.followerId)),
258+
this.blockingsRepository.findBy({ blockerId: me })
259+
.then(it => it.map(it => it.blockeeId)),
260+
this.blockingsRepository.findBy({ blockeeId: me })
261+
.then(it => it.map(it => it.blockerId)),
262+
this.mutingsRepository.findBy({ muterId: me })
263+
.then(it => it.map(it => it.muteeId)),
264+
this.renoteMutingsRepository.findBy({ muterId: me })
265+
.then(it => it.map(it => it.muteeId)),
266+
]);
267+
268+
return new Map(
269+
targets.map(target => {
270+
const following = followers.get(target) ?? null;
271+
272+
return [
273+
target,
274+
{
275+
id: target,
276+
following: following,
277+
isFollowing: following != null,
278+
isFollowed: followees.includes(target),
279+
hasPendingFollowRequestFromYou: followersRequests.includes(target),
280+
hasPendingFollowRequestToYou: followeesRequests.includes(target),
281+
isBlocking: blockers.includes(target),
282+
isBlocked: blockees.includes(target),
283+
isMuted: muters.includes(target),
284+
isRenoteMuted: renoteMuters.includes(target),
285+
},
286+
];
287+
}),
288+
);
289+
}
290+
214291
@bindThis
215292
public async getHasUnreadAntenna(userId: MiUser['id']): Promise<boolean> {
216293
/*
@@ -303,6 +380,9 @@ export class UserEntityService implements OnModuleInit {
303380
schema?: S,
304381
includeSecrets?: boolean,
305382
userProfile?: MiUserProfile,
383+
userRelations?: Map<MiUser['id'], UserRelation>,
384+
userMemos?: Map<MiUser['id'], string | null>,
385+
pinNotes?: Map<MiUser['id'], MiUserNotePining[]>,
306386
},
307387
): Promise<Packed<S>> {
308388
const opts = Object.assign({
@@ -317,13 +397,41 @@ export class UserEntityService implements OnModuleInit {
317397
const isMe = meId === user.id;
318398
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
319399

320-
const relation = meId && !isMe && isDetailed ? await this.getRelation(meId, user.id) : null;
321-
const pins = isDetailed ? await this.userNotePiningsRepository.createQueryBuilder('pin')
322-
.where('pin.userId = :userId', { userId: user.id })
323-
.innerJoinAndSelect('pin.note', 'note')
324-
.orderBy('pin.id', 'DESC')
325-
.getMany() : [];
326-
const profile = isDetailed ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
400+
const profile = isDetailed
401+
? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }))
402+
: null;
403+
404+
let relation: UserRelation | null = null;
405+
if (meId && !isMe && isDetailed) {
406+
if (opts.userRelations) {
407+
relation = opts.userRelations.get(user.id) ?? null;
408+
} else {
409+
relation = await this.getRelation(meId, user.id);
410+
}
411+
}
412+
413+
let memo: string | null = null;
414+
if (isDetailed && meId) {
415+
if (opts.userMemos) {
416+
memo = opts.userMemos.get(user.id) ?? null;
417+
} else {
418+
memo = await this.userMemosRepository.findOneBy({ userId: meId, targetUserId: user.id })
419+
.then(row => row?.memo ?? null);
420+
}
421+
}
422+
423+
let pins: MiUserNotePining[] = [];
424+
if (isDetailed) {
425+
if (opts.pinNotes) {
426+
pins = opts.pinNotes.get(user.id) ?? [];
427+
} else {
428+
pins = await this.userNotePiningsRepository.createQueryBuilder('pin')
429+
.where('pin.userId = :userId', { userId: user.id })
430+
.innerJoinAndSelect('pin.note', 'note')
431+
.orderBy('pin.id', 'DESC')
432+
.getMany();
433+
}
434+
}
327435

328436
const followingCount = profile == null ? null :
329437
(profile.followingVisibility === 'public') || isMe ? user.followingCount :
@@ -416,9 +524,7 @@ export class UserEntityService implements OnModuleInit {
416524
twoFactorEnabled: profile!.twoFactorEnabled,
417525
usePasswordLessLogin: profile!.usePasswordLessLogin,
418526
securityKeys: profile!.twoFactorEnabled
419-
? this.userSecurityKeysRepository.countBy({
420-
userId: user.id,
421-
}).then(result => result >= 1)
527+
? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
422528
: false,
423529
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
424530
id: role.id,
@@ -430,10 +536,7 @@ export class UserEntityService implements OnModuleInit {
430536
isAdministrator: role.isAdministrator,
431537
displayOrder: role.displayOrder,
432538
}))),
433-
memo: meId == null ? null : await this.userMemosRepository.findOneBy({
434-
userId: meId,
435-
targetUserId: user.id,
436-
}).then(row => row?.memo ?? null),
539+
memo: memo,
437540
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
438541
} : {}),
439542

@@ -514,14 +617,78 @@ export class UserEntityService implements OnModuleInit {
514617
return await awaitAll(packed);
515618
}
516619

517-
public packMany<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
620+
public async packMany<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
518621
users: (MiUser['id'] | MiUser)[],
519622
me?: { id: MiUser['id'] } | null | undefined,
520623
options?: {
521624
schema?: S,
522625
includeSecrets?: boolean,
523626
},
524627
): Promise<Packed<S>[]> {
525-
return Promise.all(users.map(u => this.pack(u, me, options)));
628+
// -- IDのみの要素を補完して完全なエンティティ一覧を作る
629+
630+
const _users = users.filter((user): user is MiUser => typeof user !== 'string');
631+
if (_users.length !== users.length) {
632+
_users.push(
633+
...await this.usersRepository.findBy({
634+
id: In(users.filter((user): user is string => typeof user === 'string')),
635+
}),
636+
);
637+
}
638+
const _userIds = _users.map(u => u.id);
639+
640+
// -- 特に前提条件のない値群を取得
641+
642+
const profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) })
643+
.then(profiles => new Map(profiles.map(p => [p.userId, p])));
644+
645+
// -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得
646+
647+
let userRelations: Map<MiUser['id'], UserRelation> = new Map();
648+
let userMemos: Map<MiUser['id'], string | null> = new Map();
649+
let pinNotes: Map<MiUser['id'], MiUserNotePining[]> = new Map();
650+
651+
if (options?.schema !== 'UserLite') {
652+
const meId = me ? me.id : null;
653+
if (meId) {
654+
userMemos = await this.userMemosRepository.findBy({ userId: meId })
655+
.then(memos => new Map(memos.map(memo => [memo.targetUserId, memo.memo])));
656+
657+
if (_userIds.length > 0) {
658+
userRelations = await this.getRelations(meId, _userIds);
659+
pinNotes = await this.userNotePiningsRepository.createQueryBuilder('pin')
660+
.where('pin.userId IN (:...userIds)', { userIds: _userIds })
661+
.innerJoinAndSelect('pin.note', 'note')
662+
.getMany()
663+
.then(pinsNotes => {
664+
const map = new Map<MiUser['id'], MiUserNotePining[]>();
665+
for (const note of pinsNotes) {
666+
const notes = map.get(note.userId) ?? [];
667+
notes.push(note);
668+
map.set(note.userId, notes);
669+
}
670+
for (const [, notes] of map.entries()) {
671+
// pack側ではDESCで取得しているので、それに合わせて降順に並び替えておく
672+
notes.sort((a, b) => b.id.localeCompare(a.id));
673+
}
674+
return map;
675+
});
676+
}
677+
}
678+
}
679+
680+
return Promise.all(
681+
_users.map(u => this.pack(
682+
u,
683+
me,
684+
{
685+
...options,
686+
userProfile: profilesMap.get(u.id),
687+
userRelations: userRelations,
688+
userMemos: userMemos,
689+
pinNotes: pinNotes,
690+
},
691+
)),
692+
);
526693
}
527694
}

packages/backend/src/server/api/endpoints/users/relation.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
132132
private userEntityService: UserEntityService,
133133
) {
134134
super(meta, paramDef, async (ps, me) => {
135-
const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId];
136-
137-
const relations = await Promise.all(ids.map(id => this.userEntityService.getRelation(me.id, id)));
138-
139-
return Array.isArray(ps.userId) ? relations : relations[0];
135+
return Array.isArray(ps.userId)
136+
? await this.userEntityService.getRelations(me.id, ps.userId).then(it => [...it.values()])
137+
: await this.userEntityService.getRelation(me.id, ps.userId).then(it => [it]);
140138
});
141139
}
142140
}

0 commit comments

Comments
 (0)