[mastodon-client] Improve html cache performance

This commit is contained in:
Laura Hausmann 2023-11-26 22:21:47 +01:00
parent 61c532a854
commit 7ab7edeefd
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
5 changed files with 61 additions and 33 deletions

View file

@ -617,9 +617,10 @@ export async function updatePerson(
); );
} }
await UserProfiles.update( // Get old profile to see if we need to update any matching html cache entries
{ userId: user.id }, const oldProfile = await UserProfiles.findOneBy({ userId: user.id });
{
const newProfile = {
url: url, url: url,
fields, fields,
description: person._misskey_summary description: person._misskey_summary
@ -628,9 +629,10 @@ export async function updatePerson(
? await htmlToMfm(truncate(person.summary, summaryLength), person.tag) ? await htmlToMfm(truncate(person.summary, summaryLength), person.tag)
: null, : null,
birthday: bday ? bday[0] : null, birthday: bday ? bday[0] : null,
location: person["vcard:Address"] || null, location: person["vcard:Address"] || null
}, } as Partial<UserProfile>;
);
await UserProfiles.update({ userId: user.id }, newProfile);
publishInternalEvent("remoteUserUpdated", { id: user.id }); publishInternalEvent("remoteUserUpdated", { id: user.id });
@ -639,7 +641,7 @@ export async function updatePerson(
// Mentions update, then prewarm html cache // Mentions update, then prewarm html cache
UserProfiles.updateMentions(user!.id) UserProfiles.updateMentions(user!.id)
.then(_ => UserConverter.prewarmCacheById(user!.id)); .then(_ => UserConverter.prewarmCacheById(user!.id, oldProfile));
// If the user in question is a follower, followers will also be updated. // If the user in question is a follower, followers will also be updated.
await Followings.update( await Followings.update(

View file

@ -182,7 +182,7 @@ export class NoteConverter {
return Promise.all(encoded); return Promise.all(encoded);
} }
private static async aggregateData(notes: Note[], ctx: MastoContext): Promise<void> { public static async aggregateData(notes: Note[], ctx: MastoContext): Promise<void> {
if (notes.length === 0) return; if (notes.length === 0) return;
const user = ctx.user as ILocalUser | null; const user = ctx.user as ILocalUser | null;
@ -307,7 +307,7 @@ export class NoteConverter {
return Promise.resolve(dbHit) return Promise.resolve(dbHit)
.then(res => { .then(res => {
if (res === null || (res.updatedAt !== note.updatedAt)) { if (res === null || (res.updatedAt?.getTime() !== note.updatedAt?.getTime())) {
this.prewarmCache(note); this.prewarmCache(note);
return null; return null;
} }

View file

@ -1,4 +1,4 @@
import { ILocalUser } from "@/models/entities/user.js"; import { ILocalUser, User } from "@/models/entities/user.js";
import { Notification } from "@/models/entities/notification.js"; import { Notification } from "@/models/entities/notification.js";
import { notificationTypes } from "@/types.js"; import { notificationTypes } from "@/types.js";
import { UserConverter } from "@/server/api/mastodon/converters/user.js"; import { UserConverter } from "@/server/api/mastodon/converters/user.js";
@ -9,6 +9,8 @@ import { getNote } from "@/server/api/common/getters.js";
import { getStubMastoContext, MastoContext } from "@/server/api/mastodon/index.js"; import { getStubMastoContext, MastoContext } from "@/server/api/mastodon/index.js";
import { Notifications } from "@/models/index.js"; import { Notifications } from "@/models/index.js";
import isQuote from "@/misc/is-quote.js"; import isQuote from "@/misc/is-quote.js";
import { unique } from "@/prelude/array.js";
import { Note } from "@/models/entities/note.js";
type NotificationType = typeof notificationTypes[number]; type NotificationType = typeof notificationTypes[number];
@ -51,11 +53,21 @@ export class NotificationConverter {
} }
public static async encodeMany(notifications: Notification[], ctx: MastoContext): Promise<MastodonEntity.Notification[]> { public static async encodeMany(notifications: Notification[], ctx: MastoContext): Promise<MastodonEntity.Notification[]> {
await this.aggregateData(notifications, ctx);
const encoded = notifications.map(u => this.encode(u, ctx)); const encoded = notifications.map(u => this.encode(u, ctx));
return Promise.all(encoded) return Promise.all(encoded)
.then(p => p.filter(n => n !== null) as MastodonEntity.Notification[]); .then(p => p.filter(n => n !== null) as MastodonEntity.Notification[]);
} }
private static async aggregateData(notifications: Notification[], ctx: MastoContext): Promise<void> {
if (notifications.length === 0) return;
const notes = unique(notifications.filter(p => p.note != null).map((n) => n.note as Note));
const users = unique(notifications.filter(p => p.notifier != null).map(n => n.notifier as User)
.concat(notifications.filter(p => p.notifiee != null).map(n => n.notifiee as User)));
await NoteConverter.aggregateData(notes, ctx);
await UserConverter.aggregateData(users, ctx);
}
private static encodeNotificationType(t: NotificationType): MastodonEntity.NotificationType { private static encodeNotificationType(t: NotificationType): MastodonEntity.NotificationType {
//FIXME: Implement custom notification for followRequestAccepted //FIXME: Implement custom notification for followRequestAccepted
//FIXME: Implement mastodon notification type 'update' on misskey side //FIXME: Implement mastodon notification type 'update' on misskey side

View file

@ -1,6 +1,6 @@
import { ILocalUser, User } from "@/models/entities/user.js"; import { ILocalUser, User } from "@/models/entities/user.js";
import config from "@/config/index.js"; import config from "@/config/index.js";
import {DriveFiles, Followings, HtmlUserCacheEntries, UserProfiles, Users} from "@/models/index.js"; import { DriveFiles, Followings, HtmlUserCacheEntries, UserProfiles, Users } from "@/models/index.js";
import { EmojiConverter } from "@/server/api/mastodon/converters/emoji.js"; import { EmojiConverter } from "@/server/api/mastodon/converters/emoji.js";
import { populateEmojis } from "@/misc/populate-emojis.js"; import { populateEmojis } from "@/misc/populate-emojis.js";
import { escapeMFM } from "@/server/api/mastodon/converters/mfm.js"; import { escapeMFM } from "@/server/api/mastodon/converters/mfm.js";
@ -9,7 +9,7 @@ import { awaitAll } from "@/prelude/await-all.js";
import { AccountCache, UserHelpers } from "@/server/api/mastodon/helpers/user.js"; import { AccountCache, UserHelpers } from "@/server/api/mastodon/helpers/user.js";
import { MfmHelpers } from "@/server/api/mastodon/helpers/mfm.js"; import { MfmHelpers } from "@/server/api/mastodon/helpers/mfm.js";
import { MastoContext } from "@/server/api/mastodon/index.js"; import { MastoContext } from "@/server/api/mastodon/index.js";
import {IMentionedRemoteUsers, Note} from "@/models/entities/note.js"; import { IMentionedRemoteUsers, Note } from "@/models/entities/note.js";
import { UserProfile } from "@/models/entities/user-profile.js"; import { UserProfile } from "@/models/entities/user-profile.js";
import { In } from "typeorm"; import { In } from "typeorm";
import { unique } from "@/prelude/array.js"; import { unique } from "@/prelude/array.js";
@ -35,7 +35,7 @@ export class UserConverter {
const cacheHit = cache.accounts.find(p => p.id == u.id); const cacheHit = cache.accounts.find(p => p.id == u.id);
if (cacheHit) return cacheHit; if (cacheHit) return cacheHit;
const identifier = `${u.id}:${(u.updatedAt ?? u.createdAt).getTime()}`; const identifier = `${u.id}:${(u.lastFetchedAt ?? u.createdAt).getTime()}`;
let fqn = `${u.username}@${u.host ?? config.domain}`; let fqn = `${u.username}@${u.host ?? config.domain}`;
let acct = u.username; let acct = u.username;
let acctUrl = `https://${u.host || config.host}/@${u.username}`; let acctUrl = `https://${u.host || config.host}/@${u.username}`;
@ -243,7 +243,7 @@ export class UserConverter {
return Promise.resolve(dbHit) return Promise.resolve(dbHit)
.then(res => { .then(res => {
if (res === null || (res.updatedAt !== user.updatedAt ?? user.createdAt)) { if (res === null || (res.updatedAt.getTime() !== (user.lastFetchedAt ?? user.createdAt).getTime())) {
this.prewarmCache(user, profile); this.prewarmCache(user, profile);
return null; return null;
} }
@ -251,12 +251,23 @@ export class UserConverter {
}); });
} }
public static async prewarmCache(user: User, profile?: UserProfile | null): Promise<void> { public static async prewarmCache(user: User, profile?: UserProfile | null, oldProfile?: UserProfile | null): Promise<void> {
if (!config.htmlCache?.prewarm) return; const identifier = `${user.id}:${(user.lastFetchedAt ?? user.createdAt).getTime()}`;
const identifier = `${user.id}:${(user.updatedAt ?? user.createdAt).getTime()}`;
if (profile !== null) { if (profile !== null) {
if (config.htmlCache?.dbFallback) {
if (profile === undefined) { if (profile === undefined) {
profile = await UserProfiles.findOneBy({userId: user.id}); profile = await UserProfiles.findOneBy({ userId: user.id });
}
if (oldProfile !== undefined && profile?.fields === oldProfile?.fields && profile?.description === oldProfile?.description) {
HtmlUserCacheEntries.update({ userId: user.id }, { updatedAt: user.lastFetchedAt ?? user.createdAt });
return;
}
}
if (!config.htmlCache?.prewarm) return;
if (profile === undefined) {
profile = await UserProfiles.findOneBy({ userId: user.id });
} }
if (await this.userBioHtmlCache.get(identifier) === undefined) { if (await this.userBioHtmlCache.get(identifier) === undefined) {
@ -267,7 +278,7 @@ export class UserConverter {
this.userBioHtmlCache.set(identifier, await bio); this.userBioHtmlCache.set(identifier, await bio);
if (config.htmlCache?.dbFallback) if (config.htmlCache?.dbFallback)
HtmlUserCacheEntries.upsert({ userId: user.id, bio: await bio }, ["userId"]); HtmlUserCacheEntries.upsert({ userId: user.id, updatedAt: user.lastFetchedAt ?? user.createdAt, bio: await bio }, ["userId"]);
} }
if (await this.userFieldsHtmlCache.get(identifier) === undefined) { if (await this.userFieldsHtmlCache.get(identifier) === undefined) {
@ -275,12 +286,12 @@ export class UserConverter {
this.userFieldsHtmlCache.set(identifier, fields); this.userFieldsHtmlCache.set(identifier, fields);
if (config.htmlCache?.dbFallback) if (config.htmlCache?.dbFallback)
HtmlUserCacheEntries.upsert({ userId: user.id, updatedAt: user.updatedAt ?? user.createdAt, fields: fields }, ["userId"]); HtmlUserCacheEntries.upsert({ userId: user.id, updatedAt: user.lastFetchedAt ?? user.createdAt, fields: fields }, ["userId"]);
} }
} }
} }
public static async prewarmCacheById(userId: string): Promise<void> { public static async prewarmCacheById(userId: string, oldProfile?: UserProfile | null): Promise<void> {
await this.prewarmCache(await getUser(userId)); await this.prewarmCache(await getUser(userId), undefined, oldProfile);
} }
} }

View file

@ -31,7 +31,10 @@ export class NotificationHelpers {
if (accountId !== undefined) if (accountId !== undefined)
query.andWhere("notification.notifierId = :notifierId", { notifierId: accountId }); query.andWhere("notification.notifierId = :notifierId", { notifierId: accountId });
query.leftJoinAndSelect("notification.note", "note"); query
.leftJoinAndSelect("notification.note", "note")
.leftJoinAndSelect("notification.notifier", "notifier")
.leftJoinAndSelect("notification.notifiee", "notifiee");
return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined, ctx); return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined, ctx);
} }