mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-11-28 21:08:52 -07:00
[backend] Implement heuristics for home timeline queries
After lots of performance analysis, we've ended up with a cutoff value of 250 posts in the last 7d, after which we should switch which query plan to nudge postgres towards. This should greatly improve performance of users who were previously performance edge cases.
This commit is contained in:
parent
a5b30a6adc
commit
8ecf361870
3 changed files with 53 additions and 30 deletions
|
@ -0,0 +1,48 @@
|
||||||
|
import { Brackets, SelectQueryBuilder } from "typeorm";
|
||||||
|
import { User } from "@/models/entities/user.js";
|
||||||
|
import { Followings, Notes } from "@/models/index.js";
|
||||||
|
import { Cache } from "@/misc/cache.js";
|
||||||
|
import { apiLogger } from "@/server/api/logger.js";
|
||||||
|
|
||||||
|
const cache = new Cache<number>("homeTlQueryData", 60 * 60 * 24);
|
||||||
|
const cutoff = 250; // 250 posts in the last 7 days, constant determined by comparing benchmarks for cutoff values between 100 and 2500
|
||||||
|
const logger = apiLogger.createSubLogger("heuristics");
|
||||||
|
|
||||||
|
export async function generateFollowingQuery(
|
||||||
|
q: SelectQueryBuilder<any>,
|
||||||
|
me: { id: User["id"] },
|
||||||
|
): Promise<void> {
|
||||||
|
const followingQuery = Followings.createQueryBuilder("following")
|
||||||
|
.select("following.followeeId")
|
||||||
|
.where("following.followerId = :meId");
|
||||||
|
|
||||||
|
const heuristic = await cache.fetch(me.id, async () => {
|
||||||
|
let curr = new Date();
|
||||||
|
let prev = new Date();
|
||||||
|
prev.setDate(prev.getDate() - 7);
|
||||||
|
return Notes.createQueryBuilder('note')
|
||||||
|
.where(`note.createdAt > :prev`, { prev })
|
||||||
|
.andWhere(`note.createdAt < :curr`, { curr })
|
||||||
|
.andWhere(`note.userId = ANY(array(${followingQuery.getQuery()} UNION ALL VALUES (:meId)))`, { meId: me.id })
|
||||||
|
.getCount()
|
||||||
|
.then(res => {
|
||||||
|
logger.info(`Calculating heuristics for user ${me.id} took ${new Date().getTime() - curr.getTime()}ms`);
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const shouldUseUnion = heuristic < cutoff ;
|
||||||
|
|
||||||
|
q.andWhere(
|
||||||
|
new Brackets((qb) => {
|
||||||
|
if (shouldUseUnion) {
|
||||||
|
qb.where(`note.userId = ANY(array(${followingQuery.getQuery()} UNION ALL VALUES (:meId)))`);
|
||||||
|
} else {
|
||||||
|
qb.where(`note.userId = :meId`);
|
||||||
|
qb.orWhere(`note.userId IN (${followingQuery.getQuery()})`);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
q.setParameters({ meId: me.id });
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
|
||||||
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
|
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
|
||||||
import { ApiError } from "../../error.js";
|
import { ApiError } from "../../error.js";
|
||||||
import { generateListQuery } from "@/server/api/common/generate-list-query.js";
|
import { generateListQuery } from "@/server/api/common/generate-list-query.js";
|
||||||
|
import { generateFollowingQuery } from "@/server/api/common/generate-following-query.js";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ["notes"],
|
tags: ["notes"],
|
||||||
|
@ -65,32 +66,13 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
const hasFollowing =
|
|
||||||
(await Followings.count({
|
|
||||||
where: {
|
|
||||||
followerId: user.id,
|
|
||||||
},
|
|
||||||
take: 1,
|
|
||||||
})) !== 0;
|
|
||||||
|
|
||||||
//#region Construct query
|
//#region Construct query
|
||||||
const followingQuery = Followings.createQueryBuilder("following")
|
|
||||||
.select("following.followeeId")
|
|
||||||
.where("following.followerId = :followerId", { followerId: user.id });
|
|
||||||
|
|
||||||
const query = makePaginationQuery(
|
const query = makePaginationQuery(
|
||||||
Notes.createQueryBuilder("note"),
|
Notes.createQueryBuilder("note"),
|
||||||
ps.sinceId,
|
ps.sinceId,
|
||||||
ps.untilId,
|
ps.untilId,
|
||||||
ps.sinceDate,
|
ps.sinceDate,
|
||||||
ps.untilDate,
|
ps.untilDate,
|
||||||
)
|
|
||||||
.andWhere(
|
|
||||||
new Brackets((qb) => {
|
|
||||||
qb.where("note.userId = :meId", { meId: user.id });
|
|
||||||
if (hasFollowing)
|
|
||||||
qb.orWhere(`note.userId IN (${followingQuery.getQuery()})`);
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
.innerJoinAndSelect("note.user", "user")
|
.innerJoinAndSelect("note.user", "user")
|
||||||
.leftJoinAndSelect("user.avatar", "avatar")
|
.leftJoinAndSelect("user.avatar", "avatar")
|
||||||
|
@ -102,9 +84,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
|
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
|
||||||
.leftJoinAndSelect("renote.user", "renoteUser")
|
.leftJoinAndSelect("renote.user", "renoteUser")
|
||||||
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
|
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
|
||||||
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner")
|
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
|
||||||
.setParameters(followingQuery.getParameters());
|
|
||||||
|
|
||||||
|
await generateFollowingQuery(query, user);
|
||||||
generateListQuery(query, user);
|
generateListQuery(query, user);
|
||||||
generateChannelQuery(query, user);
|
generateChannelQuery(query, user);
|
||||||
generateRepliesQuery(query, ps.withReplies, user);
|
generateRepliesQuery(query, ps.withReplies, user);
|
||||||
|
|
|
@ -21,29 +21,22 @@ import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js"
|
||||||
import { generatePaginationData } from "@/server/api/mastodon/middleware/pagination.js";
|
import { generatePaginationData } from "@/server/api/mastodon/middleware/pagination.js";
|
||||||
import { MastoContext } from "@/server/api/mastodon/index.js";
|
import { MastoContext } from "@/server/api/mastodon/index.js";
|
||||||
import { generateListQuery } from "@/server/api/common/generate-list-query.js";
|
import { generateListQuery } from "@/server/api/common/generate-list-query.js";
|
||||||
|
import { generateFollowingQuery } from "@/server/api/common/generate-following-query.js";
|
||||||
|
|
||||||
export class TimelineHelpers {
|
export class TimelineHelpers {
|
||||||
public static async getHomeTimeline(maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, ctx: MastoContext): Promise<Note[]> {
|
public static async getHomeTimeline(maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, ctx: MastoContext): Promise<Note[]> {
|
||||||
if (limit > 40) limit = 40;
|
if (limit > 40) limit = 40;
|
||||||
const user = ctx.user as ILocalUser;
|
const user = ctx.user as ILocalUser;
|
||||||
|
|
||||||
const followingQuery = Followings.createQueryBuilder("following")
|
|
||||||
.select("following.followeeId")
|
|
||||||
.where("following.followerId = :followerId", { followerId: user.id });
|
|
||||||
|
|
||||||
const query = PaginationHelpers.makePaginationQuery(
|
const query = PaginationHelpers.makePaginationQuery(
|
||||||
Notes.createQueryBuilder("note"),
|
Notes.createQueryBuilder("note"),
|
||||||
sinceId,
|
sinceId,
|
||||||
maxId,
|
maxId,
|
||||||
minId
|
minId
|
||||||
)
|
|
||||||
.andWhere(
|
|
||||||
new Brackets((qb) => {
|
|
||||||
qb.where(`note.userId IN (${followingQuery.getQuery()} UNION ALL VALUES (:meId))`, { meId: user.id });
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
.leftJoinAndSelect("note.renote", "renote");
|
.leftJoinAndSelect("note.renote", "renote");
|
||||||
|
|
||||||
|
await generateFollowingQuery(query, user);
|
||||||
generateListQuery(query, user);
|
generateListQuery(query, user);
|
||||||
generateChannelQuery(query, user);
|
generateChannelQuery(query, user);
|
||||||
generateRepliesQuery(query, true, user);
|
generateRepliesQuery(query, true, user);
|
||||||
|
|
Loading…
Reference in a new issue