// // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation import SignalCoreKit @objc public class AppReadiness: NSObject { private static let shared = AppReadiness() public typealias BlockType = () -> Void private override init() { super.init() SwiftSingletons.register(self) } private let readyFlag = ReadyFlag(name: "AppReadiness") private let readyFlagUI = ReadyFlag(name: "AppReadinessUI") @objc public static var isAppReady: Bool { shared.readyFlag.isSet } @objc public static var isUIReady: Bool { shared.readyFlagUI.isSet } @objc public static func setAppIsReady() { AssertIsOnMainThread() owsAssertDebug(!shared.readyFlag.isSet) owsAssertDebug(!shared.readyFlagUI.isSet) shared.readyFlag.setIsReady() shared.readyFlagUI.setIsReady() } @objc public static func setAppIsReadyUIStillPending() { AssertIsOnMainThread() owsAssertDebug(!shared.readyFlag.isSet) shared.readyFlag.setIsReady() } @objc public static func setUIIsReady() { AssertIsOnMainThread() shared.readyFlagUI.setIsReady() } // MARK: - Readiness Blocks // If the app and it's UI is ready, the block is called immediately; // otherwise it is called when the app and the UI becomes ready. // // The block will always be called on the main thread. // // * The "will become ready" blocks are called before the "did become ready" blocks. // * The "will become ready" blocks should be used for internal setup of components // so that they are ready to interact with other components of the system. // * The "will become ready" blocks should never use other components of the system. // // * The "did become ready" blocks should be used for any work that should be done // on app launch, especially work that uses other components. // * We should usually use "did become ready" blocks since they are safer. // // * We should use the "polite" flavor of "did become ready" blocks when the work // can be safely delayed for a second or two after the app becomes ready. // * We should use the "polite" flavor of "did become ready" blocks wherever possible // since they avoid a stampede of activity on launch. private static let defaultPriority: ReadyFlag.Priority = 0 public static func runNowOrWhenAppWillBecomeReady(_ block: @escaping BlockType, file: String = #file, function: String = #function, line: Int = #line) { let label = Self.buildLabel(file: file, function: function, line: line) let priority = Self.defaultPriority DispatchMainThreadSafe { shared.runNowOrWhenAppWillBecomeReady(block, label: label, priority: priority) } } @objc @available(swift, obsoleted: 1.0) static func runNowOrWhenAppWillBecomeReady(_ block: @escaping BlockType, label: String) { let priority = Self.defaultPriority DispatchMainThreadSafe { shared.runNowOrWhenAppWillBecomeReady(block, label: label, priority: priority) } } private func runNowOrWhenAppWillBecomeReady(_ block: @escaping BlockType, label: String, priority: ReadyFlag.Priority) { AssertIsOnMainThread() guard !CurrentAppContext().isRunningTests else { // We don't need to do any "on app ready" work in the tests. return } readyFlag.runNowOrWhenWillBecomeReady(block, label: label, priority: priority) } // MARK: - public static func runNowOrWhenUIDidBecomeReadySync( _ block: @escaping BlockType, file: String = #file, function: String = #function, line: Int = #line ) { let label = Self.buildLabel(file: file, function: function, line: line) let priority = Self.defaultPriority DispatchMainThreadSafe { shared.runNowOrWhenAppDidBecomeReadySync(block, flag: shared.readyFlagUI, label: label, priority: priority) } } public static func runNowOrWhenAppDidBecomeReadySync( _ block: @escaping BlockType, file: String = #file, function: String = #function, line: Int = #line ) { let label = Self.buildLabel(file: file, function: function, line: line) let priority = Self.defaultPriority DispatchMainThreadSafe { shared.runNowOrWhenAppDidBecomeReadySync(block, flag: shared.readyFlag, label: label, priority: priority) } } @objc @available(swift, obsoleted: 1.0) public static func runNowOrWhenUIDidBecomeReadySync(_ block: @escaping BlockType, label: String) { let priority = Self.defaultPriority DispatchMainThreadSafe { shared.runNowOrWhenAppDidBecomeReadySync(block, flag: shared.readyFlagUI, label: label, priority: priority) } } @objc @available(swift, obsoleted: 1.0) public static func runNowOrWhenAppDidBecomeReadySync(_ block: @escaping BlockType, label: String) { let priority = Self.defaultPriority DispatchMainThreadSafe { shared.runNowOrWhenAppDidBecomeReadySync(block, flag: shared.readyFlag, label: label, priority: priority) } } private func runNowOrWhenAppDidBecomeReadySync( _ block: @escaping BlockType, flag: ReadyFlag, label: String, priority: ReadyFlag.Priority ) { AssertIsOnMainThread() guard !CurrentAppContext().isRunningTests else { // We don't need to do any "on app ready" work in the tests. return } flag.runNowOrWhenDidBecomeReadySync(block, label: label, priority: priority) } // MARK: - // We now have many (36+ in best case; many more in worst case) // "app did become ready" blocks, many of which // perform database writes. This can cause a "stampede" of writes // as the app becomes ready. This can lead to 0x8badf00d crashes // as the main thread can block behind these writes while trying // to perform a checkpoint. The risk is highest on old devices // with large databases. It can also simply cause the main thread // to be less responsive. // // Most "App did become ready" blocks should be performed _soon_ // after launch but don't need to be performed sync. Therefore // any blocks we // perform them one-by-one with slight delays between them to // reduce the risk of starving the main thread, especially if // any given block is expensive. public static func runNowOrWhenAppDidBecomeReadyAsync(_ block: @escaping BlockType, file: String = #file, function: String = #function, line: Int = #line) { let label = Self.buildLabel(file: file, function: function, line: line) runNowOrWhenAppDidBecomeReadyAsync(block, label: label) } @objc static func runNowOrWhenAppDidBecomeReadyAsync(_ block: @escaping BlockType, label: String) { let priority = Self.defaultPriority DispatchMainThreadSafe { shared.runNowOrWhenAppDidBecomeReadyAsync(block, label: label, priority: priority) } } public static func runNowOrWhenMainAppDidBecomeReadyAsync(_ block: @escaping BlockType, file: String = #file, function: String = #function, line: Int = #line) { runNowOrWhenAppDidBecomeReadyAsync({ guard CurrentAppContext().isMainApp else { return } block() }, file: file, function: function, line: line ) } @objc @available(swift, obsoleted: 1.0) static func runNowOrWhenMainAppDidBecomeReadyAsync(_ block: @escaping BlockType, label: String) { runNowOrWhenAppDidBecomeReadyAsync({ guard CurrentAppContext().isMainApp else { return } block() }, label: label) } private func runNowOrWhenAppDidBecomeReadyAsync(_ block: @escaping BlockType, label: String, priority: ReadyFlag.Priority) { AssertIsOnMainThread() guard !CurrentAppContext().isRunningTests else { // We don't need to do any "on app ready" work in the tests. return } readyFlag.runNowOrWhenDidBecomeReadyAsync(block, label: label, priority: priority) } private static func buildLabel(file: String, function: String, line: Int) -> String { let filename = (file as NSString).lastPathComponent // We format the filename & line number in a format compatible // with XCode's "Open Quickly..." feature. return "[\(filename):\(line) \(function)]" } }