Move profile update handling from notes to the background
During profiling, I found that some large hangs were being caused by a large number of `notify` calls (and their handling functions) keeping the main thread overly busy. We cannot move the `notify` mechanism to a background thread (It has to be done on the main actor or else runtime warnings/errors appear), so instead this commit removes a very large source of notify calls/handling around NoteContentView, and replaces it with a background task that streams for profile updates and only updates its view when a relevant profile is updated. Changelog-Changed: Improved performance around note content views to prevent hangs Closes: https://github.com/damus-io/damus/issues/3439 Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
@@ -101,10 +101,17 @@ extension NostrNetworkManager {
|
||||
relevantStream.continuation.yield(profile)
|
||||
}
|
||||
}
|
||||
|
||||
// Notify the rest of the app so views that rely on rendered text (like mention strings)
|
||||
// can reload and pick up the freshly fetched profile metadata.
|
||||
notify(.profile_updated(.remote(pubkey: metadataEvent.pubkey)))
|
||||
}
|
||||
|
||||
/// Manually trigger profile updates for a given pubkey
|
||||
/// This is useful for local profile changes (e.g., nip05 validation, donation percentage updates)
|
||||
func notifyProfileUpdate(pubkey: Pubkey) {
|
||||
if let relevantStreams = streams[pubkey] {
|
||||
guard let profile = ndb.lookup_profile_and_copy(pubkey) else { return }
|
||||
for relevantStream in relevantStreams.values {
|
||||
relevantStream.continuation.yield(profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +128,29 @@ extension NostrNetworkManager {
|
||||
}
|
||||
}
|
||||
|
||||
func streamProfiles(pubkeys: Set<Pubkey>) -> AsyncStream<ProfileStreamItem> {
|
||||
guard !pubkeys.isEmpty else {
|
||||
return AsyncStream<ProfileStreamItem> { continuation in
|
||||
continuation.finish()
|
||||
}
|
||||
}
|
||||
|
||||
return AsyncStream<ProfileStreamItem> { continuation in
|
||||
let stream = ProfileStreamInfo(continuation: continuation)
|
||||
for pubkey in pubkeys {
|
||||
self.add(pubkey: pubkey, stream: stream)
|
||||
}
|
||||
|
||||
continuation.onTermination = { @Sendable _ in
|
||||
Task {
|
||||
for pubkey in pubkeys {
|
||||
await self.removeStream(pubkey: pubkey, id: stream.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Stream management
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ struct NoteContentView: View {
|
||||
let event: NostrEvent
|
||||
@State var blur_images: Bool
|
||||
@State var load_media: Bool = false
|
||||
@State private var requestedMentionProfiles: Set<Pubkey> = []
|
||||
let size: EventViewKind
|
||||
let preview_height: CGFloat?
|
||||
let options: EventViewOptions
|
||||
@@ -279,10 +278,12 @@ struct NoteContentView: View {
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
func ensureMentionProfilesAreFetchingIfNeeded() {
|
||||
@concurrent
|
||||
func streamProfiles() async throws {
|
||||
var mentionPubkeys: Set<Pubkey> = []
|
||||
try? NdbBlockGroup.borrowBlockGroup(event: event, using: damus_state.ndb, and: damus_state.keypair, borrow: { blockGroup in
|
||||
let _: ()? = try? blockGroup.forEachBlock({ _, block in
|
||||
let event = await self.event.clone()
|
||||
try await NdbBlockGroup.borrowBlockGroup(event: event, using: damus_state.ndb, and: damus_state.keypair, borrow: { blockGroup in
|
||||
blockGroup.forEachBlock({ _, block in
|
||||
guard let pubkey = block.mentionPubkey(tags: event.tags) else {
|
||||
return .loopContinue
|
||||
}
|
||||
@@ -290,33 +291,13 @@ struct NoteContentView: View {
|
||||
return .loopContinue
|
||||
})
|
||||
})
|
||||
|
||||
guard !mentionPubkeys.isEmpty else { return }
|
||||
|
||||
var toFetch: [Pubkey] = []
|
||||
for pubkey in mentionPubkeys {
|
||||
if requestedMentionProfiles.contains(pubkey) {
|
||||
continue
|
||||
}
|
||||
requestedMentionProfiles.insert(pubkey)
|
||||
|
||||
if damus_state.profiles.has_fresh_profile(id: pubkey) {
|
||||
continue
|
||||
}
|
||||
|
||||
toFetch.append(pubkey)
|
||||
|
||||
if mentionPubkeys.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
guard !toFetch.isEmpty else { return }
|
||||
|
||||
// Kick off metadata fetches for any missing mention profiles so their names can render once loaded.
|
||||
for pubkey in toFetch {
|
||||
Task {
|
||||
for await _ in await damus_state.nostrNetwork.profilesManager.streamProfile(pubkey: pubkey) {
|
||||
// NO-OP, we will receive the update via `notify`
|
||||
break
|
||||
}
|
||||
}
|
||||
for await profile in await damus_state.nostrNetwork.profilesManager.streamProfiles(pubkeys: mentionPubkeys) {
|
||||
await load(force_artifacts: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,8 +305,6 @@ struct NoteContentView: View {
|
||||
if case .loading = damus_state.events.get_cache_data(event.id).artifacts_model.state {
|
||||
return
|
||||
}
|
||||
|
||||
ensureMentionProfilesAreFetchingIfNeeded()
|
||||
|
||||
// always reload artifacts on load
|
||||
let plan = get_preload_plan(evcache: damus_state.events, ev: event, our_keypair: damus_state.keypair, settings: damus_state.settings)
|
||||
@@ -442,44 +421,13 @@ struct NoteContentView: View {
|
||||
|
||||
var body: some View {
|
||||
ArtifactContent
|
||||
.onReceive(handle_notify(.profile_updated)) { profile in
|
||||
try? NdbBlockGroup.borrowBlockGroup(event: event, using: damus_state.ndb, and: damus_state.keypair, borrow: { blockGroup in
|
||||
let _: Int? = blockGroup.forEachBlock { index, block in
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
guard let typ = m.bech32_type else {
|
||||
return .loopContinue
|
||||
}
|
||||
switch typ {
|
||||
case .nprofile:
|
||||
if m.bech32.nprofile.matches_pubkey(pk: profile.pubkey) {
|
||||
load(force_artifacts: true)
|
||||
}
|
||||
case .npub:
|
||||
if m.bech32.npub.matches_pubkey(pk: profile.pubkey) {
|
||||
load(force_artifacts: true)
|
||||
}
|
||||
case .nevent: return .loopContinue
|
||||
case .nrelay: return .loopContinue
|
||||
case .nsec: return .loopContinue
|
||||
case .note: return .loopContinue
|
||||
case .naddr: return .loopContinue
|
||||
}
|
||||
case .text: return .loopContinue
|
||||
case .hashtag: return .loopContinue
|
||||
case .url: return .loopContinue
|
||||
case .invoice: return .loopContinue
|
||||
case .mention_index(_): return .loopContinue
|
||||
}
|
||||
return .loopContinue
|
||||
}
|
||||
})
|
||||
.task {
|
||||
try? await streamProfiles()
|
||||
}
|
||||
.onAppear {
|
||||
load()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class NoteArtifactsParts {
|
||||
|
||||
@@ -46,7 +46,7 @@ struct NIP05DomainTimelineView: View {
|
||||
|
||||
if let pubkeys = model.filter.authors {
|
||||
for pubkey in pubkeys {
|
||||
check_nip05_validity(pubkey: pubkey, profiles: damus_state.profiles)
|
||||
check_nip05_validity(pubkey: pubkey, damus_state: damus_state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ struct EventProfileName: View {
|
||||
@State var nip05: NIP05?
|
||||
@State var donation: Int?
|
||||
@State var purple_account: DamusPurple.Account?
|
||||
@StateObject private var profileObserver: ProfileObserver
|
||||
|
||||
let size: EventViewKind
|
||||
|
||||
@@ -28,7 +27,6 @@ struct EventProfileName: View {
|
||||
let donation = damus.profiles.lookup(id: pubkey)?.damus_donation
|
||||
self._donation = State(wrappedValue: donation)
|
||||
self.purple_account = nil
|
||||
self._profileObserver = StateObject.init(wrappedValue: ProfileObserver(pubkey: pubkey, damusState: damus))
|
||||
}
|
||||
|
||||
var friend_type: FriendType? {
|
||||
@@ -102,26 +100,22 @@ struct EventProfileName: View {
|
||||
SupporterBadge(percent: self.supporter_percentage(), purple_account: self.purple_account, style: .compact)
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.profile_updated)) { update in
|
||||
if update.pubkey != pubkey {
|
||||
return
|
||||
}
|
||||
.task {
|
||||
for await profile in await damus_state.nostrNetwork.profilesManager.streamProfile(pubkey: pubkey) {
|
||||
let display_name = Profile.displayName(profile: profile, pubkey: pubkey)
|
||||
if display_name != self.display_name {
|
||||
self.display_name = display_name
|
||||
}
|
||||
|
||||
guard let profile = damus_state.profiles.lookup(id: update.pubkey) else { return }
|
||||
let nip05 = damus_state.profiles.is_validated(pubkey)
|
||||
|
||||
let display_name = Profile.displayName(profile: profile, pubkey: pubkey)
|
||||
if display_name != self.display_name {
|
||||
self.display_name = display_name
|
||||
}
|
||||
if self.nip05 != nip05 {
|
||||
self.nip05 = nip05
|
||||
}
|
||||
|
||||
let nip05 = damus_state.profiles.is_validated(pubkey)
|
||||
|
||||
if self.nip05 != nip05 {
|
||||
self.nip05 = nip05
|
||||
}
|
||||
|
||||
if self.donation != profile.damus_donation {
|
||||
donation = profile.damus_donation
|
||||
if self.donation != profile.damus_donation {
|
||||
donation = profile.damus_donation
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
|
||||
@@ -45,7 +45,6 @@ struct ProfileName: View {
|
||||
@State var donation: Int?
|
||||
@State var purple_account: DamusPurple.Account?
|
||||
@State var nip05_domain_favicon: FaviconURL?
|
||||
@StateObject var profileObserver: ProfileObserver
|
||||
|
||||
init(pubkey: Pubkey, prefix: String = "", damus: DamusState, show_nip5_domain: Bool = true, supporterBadgeStyle: SupporterBadge.Style = .compact) {
|
||||
self.pubkey = pubkey
|
||||
@@ -54,7 +53,6 @@ struct ProfileName: View {
|
||||
self.show_nip5_domain = show_nip5_domain
|
||||
self.supporterBadgeStyle = supporterBadgeStyle
|
||||
self.purple_account = nil
|
||||
self._profileObserver = StateObject.init(wrappedValue: ProfileObserver(pubkey: pubkey, damusState: damus))
|
||||
}
|
||||
|
||||
var friend_type: FriendType? {
|
||||
@@ -131,21 +129,10 @@ struct ProfileName: View {
|
||||
.largest()
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.profile_updated)) { update in
|
||||
if update.pubkey != pubkey {
|
||||
return
|
||||
.task {
|
||||
for await profile in await damus_state.nostrNetwork.profilesManager.streamProfile(pubkey: pubkey) {
|
||||
handle_profile_update(profile: profile)
|
||||
}
|
||||
|
||||
switch update {
|
||||
case .remote(let pubkey):
|
||||
guard let prof = damus_state.profiles.lookup(id: pubkey) else {
|
||||
return
|
||||
}
|
||||
handle_profile_update(profile: prof)
|
||||
case .manual(_, let prof):
|
||||
handle_profile_update(profile: prof)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,8 +75,7 @@ struct ProfilePicView: View {
|
||||
let privacy_sensitive: Bool
|
||||
|
||||
@State var picture: String?
|
||||
@StateObject private var profileObserver: ProfileObserver
|
||||
@EnvironmentObject var damusState: DamusState
|
||||
let damusState: DamusState
|
||||
|
||||
init(pubkey: Pubkey, size: CGFloat, highlight: Highlight, profiles: Profiles, disable_animation: Bool, picture: String? = nil, show_zappability: Bool? = nil, privacy_sensitive: Bool = false, damusState: DamusState) {
|
||||
self.pubkey = pubkey
|
||||
@@ -87,7 +86,7 @@ struct ProfilePicView: View {
|
||||
self.disable_animation = disable_animation
|
||||
self.zappability_indicator = show_zappability ?? false
|
||||
self.privacy_sensitive = privacy_sensitive
|
||||
self._profileObserver = StateObject.init(wrappedValue: ProfileObserver(pubkey: pubkey, damusState: damusState))
|
||||
self.damusState = damusState
|
||||
}
|
||||
|
||||
var privacy_sensitive_pubkey: Pubkey {
|
||||
@@ -110,23 +109,6 @@ struct ProfilePicView: View {
|
||||
var body: some View {
|
||||
ZStack (alignment: Alignment(horizontal: .trailing, vertical: .bottom)) {
|
||||
InnerProfilePicView(url: get_profile_url(picture: picture, pubkey: privacy_sensitive_pubkey, profiles: profiles), fallbackUrl: URL(string: robohash(privacy_sensitive_pubkey)), size: size, highlight: highlight, disable_animation: disable_animation)
|
||||
.onReceive(handle_notify(.profile_updated)) { updated in
|
||||
guard updated.pubkey == self.pubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
switch updated {
|
||||
case .manual(_, let profile):
|
||||
if let pic = profile.picture {
|
||||
self.picture = pic
|
||||
}
|
||||
case .remote(pubkey: let pk):
|
||||
let profile = profiles.lookup(id: pk)
|
||||
if let pic = profile?.picture {
|
||||
self.picture = pic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.zappability_indicator, let lnurl = self.get_lnurl(), lnurl != "" {
|
||||
Image("zap.fill")
|
||||
@@ -141,6 +123,13 @@ struct ProfilePicView: View {
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
.task {
|
||||
for await profile in await damusState.nostrNetwork.profilesManager.streamProfile(pubkey: pubkey) {
|
||||
if let pic = profile.picture {
|
||||
self.picture = pic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ struct ProfileView: View {
|
||||
return AnyView(
|
||||
VStack(spacing: 0) {
|
||||
ZStack {
|
||||
BannerImageView(pubkey: profile.pubkey, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||
BannerImageView(pubkey: profile.pubkey, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, damusState: damus_state)
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: proxy.size.width, height: minY > 0 ? bannerHeight + minY : bannerHeight)
|
||||
.clipped()
|
||||
@@ -525,7 +525,7 @@ struct ProfileView: View {
|
||||
dismiss()
|
||||
}
|
||||
.onAppear() {
|
||||
check_nip05_validity(pubkey: self.profile.pubkey, profiles: self.damus_state.profiles)
|
||||
check_nip05_validity(pubkey: self.profile.pubkey, damus_state: self.damus_state)
|
||||
profile.subscribe()
|
||||
//followers.subscribe()
|
||||
}
|
||||
@@ -568,7 +568,8 @@ extension View {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func check_nip05_validity(pubkey: Pubkey, profiles: Profiles) {
|
||||
func check_nip05_validity(pubkey: Pubkey, damus_state: DamusState) {
|
||||
let profiles = damus_state.profiles
|
||||
let profile = profiles.lookup(id: pubkey)
|
||||
|
||||
guard let nip05 = profile?.nip05,
|
||||
@@ -586,7 +587,7 @@ func check_nip05_validity(pubkey: Pubkey, profiles: Profiles) {
|
||||
Task { @MainActor in
|
||||
profiles.set_validated(pubkey, nip05: validated)
|
||||
profiles.nip05_pubkey[nip05] = pubkey
|
||||
notify(.profile_updated(.remote(pubkey: pubkey)))
|
||||
await damus_state.nostrNetwork.profilesManager.notifyProfileUpdate(pubkey: pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,7 +250,9 @@ struct NWCSettings: View {
|
||||
|
||||
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: profile.lud16, nip05: profile.nip05, damus_donation: p, reactions: profile.reactions)
|
||||
|
||||
notify(.profile_updated(.manual(pubkey: self.damus_state.pubkey, profile: prof)))
|
||||
Task {
|
||||
await damus_state.nostrNetwork.profilesManager.notifyProfileUpdate(pubkey: self.damus_state.pubkey)
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
|
||||
|
||||
@@ -90,27 +90,25 @@ struct BannerImageView: View {
|
||||
let disable_animation: Bool
|
||||
let pubkey: Pubkey
|
||||
let profiles: Profiles
|
||||
let damusState: DamusState
|
||||
|
||||
@State var banner: String?
|
||||
|
||||
init(pubkey: Pubkey, profiles: Profiles, disable_animation: Bool, banner: String? = nil) {
|
||||
init(pubkey: Pubkey, profiles: Profiles, disable_animation: Bool, banner: String? = nil, damusState: DamusState) {
|
||||
self.pubkey = pubkey
|
||||
self.profiles = profiles
|
||||
self._banner = State(initialValue: banner)
|
||||
self.disable_animation = disable_animation
|
||||
self.damusState = damusState
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
InnerBannerImageView(disable_animation: disable_animation, url: get_banner_url(banner: banner, pubkey: pubkey, profiles: profiles))
|
||||
.onReceive(handle_notify(.profile_updated)) { updated in
|
||||
guard updated.pubkey == self.pubkey,
|
||||
let profile = profiles.lookup(id: updated.pubkey)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
if let bannerImage = profile.banner, bannerImage != self.banner {
|
||||
self.banner = bannerImage
|
||||
.task {
|
||||
for await profile in await damusState.nostrNetwork.profilesManager.streamProfile(pubkey: pubkey) {
|
||||
if let bannerImage = profile.banner, bannerImage != self.banner {
|
||||
self.banner = bannerImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,7 +127,8 @@ struct BannerImageView_Previews: PreviewProvider {
|
||||
BannerImageView(
|
||||
pubkey: test_pubkey,
|
||||
profiles: make_preview_profiles(test_pubkey),
|
||||
disable_animation: false
|
||||
disable_animation: false,
|
||||
damusState: test_damus_state
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user