Signal-iOS/SignalServiceKit/Util/OWSProgress.swift
2026-04-03 16:19:24 -05:00

946 lines
39 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
/// A way to report partial progress on async tasks back to the caller; typically used to drive
/// some kind of loading bar UI.
///
/// You create a ``OWSProgressSink``, add ``OWSProgressSource``(s) to that sink,
/// and update progress on the sources as the long running task does its work.
///
/// The API has three goals/principles:
/// 1. Progress from multiple independent sources can be combined into a single net total output progress
/// 2. Adding child progress sources is async-friendly and thread safe
/// 3. Updating a single source is *fast* but **NOT** thread safe
///
/// Of note, workers that increment progress are assumed to be single threaded (or have their own locking).
/// If your worker is multi-threaded, you should probably generate one source per thread or locking context.
///
/// First, call ``OWSProgress/createSink()`` to get a stream (or with an observer block) which is called with progress updates.
/// **WARNING**: the block is escaping and strongly held by OWSProgressSink. Beware of retain cycles.
///
/// Add one or more sources to the sink with ``OWSProgressSink/addSource(withLabel:unitCount:)``.
/// When you add a source to a sink, you update the sink's total unit count.
/// Units can mean anything; what matters is that updates to sources are measured in units,
/// and progress is reported in units. (Or percentage of units completed, via convenience var).
/// See ``Foundation/NSProgress``, which uses similar "unit" semantics.
///
/// You may add a child sink with ``OWSProgressSink/addChild(withLabel:unitCount:)``.
/// Child sinks have a unit count and can themselves have their own sources (and child sinks).
/// The completed unit count of a child sink is proportional to its children's completed unit count.
/// Put another way:
/// `parent_complete_units = parent_total_units * Sum(child_completed_units) / Sum(child_total_units)`
/// In this way a child's units are independent of its parent's (and counsins') units.
///
/// For example, say you download a file and then write rows to the db.
/// Add two child sinks: "Download" and "Write" , each with a unit count of 50.
/// Add a source to "Download" with unit count of [file byte length].
/// Add a source to "Write" with unit count of [# of rows to write to db].
/// In this way, even though "Download" and "Write" use totally different units, units of progress
/// at the root represent % complete with each counting towards 50% of the work.
/// If we download half the file, the root completed unit count would be 25 (%).
///
/// Note two other implicit advantages in the example above:
/// 1. We can determine the [# of SQL rows to write] _after_ downloading, by adding
/// the Write child at the start (proportioning 50% of the "progress" to it), but only
/// adding its source later after we've downloaded.
/// 2. A DownloadManager can have a download method that takes an ``OWSProgressSink``
/// without knowing or caring whether that sink is itself a root or a child; progress units are
/// re-normalized to parent progress units transparently to callers.
///
/// Labels _should be unique_ within the scope of the direct children of a given parent. Repeating labels
/// at the same level of the tree will replace the existing source/sink.
///
/// Using a unitCount of 0 is allowed, but potentially dangerous. An OWSProgress can only
/// reach 100% complete _once_. If you add a child or source with a unitCount of 0, it will
/// complete instantly. If it is the only child, it will complete its parent instantly, too. That
/// can potentially complete the root, which completes the whole progress and silences future updates.
///
/// A note on ``Foundation/NSProgress``.
/// This type _looks_ like NSProgress but behaves very differently.
/// * NSProgress is a class class meant to be updated and observed with KVO.
/// OWSProgress is a snapshot-in-time struct;OWSProgressSink manages observation.
/// * NSProgress uses locks for updates, making rapid updates on a single thread expensive.
/// OWSProgress optimizes for single-threaded updates; batching observer updates to do so efficiently.
/// * NSProgress requires you to know unit counts for all children up-front and they must all share units.
/// OWSProgress lets you add children lazily and renormalizes disparate units at each level of the tree.
public class OWSProgress: Equatable, SomeOWSProgress, CustomStringConvertible {
public class ChildProgress: Equatable, SomeOWSProgress {
/// The completed unit count of this particular source/sink.
/// The units DO NOT necessarily correspond to the units of the root OWSProgress.
public let completedUnitCount: UInt64
/// The total unit count of this particular source/sink.
/// The units DO NOT necessarily correspond to the units of the root OWSProgress.
public let totalUnitCount: UInt64
public let label: String
// Nil if the parent is the root
public let parentLabel: String?
public init(
completedUnitCount: UInt64,
totalUnitCount: UInt64,
label: String,
parentLabel: String?,
) {
self.completedUnitCount = completedUnitCount
self.totalUnitCount = totalUnitCount
self.label = label
self.parentLabel = parentLabel
}
public var percentComplete: Float {
roundProgressPercent(completedUnitCount: completedUnitCount, totalUnitCount: totalUnitCount)
}
public var isFinished: Bool {
return totalUnitCount == completedUnitCount
}
public static func ==(lhs: OWSProgress.ChildProgress, rhs: OWSProgress.ChildProgress) -> Bool {
return lhs.completedUnitCount == rhs.completedUnitCount
&& lhs.totalUnitCount == rhs.totalUnitCount
&& lhs.label == rhs.label
}
}
/// The completed unit count across all direct children.
public let completedUnitCount: UInt64
/// The total unit count of all direct children.
public let totalUnitCount: UInt64
public init(
completedUnitCount: UInt64,
totalUnitCount: UInt64,
childProgresses: [String: [ChildProgress]] = [:],
) {
self.completedUnitCount = completedUnitCount
self.totalUnitCount = totalUnitCount
self.childProgresses = childProgresses
self.rootChildProgresses = nil
}
fileprivate init(
completedUnitCount: UInt64,
totalUnitCount: UInt64,
rootChildProgresses: [String: [OWSProgressRootNode.ChildIdentifier: ChildProgress]],
) {
self.completedUnitCount = completedUnitCount
self.totalUnitCount = totalUnitCount
self.rootChildProgresses = rootChildProgresses
self.childProgresses = nil
}
public var description: String {
"OWSProgress: \(completedUnitCount)/\(totalUnitCount), \(percentComplete * 100)%"
}
public var percentComplete: Float {
if totalUnitCount > 0 {
return roundProgressPercent(
completedUnitCount: completedUnitCount,
totalUnitCount: totalUnitCount,
)
} else if
childProgresses?.isEmpty != false,
rootChildProgresses?.isEmpty != false
{
// With no children, don't count as complete.
return 0
} else {
// We have >1 children, but the count is 0, so the
// children must have a total count of 0.
// Complete instantly.
return 1
}
}
public var isFinished: Bool {
if totalUnitCount > 0 {
return totalUnitCount == completedUnitCount
} else if
childProgresses?.isEmpty != false,
rootChildProgresses?.isEmpty != false
{
// With no children, don't count as complete.
return false
} else {
// We have >1 children, but the count is 0, so the
// children must have a total count of 0.
// Complete instantly.
return true
}
}
private let childProgresses: [String: [ChildProgress]]?
private let rootChildProgresses: [String: [OWSProgressRootNode.ChildIdentifier: ChildProgress]]?
/// Get the latest progress for any source/sink at any layer of the progress tree.
/// Maps from source/child sink label to the progress of that node.
/// Note: if there are multiple children with the same label, will pick an
/// arbitrary child. In most cases, there will be just one child and this
/// is fine and this API is provided for simplicity.
/// If not, use `progressesForAllChildren` to get the full acounting
/// of duplicate labels.
public func progressForChild(label: String) -> ChildProgress? {
return rootChildProgresses?[label]?.first?.value
?? childProgresses?[label]?.first
}
/// Get the latest progress for any source/sink at any layer of the progress tree.
/// Maps from source/child sink label to the progress of all nodes with that label.
public func progressesForAllChildren(withLabel label: String) -> [ChildProgress] {
if let dict = rootChildProgresses?[label] {
return Array(dict.values)
} else {
return childProgresses?[label] ?? []
}
}
public var allChildProgresses: [ChildProgress] {
if let rootChildProgresses {
var progresses = [ChildProgress]()
for dict in rootChildProgresses.values {
for progress in dict.values {
progresses.append(progress)
}
}
return progresses
} else {
var progresses = [ChildProgress]()
for arr in (childProgresses ?? [:]).values {
for progress in arr {
progresses.append(progress)
}
}
return progresses
}
}
#if DEBUG
public static func forPreview(_ percentComplete: Float) -> OWSProgress {
return OWSProgress(completedUnitCount: UInt64(percentComplete * 100), totalUnitCount: 100)
}
#endif
public static var zero: OWSProgress {
return OWSProgress(completedUnitCount: 0, totalUnitCount: 0)
}
/// Create a root sink, taking the single observer block of progress updates.
/// See class docs for this type for usage.
public static func createSink(_ observer: @escaping (OWSProgress) async -> Void) -> OWSProgressSink {
let (sink, stream) = Self.createSink()
Task {
for await progress in stream {
await observer(progress)
}
}
return sink
}
/// Like ``createSink(_:)``, but instead of using an observer block to emit progress values, wraps callbacks in an AsyncStream.
public static func createSink() -> (OWSProgressSink, AsyncStream<OWSProgress>) {
var streamContinuation: AsyncStream<OWSProgress>.Continuation!
let stream = AsyncStream<OWSProgress> { continuation in
streamContinuation = continuation
}
let sink = OWSProgressRootNode(streamContinuation: streamContinuation)
return (sink, stream)
}
public static func ==(lhs: OWSProgress, rhs: OWSProgress) -> Bool {
return lhs.completedUnitCount == rhs.completedUnitCount
&& lhs.totalUnitCount == rhs.totalUnitCount
&& lhs.childProgresses == rhs.childProgresses
}
}
public protocol SomeOWSProgress {
var completedUnitCount: UInt64 { get }
var totalUnitCount: UInt64 { get }
/// Percentage completion measured as (completedUnitCount / totalUnitCount)
/// 0 if no children or sources have been added.
var percentComplete: Float { get }
/// Percent == 1. False if no children or sources have been added.
var isFinished: Bool { get }
}
extension SomeOWSProgress {
/// Unit count remaining measured as (totalUnitCount - completedUnitCount).
/// 0 if no children or sources have been added.
public var remainingUnitCount: UInt64 {
guard
totalUnitCount > 0,
totalUnitCount >= completedUnitCount
else {
return 0
}
return totalUnitCount - completedUnitCount
}
}
/// Sinks are thread-safe and can have children added from any thread context.
public protocol OWSProgressSink {
/// Add a child sink, returning it.
/// Child sinks contribute to the total unit count of their parent.
/// A child sink's progress is its own unit count weighted by the completed unit count across all its children.
///
/// Using a unitCount of 0 is allowed, but potentially dangerous. An OWSProgress can only
/// reach 100% complete _once_. If you add a child or source with a unitCount of 0, it will
/// complete instantly. If it is the only child, it will complete its parent instantly, too. That
/// can potentially complete the root, which completes the whole progress and silences future updates.
///
/// **WARNING** adding a child to a parent sink after some sibling has previously updated progress
/// results in undefined behavior; old progress values are not renormalized to new total unit counts.
/// Adding grandchildren is allowed; typically you want to "reserve" proportional unit counts
/// by adding a child up-front and then adding a grandchild to that child later.
func addChild(withLabel label: String, unitCount: UInt64) async -> OWSProgressSink
/// Add a source, returning it.
/// Sources contribute to the total unit count of their parent.
/// Sources are **NOT** thread-safe and should only be updated from a single thread or locking context.
///
/// Using a unitCount of 0 is allowed, but potentially dangerous. An OWSProgress can only
/// reach 100% complete _once_. If you add a child or source with a unitCount of 0, it will
/// complete instantly. If it is the only child, it will complete its parent instantly, too. That
/// can potentially complete the root, which completes the whole progress and silences future updates.
///
/// **WARNING** adding a source to a parent sink after some sibling has previously updated progress
/// results in undefined behavior; old progress values are not renormalized to new total unit counts.
/// Adding grandchildren is allowed; typically you want to "reserve" proportional unit counts
/// by adding a child up-front and then adding a source to that child later.
func addSource(withLabel label: String, unitCount: UInt64) async -> OWSProgressSource
}
/// Sources are **NOT** thread-safe and should only be updated from a single thread or locking context.
public protocol OWSProgressSource {
var completedUnitCount: UInt64 { get }
var totalUnitCount: UInt64 { get }
/// Increment the completed unit count (which can only go up).
/// You can pass 0, though that does nothing.
/// You can also continue to increment past the total unit count; the value
/// will be internally capped to the total and further updates no-op.
func incrementCompletedUnitCount(by increment: UInt64)
}
extension OWSProgressSource {
func complete() {
incrementCompletedUnitCount(by: totalUnitCount)
}
}
extension OWSProgressSource where Self: Sendable {
func asProgressBlock() -> OWSURLSession.ProgressBlock {
return { completedByteCount, totalByteCount in
if self.completedUnitCount < completedByteCount {
self.incrementCompletedUnitCount(by: UInt64(completedByteCount) - self.completedUnitCount)
}
}
}
/// Given some block of asynchronous work, update progress
/// on the current source periodically (every ``timeInterval`` seconds)
/// until the work block completes.
/// Returns with the result of the work block when it completes.
public func updatePeriodically<T, E>(
timeInterval: TimeInterval = 0.1,
estimatedTimeToCompletion: TimeInterval,
work: @escaping () async throws(E) -> T,
) async throws(E) -> T {
let sleepDurationMillis = UInt64(timeInterval * 1000)
let source = self
let didComplete = AtomicBool(false, lock: .init())
let startDate = Date()
var lastCompletedUnitCount = source.completedUnitCount
// Minus one so the timer can never complete it.
let maxTimerCompletedUnitCount = source.totalUnitCount - 1
let timeToUnitsMultiplier = Double(source.totalUnitCount) / estimatedTimeToCompletion
let result = await withTaskGroup(of: Optional<Result<T, E>>.self) { taskGroup in
taskGroup.addTask {
while !didComplete.get() {
try? await Task.sleep(nanoseconds: sleepDurationMillis * NSEC_PER_MSEC)
let date = Date()
var units = UInt64(date.timeIntervalSince(startDate) * timeToUnitsMultiplier)
units = min(maxTimerCompletedUnitCount, units)
defer { lastCompletedUnitCount = units }
let incrementalUnits = units - lastCompletedUnitCount
if incrementalUnits > 0 {
source.incrementCompletedUnitCount(by: units)
}
}
return nil
}
taskGroup.addTask {
let result: Result<T, E>
do {
result = .success(try await work())
} catch let error as E {
didComplete.set(true)
return .failure(error)
} catch {
// Impossible; work only throws E
fatalError()
}
didComplete.set(true)
source.incrementCompletedUnitCount(by: source.totalUnitCount)
return result
}
while let result = await taskGroup.next() {
switch result {
case .none:
break
case .some(let value):
return value
}
}
// Impossible to get here; the second task in the group
// always returns some result.
fatalError()
}
return try result.get()
}
}
// MARK: - Root Node
/// Root node for OWSProgress. Does not itself have a unit count or concept of progress;
/// its children define units entirely.
private actor OWSProgressRootNode: OWSProgressSink {
private let streamContinuation: AsyncStream<OWSProgress>.Continuation
private var observerQueue = SerialTaskQueue()
// Maps from node label to a weak reference to the node
// for all nodes at all layers of the tree.
private var allNodes = [ChildIdentifier: Weak<OWSProgressChildNode>]()
enum ParentLabel: Hashable, Equatable {
case root
case childSink(ChildIdentifier)
var label: String? {
switch self {
case .root: nil
case .childSink(let childIdentifier): childIdentifier.label
}
}
var identifier: ChildIdentifier? {
switch self {
case .root: nil
case .childSink(let identifier): identifier
}
}
}
/// A label is not enough to identify a child; we identify by the sequence
/// of labels starting at the root.
fileprivate struct ChildIdentifier: Hashable, Equatable {
let labelChain: [String]
var label: String { labelChain.last! }
var parentLabel: String? {
if labelChain.count > 1 {
return labelChain[labelChain.count - 2]
} else {
return nil
}
}
static func childOfRoot(label: String) -> ChildIdentifier {
return ChildIdentifier(labelChain: [label])
}
func appending(childLabel: String) -> ChildIdentifier {
return ChildIdentifier(labelChain: labelChain + [childLabel])
}
}
// Maps from parent label (or nil for root node) to labels of direct children.
private var childLabels = [ChildIdentifier?: Set<String>]()
// Maps from child label to label of parent.
private var parentLabels = [ChildIdentifier: ParentLabel]()
// Maps from parent label to the sum of unit counts of direct
// children. We cache this since it doesn't change often and
// saves the O(n) time spent adding on every progress update.
private var totalUnitCountOfChildren = [ChildIdentifier: UInt64]()
// Maps from parent label to the sum of unit counts of direct
// children. We cache this since it doesn't change often and
// saves the O(n) time spent adding on every progress update.
private var completedUnitCountOfChildren = [ChildIdentifier: UInt64]()
// Maps from node label to the last computed unit counts
// of nodes with that label, in its parent's units.
private var childProgresses = [String: [ChildIdentifier: OWSProgress.ChildProgress]]()
// We cache these values so that we can compute diffs efficiently.
private var totalUnitCountOfDirectChildren: UInt64 = 0
private var completedUnitCountOfDirectChildren: UInt64 = 0
fileprivate init(streamContinuation: AsyncStream<OWSProgress>.Continuation) {
self.streamContinuation = streamContinuation
}
func addChild(withLabel label: String, unitCount: UInt64) async -> OWSProgressSink {
let child = OWSProgressSinkNode(
label: label,
parent: self,
rootNode: self,
)
self.addChild(child, toParent: .root, unitCount: unitCount)
return child
}
func addSource(withLabel label: String, unitCount: UInt64) async -> OWSProgressSource {
let child = OWSProgressSourceNode(
label: label,
totalUnitCount: unitCount,
parent: self,
rootNode: self,
)
self.addChild(child, toParent: .root, unitCount: unitCount)
return child
}
fileprivate func progressDidUpdate(updatedNode: OWSProgressSourceNode) {
if updatedNode.isOrphaned {
// If the node was orphaned (replaced by another node
// created with the same label), stop. Updates from
// orphan labels are ignored.
return
}
guard
updatedNode.completedUnitCount
!= childProgresses[updatedNode.label]![updatedNode.identifier]!
.completedUnitCount
else {
// No change!
return
}
self.recursiveUpdateCompletedUnitCounts(
forNodeWithIdentifier: updatedNode.identifier,
newCompletedUnitCount: updatedNode.completedUnitCount,
)
let progress = OWSProgress(
completedUnitCount: self.completedUnitCountOfDirectChildren,
totalUnitCount: self.totalUnitCountOfDirectChildren,
rootChildProgresses: self.childProgresses,
)
observerQueue.enqueue { [streamContinuation, progress] in
streamContinuation.yield(progress)
if progress.isFinished {
streamContinuation.finish()
}
}
}
// MARK: - Child Updates
fileprivate func addChild(
_ child: OWSProgressChildNode,
toParent parentLabel: ParentLabel,
unitCount: UInt64,
) {
let label = child.label
let identifier = child.identifier
switch parentLabel {
case .root:
break
case .childSink(let parentLabel):
if (allNodes[parentLabel]?.value as? OWSProgressChildNode)?.isOrphaned == true {
// If the parent was orphaned (replaced by another node
// created with the same label), stop. The new node will
// point nowhere and be ignored.
return
}
}
if allNodes[child.identifier] != nil {
// Remove any existing children first.
self.removeChild(withIdentifier: identifier)
}
// First, add the node to its parent's child references.
self.allNodes[identifier] = Weak(value: child)
self.childLabels[parentLabel.identifier] = (self.childLabels[parentLabel.identifier] ?? Set()).union([label])
self.parentLabels[identifier] = parentLabel
if child is OWSProgressParentNode {
self.totalUnitCountOfChildren[identifier] = 0
self.completedUnitCountOfChildren[identifier] = 0
}
let childProgress = OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: unitCount,
label: label,
parentLabel: parentLabel.label,
)
var labelProgresses = self.childProgresses[label] ?? [:]
labelProgresses[identifier] = childProgress
self.childProgresses[label] = labelProgresses
// Update the parent's counts
switch parentLabel {
case .root:
self.totalUnitCountOfDirectChildren += unitCount
case .childSink(let parentIdentifier):
var totalUnitCountOfChildren = self.totalUnitCountOfChildren[parentIdentifier]!
totalUnitCountOfChildren += unitCount
self.totalUnitCountOfChildren[parentIdentifier] = totalUnitCountOfChildren
// Update the progress of the parent.
let oldParentProgress = childProgresses[parentIdentifier.label]![parentIdentifier]!
// The _parent's_ total unit count is unchanged.
let newParentTotalUnitCount = oldParentProgress.totalUnitCount
// The parent's completed unit count changes proportionally.
let newParentCompletedUnitCount: UInt64
if totalUnitCountOfChildren == 0 {
newParentCompletedUnitCount = newParentTotalUnitCount
} else {
newParentCompletedUnitCount = renormalizeCompletedUnitCount(
childrensCompletedUnitCount: completedUnitCountOfChildren[parentIdentifier]!,
childrensTotalUnitCount: totalUnitCountOfChildren,
parentTotalUnitCount: newParentTotalUnitCount,
)
}
// Now update the progress values all the way up the tree.
self.recursiveUpdateCompletedUnitCounts(
forNodeWithIdentifier: parentIdentifier,
newCompletedUnitCount: newParentCompletedUnitCount,
)
}
// Lastly recompute and emit progress
let progress = OWSProgress(
completedUnitCount: completedUnitCountOfDirectChildren,
totalUnitCount: totalUnitCountOfDirectChildren,
rootChildProgresses: childProgresses,
)
observerQueue.enqueue { [streamContinuation, progress] in
streamContinuation.yield(progress)
if progress.isFinished {
streamContinuation.finish()
}
}
}
fileprivate func removeChild(
withIdentifier identifier: ChildIdentifier,
) {
// Mark the child and its children orphaned; future updates to it
// will be ignored.
var identifiersToMarkOrphaned = Set(arrayLiteral: identifier)
while let identifierToMarkOrphaned = identifiersToMarkOrphaned.popFirst() {
let childLabels = self.childLabels[identifierToMarkOrphaned] ?? Set()
identifiersToMarkOrphaned.formUnion(childLabels.map({ identifierToMarkOrphaned.appending(childLabel: $0) }))
allNodes[identifierToMarkOrphaned]?.value?.isOrphaned = true
}
// Mark all its childre
guard
let parentLabel = self.parentLabels[identifier],
let removedNodeProgress = self.childProgresses[identifier.label]?[identifier]
else {
owsFailDebug("Removing a label that didn't exist?")
return
}
// First, remove the node from its parent's child references.
var childrenOfParent = self.childLabels[parentLabel.identifier]
childrenOfParent?.remove(identifier.label)
self.childLabels[parentLabel.identifier] = childrenOfParent
// Next, update the progress on the parent.
switch parentLabel {
case .root:
self.totalUnitCountOfDirectChildren -= removedNodeProgress.totalUnitCount
self.completedUnitCountOfDirectChildren -= removedNodeProgress.completedUnitCount
case .childSink(let parentIdentifier):
// The direct parent's update is special; we've affected the total
// unit count of its children as well as the completed unit count
// of its children. This does NOT affect the total unit count
// in _its_ parent, so once we compute the direct parent's new
// completed unit count we can update recursively up the tree as normal.
var totalUnitCountOfChildren = self.totalUnitCountOfChildren[parentIdentifier]!
totalUnitCountOfChildren -= removedNodeProgress.totalUnitCount
self.totalUnitCountOfChildren[parentIdentifier] = totalUnitCountOfChildren
var completedUnitCountOfChildren = self.completedUnitCountOfChildren[parentIdentifier]!
completedUnitCountOfChildren -= removedNodeProgress.completedUnitCount
self.completedUnitCountOfChildren[parentIdentifier] = completedUnitCountOfChildren
// Update the progress of the parent.
let oldParentProgress = childProgresses[parentIdentifier.label]![parentIdentifier]!
// The _parent's_ total unit count is unchanged.
let newParentTotalUnitCount = oldParentProgress.totalUnitCount
// The parent's completed unit count changes proportionally.
let newParentCompletedUnitCount: UInt64
if totalUnitCountOfChildren == 0 {
newParentCompletedUnitCount = newParentTotalUnitCount
} else {
newParentCompletedUnitCount = renormalizeCompletedUnitCount(
childrensCompletedUnitCount: completedUnitCountOfChildren,
childrensTotalUnitCount: totalUnitCountOfChildren,
parentTotalUnitCount: newParentTotalUnitCount,
)
}
// Now update the progress values all the way up the tree.
self.recursiveUpdateCompletedUnitCounts(
forNodeWithIdentifier: parentIdentifier,
newCompletedUnitCount: newParentCompletedUnitCount,
)
}
// Last, remove it and all its children from our references.
var identifiersToRemove = Set<ChildIdentifier>(arrayLiteral: identifier)
while let identifierToRemove = identifiersToRemove.popFirst() {
let childLabels = self.childLabels[identifierToRemove] ?? Set()
identifiersToRemove.formUnion(childLabels.map({ identifierToRemove.appending(childLabel: $0) }))
self.allNodes.removeValue(forKey: identifierToRemove)
self.childLabels.removeValue(forKey: identifierToRemove)
self.parentLabels.removeValue(forKey: identifierToRemove)
self.totalUnitCountOfChildren.removeValue(forKey: identifierToRemove)
self.completedUnitCountOfChildren.removeValue(forKey: identifierToRemove)
var progressesForLabel = self.childProgresses[identifierToRemove.label] ?? [:]
progressesForLabel.removeValue(forKey: identifierToRemove)
self.childProgresses[identifierToRemove.label] = progressesForLabel
}
}
private func recursiveUpdateCompletedUnitCounts(
forNodeWithIdentifier identifier: ChildIdentifier,
newCompletedUnitCount: UInt64,
) {
var progressesForLabel = self.childProgresses[identifier.label]!
let oldChildProgress = progressesForLabel[identifier]!
let newChildProgress = OWSProgress.ChildProgress(
completedUnitCount: newCompletedUnitCount,
totalUnitCount: oldChildProgress.totalUnitCount,
label: identifier.label,
parentLabel: identifier.parentLabel,
)
progressesForLabel[identifier] = newChildProgress
self.childProgresses[identifier.label] = progressesForLabel
switch self.parentLabels[identifier]! {
case .root:
self.completedUnitCountOfDirectChildren -= oldChildProgress.completedUnitCount
self.completedUnitCountOfDirectChildren += newChildProgress.completedUnitCount
// Done.
return
case .childSink(let parentIdentifier):
// Update progress on the parent and then call recursively
var completedUnitCountOfChildren = self.completedUnitCountOfChildren[parentIdentifier]!
completedUnitCountOfChildren -= oldChildProgress.completedUnitCount
completedUnitCountOfChildren += newChildProgress.completedUnitCount
self.completedUnitCountOfChildren[parentIdentifier] = completedUnitCountOfChildren
let totalUnitCountOfChildren = self.totalUnitCountOfChildren[parentIdentifier]!
let totalUnitCount = self.childProgresses[parentIdentifier.label]![parentIdentifier]!.totalUnitCount
let newParentCompletedUnitCount = renormalizeCompletedUnitCount(
childrensCompletedUnitCount: completedUnitCountOfChildren,
childrensTotalUnitCount: totalUnitCountOfChildren,
parentTotalUnitCount: totalUnitCount,
)
return self.recursiveUpdateCompletedUnitCounts(
forNodeWithIdentifier: parentIdentifier,
newCompletedUnitCount: newParentCompletedUnitCount,
)
}
}
}
// MARK: - Private protocols
private protocol OWSProgressNode {}
private protocol OWSProgressParentNode: OWSProgressNode {}
private protocol OWSProgressChildNode: OWSProgressNode {
var label: String { get }
var identifier: OWSProgressRootNode.ChildIdentifier { get }
var parent: OWSProgressParentNode { get }
/// Should only be read from root node's isolation context.
var isOrphaned: Bool { get set }
}
extension OWSProgressRootNode: OWSProgressParentNode {}
// MARK: - Node implementations
/// A sink that is itself a child to another sink.
private class OWSProgressSinkNode: OWSProgressSink, OWSProgressParentNode, OWSProgressChildNode {
var isOrphaned: Bool = false
var label: String
var identifier: OWSProgressRootNode.ChildIdentifier
let parent: OWSProgressParentNode
let rootNode: OWSProgressRootNode
init(
label: String,
parent: OWSProgressParentNode,
rootNode: OWSProgressRootNode,
) {
self.label = label
self.parent = parent
self.rootNode = rootNode
self.identifier = (parent as? OWSProgressChildNode)?
.identifier
.appending(childLabel: label)
?? .childOfRoot(label: label)
}
func addChild(withLabel label: String, unitCount: UInt64) async -> OWSProgressSink {
let child = OWSProgressSinkNode(
label: label,
parent: self,
rootNode: rootNode,
)
// Call up to the parent to utilize its isolation context
await rootNode.addChild(child, toParent: .childSink(self.identifier), unitCount: unitCount)
return child
}
func addSource(withLabel label: String, unitCount: UInt64) async -> OWSProgressSource {
let child = OWSProgressSourceNode(
label: label,
totalUnitCount: unitCount,
parent: self,
rootNode: rootNode,
)
// Call up to the parent to utilize its isolation context
await rootNode.addChild(child, toParent: .childSink(self.identifier), unitCount: unitCount)
return child
}
}
private class OWSProgressSourceNode: OWSProgressSource, OWSProgressChildNode {
var isOrphaned: Bool = false
var label: String
var identifier: OWSProgressRootNode.ChildIdentifier
let totalUnitCount: UInt64
var completedUnitCount: UInt64
let parent: OWSProgressParentNode
let rootNode: OWSProgressRootNode
init(
label: String,
totalUnitCount: UInt64,
parent: OWSProgressParentNode,
rootNode: OWSProgressRootNode,
) {
self.label = label
self.parent = parent
self.rootNode = rootNode
self.totalUnitCount = totalUnitCount
self.completedUnitCount = 0
self.identifier = (parent as? OWSProgressChildNode)?
.identifier
.appending(childLabel: label)
?? .childOfRoot(label: label)
}
func incrementCompletedUnitCount(by increment: UInt64) {
let incrementedUnitCount: UInt64 = {
if UInt64.max - increment < completedUnitCount {
// Avoid UInt64 overflow, if necessary.
return .max
}
return completedUnitCount + increment
}()
completedUnitCount = min(
totalUnitCount,
incrementedUnitCount,
)
emitProgressIfNeeded()
}
/// Tracks whether an async progress update task has been scheduled
/// but not run yet; if true further calls to ``emitProgressIfNeeded``
/// will early exit.
private var dirtyBit = false
fileprivate func emitProgressIfNeeded() {
guard !dirtyBit else {
return
}
dirtyBit = true
// Retain self, so that if the caller updates progress
// to 100% then discards the reference to self, its
// still retained long enough to update observers.
Task { [self, rootNode] in
// It looks risky to write this value from an
// arbitrary task thread; but because we read
// the progress value after setting this it should
// never result in missed updates (just additional
// unecessary updates).
self.dirtyBit = false
await rootNode.progressDidUpdate(updatedNode: self)
}
}
}
private func roundProgressPercent(
completedUnitCount: UInt64,
totalUnitCount: UInt64,
) -> Float {
if totalUnitCount == 0 {
// The unit count assigned to the node is 0 so it
// is instantly complete.
return 1
}
if completedUnitCount >= totalUnitCount {
return 1
}
let rawPercent = Float(completedUnitCount) / Float(totalUnitCount)
if rawPercent >= 0.99 {
// Never round 99% and above to 100%. Cap at 0.99.
return 0.99
}
return rawPercent
}
private func renormalizeCompletedUnitCount(
childrensCompletedUnitCount: UInt64,
childrensTotalUnitCount: UInt64,
parentTotalUnitCount: UInt64,
) -> UInt64 {
if parentTotalUnitCount == 0 {
return 0
}
if childrensCompletedUnitCount >= childrensTotalUnitCount {
return parentTotalUnitCount
}
let rawPercent = Double(childrensCompletedUnitCount) / Double(childrensTotalUnitCount)
let rawUnitCount = UInt64(ceil(Double(parentTotalUnitCount) * rawPercent))
if rawUnitCount == parentTotalUnitCount {
// Never round up to 100%; 100% is caught by the >= check above
// and the most we should return is 99%.
return rawUnitCount - 1
} else {
return rawUnitCount
}
}