From 2bdff66f0a12a16684e5d25bcace551446ec1c78 Mon Sep 17 00:00:00 2001 From: kim Date: Wed, 15 Oct 2025 13:32:02 +0200 Subject: [performance] cache account IDs in home timeline query not in exclusive lists (#4502) this caches the stage of the home timeline query in which we calculate which account IDs should be shown in a particular user's timeline. Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4502 Co-authored-by: kim Co-committed-by: kim --- internal/db/bundb/list.go | 19 +++++- internal/db/bundb/timeline.go | 130 ++++++++++++++++++++++-------------------- 2 files changed, 84 insertions(+), 65 deletions(-) (limited to 'internal/db/bundb') diff --git a/internal/db/bundb/list.go b/internal/db/bundb/list.go index e1afa64d4..3181dafcc 100644 --- a/internal/db/bundb/list.go +++ b/internal/db/bundb/list.go @@ -20,7 +20,6 @@ package bundb import ( "context" "errors" - "fmt" "slices" "time" @@ -358,13 +357,13 @@ func (l *listDB) PopulateListEntry(ctx context.Context, listEntry *gtsmodel.List var err error if listEntry.Follow == nil { - // ListEntry follow is not set, fetch from the database. + // ListEntry follow is not set, fetch from database. listEntry.Follow, err = l.state.DB.GetFollowByID( gtscontext.SetBarebones(ctx), listEntry.FollowID, ) if err != nil { - return fmt.Errorf("error populating listEntry follow: %w", err) + return gtserror.Newf("error populating follow: %w", err) } } @@ -454,6 +453,10 @@ func (l *listDB) DeleteAllListEntriesByFollows(ctx context.Context, followIDs .. func (l *listDB) invalidateEntryCaches(ctx context.Context, listIDs, followIDs []string) { var keys []string + // Anything requested in this func + // will only ever be barbones model. + ctx = gtscontext.SetBarebones(ctx) + // Generate ListedID keys to invalidate. keys = slices.Grow(keys[:0], 2*len(listIDs)) for _, listID := range listIDs { @@ -464,6 +467,16 @@ func (l *listDB) invalidateEntryCaches(ctx context.Context, listIDs, followIDs [ // Invalidate list timeline cache by ID. l.state.Caches.Timelines.List.Clear(listID) + + // Fetch from DB the list by given ID. + list, err := l.GetListByID(ctx, listID) + if err != nil { + log.Errorf(ctx, "error getting list: %v", err) + continue + } + + // Invalidate home account IDs slice cache for list owner. + l.state.Caches.DB.HomeAccountIDs.Invalidate(list.AccountID) } // Invalidate ListedID slice cache entries. diff --git a/internal/db/bundb/timeline.go b/internal/db/bundb/timeline.go index 3b217cc5c..0e330f258 100644 --- a/internal/db/bundb/timeline.go +++ b/internal/db/bundb/timeline.go @@ -47,75 +47,18 @@ func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, page // of any paging parameters that selects by followings. func(q *bun.SelectQuery) (*bun.SelectQuery, error) { - // As this is the home timeline, it should be - // populated by statuses from accounts followed - // by accountID, and posts from accountID itself. - // - // So, begin by seeing who accountID follows. - // It should be a little cheaper to do this in - // a separate query like this, rather than using - // a join, since followIDs are cached in memory. - follows, err := t.state.DB.GetAccountFollows( - gtscontext.SetBarebones(ctx), - accountID, - nil, // select all - ) - if err != nil && !errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.Newf("db error getting follows for account %s: %w", accountID, err) - } - - // To take account of exclusive lists, get all of - // this account's lists, so we can filter out follows - // that are in contained in exclusive lists. - lists, err := t.state.DB.GetListsByAccountID(ctx, accountID) - if err != nil && !errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.Newf("db error getting lists for account %s: %w", accountID, err) - } - - // Index all follow IDs that fall in exclusive lists. - ignoreFollowIDs := make(map[string]struct{}) - for _, list := range lists { - if !*list.Exclusive { - // Not exclusive, - // we don't care. - continue - } - - // Fetch all follow IDs of the entries ccontained in this list. - listFollowIDs, err := t.state.DB.GetFollowIDsInList(ctx, list.ID, nil) - if err != nil && !errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.Newf("db error getting list entry follow ids: %w", err) - } - - // Exclusive list, index all its follow IDs. - for _, followID := range listFollowIDs { - ignoreFollowIDs[followID] = struct{}{} - } - } - - // Extract just the accountID from each follow, - // ignoring follows that are in exclusive lists. - targetAccountIDs := make([]string, 0, len(follows)+1) - for _, f := range follows { - _, ignore := ignoreFollowIDs[f.ID] - if !ignore { - targetAccountIDs = append( - targetAccountIDs, - f.TargetAccountID, - ) - } + // Get account IDs that should be in this home timeline. + accountIDs, err := t.getHomeAccountIDs(ctx, accountID) + if err != nil { + return nil, gtserror.Newf("error getting home account ids: %w", err) } - // Add accountID itself as a pseudo follow so that - // accountID can see its own posts in the timeline. - targetAccountIDs = append(targetAccountIDs, accountID) - // Select only statuses authored by // accounts with IDs in the slice. q = q.Where( "? IN (?)", bun.Ident("account_id"), - bun.In(targetAccountIDs), + bun.In(accountIDs), ) // Only include statuses that aren't pending approval. @@ -309,6 +252,69 @@ func (t *timelineDB) GetTagTimeline(ctx context.Context, tagID string, page *pag ) } +func (t *timelineDB) getHomeAccountIDs(ctx context.Context, accountID string) ([]string, error) { + return t.state.Caches.DB.HomeAccountIDs.Load(accountID, func() ([]string, error) { + // As this is the home timeline, it should be + // populated by statuses from accounts followed + // by accountID, and posts from accountID itself. + // So, begin by seeing who accountID follows. + follows, err := t.state.DB.GetAccountFollows( + gtscontext.SetBarebones(ctx), + accountID, + nil, // select all + ) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, gtserror.Newf("db error getting follows for account %s: %w", accountID, err) + } + + // To take account of exclusive lists, get all of this account's + // lists, so we can filter out follows that are in exclusive lists. + lists, err := t.state.DB.GetListsByAccountID(ctx, accountID) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, gtserror.Newf("db error getting lists for account %s: %w", accountID, err) + } + + // Index all follow IDs that fall in exclusive lists. + ignoreFollowIDs := make(map[string]struct{}) + for _, list := range lists { + if !*list.Exclusive { + // Not exclusive, + // we don't care. + continue + } + + // Fetch all follow IDs of the entries ccontained in this list. + listFollowIDs, err := t.state.DB.GetFollowIDsInList(ctx, list.ID, nil) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, gtserror.Newf("db error getting list entry follow ids: %w", err) + } + + // Exclusive list, index all its follow IDs. + for _, followID := range listFollowIDs { + ignoreFollowIDs[followID] = struct{}{} + } + } + + // Extract just the accountID from each follow, + // ignoring follows that are in exclusive lists. + targetAccountIDs := make([]string, 0, len(follows)+1) + for _, f := range follows { + _, ignore := ignoreFollowIDs[f.ID] + if !ignore { + targetAccountIDs = append( + targetAccountIDs, + f.TargetAccountID, + ) + } + } + + // Add accountID itself as a pseudo follow so that + // accountID can see its own posts in the timeline. + targetAccountIDs = append(targetAccountIDs, accountID) + return targetAccountIDs, nil + }) +} + func loadStatusTimelinePage( ctx context.Context, db *bun.DB, -- cgit v1.2.3