damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 39a324fd1ea5bc1081e987538cd889ed00e5b729
parent 3b541f2ec1b5bbe459a3839fa9916d983cab6931
Author: William Casarin <jb55@jb55.com>
Date:   Wed,  3 May 2023 09:18:09 -0700

Optimize json processing and preloading

- Preload events when added to the EventHolder queue
- Remove relative time formatting from preloader. Just do it when event appears
- Process incoming json in a background queue by default

Changelog-Fixed: Fix wrong relative times on events
Changelog-Changed: Preload events when they are queued

Diffstat:
Mdamus/Models/HomeModel.swift | 25++++++++++++++-----------
Mdamus/Models/ProfileModel.swift | 16+++++++++-------
Mdamus/Models/SearchHomeModel.swift | 5++++-
Mdamus/Models/SearchModel.swift | 5++++-
Mdamus/Nostr/RelayConnection.swift | 42++++++++++++++++++++++++------------------
Mdamus/Util/EventCache.swift | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mdamus/Util/EventHolder.swift | 13+++++++++++++
Mdamus/Util/Images/ImageMetadata.swift | 61+++++++++++++++++++++++++++++++++++++++++--------------------
Mdamus/Views/NoteContentView.swift | 35++++++++++++++++++++++++-----------
Mdamus/Views/Timeline/InnerTimelineView.swift | 2+-
10 files changed, 191 insertions(+), 90 deletions(-)

diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -48,12 +48,18 @@ class HomeModel: ObservableObject { @Published var new_events: NewEventsBits = NewEventsBits() @Published var notifications = NotificationsModel() - @Published var events = EventHolder() - + @Published var events: EventHolder = EventHolder() + init() { self.damus_state = DamusState.empty - filter_events() self.setup_debouncer() + filter_events() + events.on_queue = preloader + //self.events = EventHolder(on_queue: preloader) + } + + func preloader(ev: NostrEvent) { + preload_events(state: self.damus_state, events: [ev]) } var pool: RelayPool { @@ -528,7 +534,7 @@ class HomeModel: ObservableObject { } // TODO: will we need to process this in other places like zap request contents, etc? - process_image_metadata(cache: damus_state.events, ev: ev) + process_image_metadatas(cache: damus_state.events, ev: ev) damus_state.replies.count_replies(ev) damus_state.events.insert(ev) @@ -950,14 +956,11 @@ func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, o } if inserted { - Task.init { - let new_dms = Array(dms.dms.filter({ $0.events.count > 0 })).sorted { a, b in - return a.events.last!.created_at > b.events.last!.created_at - } - DispatchQueue.main.async { - dms.dms = new_dms - } + let new_dms = Array(dms.dms.filter({ $0.events.count > 0 })).sorted { a, b in + return a.events.last!.created_at > b.events.last!.created_at } + + dms.dms = new_dms } return new_events diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift @@ -8,20 +8,27 @@ import Foundation class ProfileModel: ObservableObject, Equatable { - var events: EventHolder = EventHolder() @Published var contacts: NostrEvent? = nil @Published var following: Int = 0 @Published var relays: [String: RelayInfo]? = nil @Published var progress: Int = 0 + var events: EventHolder let pubkey: String let damus: DamusState - var seen_event: Set<String> = Set() var sub_id = UUID().description var prof_subid = UUID().description + init(pubkey: String, damus: DamusState) { + self.pubkey = pubkey + self.damus = damus + self.events = EventHolder(on_queue: { ev in + preload_events(state: damus, events: [ev]) + }) + } + func follows(pubkey: String) -> Bool { guard let contacts = self.contacts else { return false @@ -47,11 +54,6 @@ class ProfileModel: ObservableObject, Equatable { return .pubkey(pubkey) } - init(pubkey: String, damus: DamusState) { - self.pubkey = pubkey - self.damus = damus - } - static func == (lhs: ProfileModel, rhs: ProfileModel) -> Bool { return lhs.pubkey == rhs.pubkey } diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift @@ -10,7 +10,7 @@ import Foundation /// The data model for the SearchHome view, typically something global-like class SearchHomeModel: ObservableObject { - var events: EventHolder = EventHolder() + var events: EventHolder @Published var loading: Bool = false var seen_pubkey: Set<String> = Set() @@ -21,6 +21,9 @@ class SearchHomeModel: ObservableObject { init(damus_state: DamusState) { self.damus_state = damus_state + self.events = EventHolder(on_queue: { ev in + preload_events(state: damus_state, events: [ev]) + }) } func get_base_filter() -> NostrFilter { diff --git a/damus/Models/SearchModel.swift b/damus/Models/SearchModel.swift @@ -10,7 +10,7 @@ import Foundation class SearchModel: ObservableObject { let state: DamusState - var events: EventHolder = EventHolder() + var events: EventHolder @Published var loading: Bool = false @Published var channel_name: String? = nil @@ -22,6 +22,9 @@ class SearchModel: ObservableObject { init(state: DamusState, search: NostrFilter) { self.state = state self.search = search + self.events = EventHolder(on_queue: { ev in + preload_events(state: state, events: [ev]) + }) } func filter_muted() { diff --git a/damus/Nostr/RelayConnection.swift b/damus/Nostr/RelayConnection.swift @@ -63,7 +63,7 @@ final class RelayConnection { last_connection_attempt = Date().timeIntervalSince1970 subscriptionToken = socket.subject - .receive(on: DispatchQueue.main) + .receive(on: DispatchQueue.global(qos: .default)) .sink { [weak self] completion in switch completion { case .failure(let error): @@ -97,26 +97,34 @@ final class RelayConnection { private func receive(event: WebSocketEvent) { switch event { case .connected: - backoff = 1.0 - self.isConnected = true - self.isConnecting = false + DispatchQueue.main.async { + self.backoff = 1.0 + self.isConnected = true + self.isConnecting = false + } case .message(let message): self.receive(message: message) case .disconnected(let closeCode, let reason): if closeCode != .normalClosure { print("⚠️ Warning: RelayConnection (\(self.url)) closed with code \(closeCode), reason: \(String(describing: reason))") } - isConnected = false - isConnecting = false - reconnect() + DispatchQueue.main.async { + self.isConnected = false + self.isConnecting = false + self.reconnect() + } case .error(let error): print("⚠️ Warning: RelayConnection (\(self.url)) error: \(error)") - isConnected = false - isConnecting = false - backoff *= 1.5 - reconnect_in(after: backoff) + DispatchQueue.main.async { + self.isConnected = false + self.isConnecting = false + self.backoff *= 1.5 + self.reconnect_in(after: self.backoff) + } + } + DispatchQueue.main.async { + self.handleEvent(.ws_event(event)) } - self.handleEvent(.ws_event(event)) } func reconnect() { @@ -136,13 +144,11 @@ final class RelayConnection { private func receive(message: URLSessionWebSocketTask.Message) { switch message { case .string(let messageString): - DispatchQueue.global(qos: .default).async { - if let ev = decode_nostr_event(txt: messageString) { - DispatchQueue.main.async { - self.handleEvent(.nostr_event(ev)) - } - return + if let ev = decode_nostr_event(txt: messageString) { + DispatchQueue.main.async { + self.handleEvent(.nostr_event(ev)) } + return } case .data(let messageData): if let messageString = String(data: messageData, encoding: .utf8) { diff --git a/damus/Util/EventCache.swift b/damus/Util/EventCache.swift @@ -300,8 +300,9 @@ func should_preload_translation(event: NostrEvent, our_keypair: Keypair, current struct PreloadPlan { let data: EventData + let img_metadata: [ImageMetadata] let event: NostrEvent - let load_artifacts: Bool + var load_artifacts: Bool let load_translations: Bool let load_preview: Bool } @@ -314,7 +315,8 @@ func load_preview(artifacts: NoteArtifacts) async -> Preview? { return Preview(meta: meta) } -func get_preload_plan(cache: EventData, ev: NostrEvent, our_keypair: Keypair, settings: UserSettingsStore) -> PreloadPlan? { +func get_preload_plan(evcache: EventCache, ev: NostrEvent, our_keypair: Keypair, settings: UserSettingsStore) -> PreloadPlan? { + let cache = evcache.get_cache_data(ev.id) let load_artifacts = cache.artifacts.should_preload if load_artifacts { cache.artifacts_model.state = .loading @@ -325,16 +327,28 @@ func get_preload_plan(cache: EventData, ev: NostrEvent, our_keypair: Keypair, se cache.translations_model.state = .translating } + let load_urls = event_image_metadata(ev: ev) + .reduce(into: [ImageMetadata]()) { to_load, meta in + let cached = evcache.lookup_img_metadata(url: meta.url) + guard cached == nil else { + return + } + + let m = ImageMetadataState(state: .processing, meta: meta) + evcache.store_img_metadata(url: meta.url, meta: m) + to_load.append(meta) + } + let load_preview = cache.preview.should_preload if load_preview { cache.preview_model.state = .loading } - if !load_artifacts && !load_translations && !load_preview { + if !load_artifacts && !load_translations && !load_preview && load_urls.count == 0 { return nil } - return PreloadPlan(data: cache, event: ev, load_artifacts: load_artifacts, load_translations: load_translations, load_preview: load_preview) + return PreloadPlan(data: cache, img_metadata: load_urls, event: ev, load_artifacts: load_artifacts, load_translations: load_translations, load_preview: load_preview) } func preload_image(url: URL) { @@ -351,17 +365,31 @@ func preload_image(url: URL) { } } -func preload_event(plan: PreloadPlan, profiles: Profiles, our_keypair: Keypair, settings: UserSettingsStore) async { - var artifacts: NoteArtifacts? = plan.data.artifacts.artifacts - - print("Preloading event \(plan.event.content)") - +func preload_pfp(profiles: Profiles, pubkey: String) { // preload pfp - if let profile = profiles.lookup(id: plan.event.pubkey), + if let profile = profiles.lookup(id: pubkey), let picture = profile.picture, let url = URL(string: picture) { preload_image(url: url) } +} + +func preload_event(plan: PreloadPlan, state: DamusState) async { + var artifacts: NoteArtifacts? = plan.data.artifacts.artifacts + let settings = state.settings + let profiles = state.profiles + let our_keypair = state.keypair + + print("Preloading event \(plan.event.content)") + + for meta in plan.img_metadata { + process_image_metadata(cache: state.events, meta: meta, ev: plan.event) + } + + preload_pfp(profiles: profiles, pubkey: plan.event.pubkey) + if let inner_ev = plan.event.get_inner_event(cache: state.events), inner_ev.pubkey != plan.event.pubkey { + preload_pfp(profiles: profiles, pubkey: inner_ev.pubkey) + } if artifacts == nil && plan.load_artifacts { let arts = render_note_content(ev: plan.event, profiles: profiles, privkey: our_keypair.privkey) @@ -398,28 +426,37 @@ func preload_event(plan: PreloadPlan, profiles: Profiles, our_keypair: Keypair, translations = await translate_note(profiles: profiles, privkey: our_keypair.privkey, event: plan.event, settings: settings, note_lang: note_language) } - let timeago = format_relative_time(plan.event.created_at) let ts = translations - DispatchQueue.main.async { - if let ts { - plan.data.translations_model.state = ts + if plan.data.translations_model.note_language == nil || ts != nil { + DispatchQueue.main.async { + if let ts { + plan.data.translations_model.state = ts + } + if plan.data.translations_model.note_language != note_language { + plan.data.translations_model.note_language = note_language + } } - plan.data.relative_time.value = timeago - plan.data.translations_model.note_language = note_language } + } -func preload_events(event_cache: EventCache, events: [NostrEvent], profiles: Profiles, our_keypair: Keypair, settings: UserSettingsStore) { +func preload_events(state: DamusState, events: [NostrEvent]) { + let event_cache = state.events + let our_keypair = state.keypair + let settings = state.settings let plans = events.compactMap { ev in - get_preload_plan(cache: event_cache.get_cache_data(ev.id), ev: ev, our_keypair: our_keypair, settings: settings) + get_preload_plan(evcache: event_cache, ev: ev, our_keypair: our_keypair, settings: settings) + } + + if plans.count == 0 { + return } Task.init { for plan in plans { - await preload_event(plan: plan, profiles: profiles, our_keypair: our_keypair, settings: settings) + await preload_event(plan: plan, state: state) } } - } diff --git a/damus/Util/EventHolder.swift b/damus/Util/EventHolder.swift @@ -13,6 +13,7 @@ class EventHolder: ObservableObject, ScrollQueue { @Published var events: [NostrEvent] @Published var incoming: [NostrEvent] var should_queue: Bool + var on_queue: ((NostrEvent) -> Void)? func set_should_queue(_ val: Bool) { self.should_queue = val @@ -35,6 +36,15 @@ class EventHolder: ObservableObject, ScrollQueue { self.events = [] self.incoming = [] self.has_event = Set() + self.on_queue = nil + } + + init(on_queue: @escaping (NostrEvent) -> ()) { + self.should_queue = false + self.events = [] + self.incoming = [] + self.has_event = Set() + self.on_queue = on_queue } init(events: [NostrEvent], incoming: [NostrEvent]) { @@ -42,6 +52,7 @@ class EventHolder: ObservableObject, ScrollQueue { self.events = events self.incoming = incoming self.has_event = Set() + self.on_queue = nil } func filter(_ isIncluded: (NostrEvent) -> Bool) { @@ -76,6 +87,8 @@ class EventHolder: ObservableObject, ScrollQueue { return false } + on_queue?(ev) + has_event.insert(ev.id) incoming.append(ev) diff --git a/damus/Util/Images/ImageMetadata.swift b/damus/Util/Images/ImageMetadata.swift @@ -174,34 +174,55 @@ func calculate_image_metadata(url: URL, img: UIImage, blurhash: String) -> Image } -func process_image_metadata(cache: EventCache, ev: NostrEvent) { - for tag in ev.tags { - guard tag.count >= 2 && tag[0] == "imeta" else { - continue - } - - guard let meta = ImageMetadata(tag: tag) else { - continue +func event_image_metadata(ev: NostrEvent) -> [ImageMetadata] { + return ev.tags.reduce(into: [ImageMetadata]()) { meta, tag in + guard tag.count >= 2 && tag[0] == "imeta", + let data = ImageMetadata(tag: tag) else { + return } + meta.append(data) + } +} + +func process_image_metadatas(cache: EventCache, ev: NostrEvent) { + for meta in event_image_metadata(ev: ev) { guard cache.lookup_img_metadata(url: meta.url) == nil else { continue } - let state = ImageMetadataState(state: .processing, meta: meta) + let state = ImageMetadataState(state: meta.blurhash == nil ? .not_needed : .processing, meta: meta) cache.store_img_metadata(url: meta.url, meta: state) - if let blurhash = meta.blurhash { - Task.init { - let img = await process_blurhash(blurhash: blurhash, size: meta.dim?.size) - - DispatchQueue.main.async { - if let img { - state.state = .processed(img) - } else { - state.state = .failed - } - } + guard let blurhash = meta.blurhash else { + return + } + + Task { + guard let img = await process_blurhash(blurhash: blurhash, size: meta.dim?.size) else { + return + } + Task { @MainActor in + state.state = .processed(img) + } + } + } +} + +func process_image_metadata(cache: EventCache, meta: ImageMetadata, ev: NostrEvent) { + guard let blurhash = meta.blurhash else { + return + } + Task { + let img = await process_blurhash(blurhash: blurhash, size: meta.dim?.size) + + DispatchQueue.main.async { + if let img { + let state = ImageMetadataState(state: .processed(img), meta: meta) + cache.store_img_metadata(url: meta.url, meta: state) + } else { + let state = ImageMetadataState(state: .failed, meta: meta) + cache.store_img_metadata(url: meta.url, meta: state) } } } diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift @@ -159,12 +159,28 @@ struct NoteContentView: View { } } - func load() async { - guard let plan = get_preload_plan(cache: damus_state.events.get_cache_data(event.id), ev: event, our_keypair: damus_state.keypair, settings: damus_state.settings) else { - return - } + func load(force_artifacts: Bool = false) { + // 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) + + // TODO: make this cleaner + Task { + // this is surprisingly slow + let rel = format_relative_time(event.created_at) + Task { @MainActor in + self.damus_state.events.get_cache_data(event.id).relative_time.value = rel + } - await preload_event(plan: plan, profiles: damus_state.profiles, our_keypair: damus_state.keypair, settings: damus_state.settings) + if var plan { + if force_artifacts { + plan.load_artifacts = true + } + await preload_event(plan: plan, state: damus_state) + } else if force_artifacts { + let arts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey) + self.artifacts_model.state = .loaded(arts) + } + } } var body: some View { @@ -176,10 +192,7 @@ struct NoteContentView: View { switch block { case .mention(let m): if m.type == .pubkey && m.ref.ref_id == profile.pubkey { - self.artifacts_model.state = .loading - Task.init { - await load() - } + load(force_artifacts: true) return } case .relay: return @@ -190,8 +203,8 @@ struct NoteContentView: View { } } } - .task { - await load() + .onAppear { + load() } } diff --git a/damus/Views/Timeline/InnerTimelineView.swift b/damus/Views/Timeline/InnerTimelineView.swift @@ -64,7 +64,7 @@ struct InnerTimelineView: View { indexed[safe: ind+5]?.0 ].compactMap({ $0 })) - preload_events(event_cache: state.events, events: to_preload, profiles: state.profiles, our_keypair: state.keypair, settings: state.settings) + preload_events(state: state, events: to_preload) } ThiccDivider()