From 169b103f6d0138b649241873cfa9397e71e9bbec Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 29 Oct 2022 00:39:03 -0500 Subject: [PATCH] Change widget encoding to be more parallel and only run when going into the background --- Shared/Widget/WidgetDataEncoder.swift | 198 ++++++++++++++------------ iOS/AppDelegate.swift | 5 +- 2 files changed, 112 insertions(+), 91 deletions(-) diff --git a/Shared/Widget/WidgetDataEncoder.swift b/Shared/Widget/WidgetDataEncoder.swift index e5a7af530..1b610a8b5 100644 --- a/Shared/Widget/WidgetDataEncoder.swift +++ b/Shared/Widget/WidgetDataEncoder.swift @@ -25,110 +25,132 @@ public final class WidgetDataEncoder { private lazy var imageContainer = containerURL?.appendingPathComponent("widgetImages", isDirectory: true) private lazy var dataURL = containerURL?.appendingPathComponent("widget-data.json") - private var searchWorkItem: DispatchWorkItem? - + public var isRunning = false + init () { if imageContainer != nil { try? FileManager.default.createDirectory(at: imageContainer!, withIntermediateDirectories: true, attributes: nil) } - NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil) } - func pause() { - searchWorkItem?.cancel() - } - - func resume() { - dispatchWorkItem() - } - - @objc func statusesDidChange(_ note: Notification) { - dispatchWorkItem() - } - - func dispatchWorkItem() { + func encode() { if #available(iOS 14, *) { - searchWorkItem?.cancel() - searchWorkItem = DispatchWorkItem { [weak self] in - self?.encodeWidgetData() + isRunning = true + + flushSharedContainer() + os_log(.debug, log: log, "Starting encoding widget data.") + + DispatchQueue.main.async { + self.encodeWidgetData() { latestData in + guard let latestData = latestData else { + self.isRunning = false + return + } + + let encodedData = try? JSONEncoder().encode(latestData) + + os_log(.debug, log: self.log, "Finished encoding widget data.") + + if self.fileExists() { + try? FileManager.default.removeItem(at: self.dataURL!) + os_log(.debug, log: self.log, "Removed widget data from container.") + } + + if FileManager.default.createFile(atPath: self.dataURL!.path, contents: encodedData, attributes: nil) { + os_log(.debug, log: self.log, "Wrote widget data to container.") + WidgetCenter.shared.reloadAllTimelines() + } + + self.isRunning = false + } } - DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: searchWorkItem!) } } @available(iOS 14, *) - private func encodeWidgetData() { - flushSharedContainer() - os_log(.debug, log: log, "Starting encoding widget data.") + private func encodeWidgetData(completion: @escaping (WidgetData?) -> Void) { + var dispatchGroup = DispatchGroup() + var groupError: Error? = nil - do { - let unreadArticles = Array(try AccountManager.shared.fetchArticles(.unread(fetchLimit))).sortedByDate(.orderedDescending) - let starredArticles = Array(try AccountManager.shared.fetchArticles(.starred(fetchLimit))).sortedByDate(.orderedDescending) - let todayArticles = Array(try AccountManager.shared.fetchArticles(.today(fetchLimit))).sortedByDate(.orderedDescending) - - var unread = [LatestArticle]() - var today = [LatestArticle]() - var starred = [LatestArticle]() - - for article in unreadArticles { - let latestArticle = LatestArticle(id: article.sortableArticleID, - feedTitle: article.sortableName, - articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), - articleSummary: article.summary, - feedIconPath: writeImageDataToSharedContainer(article.iconImage()?.image.dataRepresentation()), - pubDate: article.datePublished?.description ?? "") - unread.append(latestArticle) - } - - for article in starredArticles { - let latestArticle = LatestArticle(id: article.sortableArticleID, - feedTitle: article.sortableName, - articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), - articleSummary: article.summary, - feedIconPath: writeImageDataToSharedContainer(article.iconImage()?.image.dataRepresentation()), - pubDate: article.datePublished?.description ?? "") - starred.append(latestArticle) - } - - for article in todayArticles { - let latestArticle = LatestArticle(id: article.sortableArticleID, - feedTitle: article.sortableName, - articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), - articleSummary: article.summary, - feedIconPath: writeImageDataToSharedContainer(article.iconImage()?.image.dataRepresentation()), - pubDate: article.datePublished?.description ?? "") - today.append(latestArticle) - } - - let latestData = WidgetData(currentUnreadCount: SmartFeedsController.shared.unreadFeed.unreadCount, - currentTodayCount: SmartFeedsController.shared.todayFeed.unreadCount, - currentStarredCount: try AccountManager.shared.fetchCountForStarredArticles(), - unreadArticles: unread, - starredArticles: starred, - todayArticles:today, - lastUpdateTime: Date()) - - - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - - let encodedData = try? JSONEncoder().encode(latestData) - - os_log(.debug, log: self.log, "Finished encoding widget data.") - - if self.fileExists() { - try? FileManager.default.removeItem(at: self.dataURL!) - os_log(.debug, log: self.log, "Removed widget data from container.") + var unread = [LatestArticle]() + + dispatchGroup.enter() + AccountManager.shared.fetchArticlesAsync(.unread(fetchLimit)) { (articleSetResult) in + switch articleSetResult { + case .success(let articles): + for article in articles { + let latestArticle = LatestArticle(id: article.sortableArticleID, + feedTitle: article.sortableName, + articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), + articleSummary: article.summary, + feedIconPath: self.writeImageDataToSharedContainer(article.iconImage()?.image.dataRepresentation()), + pubDate: article.datePublished?.description ?? "") + unread.append(latestArticle) } - if FileManager.default.createFile(atPath: self.dataURL!.path, contents: encodedData, attributes: nil) { - os_log(.debug, log: self.log, "Wrote widget data to container.") - WidgetCenter.shared.reloadAllTimelines() - } - + case .failure(let databaseError): + groupError = databaseError } - } catch { - os_log(.error, log: log, "WidgetDataEncoder failed to write the widget data.") + dispatchGroup.leave() } + + var starred = [LatestArticle]() + + dispatchGroup.enter() + AccountManager.shared.fetchArticlesAsync(.starred(fetchLimit)) { (articleSetResult) in + switch articleSetResult { + case .success(let articles): + for article in articles { + let latestArticle = LatestArticle(id: article.sortableArticleID, + feedTitle: article.sortableName, + articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), + articleSummary: article.summary, + feedIconPath: self.writeImageDataToSharedContainer(article.iconImage()?.image.dataRepresentation()), + pubDate: article.datePublished?.description ?? "") + starred.append(latestArticle) + } + case .failure(let databaseError): + groupError = databaseError + } + dispatchGroup.leave() + } + + var today = [LatestArticle]() + + dispatchGroup.enter() + AccountManager.shared.fetchArticlesAsync(.today(fetchLimit)) { (articleSetResult) in + switch articleSetResult { + case .success(let articles): + for article in articles { + let latestArticle = LatestArticle(id: article.sortableArticleID, + feedTitle: article.sortableName, + articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), + articleSummary: article.summary, + feedIconPath: self.writeImageDataToSharedContainer(article.iconImage()?.image.dataRepresentation()), + pubDate: article.datePublished?.description ?? "") + today.append(latestArticle) + } + case .failure(let databaseError): + groupError = databaseError + } + dispatchGroup.leave() + } + + dispatchGroup.notify(queue: .main) { + if groupError != nil { + os_log(.error, log: self.log, "WidgetDataEncoder failed to write the widget data.") + completion(nil) + } else { + let latestData = WidgetData(currentUnreadCount: SmartFeedsController.shared.unreadFeed.unreadCount, + currentTodayCount: SmartFeedsController.shared.todayFeed.unreadCount, + currentStarredCount: (try? AccountManager.shared.fetchCountForStarredArticles()) ?? 0, + unreadArticles: unread, + starredArticles: starred, + todayArticles:today, + lastUpdateTime: Date()) + completion(latestData) + } + } + } private func fileExists() -> Bool { diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index cc7ea16cc..6fec34ce6 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -175,16 +175,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func prepareAccountsForBackground() { extensionFeedAddRequestFile.suspend() - widgetDataEncoder.pause() syncTimer?.invalidate() scheduleBackgroundFeedRefresh() syncArticleStatus() + widgetDataEncoder.encode() waitForSyncTasksToFinish() } func prepareAccountsForForeground() { extensionFeedAddRequestFile.resume() - widgetDataEncoder.resume() syncTimer?.update() if let lastRefresh = AppDefaults.shared.lastRefresh { @@ -298,7 +297,7 @@ private extension AppDelegate { return } - if AccountManager.shared.refreshInProgress || isSyncArticleStatusRunning { + if AccountManager.shared.refreshInProgress || isSyncArticleStatusRunning || widgetDataEncoder.isRunning { os_log("Waiting for sync to finish...", log: self.log, type: .info) DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in self?.waitToComplete(completion: completion)