Skip to content

Commit

Permalink
Optimize timeline for limit=1 (#1971)
Browse files Browse the repository at this point in the history
* optimize timeline skeleton w/ limit=1

* build
  • Loading branch information
devinivy authored Dec 15, 2023
1 parent 7d818b8 commit 80161e3
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build-and-push-bsky-aws.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ on:
push:
branches:
- main
- timeline-limit-1-opt
env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
Expand Down
54 changes: 54 additions & 0 deletions packages/bsky/src/api/app/bsky/feed/getTimeline.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { sql } from 'kysely'
import { InvalidRequestError } from '@atproto/xrpc-server'
import { Server } from '../../../../lexicon'
import { FeedAlgorithm, FeedKeyset, getFeedDateThreshold } from '../util/feed'
Expand Down Expand Up @@ -55,6 +56,11 @@ export const skeleton = async (
throw new InvalidRequestError(`Unsupported algorithm: ${algorithm}`)
}

if (limit === 1 && !cursor) {
// special case for limit=1, which is often used to check if there are new items at the top of the timeline.
return skeletonLimit1(params, ctx)
}

const keyset = new FeedKeyset(ref('feed_item.sortAt'), ref('feed_item.cid'))
const sortFrom = keyset.unpack(cursor)?.primary

Expand Down Expand Up @@ -117,6 +123,54 @@ export const skeleton = async (
}
}

// The limit=1 case is used commonly to check if there are new items at the top of the timeline.
// Since it's so common, it's optimized here. The most common strategy that postgres takes to
// build a timeline is to grab all recent content from each of the user's follow, then paginate it.
// The downside here is that it requires grabbing all recent content from all follows, even if you
// only want a single result. The approach here instead takes the single most recent post from
// each of the user's follows, then sorts only those and takes the top item.
const skeletonLimit1 = async (params: Params, ctx: Context) => {
const { viewer } = params
const { db } = ctx
const { ref } = db.db.dynamic
const creatorsQb = db.db
.selectFrom('follow')
.where('creator', '=', viewer)
.select('subjectDid as did')
.unionAll(sql`select ${viewer} as did`)
const feedItemsQb = db.db
.selectFrom(creatorsQb.as('creator'))
.innerJoinLateral(
(eb) => {
const keyset = new FeedKeyset(
ref('feed_item.sortAt'),
ref('feed_item.cid'),
)
const creatorFeedItemQb = eb
.selectFrom('feed_item')
.innerJoin('post', 'post.uri', 'feed_item.postUri')
.whereRef('feed_item.originatorDid', '=', 'creator.did')
.where('feed_item.sortAt', '>', getFeedDateThreshold(undefined, 2))
.selectAll('feed_item')
.select([
'post.replyRoot',
'post.replyParent',
'post.creator as postAuthorDid',
])
return paginate(creatorFeedItemQb, { limit: 1, keyset }).as('result')
},
(join) => join.onTrue(),
)
.selectAll('result')
const keyset = new FeedKeyset(ref('result.sortAt'), ref('result.cid'))
const feedItems = await paginate(feedItemsQb, { limit: 1, keyset }).execute()
return {
params,
feedItems,
cursor: keyset.packFromResult(feedItems),
}
}

const hydration = async (
state: SkeletonState,
ctx: Context,
Expand Down
14 changes: 14 additions & 0 deletions packages/bsky/tests/views/timeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,20 @@ describe('timeline views', () => {
expect(results(paginatedAll)).toEqual(results([full.data]))
})

it('agrees what the first item is for limit=1 and other limits', async () => {
const { data: timeline } = await agent.api.app.bsky.feed.getTimeline(
{ limit: 10 },
{ headers: await network.serviceHeaders(alice) },
)
const { data: timelineLimit1 } = await agent.api.app.bsky.feed.getTimeline(
{ limit: 1 },
{ headers: await network.serviceHeaders(alice) },
)
expect(timeline.feed.length).toBeGreaterThan(1)
expect(timelineLimit1.feed.length).toEqual(1)
expect(timelineLimit1.feed[0].post.uri).toBe(timeline.feed[0].post.uri)
})

it('reflects self-labels', async () => {
const carolTL = await agent.api.app.bsky.feed.getTimeline(
{},
Expand Down

0 comments on commit 80161e3

Please sign in to comment.