Move remote change processing to separate context to avoid blocking background context

This commit is contained in:
Shadowfacts 2023-05-11 23:03:41 -04:00
parent 4dbc4ebeb2
commit bff7585fa9
1 changed files with 41 additions and 39 deletions

View File

@ -31,18 +31,21 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
context.persistentStoreCoordinator = self.persistentStoreCoordinator
context.automaticallyMergesChangesFromParent = true
context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
context.name = "Background"
return context
}()
private(set) lazy var prefetchBackgroundContext: NSManagedObjectContext = {
// remote change processing happens on its own context, since it can sometimes take
// a really long time (upwards of a minute) and shouldn't block other things using the background context
private lazy var remoteChangesBackgroundContext: NSManagedObjectContext = {
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.persistentStoreCoordinator = self.persistentStoreCoordinator
context.automaticallyMergesChangesFromParent = true
context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
context.name = "RemoteChanges"
return context
}()
private var remoteChangeHandlerQueue = DispatchQueue(label: "PersistentStore remote changes")
private var lastRemoteChangeToken: NSPersistentHistoryToken?
// TODO: consider sending managed objects through this to avoid re-fetching things unnecessarily
@ -182,6 +185,7 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
viewContext.automaticallyMergesChangesFromParent = true
viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
viewContext.name = "View"
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: viewContext)
NotificationCenter.default.addObserver(self, selector: #selector(remoteChanges), name: .NSPersistentStoreRemoteChange, object: persistentStoreCoordinator)
@ -509,50 +513,48 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
guard let token = notification.userInfo?[NSPersistentHistoryTokenKey] as? NSPersistentHistoryToken else {
return
}
remoteChangeHandlerQueue.async {
remoteChangesBackgroundContext.perform {
defer {
self.lastRemoteChangeToken = token
}
let req = NSPersistentHistoryChangeRequest.fetchHistory(after: self.lastRemoteChangeToken)
self.backgroundContext.performAndWait {
if let result = try? self.backgroundContext.execute(req) as? NSPersistentHistoryResult,
let transactions = result.result as? [NSPersistentHistoryTransaction],
!transactions.isEmpty {
var changedHashtags = false
var changedInstances = false
var changedTimelinePositions = Set<NSManagedObjectID>()
var changedAccountPrefs = false
outer: for transaction in transactions {
for change in transaction.changes ?? [] {
if change.changedObjectID.entity.name == "SavedHashtag" {
changedHashtags = true
} else if change.changedObjectID.entity.name == "SavedInstance" {
changedInstances = true
} else if change.changedObjectID.entity.name == "TimelinePosition" {
changedTimelinePositions.insert(change.changedObjectID)
} else if change.changedObjectID.entity.name == "AccountPreferences" {
changedAccountPrefs = true
}
if let result = try? self.remoteChangesBackgroundContext.execute(req) as? NSPersistentHistoryResult,
let transactions = result.result as? [NSPersistentHistoryTransaction],
!transactions.isEmpty {
var changedHashtags = false
var changedInstances = false
var changedTimelinePositions = Set<NSManagedObjectID>()
var changedAccountPrefs = false
outer: for transaction in transactions {
for change in transaction.changes ?? [] {
if change.changedObjectID.entity.name == "SavedHashtag" {
changedHashtags = true
} else if change.changedObjectID.entity.name == "SavedInstance" {
changedInstances = true
} else if change.changedObjectID.entity.name == "TimelinePosition" {
changedTimelinePositions.insert(change.changedObjectID)
} else if change.changedObjectID.entity.name == "AccountPreferences" {
changedAccountPrefs = true
}
}
DispatchQueue.main.async {
if changedHashtags {
NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil)
}
if changedInstances {
NotificationCenter.default.post(name: .savedInstancesChanged, object: nil)
}
for id in changedTimelinePositions {
guard let timelinePosition = try? self.viewContext.existingObject(with: id) as? TimelinePosition else {
continue
}
// the kvo observer that clears the LazilyDecoding cache doesn't always fire on remote changes, so do it manually
timelinePosition.changedRemotely()
NotificationCenter.default.post(name: .timelinePositionChanged, object: timelinePosition)
}
if changedAccountPrefs {
NotificationCenter.default.post(name: .accountPreferencesChangedRemotely, object: nil)
}
DispatchQueue.main.async {
if changedHashtags {
NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil)
}
if changedInstances {
NotificationCenter.default.post(name: .savedInstancesChanged, object: nil)
}
for id in changedTimelinePositions {
guard let timelinePosition = try? self.viewContext.existingObject(with: id) as? TimelinePosition else {
continue
}
// the kvo observer that clears the LazilyDecoding cache doesn't always fire on remote changes, so do it manually
timelinePosition.changedRemotely()
NotificationCenter.default.post(name: .timelinePositionChanged, object: timelinePosition)
}
if changedAccountPrefs {
NotificationCenter.default.post(name: .accountPreferencesChangedRemotely, object: nil)
}
}
}