From 2e88b266d9d52efa2d5cbfa2ea5d5d4067baaf62 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 18 Jan 2021 14:29:32 -0500 Subject: [PATCH] Prefetch on a background queue to avoid blocking main queue with CoreData lookups --- Tusker.xcodeproj/project.pbxproj | 4 ++ .../MastodonCachePersistentStore.swift | 6 +++ .../BookmarksTableViewController.swift | 21 +++----- .../ConversationTableViewController.swift | 21 ++------ .../ProfileStatusesViewController.swift | 31 ++++------- .../TimelineTableViewController.swift | 30 ++++------- .../Utilities/StatusTablePrefetching.swift | 53 +++++++++++++++++++ 7 files changed, 94 insertions(+), 72 deletions(-) create mode 100644 Tusker/Screens/Utilities/StatusTablePrefetching.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index e51401ec..5a10b431 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -227,6 +227,7 @@ D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */; }; D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; }; D6A5BB3123BBAD87003BF21D /* JSONResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */; }; + D6A6C10525B6138A00298D0F /* StatusTablePrefetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */; }; D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */; }; D6ACE1AC240C3BAD004EA8E2 /* Ambassador.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F613023AE99E000F3CFD3 /* Ambassador.framework */; }; D6ACE1AD240C3BAD004EA8E2 /* Embassy.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F612D23AE990C00F3CFD3 /* Embassy.framework */; }; @@ -587,6 +588,7 @@ D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTests.swift; sourceTree = ""; }; D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = ""; }; D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONResponse.swift; sourceTree = ""; }; + D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTablePrefetching.swift; sourceTree = ""; }; D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSceneDelegate.swift; sourceTree = ""; }; D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = ""; }; D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMesasgeActivity.swift; sourceTree = ""; }; @@ -1389,6 +1391,7 @@ D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */, D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */, D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */, + D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */, ); path = Utilities; sourceTree = ""; @@ -2060,6 +2063,7 @@ D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */, D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */, D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */, + D6A6C10525B6138A00298D0F /* StatusTablePrefetching.swift in Sources */, D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */, D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */, D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */, diff --git a/Tusker/CoreData/MastodonCachePersistentStore.swift b/Tusker/CoreData/MastodonCachePersistentStore.swift index cf6bac49..d25881a8 100644 --- a/Tusker/CoreData/MastodonCachePersistentStore.swift +++ b/Tusker/CoreData/MastodonCachePersistentStore.swift @@ -24,6 +24,12 @@ class MastodonCachePersistentStore: NSPersistentContainer { return context }() + private(set) lazy var prefetchBackgroundContext: NSManagedObjectContext = { + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.parent = self.viewContext + return context + }() + let statusSubject = PassthroughSubject() let accountSubject = PassthroughSubject() let relationshipSubject = PassthroughSubject() diff --git a/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift b/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift index 0f7baf72..5169f501 100644 --- a/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift +++ b/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift @@ -162,24 +162,19 @@ extension BookmarksTableViewController: StatusTableViewCellDelegate { } } -extension BookmarksTableViewController: UITableViewDataSourcePrefetching { +extension BookmarksTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { - for indexPath in indexPaths { - guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { continue } - _ = ImageCache.avatars.get(status.account.avatar, completion: nil) - for attachment in status.attachments where attachment.kind == .image { - _ = ImageCache.attachments.get(attachment.url, completion: nil) - } - } + let ids = indexPaths.map { statuses[$0.row].id } + prefetchStatuses(with: ids) } func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { - for indexPath in indexPaths { - guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { continue } - ImageCache.avatars.cancelWithoutCallback(status.account.avatar) - for attachment in status.attachments where attachment.kind == .image { - ImageCache.attachments.cancelWithoutCallback(attachment.url) + let ids: [String] = indexPaths.compactMap { + guard $0.row < statuses.count else { + return nil } + return statuses[$0.row].id } + cancelPrefetchingStatuses(with: ids) } } diff --git a/Tusker/Screens/Conversation/ConversationTableViewController.swift b/Tusker/Screens/Conversation/ConversationTableViewController.swift index 34dbc9b2..f6caa388 100644 --- a/Tusker/Screens/Conversation/ConversationTableViewController.swift +++ b/Tusker/Screens/Conversation/ConversationTableViewController.swift @@ -7,7 +7,6 @@ // import UIKit -import SafariServices import Pachyderm class ConversationTableViewController: EnhancedTableViewController { @@ -188,24 +187,14 @@ extension ConversationTableViewController: StatusTableViewCellDelegate { } } -extension ConversationTableViewController: UITableViewDataSourcePrefetching { +extension ConversationTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { - for indexPath in indexPaths { - guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { continue } - _ = ImageCache.avatars.get(status.account.avatar, completion: nil) - for attachment in status.attachments where attachment.kind == .image { - _ = ImageCache.attachments.get(attachment.url, completion: nil) - } - } + let ids = indexPaths.map { statuses[$0.row].id } + prefetchStatuses(with: ids) } func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { - for indexPath in indexPaths { - guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { continue } - ImageCache.avatars.cancelWithoutCallback(status.account.avatar) - for attachment in status.attachments where attachment.kind == .image { - ImageCache.attachments.cancelWithoutCallback(attachment.url) - } - } + let ids: [String] = indexPaths.compactMap { statuses[$0.row].id } + cancelPrefetchingStatuses(with: ids) } } diff --git a/Tusker/Screens/Profile/ProfileStatusesViewController.swift b/Tusker/Screens/Profile/ProfileStatusesViewController.swift index 5deec70a..19530621 100644 --- a/Tusker/Screens/Profile/ProfileStatusesViewController.swift +++ b/Tusker/Screens/Profile/ProfileStatusesViewController.swift @@ -241,33 +241,20 @@ extension ProfileStatusesViewController: StatusTableViewCellDelegate { } } -extension ProfileStatusesViewController: UITableViewDataSourcePrefetching { +extension ProfileStatusesViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { - for indexPath in indexPaths { - let statusID = item(for: indexPath).id - - guard let status = mastodonController.persistentContainer.status(for: statusID) else { - continue - } - - _ = ImageCache.avatars.get(status.account.avatar, completion: nil) - for attachment in status.attachments where attachment.kind == .image { - _ = ImageCache.attachments.get(attachment.url, completion: nil) - } - } + let ids = indexPaths.map { item(for: $0).id } + prefetchStatuses(with: ids) } func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { - for indexPath in indexPaths { - let statusID = item(for: indexPath).id - - guard let status = mastodonController.persistentContainer.status(for: statusID) else { - continue - } - ImageCache.avatars.cancelWithoutCallback(status.account.avatar) - for attachment in status.attachments where attachment.kind == .image { - ImageCache.avatars.cancelWithoutCallback(attachment.url) + let ids: [String] = indexPaths.compactMap { + guard $0.section < sections.count, + $0.row < sections[$0.section].count else { + return nil } + return item(for: $0).id } + cancelPrefetchingStatuses(with: ids) } } diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index 9f13b982..a28fd626 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -150,32 +150,20 @@ extension TimelineTableViewController: StatusTableViewCellDelegate { } } -extension TimelineTableViewController: UITableViewDataSourcePrefetching { +extension TimelineTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { - for indexPath in indexPaths { - guard let status = mastodonController.persistentContainer.status(for: item(for: indexPath).id) else { - continue - } - _ = ImageCache.avatars.get(status.account.avatar, completion: nil) - for attachment in status.attachments where attachment.kind == .image { - _ = ImageCache.attachments.get(attachment.url, completion: nil) - } - } + let ids = indexPaths.map { item(for: $0).id } + prefetchStatuses(with: ids) } func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { - for indexPath in indexPaths { - // todo: this means when removing cells, we can't cancel prefetching - // is this an issue? - guard indexPath.section < sections.count, - indexPath.row < sections[indexPath.section].count, - let status = mastodonController.persistentContainer.status(for: item(for: indexPath).id) else { - continue - } - ImageCache.avatars.cancelWithoutCallback(status.account.avatar) - for attachment in status.attachments where attachment.kind == .image { - ImageCache.attachments.cancelWithoutCallback(attachment.url) + let ids: [String] = indexPaths.compactMap { + guard $0.section < sections.count, + $0.row < sections[$0.section].count else { + return nil } + return item(for: $0).id } + cancelPrefetchingStatuses(with: ids) } } diff --git a/Tusker/Screens/Utilities/StatusTablePrefetching.swift b/Tusker/Screens/Utilities/StatusTablePrefetching.swift new file mode 100644 index 00000000..dd5cf1e4 --- /dev/null +++ b/Tusker/Screens/Utilities/StatusTablePrefetching.swift @@ -0,0 +1,53 @@ +// +// StatusTablePrefetching.swift +// Tusker +// +// Created by Shadowfacts on 1/18/21. +// Copyright © 2021 Shadowfacts. All rights reserved. +// + +import Foundation +import CoreData + +protocol StatusTablePrefetching: TuskerNavigationDelegate { +} + +extension StatusTablePrefetching { + + func prefetchStatuses(with ids: [String]) { + let context = apiController.persistentContainer.prefetchBackgroundContext + context.perform { + guard let statuses = getStatusesWith(ids: ids, in: context) else { + return + } + for status in statuses { + _ = ImageCache.avatars.get(status.account.avatar, completion: nil) + for attachment in status.attachments where attachment.kind == .image { + _ = ImageCache.attachments.get(attachment.url, completion: nil) + } + } + } + } + + func cancelPrefetchingStatuses(with ids: [String]) { + let context = apiController.persistentContainer.prefetchBackgroundContext + context.perform { + guard let statuses = getStatusesWith(ids: ids, in: context) else { + return + } + for status in statuses { + ImageCache.avatars.cancelWithoutCallback(status.account.avatar) + for attachment in status.attachments where attachment.kind == .image { + ImageCache.attachments.cancelWithoutCallback(attachment.url) + } + } + } + } + +} + +fileprivate func getStatusesWith(ids: [String], in context: NSManagedObjectContext) -> [StatusMO]? { + let request: NSFetchRequest = StatusMO.fetchRequest() + request.predicate = NSPredicate(format: "id IN %@", ids) + return try? context.fetch(request) +}