Performance fixes with new getPosts (#525)

* Update notifications to fetch in a batch using getPosts

* Improve search perf with getPosts

* Bump @atproto/api@0.2.9

* Just use post uri for key
zio/stable
Paul Frazee 2023-04-24 17:02:58 -05:00 committed by GitHub
parent da8af38dcc
commit 1b356556c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 97 additions and 81 deletions

View File

@ -22,7 +22,7 @@
"e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "0.2.8", "@atproto/api": "0.2.9",
"@bam.tech/react-native-image-resizer": "^3.0.4", "@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@expo/webpack-config": "^18.0.1", "@expo/webpack-config": "^18.0.1",

View File

@ -11,13 +11,6 @@ import * as apilib from 'lib/api/index'
import {cleanError} from 'lib/strings/errors' import {cleanError} from 'lib/strings/errors'
import {updateDataOptimistically} from 'lib/async/revertible' import {updateDataOptimistically} from 'lib/async/revertible'
function* reactKeyGenerator(): Generator<string> {
let counter = 0
while (true) {
yield `item-${counter++}`
}
}
export class PostThreadItemModel { export class PostThreadItemModel {
// ui state // ui state
_reactKey: string = '' _reactKey: string = ''
@ -55,10 +48,9 @@ export class PostThreadItemModel {
constructor( constructor(
public rootStore: RootStoreModel, public rootStore: RootStoreModel,
reactKey: string,
v: AppBskyFeedDefs.ThreadViewPost, v: AppBskyFeedDefs.ThreadViewPost,
) { ) {
this._reactKey = reactKey this._reactKey = `thread-${v.post.uri}`
this.post = v.post this.post = v.post
if (FeedPost.isRecord(this.post.record)) { if (FeedPost.isRecord(this.post.record)) {
const valid = FeedPost.validateRecord(this.post.record) const valid = FeedPost.validateRecord(this.post.record)
@ -82,7 +74,6 @@ export class PostThreadItemModel {
} }
assignTreeModels( assignTreeModels(
keyGen: Generator<string>,
v: AppBskyFeedDefs.ThreadViewPost, v: AppBskyFeedDefs.ThreadViewPost,
higlightedPostUri: string, higlightedPostUri: string,
includeParent = true, includeParent = true,
@ -91,22 +82,12 @@ export class PostThreadItemModel {
// parents // parents
if (includeParent && v.parent) { if (includeParent && v.parent) {
if (AppBskyFeedDefs.isThreadViewPost(v.parent)) { if (AppBskyFeedDefs.isThreadViewPost(v.parent)) {
const parentModel = new PostThreadItemModel( const parentModel = new PostThreadItemModel(this.rootStore, v.parent)
this.rootStore,
keyGen.next().value,
v.parent,
)
parentModel._depth = this._depth - 1 parentModel._depth = this._depth - 1
parentModel._showChildReplyLine = true parentModel._showChildReplyLine = true
if (v.parent.parent) { if (v.parent.parent) {
parentModel._showParentReplyLine = true //parentModel.uri !== higlightedPostUri parentModel._showParentReplyLine = true //parentModel.uri !== higlightedPostUri
parentModel.assignTreeModels( parentModel.assignTreeModels(v.parent, higlightedPostUri, true, false)
keyGen,
v.parent,
higlightedPostUri,
true,
false,
)
} }
this.parent = parentModel this.parent = parentModel
} else if (AppBskyFeedDefs.isNotFoundPost(v.parent)) { } else if (AppBskyFeedDefs.isNotFoundPost(v.parent)) {
@ -118,23 +99,13 @@ export class PostThreadItemModel {
const replies = [] const replies = []
for (const item of v.replies) { for (const item of v.replies) {
if (AppBskyFeedDefs.isThreadViewPost(item)) { if (AppBskyFeedDefs.isThreadViewPost(item)) {
const itemModel = new PostThreadItemModel( const itemModel = new PostThreadItemModel(this.rootStore, item)
this.rootStore,
keyGen.next().value,
item,
)
itemModel._depth = this._depth + 1 itemModel._depth = this._depth + 1
itemModel._showParentReplyLine = itemModel._showParentReplyLine =
itemModel.parentUri !== higlightedPostUri itemModel.parentUri !== higlightedPostUri
if (item.replies?.length) { if (item.replies?.length) {
itemModel._showChildReplyLine = true itemModel._showChildReplyLine = true
itemModel.assignTreeModels( itemModel.assignTreeModels(item, higlightedPostUri, false, true)
keyGen,
item,
higlightedPostUri,
false,
true,
)
} }
replies.push(itemModel) replies.push(itemModel)
} else if (AppBskyFeedDefs.isNotFoundPost(item)) { } else if (AppBskyFeedDefs.isNotFoundPost(item)) {
@ -241,6 +212,19 @@ export class PostThreadModel {
this.params = params this.params = params
} }
static fromPostView(
rootStore: RootStoreModel,
postView: AppBskyFeedDefs.PostView,
) {
const model = new PostThreadModel(rootStore, {uri: postView.uri})
model.resolvedUri = postView.uri
model.hasLoaded = true
model.thread = new PostThreadItemModel(rootStore, {
post: postView,
})
return model
}
get hasContent() { get hasContent() {
return typeof this.thread !== 'undefined' return typeof this.thread !== 'undefined'
} }
@ -360,6 +344,9 @@ export class PostThreadModel {
} }
async _load(isRefreshing = false) { async _load(isRefreshing = false) {
if (this.hasLoaded && !isRefreshing) {
return
}
this._xLoading(isRefreshing) this._xLoading(isRefreshing)
try { try {
const res = await this.rootStore.agent.getPostThread( const res = await this.rootStore.agent.getPostThread(
@ -374,15 +361,12 @@ export class PostThreadModel {
_replaceAll(res: GetPostThread.Response) { _replaceAll(res: GetPostThread.Response) {
sortThread(res.data.thread) sortThread(res.data.thread)
const keyGen = reactKeyGenerator()
const thread = new PostThreadItemModel( const thread = new PostThreadItemModel(
this.rootStore, this.rootStore,
keyGen.next().value,
res.data.thread as AppBskyFeedDefs.ThreadViewPost, res.data.thread as AppBskyFeedDefs.ThreadViewPost,
) )
thread._isHighlightedPost = true thread._isHighlightedPost = true
thread.assignTreeModels( thread.assignTreeModels(
keyGen,
res.data.thread as AppBskyFeedDefs.ThreadViewPost, res.data.thread as AppBskyFeedDefs.ThreadViewPost,
thread.uri, thread.uri,
) )

View File

@ -2,6 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx'
import { import {
AppBskyNotificationListNotifications as ListNotifications, AppBskyNotificationListNotifications as ListNotifications,
AppBskyActorDefs, AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
AppBskyFeedRepost, AppBskyFeedRepost,
AppBskyFeedLike, AppBskyFeedLike,
@ -146,6 +147,14 @@ export class NotificationsFeedItemModel {
return false return false
} }
get additionaDataUri(): string | undefined {
if (this.isReply || this.isQuote || this.isMention) {
return this.uri
} else if (this.isLike || this.isRepost) {
return this.subjectUri
}
}
get subjectUri(): string { get subjectUri(): string {
if (this.reasonSubject) { if (this.reasonSubject) {
return this.reasonSubject return this.reasonSubject
@ -193,28 +202,11 @@ export class NotificationsFeedItemModel {
) )
} }
async fetchAdditionalData() { setAdditionalData(additionalPost: AppBskyFeedDefs.PostView) {
if (!this.needsAdditionalData) { this.additionalPost = PostThreadModel.fromPostView(
return this.rootStore,
} additionalPost,
let postUri
if (this.isReply || this.isQuote || this.isMention) {
postUri = this.uri
} else if (this.isLike || this.isRepost) {
postUri = this.subjectUri
}
if (postUri) {
this.additionalPost = new PostThreadModel(this.rootStore, {
uri: postUri,
depth: 0,
})
await this.additionalPost.setup().catch(e => {
this.rootStore.log.error(
'Failed to load post needed by notification',
e,
) )
})
}
} }
} }
@ -464,7 +456,13 @@ export class NotificationsFeedModel {
'mostRecent', 'mostRecent',
res.data.notifications[0], res.data.notifications[0],
) )
await notif.fetchAdditionalData() const addedUri = notif.additionaDataUri
if (addedUri) {
const postsRes = await this.rootStore.agent.app.bsky.feed.getPosts({
uris: [addedUri],
})
notif.setAdditionalData(postsRes.data.posts[0])
}
const filtered = this._filterNotifications([notif]) const filtered = this._filterNotifications([notif])
return filtered[0] return filtered[0]
} }
@ -536,25 +534,39 @@ export class NotificationsFeedModel {
async _fetchItemModels( async _fetchItemModels(
items: ListNotifications.Notification[], items: ListNotifications.Notification[],
): Promise<NotificationsFeedItemModel[]> { ): Promise<NotificationsFeedItemModel[]> {
const promises = [] // construct item models and track who needs more data
const itemModels: NotificationsFeedItemModel[] = [] const itemModels: NotificationsFeedItemModel[] = []
const addedPostMap = new Map<string, NotificationsFeedItemModel[]>()
for (const item of items) { for (const item of items) {
const itemModel = new NotificationsFeedItemModel( const itemModel = new NotificationsFeedItemModel(
this.rootStore, this.rootStore,
`item-${_idCounter++}`, `item-${_idCounter++}`,
item, item,
) )
if (itemModel.needsAdditionalData) { const uri = itemModel.additionaDataUri
promises.push(itemModel.fetchAdditionalData()) if (uri) {
const models = addedPostMap.get(uri) || []
models.push(itemModel)
addedPostMap.set(uri, models)
} }
itemModels.push(itemModel) itemModels.push(itemModel)
} }
await Promise.all(promises).catch(e => {
this.rootStore.log.error( // fetch additional data
'Uncaught failure during notifications _processNotifications()', if (addedPostMap.size > 0) {
e, const postsRes = await this.rootStore.agent.app.bsky.feed.getPosts({
) uris: Array.from(addedPostMap.keys()),
}) })
for (const post of postsRes.data.posts) {
const models = addedPostMap.get(post.uri)
if (models?.length) {
for (const model of models) {
model.setAdditionalData(post)
}
}
}
}
return itemModels return itemModels
} }

View File

@ -1,13 +1,14 @@
import {makeAutoObservable, runInAction} from 'mobx' import {makeAutoObservable, runInAction} from 'mobx'
import {searchProfiles, searchPosts} from 'lib/api/search' import {searchProfiles, searchPosts} from 'lib/api/search'
import {AppBskyActorDefs} from '@atproto/api' import {PostThreadModel} from '../content/post-thread'
import {AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
import {RootStoreModel} from '../root-store' import {RootStoreModel} from '../root-store'
export class SearchUIModel { export class SearchUIModel {
isPostsLoading = false isPostsLoading = false
isProfilesLoading = false isProfilesLoading = false
query: string = '' query: string = ''
postUris: string[] = [] posts: PostThreadModel[] = []
profiles: AppBskyActorDefs.ProfileView[] = [] profiles: AppBskyActorDefs.ProfileView[] = []
constructor(public rootStore: RootStoreModel) { constructor(public rootStore: RootStoreModel) {
@ -15,7 +16,7 @@ export class SearchUIModel {
} }
async fetch(q: string) { async fetch(q: string) {
this.postUris = [] this.posts = []
this.profiles = [] this.profiles = []
this.query = q this.query = q
if (!q.trim()) { if (!q.trim()) {
@ -29,8 +30,22 @@ export class SearchUIModel {
searchPosts(q).catch(_e => []), searchPosts(q).catch(_e => []),
searchProfiles(q).catch(_e => []), searchProfiles(q).catch(_e => []),
]) ])
let posts: AppBskyFeedDefs.PostView[] = []
if (postsSearch?.length) {
do {
const res = await this.rootStore.agent.app.bsky.feed.getPosts({
uris: postsSearch
.splice(0, 25)
.map(p => `at://${p.user.did}/${p.tid}`),
})
posts = posts.concat(res.data.posts)
} while (postsSearch.length)
}
runInAction(() => { runInAction(() => {
this.postUris = postsSearch?.map(p => `at://${p.user.did}/${p.tid}`) || [] this.posts = posts.map(post =>
PostThreadModel.fromPostView(this.rootStore, post),
)
this.isPostsLoading = false this.isPostsLoading = false
}) })

View File

@ -49,7 +49,7 @@ const PostResults = observer(({model}: {model: SearchUIModel}) => {
) )
} }
if (model.postUris.length === 0) { if (model.posts.length === 0) {
return ( return (
<CenteredView> <CenteredView>
<Text type="xl" style={[styles.empty, pal.text]}> <Text type="xl" style={[styles.empty, pal.text]}>
@ -61,8 +61,13 @@ const PostResults = observer(({model}: {model: SearchUIModel}) => {
return ( return (
<ScrollView style={pal.view}> <ScrollView style={pal.view}>
{model.postUris.map(uri => ( {model.posts.map(post => (
<Post key={uri} uri={uri} hideError /> <Post
key={post.resolvedUri}
uri={post.resolvedUri}
initView={post}
hideError
/>
))} ))}
<View style={s.footerSpacer} /> <View style={s.footerSpacer} />
<View style={s.footerSpacer} /> <View style={s.footerSpacer} />

View File

@ -30,10 +30,10 @@
tlds "^1.234.0" tlds "^1.234.0"
typed-emitter "^2.1.0" typed-emitter "^2.1.0"
"@atproto/api@0.2.8": "@atproto/api@0.2.9":
version "0.2.8" version "0.2.9"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.2.8.tgz#92ed413804ecb43aaa45ec18afc93d6f2b28a689" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.2.9.tgz#08e29da66d1a9001d9d3ce427548c1760d805e99"
integrity sha512-LfPgtf3UNg2W/AxHkJMJrLNT9QAD6bi16Sw5Zt3mgANrDnHWGygA7gRpeNdgVI+kFEhQfrIItemJvWLIB9BJDQ== integrity sha512-r00IqidX2YF3VUEa4MUO2Vxqp3+QhI1cSNcWgzT4LsANapzrwdDTM+rY2Ejp9na3F+unO4SWRW3o434cVmG5gw==
dependencies: dependencies:
"@atproto/common-web" "*" "@atproto/common-web" "*"
"@atproto/uri" "*" "@atproto/uri" "*"