TM-SGNL-iOS/SignalServiceKit/Util/ReadyFlag.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

266 lines
8.4 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
// We often want to enqueue work that should only be performed
// once a certain milestone is reached. e.g. when the app
// "becomes ready" or when the user's account is "registered
// and ready", etc.
//
// This class provides the following functionality:
//
// * A boolean flag whose value can be consulted from any thread.
// * "Will become ready" and "did become ready" blocks that are
// performed immediately if the flag is already set or later
// when the flag is set.
// * Additionally, there's support for a "polite" flavor of "did
// become ready" block which are performed with slight delays
// to avoid a stampede which could block the main thread. One
// of the risks there is 0x8badf00d crashes.
// * The flag can be used in various "queue modes". "App readiness"
// blocks should be enqueued and performed on the main thread.
// Other flags will want to do their work off the main thread.
@objc
public class ReadyFlag: NSObject {
private let unfairLock = UnfairLock()
public typealias ReadyBlock = @MainActor () -> Void
private struct ReadyTask {
let label: String?
let block: ReadyBlock
var displayLabel: String {
label ?? "unknown"
}
}
private let name: String
private static let blockLogDuration: TimeInterval = 0.01
private static let groupLogDuration: TimeInterval = 0.1
// This property should only be set with unfairLock.
// It can be read from any queue.
private let flag = AtomicBool(false, lock: .sharedGlobal)
// This property should only be accessed with unfairLock.
private var willBecomeReadyTasks = [ReadyTask]()
// This property should only be accessed with unfairLock.
private var didBecomeReadySyncTasks = [ReadyTask]()
// This property should only be accessed with unfairLock.
private var didBecomeReadyAsyncTasks = [ReadyTask]()
@objc
public init(name: String) {
self.name = name
}
@objc
public var isSet: Bool {
flag.get()
}
@MainActor
public func runNowOrWhenWillBecomeReady(
_ readyBlock: @escaping ReadyBlock,
label: String? = nil
) {
let task = ReadyTask(label: label, block: readyBlock)
let didEnqueue: Bool = {
unfairLock.withLock {
guard !isSet else {
return false
}
willBecomeReadyTasks.append(task)
return true
}
}()
if !didEnqueue {
// We perform the block outside unfairLock to avoid deadlock.
BenchManager.bench(
title: self.name + ".willBecomeReady " + task.displayLabel,
logIfLongerThan: Self.blockLogDuration,
logInProduction: true
) {
autoreleasepool {
task.block()
}
}
}
}
@MainActor
public func runNowOrWhenDidBecomeReadySync(
_ readyBlock: @escaping ReadyBlock,
label: String? = nil
) {
let task = ReadyTask(label: label, block: readyBlock)
let didEnqueue: Bool = {
unfairLock.withLock {
guard !isSet else {
return false
}
didBecomeReadySyncTasks.append(task)
return true
}
}()
if !didEnqueue {
// We perform the block outside unfairLock to avoid deadlock.
BenchManager.bench(
title: self.name + ".didBecomeReady " + task.displayLabel,
logIfLongerThan: Self.blockLogDuration,
logInProduction: true
) {
autoreleasepool {
task.block()
}
}
}
}
public func runNowOrWhenDidBecomeReadyAsync(
_ readyBlock: @escaping ReadyBlock,
label: String? = nil
) {
AssertIsOnMainThread()
let task = ReadyTask(label: label, block: readyBlock)
let didEnqueue: Bool = {
unfairLock.withLock {
guard !isSet else {
return false
}
didBecomeReadyAsyncTasks.append(task)
return true
}
}()
if !didEnqueue {
// We perform the block outside unfairLock to avoid deadlock.
//
// Always perform async blocks async.
DispatchQueue.main.async { () -> Void in
BenchManager.bench(
title: self.name + ".didBecomeReadyPolite " + task.displayLabel,
logIfLongerThan: Self.blockLogDuration,
logInProduction: true
) {
autoreleasepool {
task.block()
}
}
}
}
}
@MainActor
public func setIsReady() {
guard let tasksToPerform = tryToSetFlag() else {
return
}
Logger.info("\(self.name)")
let willBecomeReadyTasks = tasksToPerform.willBecomeReadyTasks
let didBecomeReadySyncTasks = tasksToPerform.didBecomeReadySyncTasks
let didBecomeReadyAsyncTasks = tasksToPerform.didBecomeReadyAsyncTasks
// We bench the blocks individually and as a group.
BenchManager.bench(title: self.name + ".willBecomeReady group",
logIfLongerThan: Self.groupLogDuration,
logInProduction: true) {
for task in willBecomeReadyTasks {
BenchManager.bench(
title: self.name + ".willBecomeReady " + task.displayLabel,
logIfLongerThan: Self.blockLogDuration,
logInProduction: true
) {
autoreleasepool {
task.block()
}
}
}
}
BenchManager.bench(
title: self.name + ".didBecomeReady group",
logIfLongerThan: Self.groupLogDuration,
logInProduction: true
) {
for task in didBecomeReadySyncTasks {
BenchManager.bench(
title: self.name + ".didBecomeReady " + task.displayLabel,
logIfLongerThan: Self.blockLogDuration,
logInProduction: true
) {
autoreleasepool {
task.block()
}
}
}
}
self.performDidBecomeReadyAsyncTasks(didBecomeReadyAsyncTasks)
}
private struct TasksToPerform {
let willBecomeReadyTasks: [ReadyTask]
let didBecomeReadySyncTasks: [ReadyTask]
let didBecomeReadyAsyncTasks: [ReadyTask]
}
private func tryToSetFlag() -> TasksToPerform? {
unfairLock.withLock {
guard flag.tryToSetFlag() else {
// We can only set the flag once. If it's already set,
// ensure that
owsAssertDebug(willBecomeReadyTasks.isEmpty)
owsAssertDebug(didBecomeReadySyncTasks.isEmpty)
owsAssertDebug(didBecomeReadyAsyncTasks.isEmpty)
return nil
}
let tasksToPerform = TasksToPerform(
willBecomeReadyTasks: self.willBecomeReadyTasks,
didBecomeReadySyncTasks: self.didBecomeReadySyncTasks,
didBecomeReadyAsyncTasks: self.didBecomeReadyAsyncTasks
)
self.willBecomeReadyTasks = []
self.didBecomeReadySyncTasks = []
self.didBecomeReadyAsyncTasks = []
return tasksToPerform
}
}
private func performDidBecomeReadyAsyncTasks(_ tasks: [ReadyTask]) {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.025) { [weak self] in
guard let self = self else {
return
}
guard let task = tasks.first else {
return
}
BenchManager.bench(
title: self.name + ".didBecomeReadyPolite " + task.displayLabel,
logIfLongerThan: Self.blockLogDuration,
logInProduction: true,
block: task.block
)
let remainder = Array(tasks.suffix(from: 1))
self.performDidBecomeReadyAsyncTasks(remainder)
}
}
}