diff --git a/clients/ios/NewsBlur.xcodeproj/project.pbxproj b/clients/ios/NewsBlur.xcodeproj/project.pbxproj index 98eb4b1a4..aedbe7f5c 100755 --- a/clients/ios/NewsBlur.xcodeproj/project.pbxproj +++ b/clients/ios/NewsBlur.xcodeproj/project.pbxproj @@ -32,7 +32,7 @@ 173CB31126BCE94700BA872A /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 173CB31026BCE94700BA872A /* SwiftUI.framework */; }; 173CB31426BCE94700BA872A /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173CB31326BCE94700BA872A /* WidgetExtension.swift */; }; 173CB31626BCE94A00BA872A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 173CB31526BCE94A00BA872A /* Assets.xcassets */; }; - 173CB31A26BCE94A00BA872A /* Widget Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 173CB30D26BCE94700BA872A /* Widget Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 173CB31A26BCE94A00BA872A /* NewsBlur Widget.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 173CB30D26BCE94700BA872A /* NewsBlur Widget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 1740C6881C10FD75005EA453 /* theme_color_dark.png in Resources */ = {isa = PBXBuildFile; fileRef = 1740C6841C10FD75005EA453 /* theme_color_dark.png */; }; 1740C6891C10FD75005EA453 /* theme_color_light.png in Resources */ = {isa = PBXBuildFile; fileRef = 1740C6851C10FD75005EA453 /* theme_color_light.png */; }; 1740C68A1C10FD75005EA453 /* theme_color_medium.png in Resources */ = {isa = PBXBuildFile; fileRef = 1740C6861C10FD75005EA453 /* theme_color_medium.png */; }; @@ -90,6 +90,7 @@ 17876BA51C99137B0055DD15 /* accessory_disclosure@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17876BA31C99137B0055DD15 /* accessory_disclosure@2x.png */; }; 1788939D249332E6004CBA4E /* g_icn_search.png in Resources */ = {isa = PBXBuildFile; fileRef = 1788939C249332E6004CBA4E /* g_icn_search.png */; }; 1791C21526C4C7BC00D815AA /* WidgetStoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1791C21426C4C7BC00D815AA /* WidgetStoryView.swift */; }; + 17997C5827A8FDD100483E69 /* WidgetDebugTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17997C5727A8FDD100483E69 /* WidgetDebugTimer.swift */; }; 179DD9CF23DFDD51007BFD21 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 179DD9CE23DFDD51007BFD21 /* CloudKit.framework */; }; 17A396D924F86A8F0023C9E2 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 17A396D824F86A8F0023C9E2 /* MainInterface.storyboard */; }; 17AACFE122279A3C00DE6EA4 /* autoscroll_resume@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17AACFD722279A3900DE6EA4 /* autoscroll_resume@2x.png */; }; @@ -702,7 +703,7 @@ files = ( 177551DF238E228A00E27818 /* Old NewsBlur Latest.appex in Embed App Extensions */, 1749391B1C251BFE003D98AA /* Share Extension.appex in Embed App Extensions */, - 173CB31A26BCE94A00BA872A /* Widget Extension.appex in Embed App Extensions */, + 173CB31A26BCE94A00BA872A /* NewsBlur Widget.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -734,7 +735,7 @@ 172AD273251D9F40000BB264 /* Storyboards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storyboards.swift; sourceTree = ""; }; 17362ADB23639B4E00A0FCCC /* OfflineFetchText.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = OfflineFetchText.h; path = offline/OfflineFetchText.h; sourceTree = ""; }; 17362ADC23639B4E00A0FCCC /* OfflineFetchText.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = OfflineFetchText.m; path = offline/OfflineFetchText.m; sourceTree = ""; }; - 173CB30D26BCE94700BA872A /* Widget Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Widget Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 173CB30D26BCE94700BA872A /* NewsBlur Widget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "NewsBlur Widget.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 173CB30E26BCE94700BA872A /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 173CB31026BCE94700BA872A /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 173CB31326BCE94700BA872A /* WidgetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetExtension.swift; sourceTree = ""; }; @@ -805,6 +806,7 @@ 17876BA31C99137B0055DD15 /* accessory_disclosure@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "accessory_disclosure@2x.png"; sourceTree = ""; }; 1788939C249332E6004CBA4E /* g_icn_search.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = g_icn_search.png; sourceTree = ""; }; 1791C21426C4C7BC00D815AA /* WidgetStoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetStoryView.swift; sourceTree = ""; }; + 17997C5727A8FDD100483E69 /* WidgetDebugTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDebugTimer.swift; sourceTree = ""; }; 179DD9CC23DFD20E007BFD21 /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BridgingHeader.h; path = "Other Sources/BridgingHeader.h"; sourceTree = ""; }; 179DD9CE23DFDD51007BFD21 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; 17A396D824F86A8F0023C9E2 /* MainInterface.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = MainInterface.storyboard; sourceTree = ""; }; @@ -1657,6 +1659,7 @@ 1723388D26BE440400610784 /* WidgetFeed.swift */, 1723388C26BE440400610784 /* WidgetStory.swift */, 1723388A26BE43EB00610784 /* WidgetLoader.swift */, + 17997C5727A8FDD100483E69 /* WidgetDebugTimer.swift */, 173CB31526BCE94A00BA872A /* Assets.xcassets */, 173CB31726BCE94A00BA872A /* Info.plist */, 173CB31B26BCE94A00BA872A /* WidgetExtension.entitlements */, @@ -1756,7 +1759,7 @@ 174939101C251BFE003D98AA /* Share Extension.appex */, FF8A94971DE3BB77000A4C31 /* Story Notification Service Extension.appex */, 177551D3238E228A00E27818 /* Old NewsBlur Latest.appex */, - 173CB30D26BCE94700BA872A /* Widget Extension.appex */, + 173CB30D26BCE94700BA872A /* NewsBlur Widget.appex */, ); name = Products; sourceTree = ""; @@ -2867,9 +2870,9 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 173CB30C26BCE94700BA872A /* Widget Extension */ = { + 173CB30C26BCE94700BA872A /* NewsBlur Widget */ = { isa = PBXNativeTarget; - buildConfigurationList = 173CB31C26BCE94A00BA872A /* Build configuration list for PBXNativeTarget "Widget Extension" */; + buildConfigurationList = 173CB31C26BCE94A00BA872A /* Build configuration list for PBXNativeTarget "NewsBlur Widget" */; buildPhases = ( 173CB30926BCE94700BA872A /* Sources */, 173CB30A26BCE94700BA872A /* Frameworks */, @@ -2879,9 +2882,9 @@ ); dependencies = ( ); - name = "Widget Extension"; + name = "NewsBlur Widget"; productName = WidgetExtension; - productReference = 173CB30D26BCE94700BA872A /* Widget Extension.appex */; + productReference = 173CB30D26BCE94700BA872A /* NewsBlur Widget.appex */; productType = "com.apple.product-type.app-extension"; }; 1749390F1C251BFE003D98AA /* Share Extension */ = { @@ -3051,7 +3054,7 @@ 1749390F1C251BFE003D98AA /* Share Extension */, FF8A94961DE3BB77000A4C31 /* Story Notification Service Extension */, 177551D2238E228A00E27818 /* Old Widget Extension */, - 173CB30C26BCE94700BA872A /* Widget Extension */, + 173CB30C26BCE94700BA872A /* NewsBlur Widget */, ); }; /* End PBXProject section */ @@ -3540,6 +3543,7 @@ 173CB31426BCE94700BA872A /* WidgetExtension.swift in Sources */, 1723388E26BE440400610784 /* WidgetStory.swift in Sources */, 1723389426C3775B00610784 /* WidgetBarView.swift in Sources */, + 17997C5827A8FDD100483E69 /* WidgetDebugTimer.swift in Sources */, 1723388F26BE440400610784 /* WidgetFeed.swift in Sources */, 1791C21526C4C7BC00D815AA /* WidgetStoryView.swift in Sources */, ); @@ -3745,7 +3749,7 @@ /* Begin PBXTargetDependency section */ 173CB31926BCE94A00BA872A /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 173CB30C26BCE94700BA872A /* Widget Extension */; + target = 173CB30C26BCE94700BA872A /* NewsBlur Widget */; targetProxy = 173CB31826BCE94A00BA872A /* PBXContainerItemProxy */; }; 1749391A1C251BFE003D98AA /* PBXTargetDependency */ = { @@ -4415,7 +4419,7 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 173CB31C26BCE94A00BA872A /* Build configuration list for PBXNativeTarget "Widget Extension" */ = { + 173CB31C26BCE94A00BA872A /* Build configuration list for PBXNativeTarget "NewsBlur Widget" */ = { isa = XCConfigurationList; buildConfigurations = ( 173CB31D26BCE94A00BA872A /* Debug */, diff --git a/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/Widget Extension.xcscheme b/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/Widget Extension.xcscheme index 6aa4b5605..4d78e8525 100644 --- a/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/Widget Extension.xcscheme +++ b/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/Widget Extension.xcscheme @@ -16,8 +16,8 @@ diff --git a/clients/ios/Resources/MainInterface.storyboard b/clients/ios/Resources/MainInterface.storyboard index 70d7d013c..bf7c68435 100644 --- a/clients/ios/Resources/MainInterface.storyboard +++ b/clients/ios/Resources/MainInterface.storyboard @@ -14,7 +14,7 @@ - + diff --git a/clients/ios/Widget Extension/WidgetCache.swift b/clients/ios/Widget Extension/WidgetCache.swift index 4d648be2e..52c937b92 100644 --- a/clients/ios/Widget Extension/WidgetCache.swift +++ b/clients/ios/Widget Extension/WidgetCache.swift @@ -248,7 +248,7 @@ class WidgetCache { stories = try decoder.decode([Story].self, from: json) } catch { - print("Error \(error)") + NSLog("Error \(error)") } } @@ -269,7 +269,7 @@ class WidgetCache { try json.write(to: url) } catch { - print("Error \(error)") + NSLog("Error \(error)") } } @@ -295,7 +295,7 @@ class WidgetCache { try image.pngData()?.write(to: imageURL) } catch { - print("Image error: \(error)") + NSLog("Image saving error: \(error)") } } @@ -319,7 +319,7 @@ class WidgetCache { storyImageCache[identifier] = nil } } catch { - print("Flush story images error: \(error)") + NSLog("Flush story images error: \(error)") } } @@ -446,7 +446,7 @@ class WidgetCache { return image } catch { - print("Image error: \(error)") + NSLog("Cached image loading error: \(error)") } return nil diff --git a/clients/ios/Widget Extension/WidgetDebugTimer.swift b/clients/ios/Widget Extension/WidgetDebugTimer.swift new file mode 100644 index 000000000..66bbfca7c --- /dev/null +++ b/clients/ios/Widget Extension/WidgetDebugTimer.swift @@ -0,0 +1,110 @@ +// +// WidgetDebugTimer.swift +// Widget Extension +// +// Created by David Sinclair on 2022-01-31. +// Based on Dejal code. +// + +import Foundation + +/// Timer for debugging performance. +class WidgetDebugTimer { + /// Private singleton shared instance. Access via the class functions. + private static let shared = WidgetDebugTimer() + + /// Private initializer to prevent others constructing a new instance. + private init() { + formatter = NumberFormatter() + formatter.minimumIntegerDigits = 1 + formatter.minimumFractionDigits = 6 + formatter.maximumFractionDigits = 6 + } + + /// Information about each timer operation. + private struct Info { + /// The date the operation was started. + var start: Date + + /// The date this step was started. + var step: Date + + /// The indentation level. + var level: Int + + // If I ever add the closure-based long-running timers, add those properties. + } + + /// A dictionary of operation info, keyed on the operation string. + private typealias InfoDictionary = [String : Info] + + /// A dictionary of operation info, keyed on the operation string. + private var info = InfoDictionary() + + /// A number formatter for the number of seconds. + private var formatter: NumberFormatter + + /// Given an operation name, starts a debug timer. Use `print(_:step:)` after the code to time. + /// + /// - Parameter operation: The name of the operation to time (used as both a key to group timers, and a debug label). + /// - Parameter level: How much to indent the operation, for nested timers. Defaults to zero (no indentation). + /// - Returns: The operation name, so it can be assigned to a variable instead of typing it again. Discardable. + @discardableResult + class func start(_ operation: String, level: Int = 0) -> String { + let date = Date() + + shared.info[operation] = Info(start: date, step: date, level: level) + + return operation + } + + /// Given an operation name, that must have been previously started via `start(_:)`, prints the total time so far and (if a step is provided) the time since that this step took, i.e. since the start or the previous step. + /// + /// - Parameter operation: The name of the operation to time. + /// - Parameter step: The name of the step of the operation. May be omitted if there's only one interesting step. + class func print(_ operation: String, step: String? = nil) { + let date = Date() + + guard let currentInfo = shared.info[operation] else { + NSLog("\(operation): forgot to call start(_:) first!") + return + } + + let totalDuration = date.timeIntervalSince(currentInfo.start) + + guard let step = step else { + NSLog("\(String(repeating: " ", count: currentInfo.level * 2))\(operation) took \(shared.formatter.string(from: NSNumber(value: totalDuration)) ?? "?") seconds") + return + } + + let stepDuration = date.timeIntervalSince(currentInfo.step) + var newInfo = currentInfo + let alert = stepDuration < 0.001 ? "" : stepDuration < 0.01 ? " 🚨" : stepDuration < 0.1 ? " 🚨🚨" : stepDuration < 1.0 ? " 🚨🚨🚨" : " 🚨🚨🚨🚨" + + NSLog("\(String(repeating: " ", count: currentInfo.level * 2))\(operation): \(step) took \(shared.formatter.string(from: NSNumber(value: stepDuration)) ?? "?") seconds (total \(shared.formatter.string(from: NSNumber(value: totalDuration)) ?? "?") seconds)\(alert)") + + newInfo.step = date + + shared.info[operation] = newInfo + } +} + +/// Convenience timer for debugging performance of a code scope; automatically prints the info when exiting the scope. +class DebugScopeTimer { + /// The current operation. + let operation: String + + /// Initializer. Assign this to a variable to establish the scope, e.g. `let debug = DebugScopeTimer("Thing")` (this will result in a warning, but that can be useful to remind me to remove the timer; can't assign to underscore, as that is immediately released). + /// + /// - Parameter operation: The name of the operation to time. + init(_ operation: String) { + self.operation = operation + + WidgetDebugTimer.start(operation) + } + + /// Deinitializer. Prints the info when exiting the scope. + deinit { + WidgetDebugTimer.print(operation) + } +} diff --git a/clients/ios/Widget Extension/WidgetExtension.swift b/clients/ios/Widget Extension/WidgetExtension.swift index 6963e3ee0..1be489351 100644 --- a/clients/ios/Widget Extension/WidgetExtension.swift +++ b/clients/ios/Widget Extension/WidgetExtension.swift @@ -23,13 +23,19 @@ struct Provider: TimelineProvider { } func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { + let operation = WidgetDebugTimer.start("🚧 getTimeline") + cache.loadCachedStories() + WidgetDebugTimer.print(operation, step: "loadCachedStories") + if context.isPreview && !cache.stories.isEmpty { return } cache.load { + WidgetDebugTimer.print(operation, step: "cache.load()") + var entries: [SimpleEntry] = [] // Generate a timeline consisting of five entries an hour apart, starting from the current date. @@ -47,6 +53,8 @@ struct Provider: TimelineProvider { let timeline = Timeline(entries: entries, policy: .atEnd) + WidgetDebugTimer.print(operation, step: "making timeline") + let imageRequestGroup = DispatchGroup() for feed in cache.feeds { @@ -66,6 +74,8 @@ struct Provider: TimelineProvider { } imageRequestGroup.notify(queue: .main) { + WidgetDebugTimer.print(operation, step: "requesting images") + completion(timeline) } } diff --git a/clients/ios/Widget Extension/WidgetFeed.swift b/clients/ios/Widget Extension/WidgetFeed.swift index e2470a585..f6444f02e 100644 --- a/clients/ios/Widget Extension/WidgetFeed.swift +++ b/clients/ios/Widget Extension/WidgetFeed.swift @@ -37,6 +37,8 @@ struct Feed: Identifiable { /// /// - Parameter dictionary: Dictionary representation. init(from dictionary: Dictionary) { + let operation = WidgetDebugTimer.start("reading feed") + id = dictionary[DictionaryKeys.id] as? String ?? "" title = dictionary[DictionaryKeys.title] as? String ?? "" @@ -51,6 +53,8 @@ struct Feed: Identifiable { } else { rightColor = Self.from(hexString: "505050") } + + WidgetDebugTimer.print(operation, step: "title: \(title)") } /// Initializer for a sample. @@ -93,7 +97,7 @@ struct Feed: Identifiable { blue = Double( hex & 0x0000FF) / 255 } - print("Reading color from '\(hexString)': red: \(red), green: \(green), blue: \(blue), alpha: \(alpha)") + NSLog("Reading color from '\(hexString)': red: \(red), green: \(green), blue: \(blue), alpha: \(alpha)") return Color(.sRGB, red: red, green: green, blue: blue, opacity: alpha) } diff --git a/clients/ios/Widget Extension/WidgetLoader.swift b/clients/ios/Widget Extension/WidgetLoader.swift index 7ad047c68..15d7901ff 100644 --- a/clients/ios/Widget Extension/WidgetLoader.swift +++ b/clients/ios/Widget Extension/WidgetLoader.swift @@ -46,17 +46,17 @@ class Loader: NSObject, URLSessionDataDelegate { completionHandler(.allow) } - + func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { - print("error: \(error.debugDescription)") + NSLog("error: \(error.debugDescription)") } - + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - print("data: \(data)") + NSLog("🚧 \(dataTask.currentRequest?.url?.path ?? "?") data: \(data)") receivedData.append(data) } - + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { if let error = error { completion(.failure(error))