From ae0d724f2afa62085775c9fab3fe299f1c9429a5 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 18 Apr 2025 14:35:30 -0700 Subject: [PATCH 01/85] move files to FoundationPreview --- .../ProgressReporter/ProgressFraction.swift | 277 ++++++++++ .../ProgressReporter+FileFormatStyle.swift | 108 ++++ .../ProgressReporter+FormatStyle.swift | 117 +++++ .../ProgressReporter+Interop.swift | 117 +++++ .../ProgressReporter+Progress.swift | 57 ++ .../ProgressReporter+Properties.swift | 47 ++ .../ProgressReporter/ProgressReporter.swift | 488 ++++++++++++++++++ 7 files changed, 1211 insertions(+) create mode 100644 Sources/FoundationEssentials/ProgressReporter/ProgressFraction.swift create mode 100644 Sources/FoundationEssentials/ProgressReporter/ProgressReporter+FileFormatStyle.swift create mode 100644 Sources/FoundationEssentials/ProgressReporter/ProgressReporter+FormatStyle.swift create mode 100644 Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift create mode 100644 Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Progress.swift create mode 100644 Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift create mode 100644 Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressFraction.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressFraction.swift new file mode 100644 index 000000000..0e3734cd5 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressFraction.swift @@ -0,0 +1,277 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +internal import _ForSwiftFoundation + +internal struct _ProgressFraction : Sendable, Equatable, CustomDebugStringConvertible { + var completed : Int + var total : Int + private(set) var overflowed : Bool + + init() { + completed = 0 + total = 0 + overflowed = false + } + + init(double: Double, overflow: Bool = false) { + if double == 0 { + self.completed = 0 + self.total = 1 + } else if double == 1 { + self.completed = 1 + self.total = 1 + } + (self.completed, self.total) = _ProgressFraction._fromDouble(double) + self.overflowed = overflow + } + + init(completed: Int, total: Int?) { + if let total { + self.total = total + self.completed = completed + } else { + self.total = 0 + self.completed = completed + } + self.overflowed = false + } + + // ---- + + // Glue code for _NSProgressFraction and _ProgressFraction + init(nsProgressFraction: _NSProgressFraction) { + self.init(completed: Int(nsProgressFraction.completed), total: Int(nsProgressFraction.total)) + } + + internal mutating func simplify() { + if self.total == 0 { + return + } + + (self.completed, self.total) = _ProgressFraction._simplify(completed, total) + } + + internal func simplified() -> _ProgressFraction { + let simplified = _ProgressFraction._simplify(completed, total) + return _ProgressFraction(completed: simplified.0, total: simplified.1) + } + + static private func _math(lhs: _ProgressFraction, rhs: _ProgressFraction, whichOperator: (_ lhs : Double, _ rhs : Double) -> Double, whichOverflow : (_ lhs: Int, _ rhs: Int) -> (Int, overflow: Bool)) -> _ProgressFraction { + // Mathematically, it is nonsense to add or subtract something with a denominator of 0. However, for the purposes of implementing Progress' fractions, we just assume that a zero-denominator fraction is "weightless" and return the other value. We still need to check for the case where they are both nonsense though. + precondition(!(lhs.total == 0 && rhs.total == 0), "Attempt to add or subtract invalid fraction") + guard lhs.total != 0 else { + return rhs + } + guard rhs.total != 0 else { + return lhs + } + + guard !lhs.overflowed && !rhs.overflowed else { + // If either has overflowed already, we preserve that + return _ProgressFraction(double: whichOperator(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } + + //TODO: rdar://148758226 Overflow check + if let lcm = _leastCommonMultiple(lhs.total, rhs.total) { + let result = whichOverflow(lhs.completed * (lcm / lhs.total), rhs.completed * (lcm / rhs.total)) + if result.overflow { + return _ProgressFraction(double: whichOperator(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } else { + return _ProgressFraction(completed: result.0, total: lcm) + } + } else { + // Overflow - simplify and then try again + let lhsSimplified = lhs.simplified() + let rhsSimplified = rhs.simplified() + + if let lcm = _leastCommonMultiple(lhsSimplified.total, rhsSimplified.total) { + let result = whichOverflow(lhsSimplified.completed * (lcm / lhsSimplified.total), rhsSimplified.completed * (lcm / rhsSimplified.total)) + if result.overflow { + // Use original lhs/rhs here + return _ProgressFraction(double: whichOperator(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } else { + return _ProgressFraction(completed: result.0, total: lcm) + } + } else { + // Still overflow + return _ProgressFraction(double: whichOperator(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } + } + } + + static internal func +(lhs: _ProgressFraction, rhs: _ProgressFraction) -> _ProgressFraction { + return _math(lhs: lhs, rhs: rhs, whichOperator: +, whichOverflow: { $0.addingReportingOverflow($1) }) + } + + static internal func -(lhs: _ProgressFraction, rhs: _ProgressFraction) -> _ProgressFraction { + return _math(lhs: lhs, rhs: rhs, whichOperator: -, whichOverflow: { $0.subtractingReportingOverflow($1) }) + } + + static internal func *(lhs: _ProgressFraction, rhs: _ProgressFraction) -> _ProgressFraction { + guard !lhs.overflowed && !rhs.overflowed else { + // If either has overflowed already, we preserve that + return _ProgressFraction(double: rhs.fractionCompleted * rhs.fractionCompleted, overflow: true) + } + + let newCompleted = lhs.completed.multipliedReportingOverflow(by: rhs.completed) + let newTotal = lhs.total.multipliedReportingOverflow(by: rhs.total) + + if newCompleted.overflow || newTotal.overflow { + // Try simplifying, then do it again + let lhsSimplified = lhs.simplified() + let rhsSimplified = rhs.simplified() + + let newCompletedSimplified = lhsSimplified.completed.multipliedReportingOverflow(by: rhsSimplified.completed) + let newTotalSimplified = lhsSimplified.total.multipliedReportingOverflow(by: rhsSimplified.total) + + if newCompletedSimplified.overflow || newTotalSimplified.overflow { + // Still overflow + return _ProgressFraction(double: lhs.fractionCompleted * rhs.fractionCompleted, overflow: true) + } else { + return _ProgressFraction(completed: newCompletedSimplified.0, total: newTotalSimplified.0) + } + } else { + return _ProgressFraction(completed: newCompleted.0, total: newTotal.0) + } + } + + static internal func /(lhs: _ProgressFraction, rhs: Int) -> _ProgressFraction { + guard !lhs.overflowed else { + // If lhs has overflowed, we preserve that + return _ProgressFraction(double: lhs.fractionCompleted / Double(rhs), overflow: true) + } + + let newTotal = lhs.total.multipliedReportingOverflow(by: rhs) + + if newTotal.overflow { + let simplified = lhs.simplified() + + let newTotalSimplified = simplified.total.multipliedReportingOverflow(by: rhs) + + if newTotalSimplified.overflow { + // Still overflow + return _ProgressFraction(double: lhs.fractionCompleted / Double(rhs), overflow: true) + } else { + return _ProgressFraction(completed: lhs.completed, total: newTotalSimplified.0) + } + } else { + return _ProgressFraction(completed: lhs.completed, total: newTotal.0) + } + } + + static internal func ==(lhs: _ProgressFraction, rhs: _ProgressFraction) -> Bool { + if lhs.isNaN || rhs.isNaN { + // NaN fractions are never equal + return false + } else if lhs.completed == rhs.completed && lhs.total == rhs.total { + return true + } else if lhs.total == rhs.total { + // Direct comparison of numerator + return lhs.completed == rhs.completed + } else if lhs.completed == 0 && rhs.completed == 0 { + return true + } else if lhs.completed == lhs.total && rhs.completed == rhs.total { + // Both finished (1) + return true + } else if (lhs.completed == 0 && rhs.completed != 0) || (lhs.completed != 0 && rhs.completed == 0) { + // One 0, one not 0 + return false + } else { + // Cross-multiply + let left = lhs.completed.multipliedReportingOverflow(by: rhs.total) + let right = lhs.total.multipliedReportingOverflow(by: rhs.completed) + + if !left.overflow && !right.overflow { + if left.0 == right.0 { + return true + } + } else { + // Try simplifying then cross multiply again + let lhsSimplified = lhs.simplified() + let rhsSimplified = rhs.simplified() + + let leftSimplified = lhsSimplified.completed.multipliedReportingOverflow(by: rhsSimplified.total) + let rightSimplified = lhsSimplified.total.multipliedReportingOverflow(by: rhsSimplified.completed) + + if !leftSimplified.overflow && !rightSimplified.overflow { + if leftSimplified.0 == rightSimplified.0 { + return true + } + } else { + // Ok... fallback to doubles. This doesn't use an epsilon + return lhs.fractionCompleted == rhs.fractionCompleted + } + } + } + + return false + } + + // ---- + + internal var isFinished: Bool { + return completed >= total && completed > 0 && total > 0 + } + + + internal var fractionCompleted : Double { + return Double(completed) / Double(total) + } + + + internal var isNaN : Bool { + return total == 0 + } + + internal var debugDescription : String { + return "\(completed) / \(total) (\(fractionCompleted))" + } + + // ---- + + private static func _fromDouble(_ d : Double) -> (Int, Int) { + // This simplistic algorithm could someday be replaced with something better. + // Basically - how many 1/Nths is this double? + // And we choose to use 131072 for N + let denominator : Int = 131072 + let numerator = Int(d / (1.0 / Double(denominator))) + return (numerator, denominator) + } + + private static func _greatestCommonDivisor(_ inA : Int, _ inB : Int) -> Int { + // This is Euclid's algorithm. There are faster ones, like Knuth, but this is the simplest one for now. + var a = inA + var b = inB + repeat { + let tmp = b + b = a % b + a = tmp + } while (b != 0) + return a + } + + private static func _leastCommonMultiple(_ a : Int, _ b : Int) -> Int? { + // This division always results in an integer value because gcd(a,b) is a divisor of a. + // lcm(a,b) == (|a|/gcd(a,b))*b == (|b|/gcd(a,b))*a + let result = (a / _greatestCommonDivisor(a, b)).multipliedReportingOverflow(by: b) + if result.overflow { + return nil + } else { + return result.0 + } + } + + private static func _simplify(_ n : Int, _ d : Int) -> (Int, Int) { + let gcd = _greatestCommonDivisor(n, d) + return (n / gcd, d / gcd) + } +} diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+FileFormatStyle.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+FileFormatStyle.swift new file mode 100644 index 000000000..c7bcfa1a5 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+FileFormatStyle.swift @@ -0,0 +1,108 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +@_spi(Progress) +@available(FoundationPreview 6.2, *) +extension ProgressReporter { + //TODO: rdar://149092406 Manual Codable Conformance + public struct FileFormatStyle: Sendable, Codable, Equatable, Hashable { + + internal struct Option: Sendable, Codable, Equatable, Hashable { + + internal static var file: Option { Option(.file) } + + fileprivate enum RawOption: Codable, Equatable, Hashable { + case file + } + + fileprivate var rawOption: RawOption + + private init( + _ rawOption: RawOption, + ) { + self.rawOption = rawOption + } + } + + public var locale: Locale + let option: Option + + internal init(_ option: Option, locale: Locale = .autoupdatingCurrent) { + self.locale = locale + self.option = option + } + } +} + + +@_spi(Progress) +@available(FoundationPreview 6.2, *) +extension ProgressReporter.FileFormatStyle: FormatStyle { + + public func locale(_ locale: Locale) -> ProgressReporter.FileFormatStyle { + .init(self.option, locale: locale) + } + + public func format(_ reporter: ProgressReporter) -> String { + switch self.option.rawOption { + + case .file: + var fileCountLSR: LocalizedStringResource? + var byteCountLSR: LocalizedStringResource? + var throughputLSR: LocalizedStringResource? + var timeRemainingLSR: LocalizedStringResource? + + let properties = reporter.withProperties(\.self) + + if let totalFileCount = properties.totalFileCount { + let completedFileCount = properties.completedFileCount ?? 0 + fileCountLSR = LocalizedStringResource("\(completedFileCount, format: IntegerFormatStyle()) of \(totalFileCount, format: IntegerFormatStyle()) files", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + } + + if let totalByteCount = properties.totalByteCount { + let completedByteCount = properties.completedByteCount ?? 0 + byteCountLSR = LocalizedStringResource("\(completedByteCount, format: ByteCountFormatStyle()) of \(totalByteCount, format: ByteCountFormatStyle())", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + } + + if let throughput = properties.throughput { + throughputLSR = LocalizedStringResource("\(throughput, format: ByteCountFormatStyle())/s", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + } + + if let timeRemaining = properties.estimatedTimeRemaining { + timeRemainingLSR = LocalizedStringResource("\(timeRemaining, format: Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .wide)) remaining", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + } + + return """ + \(String(localized: fileCountLSR ?? "")) + \(String(localized: byteCountLSR ?? "")) + \(String(localized: throughputLSR ?? "")) + \(String(localized: timeRemainingLSR ?? "")) + """ + } + } +} + +@_spi(Progress) +@available(FoundationPreview 6.2, *) +// Make access easier to format ProgressReporter +extension ProgressReporter { + public func formatted(_ style: ProgressReporter.FileFormatStyle) -> String { + style.format(self) + } +} + +@_spi(Progress) +@available(FoundationPreview 6.2, *) +extension FormatStyle where Self == ProgressReporter.FileFormatStyle { + public static var file: Self { + .init(.file) + } +} diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+FormatStyle.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+FormatStyle.swift new file mode 100644 index 000000000..be6535db1 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+FormatStyle.swift @@ -0,0 +1,117 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +// Outlines the options available to format ProgressReporter +@_spi(Progress) +@available(FoundationPreview 6.2, *) +extension ProgressReporter { + + public struct FormatStyle: Sendable, Codable, Equatable, Hashable { + + // Outlines the options available to format ProgressReporter + internal struct Option: Sendable, Codable, Hashable, Equatable { + + /// Option specifying`fractionCompleted`. + /// + /// For example, 20% completed. + /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance that should be used to format `fractionCompleted`. + /// - Returns: A `LocalizedStringResource` for formatted `fractionCompleted`. + internal static func fractionCompleted(format style: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent() + ) -> Option { + return Option(.fractionCompleted(style)) + } + + /// Option specifying `completedCount` / `totalCount`. + /// + /// For example, 5 of 10. + /// - Parameter style: An `IntegerFormatStyle` instance that should be used to format `completedCount` and `totalCount`. + /// - Returns: A `LocalizedStringResource` for formatted `completedCount` / `totalCount`. + internal static func count(format style: IntegerFormatStyle = IntegerFormatStyle() + ) -> Option { + return Option(.count(style)) + } + + fileprivate enum RawOption: Codable, Hashable, Equatable { + case count(IntegerFormatStyle) + case fractionCompleted(FloatingPointFormatStyle.Percent) + } + + fileprivate var rawOption: RawOption + + private init(_ rawOption: RawOption) { + self.rawOption = rawOption + } + } + + public var locale: Locale + let option: Option + + internal init(_ option: Option, locale: Locale = .autoupdatingCurrent) { + self.locale = locale + self.option = option + } + } +} + +@_spi(Progress) +@available(FoundationPreview 6.2, *) +extension ProgressReporter.FormatStyle: FormatStyle { + + public func locale(_ locale: Locale) -> ProgressReporter.FormatStyle { + .init(self.option, locale: locale) + } + + public func format(_ reporter: ProgressReporter) -> String { + switch self.option.rawOption { + case .count(let countStyle): + let count = reporter.withProperties { p in + return (p.completedCount, p.totalCount) + } + let countLSR = LocalizedStringResource("\(count.0, format: countStyle) of \(count.1 ?? 0, format: countStyle)", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + return String(localized: countLSR) + + case .fractionCompleted(let fractionStyle): + let fractionLSR = LocalizedStringResource("\(reporter.fractionCompleted, format: fractionStyle) completed", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + return String(localized: fractionLSR) + } + } +} + +@_spi(Progress) +@available(FoundationPreview 6.2, *) +// Make access easier to format ProgressReporter +extension ProgressReporter { + public func formatted(_ style: F) -> F.FormatOutput where F.FormatInput == ProgressReporter { + style.format(self) + } + + public func formatted() -> String { + self.formatted(.fractionCompleted()) + } + +} + +@_spi(Progress) +@available(FoundationPreview 6.2, *) +extension FormatStyle where Self == ProgressReporter.FormatStyle { + public static func fractionCompleted( + format: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent() + ) -> Self { + .init(.fractionCompleted(format: format)) + } + + public static func count( + format: IntegerFormatStyle = IntegerFormatStyle() + ) -> Self { + .init(.count(format: format)) + } +} diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift new file mode 100644 index 000000000..b478f376a --- /dev/null +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift @@ -0,0 +1,117 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +internal import _ForSwiftFoundation + +@_spi(Progress) +@available(FoundationPreview 6.2, *) +//MARK: Progress Parent - ProgressReporter Child Interop +// Actual Progress Parent +// Ghost Progress Parent +// Ghost ProgressReporter Child +// Actual ProgressReporter Child +extension Progress { + + /// Returns a ProgressReporter.Progress which can be passed to any method that reports progress + /// and can be initialized into a child `ProgressReporter` to the `self`. + /// + /// Delegates a portion of totalUnitCount to a future child `ProgressReporter` instance. + /// + /// - Parameter count: Number of units delegated to a child instance of `ProgressReporter` + /// which may be instantiated by `ProgressReporter.Progress` later when `reporter(totalCount:)` is called. + /// - Returns: A `ProgressReporter.Progress` instance. + public func makeChild(withPendingUnitCount count: Int) -> ProgressReporter.Progress { + + // Make ghost parent & add it to actual parent's children list + let ghostProgressParent = Progress(totalUnitCount: Int64(count)) + self.addChild(ghostProgressParent, withPendingUnitCount: Int64(count)) + + // Make ghost child + let ghostReporterChild = ProgressReporter(totalCount: count) + + // Make observation instance + let observation = _ProgressParentProgressReporterChild(ghostParent: ghostProgressParent, ghostChild: ghostReporterChild) + + // Make actual child with ghost child being parent + var actualProgress = ghostReporterChild.assign(count: count) + actualProgress.observation = observation + actualProgress.ghostReporter = ghostReporterChild + actualProgress.interopWithProgressParent = true + return actualProgress + } +} + +private final class _ProgressParentProgressReporterChild: Sendable { + private let ghostParent: Progress + private let ghostChild: ProgressReporter + + fileprivate init(ghostParent: Progress, ghostChild: ProgressReporter) { + self.ghostParent = ghostParent + self.ghostChild = ghostChild + + // Set up mirroring observation relationship between ghostChild and ghostParent + // - Ghost Parent should mirror values from Ghost Child, and Ghost Child just mirrors values of Actual Child + ghostChild.addObserver { [weak self] observerState in + guard let self else { + return + } + + switch observerState { + case .totalCountUpdated: + self.ghostParent.totalUnitCount = Int64(self.ghostChild.totalCount ?? 0) + + case .fractionUpdated: + let count = self.ghostChild.withProperties { p in + return (p.completedCount, p.totalCount) + } + self.ghostParent.completedUnitCount = Int64(count.0) + self.ghostParent.totalUnitCount = Int64(count.1 ?? 0) + } + } + } +} + +@_spi(Progress) +@available(FoundationPreview 6.2, *) +//MARK: ProgressReporter Parent - Progress Child Interop +extension ProgressReporter { + + /// Adds a Foundation's `Progress` instance as a child which constitutes a certain `count` of `self`'s `totalCount`. + /// - Parameters: + /// - count: Number of units delegated from `self`'s `totalCount`. + /// - progress: `Progress` which receives the delegated `count`. + public func assign(count: Int, to progress: Foundation.Progress) { + let parentBridge = _NSProgressParentBridge(reporterParent: self) + progress._setParent(parentBridge, portion: Int64(count)) + + // Save ghost parent in ProgressReporter so it doesn't go out of scope after assign method ends + // So that when NSProgress increases completedUnitCount and queries for parent there is still a reference to ghostParent and parent doesn't show 0x0 (portion: 5) + self.setParentBridge(parentBridge: parentBridge) + } +} + +// Subclass of Foundation.Progress +internal final class _NSProgressParentBridge: Progress, @unchecked Sendable { + + let actualParent: ProgressReporter + + init(reporterParent: ProgressReporter) { + self.actualParent = reporterParent + super.init(parent: nil, userInfo: nil) + } + + // Overrides the _updateChild func that Foundation.Progress calls to update parent + // so that the parent that gets updated is the ProgressReporter parent + override func _updateChild(_ child: Foundation.Progress, fraction: _NSProgressFractionTuple, portion: Int64) { + actualParent.updateChildFraction(from: _ProgressFraction(nsProgressFraction: fraction.previous), to: _ProgressFraction(nsProgressFraction: fraction.next), portion: Int(portion)) + } +} diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Progress.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Progress.swift new file mode 100644 index 000000000..d0fb66098 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Progress.swift @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@_spi(Progress) +@available(FoundationPreview 6.2, *) +// ProgressReporter.Progress +extension ProgressReporter { + /// ProgressReporter.Progress is a nested ~Copyable struct used to establish parent-child relationship between two instances of ProgressReporter. + /// + /// ProgressReporter.Progress is returned from a call to `assign(count:)` by a parent ProgressReporter. + /// A child ProgressReporter is then returned by calling`reporter(totalCount:)` on a ProgressReporter.Progress. + public struct Progress: ~Copyable, Sendable { + internal var parent: ProgressReporter + internal var portionOfParent: Int + internal var isInitializedToProgressReporter: Bool + + // Interop variables for Progress - ProgressReporter Interop + internal var interopWithProgressParent: Bool = false + // To be kept alive in ProgressReporter + internal var observation: (any Sendable)? + internal var ghostReporter: ProgressReporter? + + internal init(parent: ProgressReporter, portionOfParent: Int) { + self.parent = parent + self.portionOfParent = portionOfParent + self.isInitializedToProgressReporter = false + } + + /// Instantiates a ProgressReporter which is a child to the parent from which `self` is returned. + /// - Parameter totalCount: Total count of returned child `ProgressReporter` instance. + /// - Returns: A `ProgressReporter` instance. + public consuming func reporter(totalCount: Int?) -> ProgressReporter { + isInitializedToProgressReporter = true + + let childReporter = ProgressReporter(total: totalCount, parent: parent, portionOfParent: portionOfParent, ghostReporter: ghostReporter, interopObservation: observation) + + if interopWithProgressParent { + // Set interop child of ghost reporter so ghost reporter reads from here + ghostReporter?.setInteropChild(interopChild: childReporter) + } else { + // Add child to parent's _children list + parent.addToChildren(childReporter: childReporter) + } + + return childReporter + } + } +} diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift new file mode 100644 index 000000000..7d9943d83 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +@_spi(Progress) +@available(FoundationPreview 6.2, *) +extension ProgressReporter { + // Namespace for properties specific to operations reported on + public struct Properties: Sendable { + public var totalFileCount: TotalFileCount.Type { TotalFileCount.self } + public struct TotalFileCount: Sendable, Property { + public typealias T = Int + } + + public var completedFileCount: CompletedFileCount.Type { CompletedFileCount.self } + public struct CompletedFileCount: Sendable, Property { + public typealias T = Int + } + + public var totalByteCount: TotalByteCount.Type { TotalByteCount.self } + public struct TotalByteCount: Sendable, Property { + public typealias T = Int64 + } + + public var completedByteCount: CompletedByteCount.Type { CompletedByteCount.self } + public struct CompletedByteCount: Sendable, Property { + public typealias T = Int64 + } + + public var throughput: Throughput.Type { Throughput.self } + public struct Throughput: Sendable, Property { + public typealias T = Int64 + } + + public var estimatedTimeRemaining: EstimatedTimeRemaining.Type { EstimatedTimeRemaining.self } + public struct EstimatedTimeRemaining: Sendable, Property { + public typealias T = Duration + } + } +} diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift new file mode 100644 index 000000000..3066d0b8d --- /dev/null +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -0,0 +1,488 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Observation + +internal struct FractionState { + var indeterminate: Bool + var selfFraction: _ProgressFraction + var childFraction: _ProgressFraction + var overallFraction: _ProgressFraction { + selfFraction + childFraction + } + var interopChild: ProgressReporter? // read from this if self is actually an interop ghost +} + +internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { + let metatype: Any.Type + + internal static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.metatype == rhs.metatype + } + + internal func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(metatype)) + } +} + +@_spi(Progress) +@available(FoundationPreview 6.2, *) +// ProgressReporter +/// An object that conveys ongoing progress to the user for a specified task. +@Observable public final class ProgressReporter: Sendable { + + // Stores all the state of properties + internal struct State { + var fractionState: FractionState + var otherProperties: [AnyMetatypeWrapper: (any Sendable)] + } + + // Interop states + internal enum ObserverState { + case fractionUpdated + case totalCountUpdated + } + + // Interop properties - Just kept alive + internal let interopObservation: (any Sendable)? // set at init + internal let parentBridge: LockedState = LockedState(initialState: nil) // dummy, set upon calling setParentBridge + + // Interop properties - Actually set and called + internal let ghostReporter: ProgressReporter? // set at init, used to call notify observers + internal let observers: LockedState<[@Sendable (ObserverState) -> Void]> = LockedState(initialState: [])// storage for all observers, set upon calling addObservers + + /// The total units of work. + public var totalCount: Int? { + _$observationRegistrar.access(self, keyPath: \.totalCount) + return state.withLock { state in + getTotalCount(fractionState: &state.fractionState) + } + } + + /// The completed units of work. + /// If `self` is indeterminate, the value will be 0. + public var completedCount: Int { + _$observationRegistrar.access(self, keyPath: \.completedCount) + return state.withLock { state in + getCompletedCount(fractionState: &state.fractionState) + } + } + + /// The proportion of work completed. + /// This takes into account the fraction completed in its children instances if children are present. + /// If `self` is indeterminate, the value will be 0. + public var fractionCompleted: Double { + _$observationRegistrar.access(self, keyPath: \.fractionCompleted) + return state.withLock { state in + getFractionCompleted(fractionState: &state.fractionState) + } + } + + /// The state of initialization of `totalCount`. + /// If `totalCount` is `nil`, the value will be `true`. + public var isIndeterminate: Bool { + _$observationRegistrar.access(self, keyPath: \.isIndeterminate) + return state.withLock { state in + getIsIndeterminate(fractionState: &state.fractionState) + } + } + + /// The state of completion of work. + /// If `completedCount` >= `totalCount`, the value will be `true`. + public var isFinished: Bool { + _$observationRegistrar.access(self, keyPath: \.isFinished) + return state.withLock { state in + getIsFinished(fractionState: &state.fractionState) + } + } + + /// A type that conveys task-specific information on progress. + public protocol Property { + + associatedtype T: Sendable + + /// Aggregates an array of `T` into a single value `T`. + /// - Parameter all: Array of `T` to be aggregated. + /// - Returns: A new instance of `T`. + static func reduce(_ all: [T]) -> T + } + + /// A container that holds values for properties that specify information on progress. + @dynamicMemberLookup + public struct Values : Sendable { + //TODO: rdar://149225947 Non-escapable conformance + let reporter: ProgressReporter + var state: State + + /// The total units of work. + public var totalCount: Int? { + mutating get { + reporter.getTotalCount(fractionState: &state.fractionState) + } + + set { + let previous = state.fractionState.overallFraction + if state.fractionState.selfFraction.total != newValue && state.fractionState.selfFraction.total > 0 { + state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: newValue ?? 1) + } + state.fractionState.selfFraction.total = newValue ?? 0 + + // if newValue is nil, reset indeterminate to true + if newValue != nil { + state.fractionState.indeterminate = false + } else { + state.fractionState.indeterminate = true + } + //TODO: rdar://149015734 Check throttling + reporter.updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) + + reporter.ghostReporter?.notifyObservers(with: .totalCountUpdated) + + } + } + + + /// The completed units of work. + public var completedCount: Int { + mutating get { + reporter.getCompletedCount(fractionState: &state.fractionState) + } + + set { + let prev = state.fractionState.overallFraction + state.fractionState.selfFraction.completed = newValue + reporter.updateFractionCompleted(from: prev, to: state.fractionState.overallFraction) + reporter.ghostReporter?.notifyObservers(with: .fractionUpdated) + } + } + + /// Returns a property value that a key path indicates. + public subscript(dynamicMember key: KeyPath) -> P.T? { + get { + state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T + } + + set { + state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = newValue + // Update Parent's P.self value + updateParentOtherPropertiesEntry(of: reporter, metatype: P.self, updatedValue: newValue, state: &state) + } + } + + private func updateParentOtherPropertiesEntry(of reporter: ProgressReporter, metatype: P.Type, updatedValue: P.T?, state: inout State) { + // Check if parent exists to continue propagating values up + if let parent = reporter.parent { + parent.children.withLock { children in + // Array containing all children's values to pass into reduce + let childrenValues: LockedState<[P.T]> = LockedState(initialState: []) + // Add self's updatedValue to array + if let updatedValue = updatedValue { + childrenValues.withLock { values in + values.append(updatedValue) + } + } + // Add other children's values to array, skip over existing child's existing value + for child in children { + if child != reporter { + let childValue = child?.state.withLock { $0.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T } + if let childValue = childValue { + childrenValues.withLock { values in + values.append(childValue) + } + } + } + } + if !childrenValues.withLock(\.self).isEmpty { + parent.state.withLock { state in + // Set property in parent + let reducedValue = metatype.reduce(childrenValues.withLock(\.self)) + state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] = reducedValue + // Recursive call to parent's parent + updateParentOtherPropertiesEntry(of: parent, metatype: metatype, updatedValue: reducedValue, state: &state) + } + } + } + } + } + + } + + private let portionOfParent: Int + internal let parent: ProgressReporter? + private let children: LockedState> + private let state: LockedState + + internal init(total: Int?, parent: ProgressReporter?, portionOfParent: Int, ghostReporter: ProgressReporter?, interopObservation: (any Sendable)?) { + self.portionOfParent = portionOfParent + self.parent = parent + self.children = .init(initialState: []) + let fractionState = FractionState( + indeterminate: total == nil ? true : false, + selfFraction: _ProgressFraction(completed: 0, total: total ?? 0), + childFraction: _ProgressFraction(completed: 0, total: 1), + interopChild: nil + ) + let state = State(fractionState: fractionState, otherProperties: [:]) + self.state = LockedState(initialState: state) + self.interopObservation = interopObservation + self.ghostReporter = ghostReporter + } + + /// Initializes `self` with `totalCount`. + /// + /// If `totalCount` is set to `nil`, `self` is indeterminate. + /// - Parameter totalCount: Total units of work. + public convenience init(totalCount: Int?) { + self.init(total: totalCount, parent: nil, portionOfParent: 0, ghostReporter: nil, interopObservation: nil) + } + + + /// Sets `totalCount`. + /// - Parameter newTotal: Total units of work. + public func setTotalCount(_ newTotal: Int?) { + state.withLock { state in + let previous = state.fractionState.overallFraction + if state.fractionState.selfFraction.total != newTotal && state.fractionState.selfFraction.total > 0 { + state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: newTotal ?? 1) + } + state.fractionState.selfFraction.total = newTotal ?? 0 + + // if newValue is nil, reset indeterminate to true + if newTotal != nil { + state.fractionState.indeterminate = false + } else { + state.fractionState.indeterminate = true + } + updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) + + ghostReporter?.notifyObservers(with: .totalCountUpdated) + } + } + + + /// Returns a `ProgressReporter.Progress` representing a portion of `self`which can be passed to any method that reports progress. + /// + /// - Parameter count: Units, which is a portion of `totalCount`delegated to an instance of `ProgressReporter.Progress`. + /// - Returns: A `ProgressReporter.Progress` instance. + public func assign(count portionOfParent: Int) -> Progress { + precondition(portionOfParent > 0, "Giving out zero units is not a valid operation.") + let childProgress = Progress(parent: self, portionOfParent: portionOfParent) + return childProgress + } + + + /// Increases `completedCount` by `count`. + /// - Parameter count: Units of work. + public func complete(count: Int) { + let updateState = updateCompletedCount(count: count) + updateFractionCompleted(from: updateState.previous, to: updateState.current) + ghostReporter?.notifyObservers(with: .fractionUpdated) + } + + private struct UpdateState { + let previous: _ProgressFraction + let current: _ProgressFraction + } + + private func updateCompletedCount(count: Int) -> UpdateState { + // Acquire and release child's lock + let (previous, current) = state.withLock { state in + let prev = state.fractionState.overallFraction + state.fractionState.selfFraction.completed += count + return (prev, state.fractionState.overallFraction) + } + return UpdateState(previous: previous, current: current) + } + + /// Mutates any settable properties that convey information about progress. + public func withProperties(_ closure: @Sendable (inout Values) throws -> T) rethrows -> T { + return try state.withLock { state in + var values = Values(reporter: self, state: state) + // This is done to avoid copy on write later + state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction()), otherProperties: [:]) + let result = try closure(&values) + state = values.state + return result + } + } + + //MARK: ProgressReporter Properties getters + /// Returns nil if `self` was instantiated without total units; + /// returns a `Int` value otherwise. + private func getTotalCount(fractionState: inout FractionState) -> Int? { + if let interopChild = fractionState.interopChild { + return interopChild.totalCount + } + if fractionState.indeterminate { + return nil + } else { + return fractionState.selfFraction.total + } + } + + /// Returns nil if `self` has `nil` total units; + /// returns a `Int` value otherwise. + private func getCompletedCount(fractionState: inout FractionState) -> Int { + if let interopChild = fractionState.interopChild { + return interopChild.completedCount + } + return fractionState.selfFraction.completed + } + + /// Returns 0.0 if `self` has `nil` total units; + /// returns a `Double` otherwise. + /// If `indeterminate`, return 0.0. + /// + /// The calculation of fraction completed for a ProgressReporter instance that has children + /// will take into account children's fraction completed as well. + private func getFractionCompleted(fractionState: inout FractionState) -> Double { + if let interopChild = fractionState.interopChild { + return interopChild.fractionCompleted + } + if fractionState.indeterminate { + return 0.0 + } + guard fractionState.selfFraction.total > 0 else { + return fractionState.selfFraction.fractionCompleted + } + return (fractionState.selfFraction + fractionState.childFraction).fractionCompleted + } + + + /// Returns `true` if completed and total units are not `nil` and completed units is greater than or equal to total units; + /// returns `false` otherwise. + private func getIsFinished(fractionState: inout FractionState) -> Bool { + return fractionState.selfFraction.isFinished + } + + + /// Returns `true` if `self` has `nil` total units. + private func getIsIndeterminate(fractionState: inout FractionState) -> Bool { + return fractionState.indeterminate + } + + //MARK: FractionCompleted Calculation methods + private func updateFractionCompleted(from: _ProgressFraction, to: _ProgressFraction) { + _$observationRegistrar.withMutation(of: self, keyPath: \.fractionCompleted) { + if from != to { + parent?.updateChildFraction(from: from, to: to, portion: portionOfParent) + } + } + } + + /// A child progress has been updated, which changes our own fraction completed. + internal func updateChildFraction(from previous: _ProgressFraction, to next: _ProgressFraction, portion: Int) { + // Acquire and release parent's lock + let updateState = state.withLock { state in + let previousOverallFraction = state.fractionState.overallFraction + let multiple = _ProgressFraction(completed: portion, total: state.fractionState.selfFraction.total) + let oldFractionOfParent = previous * multiple + + if previous.total != 0 { + state.fractionState.childFraction = state.fractionState.childFraction - oldFractionOfParent + } + + if next.total != 0 { + state.fractionState.childFraction = state.fractionState.childFraction + (next * multiple) + + if next.isFinished { + // Remove from children list + _ = children.withLock { $0.remove(self) } + + if portion != 0 { + // Update our self completed units + state.fractionState.selfFraction.completed += portion + } + + // Subtract the (child's fraction completed * multiple) from our child fraction + state.fractionState.childFraction = state.fractionState.childFraction - (multiple * next) + } + } + return UpdateState(previous: previousOverallFraction, current: state.fractionState.overallFraction) + } + updateFractionCompleted(from: updateState.previous, to: updateState.current) + } + + //MARK: Interop-related internal methods + /// Adds `observer` to list of `_observers` in `self`. + internal func addObserver(observer: @escaping @Sendable (ObserverState) -> Void) { + observers.withLock { observers in + observers.append(observer) + } + } + + /// Notifies all `_observers` of `self` when `state` changes. + private func notifyObservers(with state: ObserverState) { + observers.withLock { observers in + for observer in observers { + observer(state) + } + } + } + + //MARK: Internal methods to mutate locked context + internal func setParentBridge(parentBridge: Foundation.Progress) { + self.parentBridge.withLock { bridge in + bridge = parentBridge + } + } + + internal func setInteropChild(interopChild: ProgressReporter) { + state.withLock { state in + state.fractionState.interopChild = interopChild + } + } + + internal func addToChildren(childReporter: ProgressReporter) { + _ = children.withLock { children in + children.insert(childReporter) + } + } +} + +@_spi(Progress) +@available(FoundationPreview 6.2, *) +// Default Implementation for reduce +extension ProgressReporter.Property where T : AdditiveArithmetic { + public static func reduce(_ all: [T]) -> T { + precondition(all.isEmpty == false, "Cannot reduce an empty array") + let first = all.first! + let rest = all.dropFirst() + guard !rest.isEmpty else { + return first + } + return rest.reduce(first, +) + } +} + +@_spi(Progress) +@available(FoundationPreview 6.2, *) +// Hashable & Equatable Conformance +extension ProgressReporter: Hashable, Equatable { + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } + + /// Returns `true` if pointer of `lhs` is equal to pointer of `rhs`. + public static func ==(lhs: ProgressReporter, rhs: ProgressReporter) -> Bool { + return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } +} + +@_spi(Progress) +@available(FoundationPreview 6.2, *) +extension ProgressReporter: CustomDebugStringConvertible { + /// The description for `completedCount` and `totalCount`. + public var debugDescription: String { + return "\(completedCount) / \(totalCount ?? 0)" + } +} From 469aea8744b6c08f2aef439d70395d0dda4c5b1b Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 21 Apr 2025 10:12:40 -0700 Subject: [PATCH 02/85] change import header --- .../ProgressReporter/ProgressFraction.swift | 3 +++ .../ProgressReporter/ProgressReporter+Interop.swift | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressFraction.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressFraction.swift index 0e3734cd5..99efe9a59 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressFraction.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressFraction.swift @@ -9,7 +9,10 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// +#if FOUNDATION_FRAMEWORK +// For feature flag internal import _ForSwiftFoundation +#endif internal struct _ProgressFraction : Sendable, Equatable, CustomDebugStringConvertible { var completed : Int diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift index b478f376a..c021d53b0 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift @@ -10,7 +10,10 @@ // //===----------------------------------------------------------------------===// +#if FOUNDATION_FRAMEWORK +// For feature flag internal import _ForSwiftFoundation +#endif @_spi(Progress) @available(FoundationPreview 6.2, *) From 23d9f29972dfb4f062cb2ac3c7eaff77a4165e33 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 21 Apr 2025 14:05:12 -0700 Subject: [PATCH 03/85] moving files to open source --- .../ProgressReporter/ProgressFraction.swift | 4 +- .../ProgressReporter+Interop.swift | 5 +- .../ProgressReporter+Progress.swift | 1 - .../ProgressReporter+Properties.swift | 1 - .../ProgressReporter/ProgressReporter.swift | 9 +- .../ProgressReporter+FileFormatStyle.swift | 8 +- .../ProgressReporter+FormatStyle.swift | 64 ++- .../ProgressFractionTests.swift | 152 ++++++ .../ProgressReporterTests.swift | 472 ++++++++++++++++++ 9 files changed, 691 insertions(+), 25 deletions(-) rename Sources/{FoundationEssentials => FoundationInternationalization}/ProgressReporter/ProgressReporter+FileFormatStyle.swift (98%) rename Sources/{FoundationEssentials => FoundationInternationalization}/ProgressReporter/ProgressReporter+FormatStyle.swift (61%) create mode 100644 Tests/FoundationEssentialsTests/ProgressReporter/ProgressFractionTests.swift create mode 100644 Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressFraction.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressFraction.swift index 99efe9a59..6c5a9b19a 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressFraction.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressFraction.swift @@ -10,7 +10,6 @@ // //===----------------------------------------------------------------------===// #if FOUNDATION_FRAMEWORK -// For feature flag internal import _ForSwiftFoundation #endif @@ -50,10 +49,13 @@ internal struct _ProgressFraction : Sendable, Equatable, CustomDebugStringConver // ---- +#if FOUNDATION_FRAMEWORK // Glue code for _NSProgressFraction and _ProgressFraction init(nsProgressFraction: _NSProgressFraction) { self.init(completed: Int(nsProgressFraction.completed), total: Int(nsProgressFraction.total)) } +#endif + internal mutating func simplify() { if self.total == 0 { diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift index c021d53b0..58b3da331 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift @@ -11,11 +11,8 @@ //===----------------------------------------------------------------------===// #if FOUNDATION_FRAMEWORK -// For feature flag internal import _ForSwiftFoundation -#endif -@_spi(Progress) @available(FoundationPreview 6.2, *) //MARK: Progress Parent - ProgressReporter Child Interop // Actual Progress Parent @@ -83,7 +80,6 @@ private final class _ProgressParentProgressReporterChild: Sendable { } } -@_spi(Progress) @available(FoundationPreview 6.2, *) //MARK: ProgressReporter Parent - Progress Child Interop extension ProgressReporter { @@ -118,3 +114,4 @@ internal final class _NSProgressParentBridge: Progress, @unchecked Sendable { actualParent.updateChildFraction(from: _ProgressFraction(nsProgressFraction: fraction.previous), to: _ProgressFraction(nsProgressFraction: fraction.next), portion: Int(portion)) } } +#endif diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Progress.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Progress.swift index d0fb66098..94854201e 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Progress.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Progress.swift @@ -10,7 +10,6 @@ // //===----------------------------------------------------------------------===// -@_spi(Progress) @available(FoundationPreview 6.2, *) // ProgressReporter.Progress extension ProgressReporter { diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift index 7d9943d83..9d74a0098 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift @@ -9,7 +9,6 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// -@_spi(Progress) @available(FoundationPreview 6.2, *) extension ProgressReporter { // Namespace for properties specific to operations reported on diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index 3066d0b8d..ac0c781e6 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -34,7 +34,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } -@_spi(Progress) @available(FoundationPreview 6.2, *) // ProgressReporter /// An object that conveys ongoing progress to the user for a specified task. @@ -54,8 +53,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Interop properties - Just kept alive internal let interopObservation: (any Sendable)? // set at init + #if FOUNDATION_FRAMEWORK internal let parentBridge: LockedState = LockedState(initialState: nil) // dummy, set upon calling setParentBridge - + #endif // Interop properties - Actually set and called internal let ghostReporter: ProgressReporter? // set at init, used to call notify observers internal let observers: LockedState<[@Sendable (ObserverState) -> Void]> = LockedState(initialState: [])// storage for all observers, set upon calling addObservers @@ -430,11 +430,13 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } //MARK: Internal methods to mutate locked context +#if FOUNDATION_FRAMEWORK internal func setParentBridge(parentBridge: Foundation.Progress) { self.parentBridge.withLock { bridge in bridge = parentBridge } } +#endif internal func setInteropChild(interopChild: ProgressReporter) { state.withLock { state in @@ -449,7 +451,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } -@_spi(Progress) @available(FoundationPreview 6.2, *) // Default Implementation for reduce extension ProgressReporter.Property where T : AdditiveArithmetic { @@ -464,7 +465,6 @@ extension ProgressReporter.Property where T : AdditiveArithmetic { } } -@_spi(Progress) @available(FoundationPreview 6.2, *) // Hashable & Equatable Conformance extension ProgressReporter: Hashable, Equatable { @@ -478,7 +478,6 @@ extension ProgressReporter: Hashable, Equatable { } } -@_spi(Progress) @available(FoundationPreview 6.2, *) extension ProgressReporter: CustomDebugStringConvertible { /// The description for `completedCount` and `totalCount`. diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+FileFormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift similarity index 98% rename from Sources/FoundationEssentials/ProgressReporter/ProgressReporter+FileFormatStyle.swift rename to Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift index c7bcfa1a5..8dcee8892 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+FileFormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift @@ -9,7 +9,10 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// -@_spi(Progress) +#if canImport(FoundationEssentials) +import FoundationEssentials +#endif + @available(FoundationPreview 6.2, *) extension ProgressReporter { //TODO: rdar://149092406 Manual Codable Conformance @@ -43,7 +46,6 @@ extension ProgressReporter { } -@_spi(Progress) @available(FoundationPreview 6.2, *) extension ProgressReporter.FileFormatStyle: FormatStyle { @@ -90,7 +92,6 @@ extension ProgressReporter.FileFormatStyle: FormatStyle { } } -@_spi(Progress) @available(FoundationPreview 6.2, *) // Make access easier to format ProgressReporter extension ProgressReporter { @@ -99,7 +100,6 @@ extension ProgressReporter { } } -@_spi(Progress) @available(FoundationPreview 6.2, *) extension FormatStyle where Self == ProgressReporter.FileFormatStyle { public static var file: Self { diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+FormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift similarity index 61% rename from Sources/FoundationEssentials/ProgressReporter/ProgressReporter+FormatStyle.swift rename to Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift index be6535db1..515ab395b 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+FormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift @@ -9,9 +9,10 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// - +#if canImport(FoundationEssentials) +import FoundationEssentials +#endif // Outlines the options available to format ProgressReporter -@_spi(Progress) @available(FoundationPreview 6.2, *) extension ProgressReporter { @@ -20,12 +21,33 @@ extension ProgressReporter { // Outlines the options available to format ProgressReporter internal struct Option: Sendable, Codable, Hashable, Equatable { + #if FOUNDATION_FRAMEWORK + /// Option specifying`fractionCompleted`. + /// + /// For example, 20% completed. + /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance that should be used to format `fractionCompleted`. + /// - Returns: A `LocalizedStringResource` for formatted `fractionCompleted`. + internal static func fractionCompleted(format style: Foundation.FloatingPointFormatStyle.Percent = Foundation.FloatingPointFormatStyle.Percent() + ) -> Option { + return Option(.fractionCompleted(style)) + } + + /// Option specifying `completedCount` / `totalCount`. + /// + /// For example, 5 of 10. + /// - Parameter style: An `IntegerFormatStyle` instance that should be used to format `completedCount` and `totalCount`. + /// - Returns: A `LocalizedStringResource` for formatted `completedCount` / `totalCount`. + internal static func count(format style: Foundation.IntegerFormatStyle = Foundation.IntegerFormatStyle() + ) -> Option { + return Option(.count(style)) + } + #else /// Option specifying`fractionCompleted`. /// /// For example, 20% completed. /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance that should be used to format `fractionCompleted`. /// - Returns: A `LocalizedStringResource` for formatted `fractionCompleted`. - internal static func fractionCompleted(format style: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent() + internal static func fractionCompleted(format style: FoundationInternationalization.FloatingPointFormatStyle.Percent = FoundationInternationalization.FloatingPointFormatStyle.Percent() ) -> Option { return Option(.fractionCompleted(style)) } @@ -35,10 +57,12 @@ extension ProgressReporter { /// For example, 5 of 10. /// - Parameter style: An `IntegerFormatStyle` instance that should be used to format `completedCount` and `totalCount`. /// - Returns: A `LocalizedStringResource` for formatted `completedCount` / `totalCount`. - internal static func count(format style: IntegerFormatStyle = IntegerFormatStyle() + internal static func count(format style: FoundationInternationalization.IntegerFormatStyle = FoundationInternationalization.IntegerFormatStyle() ) -> Option { return Option(.count(style)) } + #endif // FOUNDATION_FRAMEWORK + fileprivate enum RawOption: Codable, Hashable, Equatable { case count(IntegerFormatStyle) @@ -62,7 +86,6 @@ extension ProgressReporter { } } -@_spi(Progress) @available(FoundationPreview 6.2, *) extension ProgressReporter.FormatStyle: FormatStyle { @@ -86,32 +109,55 @@ extension ProgressReporter.FormatStyle: FormatStyle { } } -@_spi(Progress) @available(FoundationPreview 6.2, *) // Make access easier to format ProgressReporter extension ProgressReporter { + +#if FOUNDATION_FRAMEWORK public func formatted(_ style: F) -> F.FormatOutput where F.FormatInput == ProgressReporter { style.format(self) } +#else + public func formatted(_ style: F) -> F.FormatOutput where F.FormatInput == ProgressReporter { + style.format(self) + } +#endif // FOUNDATION_FRAMEWORK + public func formatted() -> String { self.formatted(.fractionCompleted()) } } -@_spi(Progress) @available(FoundationPreview 6.2, *) extension FormatStyle where Self == ProgressReporter.FormatStyle { + +#if FOUNDATION_FRAMEWORK public static func fractionCompleted( - format: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent() + format: Foundation.FloatingPointFormatStyle.Percent = Foundation.FloatingPointFormatStyle.Percent() ) -> Self { .init(.fractionCompleted(format: format)) } public static func count( - format: IntegerFormatStyle = IntegerFormatStyle() + format: Foundation.IntegerFormatStyle = Foundation.IntegerFormatStyle() ) -> Self { .init(.count(format: format)) } +#else + public static func fractionCompleted( + format: FoundationInternationalization.FloatingPointFormatStyle.Percent = FoundationInternationalization.FloatingPointFormatStyle.Percent() + ) -> Self { + .init(.fractionCompleted(format: format)) + } + + public static func count( + format: FoundationInternationalization.IntegerFormatStyle = FoundationInternationalization.IntegerFormatStyle() + ) -> Self { + .init(.count(format: format)) + } +#endif + + } diff --git a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressFractionTests.swift b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressFractionTests.swift new file mode 100644 index 000000000..89dc77ec7 --- /dev/null +++ b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressFractionTests.swift @@ -0,0 +1,152 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import XCTest + +#if FOUNDATION_FRAMEWORK +@testable import Foundation +#else +@testable import FoundationEssentials +#endif // FOUNDATION_FRAMEWORK + +final class ProgressFractionTests: XCTestCase { + func test_equal() { + let f1 = _ProgressFraction(completed: 5, total: 10) + let f2 = _ProgressFraction(completed: 100, total: 200) + + XCTAssertEqual(f1, f2) + + let f3 = _ProgressFraction(completed: 3, total: 10) + XCTAssertNotEqual(f1, f3) + + let f4 = _ProgressFraction(completed: 5, total: 10) + XCTAssertEqual(f1, f4) + } + + func test_addSame() { + let f1 = _ProgressFraction(completed: 5, total: 10) + let f2 = _ProgressFraction(completed: 3, total: 10) + + let r = f1 + f2 + XCTAssertEqual(r.completed, 8) + XCTAssertEqual(r.total, 10) + } + + func test_addDifferent() { + let f1 = _ProgressFraction(completed: 5, total: 10) + let f2 = _ProgressFraction(completed: 300, total: 1000) + + let r = f1 + f2 + XCTAssertEqual(r.completed, 800) + XCTAssertEqual(r.total, 1000) + } + + func test_subtract() { + let f1 = _ProgressFraction(completed: 5, total: 10) + let f2 = _ProgressFraction(completed: 3, total: 10) + + let r = f1 - f2 + XCTAssertEqual(r.completed, 2) + XCTAssertEqual(r.total, 10) + } + + func test_multiply() { + let f1 = _ProgressFraction(completed: 5, total: 10) + let f2 = _ProgressFraction(completed: 1, total: 2) + + let r = f1 * f2 + XCTAssertEqual(r.completed, 5) + XCTAssertEqual(r.total, 20) + } + + func test_simplify() { + let f1 = _ProgressFraction(completed: 5, total: 10) + let f2 = _ProgressFraction(completed: 3, total: 10) + + let r = (f1 + f2).simplified() + + XCTAssertEqual(r.completed, 4) + XCTAssertEqual(r.total, 5) + } + + func test_overflow() { + // These prime numbers are problematic for overflowing + let denominators : [Int] = [5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 69] + + var f1 = _ProgressFraction(completed: 1, total: 3) + for d in denominators { + f1 = f1 + _ProgressFraction(completed: 1, total: d) + } + + let fractionResult = f1.fractionCompleted + var expectedResult = 1.0 / 3.0 + for d in denominators { + expectedResult = expectedResult + 1.0 / Double(d) + } + + XCTAssertEqual(fractionResult, expectedResult, accuracy: 0.00001) + } + + func test_addOverflow() { + // These prime numbers are problematic for overflowing + let denominators : [Int] = [5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 69] + var f1 = _ProgressFraction(completed: 1, total: 3) + for d in denominators { + f1 = f1 + _ProgressFraction(completed: 1, total: d) + } + + // f1 should be in overflow + XCTAssertTrue(f1.overflowed) + + let f2 = _ProgressFraction(completed: 1, total: 4) + f1 + + // f2 should also be in overflow + XCTAssertTrue(f2.overflowed) + + // And it should have completed value of about 1.0/4.0 + f1.fractionCompleted + let expected = (1.0 / 4.0) + f1.fractionCompleted + + XCTAssertEqual(expected, f2.fractionCompleted, accuracy: 0.00001) + } + +#if _pointerBitWidth(_64) // These tests assumes Int is Int64 + func test_andAndSubtractOverflow() { + let f1 = _ProgressFraction(completed: 48, total: 60) + let f2 = _ProgressFraction(completed: 5880, total: 7200) + let f3 = _ProgressFraction(completed: 7048893638467736640, total: 8811117048084670800) + + let result1 = (f3 - f1) + f2 + XCTAssertTrue(result1.completed > 0) + + let result2 = (f3 - f2) + f1 + XCTAssertTrue(result2.completed < 60) + } +#endif + + func test_fractionFromDouble() { + let d = 4.25 // exactly representable in binary + let f1 = _ProgressFraction(double: d) + + let simplified = f1.simplified() + XCTAssertEqual(simplified.completed, 17) + XCTAssertEqual(simplified.total, 4) + } + + func test_unnecessaryOverflow() { + // just because a fraction has a large denominator doesn't mean it needs to overflow + let f1 = _ProgressFraction(completed: (Int.max - 1) / 2, total: Int.max - 1) + let f2 = _ProgressFraction(completed: 1, total: 16) + + let r = f1 + f2 + XCTAssertFalse(r.overflowed) + } +} diff --git a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift new file mode 100644 index 000000000..5841461e2 --- /dev/null +++ b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift @@ -0,0 +1,472 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import XCTest + +#if FOUNDATION_FRAMEWORK +@testable import Foundation +#else +@testable import FoundationEssentials +#endif // FOUNDATION_FRAMEWORK + +/// Unit tests for basic functionalities of ProgressReporter +class TestProgressReporter: XCTestCase { + /// MARK: Helper methods that report progress + func doBasicOperationV1(reportTo progress: consuming ProgressReporter.Progress) async { + let reporter = progress.reporter(totalCount: 8) + for i in 1...8 { + reporter.complete(count: 1) + XCTAssertEqual(reporter.completedCount, i) + XCTAssertEqual(reporter.fractionCompleted, Double(i) / Double(8)) + } + } + + func doBasicOperationV2(reportTo progress: consuming ProgressReporter.Progress) async { + let reporter = progress.reporter(totalCount: 7) + for i in 1...7 { + reporter.complete(count: 1) + XCTAssertEqual(reporter.completedCount, i) + XCTAssertEqual(reporter.fractionCompleted,Double(i) / Double(7)) + } + } + + func doBasicOperationV3(reportTo progress: consuming ProgressReporter.Progress) async { + let reporter = progress.reporter(totalCount: 11) + for i in 1...11 { + reporter.complete(count: 1) + XCTAssertEqual(reporter.completedCount, i) + XCTAssertEqual(reporter.fractionCompleted, Double(i) / Double(11)) + } + } + + func doFileOperation(reportTo progress: consuming ProgressReporter.Progress) async { + let reporter = progress.reporter(totalCount: 100) + reporter.withProperties { properties in + properties.totalFileCount = 100 + } + + XCTAssertEqual(reporter.withProperties(\.totalFileCount), 100) + XCTAssertNil(reporter.withProperties(\.completedFileCount)) + + reporter.complete(count: 100) + XCTAssertEqual(reporter.fractionCompleted, 1.0) + XCTAssertTrue(reporter.isFinished) + + reporter.withProperties { properties in + properties.completedFileCount = 100 + } + XCTAssertEqual(reporter.withProperties(\.completedFileCount), 100) + XCTAssertEqual(reporter.withProperties(\.totalFileCount), 100) + } + + /// MARK: Tests calculations based on change in totalCount + func testTotalCountNil() async throws { + let overall = ProgressReporter(totalCount: nil) + overall.complete(count: 10) + XCTAssertEqual(overall.completedCount, 10) + XCTAssertEqual(overall.fractionCompleted, 0.0) + XCTAssertTrue(overall.isIndeterminate) + XCTAssertNil(overall.totalCount) + } + + func testTotalCountReset() async throws { + let overall = ProgressReporter(totalCount: 10) + overall.complete(count: 5) + XCTAssertEqual(overall.completedCount, 5) + XCTAssertEqual(overall.totalCount, 10) + XCTAssertEqual(overall.fractionCompleted, 0.5) + XCTAssertFalse(overall.isIndeterminate) + + overall.withProperties { p in + p.totalCount = nil + p.completedCount += 1 + } + XCTAssertEqual(overall.completedCount, 6) + XCTAssertNil(overall.totalCount) + XCTAssertEqual(overall.fractionCompleted, 0.0) + XCTAssertTrue(overall.isIndeterminate) + XCTAssertFalse(overall.isFinished) + + overall.withProperties { p in + p.totalCount = 12 + p.completedCount += 2 + } + XCTAssertEqual(overall.completedCount, 8) + XCTAssertEqual(overall.totalCount, 12) + XCTAssertEqual(overall.fractionCompleted, Double(8) / Double(12)) + XCTAssertFalse(overall.isIndeterminate) + XCTAssertFalse(overall.isFinished) + } + + func testTotalCountNilWithChild() async throws { + let overall = ProgressReporter(totalCount: nil) + XCTAssertEqual(overall.completedCount, 0) + XCTAssertNil(overall.totalCount) + XCTAssertEqual(overall.fractionCompleted, 0.0) + XCTAssertTrue(overall.isIndeterminate) + XCTAssertFalse(overall.isFinished) + + let progress1 = overall.assign(count: 2) + let reporter1 = progress1.reporter(totalCount: 1) + + reporter1.complete(count: 1) + XCTAssertEqual(reporter1.totalCount, 1) + XCTAssertEqual(reporter1.completedCount, 1) + XCTAssertEqual(reporter1.fractionCompleted, 1.0) + XCTAssertFalse(reporter1.isIndeterminate) + XCTAssertTrue(reporter1.isFinished) + + XCTAssertEqual(overall.completedCount, 2) + XCTAssertEqual(overall.totalCount, nil) + XCTAssertEqual(overall.fractionCompleted, 0.0) + XCTAssertTrue(overall.isIndeterminate) + XCTAssertFalse(overall.isFinished) + + overall.withProperties { p in + p.totalCount = 5 + } + XCTAssertEqual(overall.completedCount, 2) + XCTAssertEqual(overall.totalCount, 5) + XCTAssertEqual(overall.fractionCompleted, 0.4) + XCTAssertFalse(overall.isIndeterminate) + XCTAssertFalse(overall.isFinished) + } + + func testTotalCountFinishesWithLessCompletedCount() async throws { + let overall = ProgressReporter(totalCount: 10) + overall.complete(count: 5) + + let progress1 = overall.assign(count: 8) + let reporter1 = progress1.reporter(totalCount: 1) + reporter1.complete(count: 1) + + XCTAssertEqual(overall.completedCount, 13) + XCTAssertEqual(overall.totalCount, 10) + XCTAssertEqual(overall.fractionCompleted, 1.3) + XCTAssertFalse(overall.isIndeterminate) + XCTAssertTrue(overall.isFinished) + } + + /// MARK: Tests single-level tree + func testDiscreteReporter() async throws { + let reporter = ProgressReporter(totalCount: 3) + await doBasicOperationV1(reportTo: reporter.assign(count: 3)) + XCTAssertEqual(reporter.fractionCompleted, 1.0) + XCTAssertEqual(reporter.completedCount, 3) + XCTAssertTrue(reporter.isFinished) + } + + func testDiscreteReporterWithFileProperties() async throws { + let fileReporter = ProgressReporter(totalCount: 3) + await doFileOperation(reportTo: fileReporter.assign(count: 3)) + XCTAssertEqual(fileReporter.fractionCompleted, 1.0) + XCTAssertEqual(fileReporter.completedCount, 3) + XCTAssertTrue(fileReporter.isFinished) + } + + /// MARK: Tests multiple-level trees + func testEmptyDiscreteReporter() async throws { + let reporter = ProgressReporter(totalCount: nil) + XCTAssertTrue(reporter.isIndeterminate) + + reporter.withProperties { p in + p.totalCount = 10 + } + XCTAssertFalse(reporter.isIndeterminate) + XCTAssertEqual(reporter.totalCount, 10) + + await doBasicOperationV1(reportTo: reporter.assign(count: 10)) + XCTAssertEqual(reporter.fractionCompleted, 1.0) + XCTAssertEqual(reporter.completedCount, 10) + XCTAssertTrue(reporter.isFinished) + } + + func testTwoLevelTreeWithOneChildWithFileProperties() async throws { + let overall = ProgressReporter(totalCount: 2) + + let progress1 = overall.assign(count: 1) + let reporter1 = progress1.reporter(totalCount: 10) + reporter1.withProperties { properties in + properties.totalFileCount = 10 + properties.completedFileCount = 0 + } + reporter1.complete(count: 10) + + XCTAssertEqual(overall.fractionCompleted, 0.5) + // This should call reduce and get 10 + XCTAssertEqual(overall.withProperties(\.totalFileCount), 10) + } + + func testTwoLevelTreeWithTwoChildrenWithFileProperties() async throws { + let overall = ProgressReporter(totalCount: 2) + + let progress1 = overall.assign(count: 1) + let reporter1 = progress1.reporter(totalCount: 10) + + reporter1.withProperties { properties in + properties.totalFileCount = 11 + properties.completedFileCount = 0 + } + + let progress2 = overall.assign(count: 1) + let reporter2 = progress2.reporter(totalCount: 10) + + reporter2.withProperties { properties in + properties.totalFileCount = 9 + properties.completedFileCount = 0 + } + + XCTAssertEqual(overall.fractionCompleted, 0.0) + XCTAssertEqual(overall.withProperties(\.totalFileCount), 20) + + // Update FileCounts + reporter1.withProperties { properties in + properties.completedFileCount = 1 + } + + reporter2.withProperties { properties in + properties.completedFileCount = 1 + } + + XCTAssertEqual(overall.withProperties(\.completedFileCount), 2) + } + + func testThreeLevelTreeWithFileProperties() async throws { + let overall = ProgressReporter(totalCount: 1) + + let progress1 = overall.assign(count: 1) + let reporter1 = progress1.reporter(totalCount: 5) + + let childProgress1 = reporter1.assign(count: 3) + let childReporter1 = childProgress1.reporter(totalCount: nil) + childReporter1.withProperties { properties in + properties.totalFileCount = 10 + } + + let childProgress2 = reporter1.assign(count: 2) + let childReporter2 = childProgress2.reporter(totalCount: nil) + childReporter2.withProperties { properties in + properties.totalFileCount = 10 + } + + XCTAssertEqual(reporter1.withProperties(\.totalFileCount), 20) + + // Tests that totalFileCount propagates to root level + XCTAssertEqual(overall.withProperties(\.totalFileCount), 20) + } + + func testTwoLevelTreeWithTwoChildren() async throws { + let overall = ProgressReporter(totalCount: 2) + + await doBasicOperationV1(reportTo: overall.assign(count: 1)) + XCTAssertEqual(overall.fractionCompleted, 0.5) + XCTAssertEqual(overall.completedCount, 1) + XCTAssertFalse(overall.isFinished) + XCTAssertFalse(overall.isIndeterminate) + + await doBasicOperationV2(reportTo: overall.assign(count: 1)) + XCTAssertEqual(overall.fractionCompleted, 1.0) + XCTAssertEqual(overall.completedCount, 2) + XCTAssertTrue(overall.isFinished) + XCTAssertFalse(overall.isIndeterminate) + } + + func testTwoLevelTreeWithTwoChildrenWithOneFileProperty() async throws { + let overall = ProgressReporter(totalCount: 2) + + let progress1 = overall.assign(count: 1) + let reporter1 = progress1.reporter(totalCount: 5) + reporter1.complete(count: 5) + + let progress2 = overall.assign(count: 1) + let reporter2 = progress2.reporter(totalCount: 5) + reporter2.withProperties { properties in + properties.totalFileCount = 10 + } + + XCTAssertEqual(overall.fractionCompleted, 0.5) + // Parent is expected to get totalFileCount from one of the children with a totalFileCount + XCTAssertEqual(overall.withProperties(\.totalFileCount), 10) + } + + func testTwoLevelTreeWithMultipleChildren() async throws { + let overall = ProgressReporter(totalCount: 3) + + await doBasicOperationV1(reportTo: overall.assign(count:1)) + XCTAssertEqual(overall.fractionCompleted, Double(1) / Double(3)) + XCTAssertEqual(overall.completedCount, 1) + + await doBasicOperationV2(reportTo: overall.assign(count:1)) + XCTAssertEqual(overall.fractionCompleted, Double(2) / Double(3)) + XCTAssertEqual(overall.completedCount, 2) + + await doBasicOperationV3(reportTo: overall.assign(count:1)) + XCTAssertEqual(overall.fractionCompleted, Double(3) / Double(3)) + XCTAssertEqual(overall.completedCount, 3) + } + + func testThreeLevelTree() async throws { + let overall = ProgressReporter(totalCount: 100) + XCTAssertEqual(overall.fractionCompleted, 0.0) + + let child1 = overall.assign(count: 100) + let reporter1 = child1.reporter(totalCount: 100) + + let grandchild1 = reporter1.assign(count: 100) + let grandchildReporter1 = grandchild1.reporter(totalCount: 100) + + XCTAssertEqual(overall.fractionCompleted, 0.0) + + grandchildReporter1.complete(count: 50) + XCTAssertEqual(reporter1.fractionCompleted, 0.5) + XCTAssertEqual(overall.fractionCompleted, 0.5) + + grandchildReporter1.complete(count: 50) + XCTAssertEqual(reporter1.fractionCompleted, 1.0) + XCTAssertEqual(overall.fractionCompleted, 1.0) + + XCTAssertTrue(grandchildReporter1.isFinished) + XCTAssertTrue(reporter1.isFinished) + XCTAssertTrue(overall.isFinished) + } + + func testFourLevelTree() async throws { + let overall = ProgressReporter(totalCount: 100) + XCTAssertEqual(overall.fractionCompleted, 0.0) + + let child1 = overall.assign(count: 100) + let reporter1 = child1.reporter(totalCount: 100) + + let grandchild1 = reporter1.assign(count: 100) + let grandchildReporter1 = grandchild1.reporter(totalCount: 100) + + XCTAssertEqual(overall.fractionCompleted, 0.0) + + + let greatGrandchild1 = grandchildReporter1.assign(count: 100) + let greatGrandchildReporter1 = greatGrandchild1.reporter(totalCount: 100) + + greatGrandchildReporter1.complete(count: 50) + XCTAssertEqual(overall.fractionCompleted, 0.5) + + greatGrandchildReporter1.complete(count: 50) + XCTAssertEqual(overall.fractionCompleted, 1.0) + + XCTAssertTrue(greatGrandchildReporter1.isFinished) + XCTAssertTrue(grandchildReporter1.isFinished) + XCTAssertTrue(reporter1.isFinished) + XCTAssertTrue(overall.isFinished) + } +} + + +/// Unit tests for interop methods that support building Progress trees with both Progress and ProgressReporter +class TestProgressReporterInterop: XCTestCase { + func doSomethingWithProgress(expectation1: XCTestExpectation, expectation2: XCTestExpectation) async -> Progress { + let p = Progress(totalUnitCount: 2) + Task.detached { + p.completedUnitCount = 1 + expectation1.fulfill() + p.completedUnitCount = 2 + expectation2.fulfill() + } + return p + } + + func doSomethingWithReporter(progress: consuming ProgressReporter.Progress?) async { + let reporter = progress?.reporter(totalCount: 4) + reporter?.complete(count: 2) + reporter?.complete(count: 2) + } + + func testInteropProgressParentProgressReporterChild() async throws { + // Initialize a Progress Parent + let overall = Progress.discreteProgress(totalUnitCount: 10) + + // Add Progress as Child + let expectation1 = XCTestExpectation(description: "Set completed unit count to 1") + let expectation2 = XCTestExpectation(description: "Set completed unit count to 2") + let p1 = await doSomethingWithProgress(expectation1: expectation1, expectation2: expectation2) + overall.addChild(p1, withPendingUnitCount: 5) + + await fulfillment(of: [expectation1, expectation2], timeout: 10.0) + + // Check if ProgressReporter values propagate to Progress parent + XCTAssertEqual(overall.fractionCompleted, 0.5) + XCTAssertEqual(overall.completedUnitCount, 5) + + // Add ProgressReporter as Child + let p2 = overall.makeChild(withPendingUnitCount: 5) + await doSomethingWithReporter(progress: p2) + + // Check if Progress values propagate to Progress parent + XCTAssertEqual(overall.fractionCompleted, 1.0) + XCTAssertEqual(overall.completedUnitCount, 10) + } + + func testInteropProgressReporterParentProgressChild() async throws { + // Initialize ProgressReporter parent + let overallReporter = ProgressReporter(totalCount: 10) + + // Add ProgressReporter as Child + await doSomethingWithReporter(progress: overallReporter.assign(count: 5)) + + // Check if ProgressReporter values propagate to ProgressReporter parent + XCTAssertEqual(overallReporter.fractionCompleted, 0.5) + XCTAssertEqual(overallReporter.completedCount, 5) + + // Interop: Add Progress as Child + let expectation1 = XCTestExpectation(description: "Set completed unit count to 1") + let expectation2 = XCTestExpectation(description: "Set completed unit count to 2") + let p2 = await doSomethingWithProgress(expectation1: expectation1, expectation2: expectation2) + overallReporter.assign(count: 5, to: p2) + + await fulfillment(of: [expectation1, expectation2], timeout: 10.0) + + // Check if Progress values propagate to ProgressRerpoter parent + XCTAssertEqual(overallReporter.completedCount, 10) + XCTAssertEqual(overallReporter.totalCount, 10) + XCTAssertEqual(overallReporter.fractionCompleted, 1.0) + } + + func getProgressWithTotalCountInitialized() -> Progress { + return Progress(totalUnitCount: 5) + } + + func receiveProgress(progress: consuming ProgressReporter.Progress) { + let _ = progress.reporter(totalCount: 5) + } + + func testInteropProgressReporterParentProgressChildConsistency() async throws { + let overallReporter = ProgressReporter(totalCount: nil) + let child = overallReporter.assign(count: 5) + receiveProgress(progress: child) + XCTAssertNil(overallReporter.totalCount) + + let overallReporter2 = ProgressReporter(totalCount: nil) + let interopChild = getProgressWithTotalCountInitialized() + overallReporter2.assign(count: 5, to: interopChild) + XCTAssertNil(overallReporter2.totalCount) + } + + func testInteropProgressParentProgressReporterChildConsistency() async throws { + let overallProgress = Progress() + let child = Progress(totalUnitCount: 5) + overallProgress.addChild(child, withPendingUnitCount: 5) + XCTAssertEqual(overallProgress.totalUnitCount, 0) + + let overallProgress2 = Progress() + let interopChild = overallProgress2.makeChild(withPendingUnitCount: 5) + receiveProgress(progress: interopChild) + XCTAssertEqual(overallProgress2.totalUnitCount, 0) + } +} + From db15363a41e4c5b436a2553a68afd73350e86e69 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 21 Apr 2025 15:04:39 -0700 Subject: [PATCH 04/85] resolve build issues with ProgressReporter as API" --- .../ProgressReporter+FileFormatStyle.swift | 37 ++++++++++++ .../ProgressReporter+FormatStyle.swift | 58 +++++-------------- 2 files changed, 50 insertions(+), 45 deletions(-) diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift index 8dcee8892..5605733b7 100644 --- a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift @@ -57,6 +57,7 @@ extension ProgressReporter.FileFormatStyle: FormatStyle { switch self.option.rawOption { case .file: + #if FOUNDATION_FRAMEWORK var fileCountLSR: LocalizedStringResource? var byteCountLSR: LocalizedStringResource? var throughputLSR: LocalizedStringResource? @@ -88,6 +89,42 @@ extension ProgressReporter.FileFormatStyle: FormatStyle { \(String(localized: throughputLSR ?? "")) \(String(localized: timeRemainingLSR ?? "")) """ + #else + + var fileCountString: String? + var byteCountString: String? + var throughputString: String? + var timeRemainingString: String? + + let properties = reporter.withProperties(\.self) + + if let totalFileCount = properties.totalFileCount { + let completedFileCount = properties.completedFileCount ?? 0 + fileCountString = "\(completedFileCount.formatted(IntegerFormatStyle(locale: self.locale))) / \(totalFileCount.formatted(IntegerFormatStyle(locale: self.locale)))" + } + + if let totalByteCount = properties.totalByteCount { + let completedByteCount = properties.completedByteCount ?? 0 + byteCountString = "\(completedByteCount.formatted(ByteCountFormatStyle(locale: self.locale))) / \(totalByteCount.formatted(ByteCountFormatStyle(locale: self.locale)))" + } + + if let throughput = properties.throughput { + throughputString = "\(throughput.formatted(ByteCountFormatStyle(locale: self.locale)))/s" + } + + if let timeRemaining = properties.estimatedTimeRemaining { + var formatStyle = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .wide) + formatStyle.locale = self.locale + timeRemainingString = "\(timeRemaining.formatted(formatStyle)) remaining" + } + + return """ + \(fileCountString ?? "") + \(byteCountString ?? "") + \(throughputString ?? "") + \(timeRemainingString ?? "") + """ + #endif } } } diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift index 515ab395b..4b9fd7932 100644 --- a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift @@ -21,13 +21,12 @@ extension ProgressReporter { // Outlines the options available to format ProgressReporter internal struct Option: Sendable, Codable, Hashable, Equatable { - #if FOUNDATION_FRAMEWORK /// Option specifying`fractionCompleted`. /// /// For example, 20% completed. /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance that should be used to format `fractionCompleted`. /// - Returns: A `LocalizedStringResource` for formatted `fractionCompleted`. - internal static func fractionCompleted(format style: Foundation.FloatingPointFormatStyle.Percent = Foundation.FloatingPointFormatStyle.Percent() + internal static func fractionCompleted(format style: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent() ) -> Option { return Option(.fractionCompleted(style)) } @@ -37,33 +36,11 @@ extension ProgressReporter { /// For example, 5 of 10. /// - Parameter style: An `IntegerFormatStyle` instance that should be used to format `completedCount` and `totalCount`. /// - Returns: A `LocalizedStringResource` for formatted `completedCount` / `totalCount`. - internal static func count(format style: Foundation.IntegerFormatStyle = Foundation.IntegerFormatStyle() + internal static func count(format style: IntegerFormatStyle = IntegerFormatStyle() ) -> Option { return Option(.count(style)) } - #else - /// Option specifying`fractionCompleted`. - /// - /// For example, 20% completed. - /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance that should be used to format `fractionCompleted`. - /// - Returns: A `LocalizedStringResource` for formatted `fractionCompleted`. - internal static func fractionCompleted(format style: FoundationInternationalization.FloatingPointFormatStyle.Percent = FoundationInternationalization.FloatingPointFormatStyle.Percent() - ) -> Option { - return Option(.fractionCompleted(style)) - } - - /// Option specifying `completedCount` / `totalCount`. - /// - /// For example, 5 of 10. - /// - Parameter style: An `IntegerFormatStyle` instance that should be used to format `completedCount` and `totalCount`. - /// - Returns: A `LocalizedStringResource` for formatted `completedCount` / `totalCount`. - internal static func count(format style: FoundationInternationalization.IntegerFormatStyle = FoundationInternationalization.IntegerFormatStyle() - ) -> Option { - return Option(.count(style)) - } - #endif // FOUNDATION_FRAMEWORK - - + fileprivate enum RawOption: Codable, Hashable, Equatable { case count(IntegerFormatStyle) case fractionCompleted(FloatingPointFormatStyle.Percent) @@ -99,12 +76,20 @@ extension ProgressReporter.FormatStyle: FormatStyle { let count = reporter.withProperties { p in return (p.completedCount, p.totalCount) } + #if FOUNDATION_FRAMEWORK let countLSR = LocalizedStringResource("\(count.0, format: countStyle) of \(count.1 ?? 0, format: countStyle)", locale: self.locale, bundle: .forClass(ProgressReporter.self)) return String(localized: countLSR) + #else + return "\(count.0.formatted(countStyle.locale(self.locale))) / \((count.1 ?? 0).formatted(countStyle.locale(self.locale)))" + #endif case .fractionCompleted(let fractionStyle): + #if FOUNDATION_FRAMEWORK let fractionLSR = LocalizedStringResource("\(reporter.fractionCompleted, format: fractionStyle) completed", locale: self.locale, bundle: .forClass(ProgressReporter.self)) return String(localized: fractionLSR) + #else + return "\(reporter.fractionCompleted.formatted(fractionStyle.locale(self.locale)))" + #endif } } } @@ -123,7 +108,6 @@ extension ProgressReporter { } #endif // FOUNDATION_FRAMEWORK - public func formatted() -> String { self.formatted(.fractionCompleted()) } @@ -133,31 +117,15 @@ extension ProgressReporter { @available(FoundationPreview 6.2, *) extension FormatStyle where Self == ProgressReporter.FormatStyle { -#if FOUNDATION_FRAMEWORK - public static func fractionCompleted( - format: Foundation.FloatingPointFormatStyle.Percent = Foundation.FloatingPointFormatStyle.Percent() - ) -> Self { - .init(.fractionCompleted(format: format)) - } - - public static func count( - format: Foundation.IntegerFormatStyle = Foundation.IntegerFormatStyle() - ) -> Self { - .init(.count(format: format)) - } -#else public static func fractionCompleted( - format: FoundationInternationalization.FloatingPointFormatStyle.Percent = FoundationInternationalization.FloatingPointFormatStyle.Percent() + format: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent() ) -> Self { .init(.fractionCompleted(format: format)) } public static func count( - format: FoundationInternationalization.IntegerFormatStyle = FoundationInternationalization.IntegerFormatStyle() + format: IntegerFormatStyle = IntegerFormatStyle() ) -> Self { .init(.count(format: format)) } -#endif - - } From 49c433abf4bee96f28998d8c89c560e5d4b761a8 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 21 Apr 2025 15:34:31 -0700 Subject: [PATCH 05/85] apply naming changes --- .../ProgressReporter+Interop.swift | 12 +-- .../ProgressReporter+Progress.swift | 74 +++++++++---------- .../ProgressReporter/ProgressReporter.swift | 13 ++-- .../ProgressReporterTests.swift | 66 ++++++++--------- 4 files changed, 81 insertions(+), 84 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift index 58b3da331..36c12a7fd 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift @@ -21,15 +21,15 @@ internal import _ForSwiftFoundation // Actual ProgressReporter Child extension Progress { - /// Returns a ProgressReporter.Progress which can be passed to any method that reports progress + /// Returns a Subprogress which can be passed to any method that reports progress /// and can be initialized into a child `ProgressReporter` to the `self`. /// /// Delegates a portion of totalUnitCount to a future child `ProgressReporter` instance. /// /// - Parameter count: Number of units delegated to a child instance of `ProgressReporter` - /// which may be instantiated by `ProgressReporter.Progress` later when `reporter(totalCount:)` is called. - /// - Returns: A `ProgressReporter.Progress` instance. - public func makeChild(withPendingUnitCount count: Int) -> ProgressReporter.Progress { + /// which may be instantiated by `Subprogress` later when `reporter(totalCount:)` is called. + /// - Returns: A `Subprogress` instance. + public func makeChild(withPendingUnitCount count: Int) -> Subprogress { // Make ghost parent & add it to actual parent's children list let ghostProgressParent = Progress(totalUnitCount: Int64(count)) @@ -42,7 +42,7 @@ extension Progress { let observation = _ProgressParentProgressReporterChild(ghostParent: ghostProgressParent, ghostChild: ghostReporterChild) // Make actual child with ghost child being parent - var actualProgress = ghostReporterChild.assign(count: count) + var actualProgress = ghostReporterChild.subprogress(assigningCount: count) actualProgress.observation = observation actualProgress.ghostReporter = ghostReporterChild actualProgress.interopWithProgressParent = true @@ -88,7 +88,7 @@ extension ProgressReporter { /// - Parameters: /// - count: Number of units delegated from `self`'s `totalCount`. /// - progress: `Progress` which receives the delegated `count`. - public func assign(count: Int, to progress: Foundation.Progress) { + public func subprogress(assigningCount count: Int, to progress: Foundation.Progress) { let parentBridge = _NSProgressParentBridge(reporterParent: self) progress._setParent(parentBridge, portion: Int64(count)) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Progress.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Progress.swift index 94854201e..6e8b1f540 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Progress.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Progress.swift @@ -11,46 +11,44 @@ //===----------------------------------------------------------------------===// @available(FoundationPreview 6.2, *) -// ProgressReporter.Progress -extension ProgressReporter { - /// ProgressReporter.Progress is a nested ~Copyable struct used to establish parent-child relationship between two instances of ProgressReporter. - /// - /// ProgressReporter.Progress is returned from a call to `assign(count:)` by a parent ProgressReporter. - /// A child ProgressReporter is then returned by calling`reporter(totalCount:)` on a ProgressReporter.Progress. - public struct Progress: ~Copyable, Sendable { - internal var parent: ProgressReporter - internal var portionOfParent: Int - internal var isInitializedToProgressReporter: Bool +/// Subprogress is a nested ~Copyable struct used to establish parent-child relationship between two instances of ProgressReporter. +/// +/// Subprogress is returned from a call to `subprogress(assigningCount:)` by a parent ProgressReporter. +/// A child ProgressReporter is then returned by calling`reporter(totalCount:)` on a Subprogress. +public struct Subprogress: ~Copyable, Sendable { + internal var parent: ProgressReporter + internal var portionOfParent: Int + internal var isInitializedToProgressReporter: Bool + + // Interop variables for Progress - ProgressReporter Interop + internal var interopWithProgressParent: Bool = false + // To be kept alive in ProgressReporter + internal var observation: (any Sendable)? + internal var ghostReporter: ProgressReporter? + + internal init(parent: ProgressReporter, portionOfParent: Int) { + self.parent = parent + self.portionOfParent = portionOfParent + self.isInitializedToProgressReporter = false + } + + /// Instantiates a ProgressReporter which is a child to the parent from which `self` is returned. + /// - Parameter totalCount: Total count of returned child `ProgressReporter` instance. + /// - Returns: A `ProgressReporter` instance. + public consuming func reporter(totalCount: Int?) -> ProgressReporter { + isInitializedToProgressReporter = true - // Interop variables for Progress - ProgressReporter Interop - internal var interopWithProgressParent: Bool = false - // To be kept alive in ProgressReporter - internal var observation: (any Sendable)? - internal var ghostReporter: ProgressReporter? - - internal init(parent: ProgressReporter, portionOfParent: Int) { - self.parent = parent - self.portionOfParent = portionOfParent - self.isInitializedToProgressReporter = false - } + let childReporter = ProgressReporter(total: totalCount, parent: parent, portionOfParent: portionOfParent, ghostReporter: ghostReporter, interopObservation: observation) - /// Instantiates a ProgressReporter which is a child to the parent from which `self` is returned. - /// - Parameter totalCount: Total count of returned child `ProgressReporter` instance. - /// - Returns: A `ProgressReporter` instance. - public consuming func reporter(totalCount: Int?) -> ProgressReporter { - isInitializedToProgressReporter = true - - let childReporter = ProgressReporter(total: totalCount, parent: parent, portionOfParent: portionOfParent, ghostReporter: ghostReporter, interopObservation: observation) - - if interopWithProgressParent { - // Set interop child of ghost reporter so ghost reporter reads from here - ghostReporter?.setInteropChild(interopChild: childReporter) - } else { - // Add child to parent's _children list - parent.addToChildren(childReporter: childReporter) - } - - return childReporter + if interopWithProgressParent { + // Set interop child of ghost reporter so ghost reporter reads from here + ghostReporter?.setInteropChild(interopChild: childReporter) + } else { + // Add child to parent's _children list + parent.addToChildren(childReporter: childReporter) } + + return childReporter } } + diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index ac0c781e6..e9cfbe5d6 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -269,17 +269,16 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } - /// Returns a `ProgressReporter.Progress` representing a portion of `self`which can be passed to any method that reports progress. + /// Returns a `Subprogress` representing a portion of `self`which can be passed to any method that reports progress. /// - /// - Parameter count: Units, which is a portion of `totalCount`delegated to an instance of `ProgressReporter.Progress`. - /// - Returns: A `ProgressReporter.Progress` instance. - public func assign(count portionOfParent: Int) -> Progress { + /// - Parameter count: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. + /// - Returns: A `Subprogress` instance. + public func subprogress(assigningCount portionOfParent: Int) -> Subprogress { precondition(portionOfParent > 0, "Giving out zero units is not a valid operation.") - let childProgress = Progress(parent: self, portionOfParent: portionOfParent) - return childProgress + let subprogress = Subprogress(parent: self, portionOfParent: portionOfParent) + return subprogress } - /// Increases `completedCount` by `count`. /// - Parameter count: Units of work. public func complete(count: Int) { diff --git a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift index 5841461e2..feb7c0399 100644 --- a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift @@ -20,7 +20,7 @@ import XCTest /// Unit tests for basic functionalities of ProgressReporter class TestProgressReporter: XCTestCase { /// MARK: Helper methods that report progress - func doBasicOperationV1(reportTo progress: consuming ProgressReporter.Progress) async { + func doBasicOperationV1(reportTo progress: consuming Subprogress) async { let reporter = progress.reporter(totalCount: 8) for i in 1...8 { reporter.complete(count: 1) @@ -29,7 +29,7 @@ class TestProgressReporter: XCTestCase { } } - func doBasicOperationV2(reportTo progress: consuming ProgressReporter.Progress) async { + func doBasicOperationV2(reportTo progress: consuming Subprogress) async { let reporter = progress.reporter(totalCount: 7) for i in 1...7 { reporter.complete(count: 1) @@ -38,7 +38,7 @@ class TestProgressReporter: XCTestCase { } } - func doBasicOperationV3(reportTo progress: consuming ProgressReporter.Progress) async { + func doBasicOperationV3(reportTo progress: consuming Subprogress) async { let reporter = progress.reporter(totalCount: 11) for i in 1...11 { reporter.complete(count: 1) @@ -47,7 +47,7 @@ class TestProgressReporter: XCTestCase { } } - func doFileOperation(reportTo progress: consuming ProgressReporter.Progress) async { + func doFileOperation(reportTo progress: consuming Subprogress) async { let reporter = progress.reporter(totalCount: 100) reporter.withProperties { properties in properties.totalFileCount = 100 @@ -114,7 +114,7 @@ class TestProgressReporter: XCTestCase { XCTAssertTrue(overall.isIndeterminate) XCTAssertFalse(overall.isFinished) - let progress1 = overall.assign(count: 2) + let progress1 = overall.subprogress(assigningCount: 2) let reporter1 = progress1.reporter(totalCount: 1) reporter1.complete(count: 1) @@ -144,7 +144,7 @@ class TestProgressReporter: XCTestCase { let overall = ProgressReporter(totalCount: 10) overall.complete(count: 5) - let progress1 = overall.assign(count: 8) + let progress1 = overall.subprogress(assigningCount: 8) let reporter1 = progress1.reporter(totalCount: 1) reporter1.complete(count: 1) @@ -158,7 +158,7 @@ class TestProgressReporter: XCTestCase { /// MARK: Tests single-level tree func testDiscreteReporter() async throws { let reporter = ProgressReporter(totalCount: 3) - await doBasicOperationV1(reportTo: reporter.assign(count: 3)) + await doBasicOperationV1(reportTo: reporter.subprogress(assigningCount: 3)) XCTAssertEqual(reporter.fractionCompleted, 1.0) XCTAssertEqual(reporter.completedCount, 3) XCTAssertTrue(reporter.isFinished) @@ -166,7 +166,7 @@ class TestProgressReporter: XCTestCase { func testDiscreteReporterWithFileProperties() async throws { let fileReporter = ProgressReporter(totalCount: 3) - await doFileOperation(reportTo: fileReporter.assign(count: 3)) + await doFileOperation(reportTo: fileReporter.subprogress(assigningCount: 3)) XCTAssertEqual(fileReporter.fractionCompleted, 1.0) XCTAssertEqual(fileReporter.completedCount, 3) XCTAssertTrue(fileReporter.isFinished) @@ -183,7 +183,7 @@ class TestProgressReporter: XCTestCase { XCTAssertFalse(reporter.isIndeterminate) XCTAssertEqual(reporter.totalCount, 10) - await doBasicOperationV1(reportTo: reporter.assign(count: 10)) + await doBasicOperationV1(reportTo: reporter.subprogress(assigningCount: 10)) XCTAssertEqual(reporter.fractionCompleted, 1.0) XCTAssertEqual(reporter.completedCount, 10) XCTAssertTrue(reporter.isFinished) @@ -192,7 +192,7 @@ class TestProgressReporter: XCTestCase { func testTwoLevelTreeWithOneChildWithFileProperties() async throws { let overall = ProgressReporter(totalCount: 2) - let progress1 = overall.assign(count: 1) + let progress1 = overall.subprogress(assigningCount: 1) let reporter1 = progress1.reporter(totalCount: 10) reporter1.withProperties { properties in properties.totalFileCount = 10 @@ -208,7 +208,7 @@ class TestProgressReporter: XCTestCase { func testTwoLevelTreeWithTwoChildrenWithFileProperties() async throws { let overall = ProgressReporter(totalCount: 2) - let progress1 = overall.assign(count: 1) + let progress1 = overall.subprogress(assigningCount: 1) let reporter1 = progress1.reporter(totalCount: 10) reporter1.withProperties { properties in @@ -216,7 +216,7 @@ class TestProgressReporter: XCTestCase { properties.completedFileCount = 0 } - let progress2 = overall.assign(count: 1) + let progress2 = overall.subprogress(assigningCount: 1) let reporter2 = progress2.reporter(totalCount: 10) reporter2.withProperties { properties in @@ -242,16 +242,16 @@ class TestProgressReporter: XCTestCase { func testThreeLevelTreeWithFileProperties() async throws { let overall = ProgressReporter(totalCount: 1) - let progress1 = overall.assign(count: 1) + let progress1 = overall.subprogress(assigningCount: 1) let reporter1 = progress1.reporter(totalCount: 5) - let childProgress1 = reporter1.assign(count: 3) + let childProgress1 = reporter1.subprogress(assigningCount: 3) let childReporter1 = childProgress1.reporter(totalCount: nil) childReporter1.withProperties { properties in properties.totalFileCount = 10 } - let childProgress2 = reporter1.assign(count: 2) + let childProgress2 = reporter1.subprogress(assigningCount: 2) let childReporter2 = childProgress2.reporter(totalCount: nil) childReporter2.withProperties { properties in properties.totalFileCount = 10 @@ -266,13 +266,13 @@ class TestProgressReporter: XCTestCase { func testTwoLevelTreeWithTwoChildren() async throws { let overall = ProgressReporter(totalCount: 2) - await doBasicOperationV1(reportTo: overall.assign(count: 1)) + await doBasicOperationV1(reportTo: overall.subprogress(assigningCount: 1)) XCTAssertEqual(overall.fractionCompleted, 0.5) XCTAssertEqual(overall.completedCount, 1) XCTAssertFalse(overall.isFinished) XCTAssertFalse(overall.isIndeterminate) - await doBasicOperationV2(reportTo: overall.assign(count: 1)) + await doBasicOperationV2(reportTo: overall.subprogress(assigningCount: 1)) XCTAssertEqual(overall.fractionCompleted, 1.0) XCTAssertEqual(overall.completedCount, 2) XCTAssertTrue(overall.isFinished) @@ -282,11 +282,11 @@ class TestProgressReporter: XCTestCase { func testTwoLevelTreeWithTwoChildrenWithOneFileProperty() async throws { let overall = ProgressReporter(totalCount: 2) - let progress1 = overall.assign(count: 1) + let progress1 = overall.subprogress(assigningCount: 1) let reporter1 = progress1.reporter(totalCount: 5) reporter1.complete(count: 5) - let progress2 = overall.assign(count: 1) + let progress2 = overall.subprogress(assigningCount: 1) let reporter2 = progress2.reporter(totalCount: 5) reporter2.withProperties { properties in properties.totalFileCount = 10 @@ -300,15 +300,15 @@ class TestProgressReporter: XCTestCase { func testTwoLevelTreeWithMultipleChildren() async throws { let overall = ProgressReporter(totalCount: 3) - await doBasicOperationV1(reportTo: overall.assign(count:1)) + await doBasicOperationV1(reportTo: overall.subprogress(assigningCount:1)) XCTAssertEqual(overall.fractionCompleted, Double(1) / Double(3)) XCTAssertEqual(overall.completedCount, 1) - await doBasicOperationV2(reportTo: overall.assign(count:1)) + await doBasicOperationV2(reportTo: overall.subprogress(assigningCount:1)) XCTAssertEqual(overall.fractionCompleted, Double(2) / Double(3)) XCTAssertEqual(overall.completedCount, 2) - await doBasicOperationV3(reportTo: overall.assign(count:1)) + await doBasicOperationV3(reportTo: overall.subprogress(assigningCount:1)) XCTAssertEqual(overall.fractionCompleted, Double(3) / Double(3)) XCTAssertEqual(overall.completedCount, 3) } @@ -317,10 +317,10 @@ class TestProgressReporter: XCTestCase { let overall = ProgressReporter(totalCount: 100) XCTAssertEqual(overall.fractionCompleted, 0.0) - let child1 = overall.assign(count: 100) + let child1 = overall.subprogress(assigningCount: 100) let reporter1 = child1.reporter(totalCount: 100) - let grandchild1 = reporter1.assign(count: 100) + let grandchild1 = reporter1.subprogress(assigningCount: 100) let grandchildReporter1 = grandchild1.reporter(totalCount: 100) XCTAssertEqual(overall.fractionCompleted, 0.0) @@ -342,16 +342,16 @@ class TestProgressReporter: XCTestCase { let overall = ProgressReporter(totalCount: 100) XCTAssertEqual(overall.fractionCompleted, 0.0) - let child1 = overall.assign(count: 100) + let child1 = overall.subprogress(assigningCount: 100) let reporter1 = child1.reporter(totalCount: 100) - let grandchild1 = reporter1.assign(count: 100) + let grandchild1 = reporter1.subprogress(assigningCount: 100) let grandchildReporter1 = grandchild1.reporter(totalCount: 100) XCTAssertEqual(overall.fractionCompleted, 0.0) - let greatGrandchild1 = grandchildReporter1.assign(count: 100) + let greatGrandchild1 = grandchildReporter1.subprogress(assigningCount: 100) let greatGrandchildReporter1 = greatGrandchild1.reporter(totalCount: 100) greatGrandchildReporter1.complete(count: 50) @@ -381,7 +381,7 @@ class TestProgressReporterInterop: XCTestCase { return p } - func doSomethingWithReporter(progress: consuming ProgressReporter.Progress?) async { + func doSomethingWithReporter(progress: consuming Subprogress?) async { let reporter = progress?.reporter(totalCount: 4) reporter?.complete(count: 2) reporter?.complete(count: 2) @@ -417,7 +417,7 @@ class TestProgressReporterInterop: XCTestCase { let overallReporter = ProgressReporter(totalCount: 10) // Add ProgressReporter as Child - await doSomethingWithReporter(progress: overallReporter.assign(count: 5)) + await doSomethingWithReporter(progress: overallReporter.subprogress(assigningCount: 5)) // Check if ProgressReporter values propagate to ProgressReporter parent XCTAssertEqual(overallReporter.fractionCompleted, 0.5) @@ -427,7 +427,7 @@ class TestProgressReporterInterop: XCTestCase { let expectation1 = XCTestExpectation(description: "Set completed unit count to 1") let expectation2 = XCTestExpectation(description: "Set completed unit count to 2") let p2 = await doSomethingWithProgress(expectation1: expectation1, expectation2: expectation2) - overallReporter.assign(count: 5, to: p2) + overallReporter.subprogress(assigningCount: 5, to: p2) await fulfillment(of: [expectation1, expectation2], timeout: 10.0) @@ -441,19 +441,19 @@ class TestProgressReporterInterop: XCTestCase { return Progress(totalUnitCount: 5) } - func receiveProgress(progress: consuming ProgressReporter.Progress) { + func receiveProgress(progress: consuming Subprogress) { let _ = progress.reporter(totalCount: 5) } func testInteropProgressReporterParentProgressChildConsistency() async throws { let overallReporter = ProgressReporter(totalCount: nil) - let child = overallReporter.assign(count: 5) + let child = overallReporter.subprogress(assigningCount: 5) receiveProgress(progress: child) XCTAssertNil(overallReporter.totalCount) let overallReporter2 = ProgressReporter(totalCount: nil) let interopChild = getProgressWithTotalCountInitialized() - overallReporter2.assign(count: 5, to: interopChild) + overallReporter2.subprogress(assigningCount: 5, to: interopChild) XCTAssertNil(overallReporter2.totalCount) } From 4738c66fa497cc573119ebe1ed444bb03c5512cb Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 21 Apr 2025 15:46:00 -0700 Subject: [PATCH 06/85] remove extra comma --- .../ProgressReporter/ProgressReporter+FileFormatStyle.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift index 5605733b7..33fc32f86 100644 --- a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift @@ -28,9 +28,7 @@ extension ProgressReporter { fileprivate var rawOption: RawOption - private init( - _ rawOption: RawOption, - ) { + private init(_ rawOption: RawOption) { self.rawOption = rawOption } } From 70f28ab7aaff764aafdb7d77787ab9f7d54b64ad Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 21 Apr 2025 16:16:51 -0700 Subject: [PATCH 07/85] only run interop tests if foundation framework --- .../ProgressReporter/ProgressReporterTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift index feb7c0399..bd5cece6a 100644 --- a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift @@ -367,7 +367,7 @@ class TestProgressReporter: XCTestCase { } } - +#if FOUNDATION_FRAMEWORK /// Unit tests for interop methods that support building Progress trees with both Progress and ProgressReporter class TestProgressReporterInterop: XCTestCase { func doSomethingWithProgress(expectation1: XCTestExpectation, expectation2: XCTestExpectation) async -> Progress { @@ -469,4 +469,4 @@ class TestProgressReporterInterop: XCTestCase { XCTAssertEqual(overallProgress2.totalUnitCount, 0) } } - +#endif From 13cb72ac7747ef2459583357a76d95a4eb0f116e Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 21 Apr 2025 16:17:39 -0700 Subject: [PATCH 08/85] rename file to subprogress --- .../{ProgressReporter+Progress.swift => Subprogress.swift} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Sources/FoundationEssentials/ProgressReporter/{ProgressReporter+Progress.swift => Subprogress.swift} (100%) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Progress.swift b/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift similarity index 100% rename from Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Progress.swift rename to Sources/FoundationEssentials/ProgressReporter/Subprogress.swift From 07510ee5292123ab21f3fa8fa63aa0704db590ee Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 22 Apr 2025 11:00:48 -0700 Subject: [PATCH 09/85] add CMake files --- Sources/FoundationEssentials/CMakeLists.txt | 1 + .../ProgressReporter/CMakeLists.txt | 19 +++++++++++++++++++ .../ProgressReporter/CMakeLists.txt | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 Sources/FoundationEssentials/ProgressReporter/CMakeLists.txt create mode 100644 Sources/FoundationInternationalization/ProgressReporter/CMakeLists.txt diff --git a/Sources/FoundationEssentials/CMakeLists.txt b/Sources/FoundationEssentials/CMakeLists.txt index 79435c105..8feb8e85b 100644 --- a/Sources/FoundationEssentials/CMakeLists.txt +++ b/Sources/FoundationEssentials/CMakeLists.txt @@ -44,6 +44,7 @@ add_subdirectory(JSON) add_subdirectory(Locale) add_subdirectory(Predicate) add_subdirectory(ProcessInfo) +add_subdirectory(ProgressReporter) add_subdirectory(PropertyList) add_subdirectory(String) add_subdirectory(TimeZone) diff --git a/Sources/FoundationEssentials/ProgressReporter/CMakeLists.txt b/Sources/FoundationEssentials/ProgressReporter/CMakeLists.txt new file mode 100644 index 000000000..575ba749f --- /dev/null +++ b/Sources/FoundationEssentials/ProgressReporter/CMakeLists.txt @@ -0,0 +1,19 @@ +##===----------------------------------------------------------------------===## +## +## This source file is part of the Swift open source project +## +## Copyright (c) 2025 Apple Inc. and the Swift project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.md for the list of Swift project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## +target_sources(FoundationEssentials PRIVATE + ProgressFraction.swift + ProgressReporter.swift + ProgressReporter+Interop.swift + ProgressReporter+Properties.swift + Subprogress) diff --git a/Sources/FoundationInternationalization/ProgressReporter/CMakeLists.txt b/Sources/FoundationInternationalization/ProgressReporter/CMakeLists.txt new file mode 100644 index 000000000..5cc2cc948 --- /dev/null +++ b/Sources/FoundationInternationalization/ProgressReporter/CMakeLists.txt @@ -0,0 +1,18 @@ +##===----------------------------------------------------------------------===## +## +## This source file is part of the Swift open source project +## +## Copyright (c) 2025 Apple Inc. and the Swift project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.md for the list of Swift project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +target_include_directories(FoundationInternationalization PRIVATE .) +target_sources(FoundationInternationalization PRIVATE + ProgressReporter+FileFormatStyle.swift + ProgressReporter+FormatStyle.swift) From 3defc0394381cfddb41b2d618049b5d3d1cd1c2c Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 22 Apr 2025 11:01:53 -0700 Subject: [PATCH 10/85] CMake --- Sources/FoundationInternationalization/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/FoundationInternationalization/CMakeLists.txt b/Sources/FoundationInternationalization/CMakeLists.txt index 6cf0629ef..6c9ace4a0 100644 --- a/Sources/FoundationInternationalization/CMakeLists.txt +++ b/Sources/FoundationInternationalization/CMakeLists.txt @@ -25,6 +25,7 @@ add_subdirectory(Formatting) add_subdirectory(ICU) add_subdirectory(Locale) add_subdirectory(Predicate) +add_subdirectory(ProgressReporter) add_subdirectory(String) add_subdirectory(TimeZone) From 23f39f8c41330c882189d370a7c7454c1f829b56 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 22 Apr 2025 11:02:47 -0700 Subject: [PATCH 11/85] Correct CMake --- Sources/FoundationEssentials/ProgressReporter/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/CMakeLists.txt b/Sources/FoundationEssentials/ProgressReporter/CMakeLists.txt index 575ba749f..73004105d 100644 --- a/Sources/FoundationEssentials/ProgressReporter/CMakeLists.txt +++ b/Sources/FoundationEssentials/ProgressReporter/CMakeLists.txt @@ -16,4 +16,4 @@ target_sources(FoundationEssentials PRIVATE ProgressReporter.swift ProgressReporter+Interop.swift ProgressReporter+Properties.swift - Subprogress) + Subprogress.swift) From 351b77a7769bf2d422178a1e1f24d922b20362e1 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 22 Apr 2025 11:11:23 -0700 Subject: [PATCH 12/85] exclude CMake from Package.swift --- Package.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 327c8cfbb..f30b8a646 100644 --- a/Package.swift +++ b/Package.swift @@ -133,7 +133,8 @@ let package = Package( "CMakeLists.txt", "ProcessInfo/CMakeLists.txt", "FileManager/CMakeLists.txt", - "URL/CMakeLists.txt" + "URL/CMakeLists.txt", + "ProgressReporter/CMakeLists.txt" ], cSettings: [ .define("_GNU_SOURCE", .when(platforms: [.linux])) @@ -195,7 +196,8 @@ let package = Package( "Locale/CMakeLists.txt", "Calendar/CMakeLists.txt", "CMakeLists.txt", - "Predicate/CMakeLists.txt" + "Predicate/CMakeLists.txt", + "ProgressReporter/CMakeLists.txt" ], cSettings: wasiLibcCSettings, swiftSettings: [ From 12bed94ec12b3162003f3347e044aea8a2f3e6cd Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 22 Apr 2025 13:21:50 -0700 Subject: [PATCH 13/85] add trailing comma in Package.swift --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index f30b8a646..d960d4838 100644 --- a/Package.swift +++ b/Package.swift @@ -197,7 +197,7 @@ let package = Package( "Calendar/CMakeLists.txt", "CMakeLists.txt", "Predicate/CMakeLists.txt", - "ProgressReporter/CMakeLists.txt" + "ProgressReporter/CMakeLists.txt", ], cSettings: wasiLibcCSettings, swiftSettings: [ From 97b15f72950303b5a7c8bc9c1dc2ec2246cd497a Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 23 Apr 2025 16:21:46 -0700 Subject: [PATCH 14/85] formatting changes --- Package.swift | 2 +- .../ProgressReporter/ProgressReporter+FormatStyle.swift | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index d960d4838..75545f0ee 100644 --- a/Package.swift +++ b/Package.swift @@ -134,7 +134,7 @@ let package = Package( "ProcessInfo/CMakeLists.txt", "FileManager/CMakeLists.txt", "URL/CMakeLists.txt", - "ProgressReporter/CMakeLists.txt" + "ProgressReporter/CMakeLists.txt", ], cSettings: [ .define("_GNU_SOURCE", .when(platforms: [.linux])) diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift index 4b9fd7932..87047a20a 100644 --- a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift @@ -21,7 +21,7 @@ extension ProgressReporter { // Outlines the options available to format ProgressReporter internal struct Option: Sendable, Codable, Hashable, Equatable { - /// Option specifying`fractionCompleted`. + /// Option specifying `fractionCompleted`. /// /// For example, 20% completed. /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance that should be used to format `fractionCompleted`. @@ -129,3 +129,6 @@ extension FormatStyle where Self == ProgressReporter.FormatStyle { .init(.count(format: format)) } } + + +ProgressReporter.FormatStyle.fractionCompleted().format(<#T##reporter: ProgressReporter##ProgressReporter#>) From 41130b66a00af67b8b9489e5b4c9d0e0c4b41f4d Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 23 Apr 2025 16:32:36 -0700 Subject: [PATCH 15/85] remove unused draft --- .../ProgressReporter/ProgressReporter+FormatStyle.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift index 87047a20a..12ef9fbec 100644 --- a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift @@ -129,6 +129,3 @@ extension FormatStyle where Self == ProgressReporter.FormatStyle { .init(.count(format: format)) } } - - -ProgressReporter.FormatStyle.fractionCompleted().format(<#T##reporter: ProgressReporter##ProgressReporter#>) From 3e3ccb055a98aaa2187beb6d961a99dac9aa813f Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 24 Apr 2025 16:18:20 -0700 Subject: [PATCH 16/85] add manual codable conformance --- .../ProgressReporter+FileFormatStyle.swift | 30 ++++++++++++++++++ .../ProgressReporter+FormatStyle.swift | 31 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift index 33fc32f86..dc3f36b0d 100644 --- a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift @@ -20,6 +20,20 @@ extension ProgressReporter { internal struct Option: Sendable, Codable, Equatable, Hashable { + enum CodingKeys: String, CodingKey { + case rawOption + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + rawOption = try container.decode(RawOption.self, forKey: .rawOption) + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(rawOption, forKey: .rawOption) + } + internal static var file: Option { Option(.file) } fileprivate enum RawOption: Codable, Equatable, Hashable { @@ -32,6 +46,22 @@ extension ProgressReporter { self.rawOption = rawOption } } + enum CodingKeys: String, CodingKey { + case locale + case option + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + locale = try container.decode(Locale.self, forKey: .locale) + option = try container.decode(Option.self, forKey: .option) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(locale, forKey: .locale) + try container.encode(option, forKey: .option) + } public var locale: Locale let option: Option diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift index 12ef9fbec..9f83d5f59 100644 --- a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift @@ -21,6 +21,20 @@ extension ProgressReporter { // Outlines the options available to format ProgressReporter internal struct Option: Sendable, Codable, Hashable, Equatable { + enum CodingKeys: String, CodingKey { + case rawOption + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + rawOption = try container.decode(RawOption.self, forKey: .rawOption) + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(rawOption, forKey: .rawOption) + } + /// Option specifying `fractionCompleted`. /// /// For example, 20% completed. @@ -53,6 +67,23 @@ extension ProgressReporter { } } + enum CodingKeys: String, CodingKey { + case locale + case option + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + locale = try container.decode(Locale.self, forKey: .locale) + option = try container.decode(Option.self, forKey: .option) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(locale, forKey: .locale) + try container.encode(option, forKey: .option) + } + public var locale: Locale let option: Option From f5e85223287066102ca6ea8dd04dba6f199a055d Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 24 Apr 2025 16:56:54 -0700 Subject: [PATCH 17/85] new manual codable conformance for FileFormatStyle --- .../ProgressReporter+FileFormatStyle.swift | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift index dc3f36b0d..60c898928 100644 --- a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift @@ -46,21 +46,27 @@ extension ProgressReporter { self.rawOption = rawOption } } - enum CodingKeys: String, CodingKey { - case locale - case option + + struct CodableRepresentation: Codable { + let locale: Locale + let includeFileDescription: Bool + } + + var codableRepresentation: CodableRepresentation { + .init(locale: self.locale, includeFileDescription: option.rawOption == .file) } public init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - locale = try container.decode(Locale.self, forKey: .locale) - option = try container.decode(Option.self, forKey: .option) + //TODO: Fix this later, codableRepresentation is not settable + let container = try decoder.singleValueContainer() + let rep = try container.decode(CodableRepresentation.self) + locale = rep.locale + option = .file } - + public func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(locale, forKey: .locale) - try container.encode(option, forKey: .option) + var container = encoder.singleValueContainer() + try container.encode(codableRepresentation) } public var locale: Locale From d840b1cd839e8b74eafea8615361eaf61583c033 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 28 Apr 2025 18:37:05 -0700 Subject: [PATCH 18/85] add default value static var to Property protocol --- .../ProgressReporter+Properties.swift | 12 ++++++++++++ .../ProgressReporter/ProgressReporter.swift | 10 ++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift index 9d74a0098..8f449cc54 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift @@ -15,31 +15,43 @@ extension ProgressReporter { public struct Properties: Sendable { public var totalFileCount: TotalFileCount.Type { TotalFileCount.self } public struct TotalFileCount: Sendable, Property { + public static var defaultValue: Int { return 0 } + public typealias T = Int } public var completedFileCount: CompletedFileCount.Type { CompletedFileCount.self } public struct CompletedFileCount: Sendable, Property { + public static var defaultValue: Int { return 0 } + public typealias T = Int } public var totalByteCount: TotalByteCount.Type { TotalByteCount.self } public struct TotalByteCount: Sendable, Property { + public static var defaultValue: Int64 { return 0 } + public typealias T = Int64 } public var completedByteCount: CompletedByteCount.Type { CompletedByteCount.self } public struct CompletedByteCount: Sendable, Property { + public static var defaultValue: Int64 { return 0 } + public typealias T = Int64 } public var throughput: Throughput.Type { Throughput.self } public struct Throughput: Sendable, Property { + public static var defaultValue: Int64 { return 0 } + public typealias T = Int64 } public var estimatedTimeRemaining: EstimatedTimeRemaining.Type { EstimatedTimeRemaining.self } public struct EstimatedTimeRemaining: Sendable, Property { + public static var defaultValue: Duration { return Duration.seconds(0) } + public typealias T = Duration } } diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index e9cfbe5d6..d28d9eb9f 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -110,6 +110,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { associatedtype T: Sendable + static var defaultValue: T { get } + /// Aggregates an array of `T` into a single value `T`. /// - Parameter all: Array of `T` to be aggregated. /// - Returns: A new instance of `T`. @@ -166,9 +168,13 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } /// Returns a property value that a key path indicates. - public subscript(dynamicMember key: KeyPath) -> P.T? { + public subscript(dynamicMember key: KeyPath) -> P.T { get { - state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T + if let val = state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T { + return val + } else { + return P.Type.defaultValue + } } set { From 1afa6511e5d3301d0ec056c7f9ba850f53f035b3 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 28 Apr 2025 18:42:38 -0700 Subject: [PATCH 19/85] code change for defaultValue --- .../ProgressReporter/ProgressReporter.swift | 2 +- .../ProgressReporter+FileFormatStyle.swift | 20 ++++++------------- .../ProgressReporterTests.swift | 5 ++--- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index d28d9eb9f..f95544503 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -173,7 +173,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { if let val = state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T { return val } else { - return P.Type.defaultValue + return P.defaultValue } } diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift index 60c898928..e0acc234f 100644 --- a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift @@ -99,23 +99,15 @@ extension ProgressReporter.FileFormatStyle: FormatStyle { let properties = reporter.withProperties(\.self) - if let totalFileCount = properties.totalFileCount { - let completedFileCount = properties.completedFileCount ?? 0 - fileCountLSR = LocalizedStringResource("\(completedFileCount, format: IntegerFormatStyle()) of \(totalFileCount, format: IntegerFormatStyle()) files", locale: self.locale, bundle: .forClass(ProgressReporter.self)) - } + fileCountLSR = LocalizedStringResource("\(properties.completedFileCount, format: IntegerFormatStyle()) of \(properties.totalFileCount, format: IntegerFormatStyle()) files", locale: self.locale, bundle: .forClass(ProgressReporter.self)) - if let totalByteCount = properties.totalByteCount { - let completedByteCount = properties.completedByteCount ?? 0 - byteCountLSR = LocalizedStringResource("\(completedByteCount, format: ByteCountFormatStyle()) of \(totalByteCount, format: ByteCountFormatStyle())", locale: self.locale, bundle: .forClass(ProgressReporter.self)) - } - if let throughput = properties.throughput { - throughputLSR = LocalizedStringResource("\(throughput, format: ByteCountFormatStyle())/s", locale: self.locale, bundle: .forClass(ProgressReporter.self)) - } + byteCountLSR = LocalizedStringResource("\(properties.completedByteCount, format: ByteCountFormatStyle()) of \(properties.totalByteCount, format: ByteCountFormatStyle())", locale: self.locale, bundle: .forClass(ProgressReporter.self)) - if let timeRemaining = properties.estimatedTimeRemaining { - timeRemainingLSR = LocalizedStringResource("\(timeRemaining, format: Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .wide)) remaining", locale: self.locale, bundle: .forClass(ProgressReporter.self)) - } + + throughputLSR = LocalizedStringResource("\(properties.throughput, format: ByteCountFormatStyle())/s", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + + timeRemainingLSR = LocalizedStringResource("\(properties.estimatedTimeRemaining, format: Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .wide)) remaining", locale: self.locale, bundle: .forClass(ProgressReporter.self)) return """ \(String(localized: fileCountLSR ?? "")) diff --git a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift index bd5cece6a..a6c21acb1 100644 --- a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift @@ -54,7 +54,6 @@ class TestProgressReporter: XCTestCase { } XCTAssertEqual(reporter.withProperties(\.totalFileCount), 100) - XCTAssertNil(reporter.withProperties(\.completedFileCount)) reporter.complete(count: 100) XCTAssertEqual(reporter.fractionCompleted, 1.0) @@ -248,13 +247,13 @@ class TestProgressReporter: XCTestCase { let childProgress1 = reporter1.subprogress(assigningCount: 3) let childReporter1 = childProgress1.reporter(totalCount: nil) childReporter1.withProperties { properties in - properties.totalFileCount = 10 + properties.totalFileCount += 10 } let childProgress2 = reporter1.subprogress(assigningCount: 2) let childReporter2 = childProgress2.reporter(totalCount: nil) childReporter2.withProperties { properties in - properties.totalFileCount = 10 + properties.totalFileCount += 10 } XCTAssertEqual(reporter1.withProperties(\.totalFileCount), 20) From c4b94e45e5bfe0e8075efaba164105516ad5ec61 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 28 Apr 2025 19:48:35 -0700 Subject: [PATCH 20/85] fix code --- .../ProgressReporter+FileFormatStyle.swift | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift index e0acc234f..3ccf75870 100644 --- a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift @@ -124,26 +124,18 @@ extension ProgressReporter.FileFormatStyle: FormatStyle { let properties = reporter.withProperties(\.self) - if let totalFileCount = properties.totalFileCount { - let completedFileCount = properties.completedFileCount ?? 0 - fileCountString = "\(completedFileCount.formatted(IntegerFormatStyle(locale: self.locale))) / \(totalFileCount.formatted(IntegerFormatStyle(locale: self.locale)))" - } - - if let totalByteCount = properties.totalByteCount { - let completedByteCount = properties.completedByteCount ?? 0 - byteCountString = "\(completedByteCount.formatted(ByteCountFormatStyle(locale: self.locale))) / \(totalByteCount.formatted(ByteCountFormatStyle(locale: self.locale)))" - } - - if let throughput = properties.throughput { - throughputString = "\(throughput.formatted(ByteCountFormatStyle(locale: self.locale)))/s" - } - - if let timeRemaining = properties.estimatedTimeRemaining { - var formatStyle = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .wide) - formatStyle.locale = self.locale - timeRemainingString = "\(timeRemaining.formatted(formatStyle)) remaining" - } + + fileCountString = "\(properties.completedFileCount.formatted(IntegerFormatStyle(locale: self.locale))) / \(properties.totalFileCount.formatted(IntegerFormatStyle(locale: self.locale)))" + + byteCountString = "\(properties.completedByteCount.formatted(ByteCountFormatStyle(locale: self.locale))) / \(properties.totalByteCount.formatted(ByteCountFormatStyle(locale: self.locale)))" + + throughputString = "\(properties.throughput.formatted(ByteCountFormatStyle(locale: self.locale)))/s" + + var formatStyle = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .wide) + formatStyle.locale = self.locale + timeRemainingString = "\(properties.estimatedTimeRemaining.formatted(formatStyle)) remaining" + return """ \(fileCountString ?? "") \(byteCountString ?? "") From 5d4ee8ef62b34c9f67b9a0f51332117320424fcc Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 28 Apr 2025 22:26:17 -0700 Subject: [PATCH 21/85] use SingleValueContainer to encode and decode FileFormatStyle --- .../ProgressReporter+FileFormatStyle.swift | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift index 3ccf75870..9da9fd079 100644 --- a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift @@ -19,19 +19,15 @@ extension ProgressReporter { public struct FileFormatStyle: Sendable, Codable, Equatable, Hashable { internal struct Option: Sendable, Codable, Equatable, Hashable { - - enum CodingKeys: String, CodingKey { - case rawOption - } init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - rawOption = try container.decode(RawOption.self, forKey: .rawOption) + let container = try decoder.singleValueContainer() + self.rawOption = try container.decode(RawOption.self) } func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(rawOption, forKey: .rawOption) + var container = encoder.singleValueContainer() + try container.encode(rawOption) } internal static var file: Option { Option(.file) } @@ -49,19 +45,18 @@ extension ProgressReporter { struct CodableRepresentation: Codable { let locale: Locale - let includeFileDescription: Bool + let option: Option } var codableRepresentation: CodableRepresentation { - .init(locale: self.locale, includeFileDescription: option.rawOption == .file) + .init(locale: self.locale, option: .file) } public init(from decoder: any Decoder) throws { - //TODO: Fix this later, codableRepresentation is not settable let container = try decoder.singleValueContainer() - let rep = try container.decode(CodableRepresentation.self) - locale = rep.locale - option = .file + let rawValue = try container.decode(CodableRepresentation.self) + self.locale = rawValue.locale + self.option = rawValue.option } public func encode(to encoder: any Encoder) throws { From 26098951b70f9a8443ae1cc35b1f5fcee1a50b97 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 28 Apr 2025 22:37:25 -0700 Subject: [PATCH 22/85] update Codable implementation --- .../ProgressReporter+FileFormatStyle.swift | 2 +- .../ProgressReporter+FormatStyle.swift | 34 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift index 9da9fd079..0e2b15f81 100644 --- a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift @@ -49,7 +49,7 @@ extension ProgressReporter { } var codableRepresentation: CodableRepresentation { - .init(locale: self.locale, option: .file) + .init(locale: self.locale, option: self.option) } public init(from decoder: any Decoder) throws { diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift index 9f83d5f59..d44dcd6ce 100644 --- a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift @@ -21,18 +21,14 @@ extension ProgressReporter { // Outlines the options available to format ProgressReporter internal struct Option: Sendable, Codable, Hashable, Equatable { - enum CodingKeys: String, CodingKey { - case rawOption - } - init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - rawOption = try container.decode(RawOption.self, forKey: .rawOption) + let container = try decoder.singleValueContainer() + rawOption = try container.decode(RawOption.self) } func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(rawOption, forKey: .rawOption) + var container = encoder.singleValueContainer() + try container.encode(rawOption) } /// Option specifying `fractionCompleted`. @@ -67,21 +63,25 @@ extension ProgressReporter { } } - enum CodingKeys: String, CodingKey { - case locale - case option + struct CodableRepresentation: Codable { + let locale: Locale + let option: Option + } + + var codableRepresentation: CodableRepresentation { + .init(locale: self.locale, option: self.option) } public init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - locale = try container.decode(Locale.self, forKey: .locale) - option = try container.decode(Option.self, forKey: .option) + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(CodableRepresentation.self) + self.locale = rawValue.locale + self.option = rawValue.option } public func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(locale, forKey: .locale) - try container.encode(option, forKey: .option) + var container = encoder.singleValueContainer() + try container.encode(codableRepresentation) } public var locale: Locale From 8d1bad950bbc5a3983b901b2908a38bff7094185 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 30 Apr 2025 14:01:14 -0700 Subject: [PATCH 23/85] draft: reduce implementation keeping children values in array --- .../ProgressReporter/ProgressReporter.swift | 109 +++++++++--------- 1 file changed, 52 insertions(+), 57 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index f95544503..b7d05adbd 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -43,6 +43,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { internal struct State { var fractionState: FractionState var otherProperties: [AnyMetatypeWrapper: (any Sendable)] + var childrenOtherProperties: [AnyMetatypeWrapper: [(any Sendable)]] } // Interop states @@ -108,14 +109,16 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// A type that conveys task-specific information on progress. public protocol Property { - associatedtype T: Sendable + associatedtype T: Sendable, Hashable, Equatable static var defaultValue: T { get } - /// Aggregates an array of `T` into a single value `T`. - /// - Parameter all: Array of `T` to be aggregated. + /// Aggregates current `T` and an array of `T` into a single value `T`. + /// - Parameters: + /// - current: `T` of self. + /// - children: `T` of children. /// - Returns: A new instance of `T`. - static func reduce(_ all: [T]) -> T + static func reduce(current: T?, children: [T]) -> T } /// A container that holds values for properties that specify information on progress. @@ -130,7 +133,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { mutating get { reporter.getTotalCount(fractionState: &state.fractionState) } - + set { let previous = state.fractionState.overallFraction if state.fractionState.selfFraction.total != newValue && state.fractionState.selfFraction.total > 0 { @@ -170,56 +173,21 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// Returns a property value that a key path indicates. public subscript(dynamicMember key: KeyPath) -> P.T { get { - if let val = state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T { - return val - } else { + let currentValue = state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T + let childrenValues = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] as? [P.T] + if currentValue == nil && childrenValues == [] { return P.defaultValue + } else { + return P.self.reduce(current: currentValue, children: childrenValues ?? []) } } set { + let oldValue = state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = newValue - // Update Parent's P.self value - updateParentOtherPropertiesEntry(of: reporter, metatype: P.self, updatedValue: newValue, state: &state) - } - } - - private func updateParentOtherPropertiesEntry(of reporter: ProgressReporter, metatype: P.Type, updatedValue: P.T?, state: inout State) { - // Check if parent exists to continue propagating values up - if let parent = reporter.parent { - parent.children.withLock { children in - // Array containing all children's values to pass into reduce - let childrenValues: LockedState<[P.T]> = LockedState(initialState: []) - // Add self's updatedValue to array - if let updatedValue = updatedValue { - childrenValues.withLock { values in - values.append(updatedValue) - } - } - // Add other children's values to array, skip over existing child's existing value - for child in children { - if child != reporter { - let childValue = child?.state.withLock { $0.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T } - if let childValue = childValue { - childrenValues.withLock { values in - values.append(childValue) - } - } - } - } - if !childrenValues.withLock(\.self).isEmpty { - parent.state.withLock { state in - // Set property in parent - let reducedValue = metatype.reduce(childrenValues.withLock(\.self)) - state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] = reducedValue - // Recursive call to parent's parent - updateParentOtherPropertiesEntry(of: parent, metatype: metatype, updatedValue: reducedValue, state: &state) - } - } - } + reporter.parent?.updateChildrenOtherProperties(property: P.self, oldValue: oldValue, newValue: newValue) } } - } private let portionOfParent: Int @@ -237,7 +205,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { childFraction: _ProgressFraction(completed: 0, total: 1), interopChild: nil ) - let state = State(fractionState: fractionState, otherProperties: [:]) + let state = State(fractionState: fractionState, otherProperties: [:], childrenOtherProperties: [:]) self.state = LockedState(initialState: state) self.interopObservation = interopObservation self.ghostReporter = ghostReporter @@ -313,7 +281,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { return try state.withLock { state in var values = Values(reporter: self, state: state) // This is done to avoid copy on write later - state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction()), otherProperties: [:]) + state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction()), otherProperties: [:], childrenOtherProperties: [:]) let result = try closure(&values) state = values.state return result @@ -454,19 +422,46 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { children.insert(childReporter) } } + + internal func updateChildrenOtherProperties(property metatype: P.Type, oldValue: P.T?, newValue: P.T) { + state.withLock { state in + var myOldValue: P.T + var myNewValue: P.T + var newEntries: [P.T] = [] + let oldEntries = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [P.T] + myOldValue = metatype.reduce(current: oldValue, children: oldEntries ?? []) + if oldValue == nil { + newEntries.append(newValue) + } else if let oldEntries = oldEntries { + if oldEntries.isEmpty { + newEntries.append(newValue) + } + } else { + if let oldEntries = oldEntries, let old = oldValue { + newEntries = oldEntries.map { $0 } + if let index = oldEntries.firstIndex(of: old) { + newEntries[index] = newValue + } + } + } + myNewValue = metatype.reduce(current: state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T, children: newEntries) + state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] = newEntries + self.parent?.updateChildrenOtherProperties(property: metatype, oldValue: myOldValue, newValue: myNewValue) + } + } } - + @available(FoundationPreview 6.2, *) // Default Implementation for reduce extension ProgressReporter.Property where T : AdditiveArithmetic { - public static func reduce(_ all: [T]) -> T { - precondition(all.isEmpty == false, "Cannot reduce an empty array") - let first = all.first! - let rest = all.dropFirst() - guard !rest.isEmpty else { - return first + public static func reduce(current: T?, children: [T]) -> T { + guard !children.isEmpty else { + return current ?? 0 as! Self.T + } + guard current != nil else { + return children.reduce(0 as! Self.T, +) } - return rest.reduce(first, +) + return children.reduce(current ?? 0 as! Self.T, +) } } From 93c674106fc82e2174b382b0a29016d60e5761f4 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 30 Apr 2025 15:18:36 -0700 Subject: [PATCH 24/85] draft: working version of reduce with children list, to be expanded to higher levels --- .../ProgressReporter/ProgressReporter.swift | 52 ++++++++----------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index b7d05adbd..8678a1166 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -149,9 +149,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } //TODO: rdar://149015734 Check throttling reporter.updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) - reporter.ghostReporter?.notifyObservers(with: .totalCountUpdated) - } } @@ -174,17 +172,16 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public subscript(dynamicMember key: KeyPath) -> P.T { get { let currentValue = state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T - let childrenValues = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] as? [P.T] - if currentValue == nil && childrenValues == [] { - return P.defaultValue - } else { - return P.self.reduce(current: currentValue, children: childrenValues ?? []) - } + let childrenValues: [P.T] = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] as? [P.T] ?? [] + print("values passed to reduce \(currentValue) and \(childrenValues)") + return P.self.reduce(current: currentValue, children: childrenValues) } set { let oldValue = state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T + print("old value is \(oldValue)") state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = newValue + print("new value is \(newValue)") reporter.parent?.updateChildrenOtherProperties(property: P.self, oldValue: oldValue, newValue: newValue) } } @@ -424,29 +421,21 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } internal func updateChildrenOtherProperties(property metatype: P.Type, oldValue: P.T?, newValue: P.T) { + // The point of this is to update my own entry of my children values when one child value changes, and call up to my parent recursively to do so state.withLock { state in - var myOldValue: P.T - var myNewValue: P.T - var newEntries: [P.T] = [] - let oldEntries = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [P.T] - myOldValue = metatype.reduce(current: oldValue, children: oldEntries ?? []) - if oldValue == nil { - newEntries.append(newValue) - } else if let oldEntries = oldEntries { - if oldEntries.isEmpty { - newEntries.append(newValue) - } - } else { - if let oldEntries = oldEntries, let old = oldValue { - newEntries = oldEntries.map { $0 } - if let index = oldEntries.firstIndex(of: old) { - newEntries[index] = newValue - } + var newEntries: [P.T] = [newValue] + let oldEntries: [P.T] = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [P.T] ?? [] + for entry in oldEntries { + newEntries.append(entry) + } + if let oldValue = oldValue { + if let i = newEntries.firstIndex(of: oldValue) { + print("remove at index \(i)") + newEntries.remove(at: i) } } - myNewValue = metatype.reduce(current: state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T, children: newEntries) state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] = newEntries - self.parent?.updateChildrenOtherProperties(property: metatype, oldValue: myOldValue, newValue: myNewValue) + print("old entries \(oldEntries) -> new entries \(newEntries)") } } } @@ -455,13 +444,14 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Default Implementation for reduce extension ProgressReporter.Property where T : AdditiveArithmetic { public static func reduce(current: T?, children: [T]) -> T { - guard !children.isEmpty else { - return current ?? 0 as! Self.T + if current == nil && children.isEmpty { + return self.defaultValue } - guard current != nil else { + if current != nil { + return children.reduce(current ?? 0 as! Self.T, +) + } else { return children.reduce(0 as! Self.T, +) } - return children.reduce(current ?? 0 as! Self.T, +) } } From f18654869d5176275f1a4142a4880c5140512d76 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 30 Apr 2025 16:42:48 -0700 Subject: [PATCH 25/85] draft: reduce method with children list, recusive implementation done --- .../ProgressReporter/ProgressReporter.swift | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index 8678a1166..2cc4b7571 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -173,17 +173,25 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { get { let currentValue = state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T let childrenValues: [P.T] = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] as? [P.T] ?? [] - print("values passed to reduce \(currentValue) and \(childrenValues)") return P.self.reduce(current: currentValue, children: childrenValues) } - set { - let oldValue = state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T - print("old value is \(oldValue)") - state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = newValue - print("new value is \(newValue)") - reporter.parent?.updateChildrenOtherProperties(property: P.self, oldValue: oldValue, newValue: newValue) - } +// set { +// let oldValue = state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T +// print("old value is \(oldValue)") +// state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = newValue +// print("new value is \(newValue)") +// reporter.parent?.updateChildrenOtherProperties(property: P.self, oldValue: oldValue, newValue: newValue) +// } + } + } + + public func setAdditionalProperty(type: P.Type, value: P.T) { + state.withLock { state in + var oldValue: P.T? = nil + oldValue = state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T + state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = value + parent?.updateChildrenOtherProperties(property: type, oldValue: oldValue, newValue: value) } } @@ -423,19 +431,22 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { internal func updateChildrenOtherProperties(property metatype: P.Type, oldValue: P.T?, newValue: P.T) { // The point of this is to update my own entry of my children values when one child value changes, and call up to my parent recursively to do so state.withLock { state in + var oldReducedValue: P.T? + var newReducedValue: P.T var newEntries: [P.T] = [newValue] let oldEntries: [P.T] = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [P.T] ?? [] + oldReducedValue = metatype.reduce(current: state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T, children: oldEntries) for entry in oldEntries { newEntries.append(entry) } if let oldValue = oldValue { if let i = newEntries.firstIndex(of: oldValue) { - print("remove at index \(i)") newEntries.remove(at: i) } } state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] = newEntries - print("old entries \(oldEntries) -> new entries \(newEntries)") + newReducedValue = metatype.reduce(current: state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T, children: newEntries) + parent?.updateChildrenOtherProperties(property: metatype, oldValue: oldReducedValue, newValue: newReducedValue) } } } @@ -444,11 +455,12 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Default Implementation for reduce extension ProgressReporter.Property where T : AdditiveArithmetic { public static func reduce(current: T?, children: [T]) -> T { + print("reducing \(current) and \(children)") if current == nil && children.isEmpty { return self.defaultValue } if current != nil { - return children.reduce(current ?? 0 as! Self.T, +) + return children.reduce(current ?? self.defaultValue, +) } else { return children.reduce(0 as! Self.T, +) } From bf20bb4d0348ebb4d16fb76b2324e375df2a1c1a Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 30 Apr 2025 17:08:03 -0700 Subject: [PATCH 26/85] cleanup: replace vars with lets where needed --- .../ProgressReporter/ProgressReporter.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index 2cc4b7571..6ced26ecc 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -188,8 +188,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public func setAdditionalProperty(type: P.Type, value: P.T) { state.withLock { state in - var oldValue: P.T? = nil - oldValue = state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T + let oldValue = state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = value parent?.updateChildrenOtherProperties(property: type, oldValue: oldValue, newValue: value) } @@ -431,11 +430,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { internal func updateChildrenOtherProperties(property metatype: P.Type, oldValue: P.T?, newValue: P.T) { // The point of this is to update my own entry of my children values when one child value changes, and call up to my parent recursively to do so state.withLock { state in - var oldReducedValue: P.T? - var newReducedValue: P.T var newEntries: [P.T] = [newValue] let oldEntries: [P.T] = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [P.T] ?? [] - oldReducedValue = metatype.reduce(current: state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T, children: oldEntries) + let oldReducedValue = metatype.reduce(current: state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T, children: oldEntries) for entry in oldEntries { newEntries.append(entry) } @@ -445,7 +442,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] = newEntries - newReducedValue = metatype.reduce(current: state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T, children: newEntries) + let newReducedValue = metatype.reduce(current: state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T, children: newEntries) parent?.updateChildrenOtherProperties(property: metatype, oldValue: oldReducedValue, newValue: newReducedValue) } } @@ -455,7 +452,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Default Implementation for reduce extension ProgressReporter.Property where T : AdditiveArithmetic { public static func reduce(current: T?, children: [T]) -> T { - print("reducing \(current) and \(children)") if current == nil && children.isEmpty { return self.defaultValue } From 55c1dafd3bbc484227b65cf196cc38adf27ba209 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 2 May 2025 10:51:19 -0700 Subject: [PATCH 27/85] additional property dual node: version 3 implementation --- .../ProgressReporter/ProgressReporter.swift | 155 +++++++++++------- .../ProgressReporter/Subprogress.swift | 5 +- 2 files changed, 100 insertions(+), 60 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index 6ced26ecc..ed0f98ab0 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -41,9 +41,11 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Stores all the state of properties internal struct State { + var positionInParent: Int? var fractionState: FractionState var otherProperties: [AnyMetatypeWrapper: (any Sendable)] - var childrenOtherProperties: [AnyMetatypeWrapper: [(any Sendable)]] + // Type: Array of Array + var childrenOtherProperties: [AnyMetatypeWrapper: [[(any Sendable)]]] } // Interop states @@ -112,13 +114,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { associatedtype T: Sendable, Hashable, Equatable static var defaultValue: T { get } - - /// Aggregates current `T` and an array of `T` into a single value `T`. - /// - Parameters: - /// - current: `T` of self. - /// - children: `T` of children. - /// - Returns: A new instance of `T`. - static func reduce(current: T?, children: [T]) -> T } /// A container that holds values for properties that specify information on progress. @@ -171,32 +166,32 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// Returns a property value that a key path indicates. public subscript(dynamicMember key: KeyPath) -> P.T { get { - let currentValue = state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T - let childrenValues: [P.T] = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] as? [P.T] ?? [] - return P.self.reduce(current: currentValue, children: childrenValues) + return state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T ?? P.self.defaultValue } -// set { -// let oldValue = state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T -// print("old value is \(oldValue)") -// state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = newValue -// print("new value is \(newValue)") -// reporter.parent?.updateChildrenOtherProperties(property: P.self, oldValue: oldValue, newValue: newValue) -// } - } - } - - public func setAdditionalProperty(type: P.Type, value: P.T) { - state.withLock { state in - let oldValue = state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T - state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = value - parent?.updateChildrenOtherProperties(property: type, oldValue: oldValue, newValue: value) + set { + // Update my own other properties entry + state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = newValue + // Flatten myself + myChildren to be sent to parent + var updateValueForParent: [P.T?] = [newValue] + let childrenValues: [[P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] as? [[P.T?]] + let flattenedChildrenValues: [P.T?] = { + guard let values = childrenValues else { return [] } + // Use flatMap to flatten the array but preserve nil values + return values.flatMap { innerArray -> [P.T?] in + // Each inner array element is preserved, including nil values + return innerArray.map { $0 } + } + }() + updateValueForParent += flattenedChildrenValues + reporter.parent?.updateChildrenOtherProperties(property: P.self, idx: state.positionInParent!, value: updateValueForParent) + } } } private let portionOfParent: Int internal let parent: ProgressReporter? - private let children: LockedState> + private let children: LockedState<[ProgressReporter?]> private let state: LockedState internal init(total: Int?, parent: ProgressReporter?, portionOfParent: Int, ghostReporter: ProgressReporter?, interopObservation: (any Sendable)?) { @@ -358,7 +353,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// A child progress has been updated, which changes our own fraction completed. internal func updateChildFraction(from previous: _ProgressFraction, to next: _ProgressFraction, portion: Int) { - // Acquire and release parent's lock let updateState = state.withLock { state in let previousOverallFraction = state.fractionState.overallFraction let multiple = _ProgressFraction(completed: portion, total: state.fractionState.selfFraction.total) @@ -373,7 +367,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { if next.isFinished { // Remove from children list - _ = children.withLock { $0.remove(self) } +// _ = children.withLock { $0.remove(self) } if portion != 0 { // Update our self completed units @@ -421,48 +415,93 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } - internal func addToChildren(childReporter: ProgressReporter) { - _ = children.withLock { children in - children.insert(childReporter) + // Returns position of child in parent + internal func addToChildren(childReporter: ProgressReporter) -> Int { + let childPosition = children.withLock { children in + children.append(childReporter) + return children.count - 1 } + return childPosition } - internal func updateChildrenOtherProperties(property metatype: P.Type, oldValue: P.T?, newValue: P.T) { - // The point of this is to update my own entry of my children values when one child value changes, and call up to my parent recursively to do so + internal func setPositionInParent(to position: Int) { state.withLock { state in - var newEntries: [P.T] = [newValue] - let oldEntries: [P.T] = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [P.T] ?? [] - let oldReducedValue = metatype.reduce(current: state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T, children: oldEntries) - for entry in oldEntries { - newEntries.append(entry) - } - if let oldValue = oldValue { - if let i = newEntries.firstIndex(of: oldValue) { - newEntries.remove(at: i) + state.positionInParent = position + } + } + + internal func updateChildrenOtherProperties(property metatype: P.Type, idx: Int, value: [P.T?]) { + state.withLock { state in + let myEntries: [[P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [[P.T?]] + if let entries = myEntries { + // If entries is not nil, make sure it is a valid index, then update my entry of children values + let entriesLength = entries.count + // Check if entries need resizing + if idx >= entriesLength { + // Entries need resizing + state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] = resizeArray(array: &state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)], to: idx+1) + state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)]![idx] = value + } else { + // Entries don't need resizing + state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)]![idx] = value } + + } else { + // If entries is nil + state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] = resizeArray(array: &state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)], to: idx+1) + state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)]![idx] = value } - state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] = newEntries - let newReducedValue = metatype.reduce(current: state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T, children: newEntries) - parent?.updateChildrenOtherProperties(property: metatype, oldValue: oldReducedValue, newValue: newReducedValue) + // Ask parent to update their entry with my value + new children value + let newChildrenValues: [[P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [[P.T?]] + let flattenedChildrenValues: [P.T?] = { + guard let values = newChildrenValues else { return [] } + // Use flatMap to flatten the array but preserve nil values + return values.flatMap { innerArray -> [P.T?] in + // Each inner array element is preserved, including nil values + return innerArray.map { $0 } + } + }() + let newValueForParent: [P.T?] = [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T] + flattenedChildrenValues + parent?.updateChildrenOtherProperties(property: metatype, idx: state.positionInParent!, value: newValueForParent) } } -} -@available(FoundationPreview 6.2, *) -// Default Implementation for reduce -extension ProgressReporter.Property where T : AdditiveArithmetic { - public static func reduce(current: T?, children: [T]) -> T { - if current == nil && children.isEmpty { - return self.defaultValue + // Copy elements of array to new array with the correct size + private func resizeArray(array: inout [[any Sendable]]?, to size: Int) -> [[any Sendable]] { + var newArray: [[any Sendable]] = Array(repeating: [], count: size) + if array == nil && size == 1 { + return newArray } - if current != nil { - return children.reduce(current ?? self.defaultValue, +) - } else { - return children.reduce(0 as! Self.T, +) + if let oldArray = array { + // Use array.count to avoid invalid index + for idx in 0..(property metatype: P.Type) -> [P.T?] { + return state.withLock { state in + let childrenValues: [[P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [[P.T?]] + let flattenedChildrenValues: [P.T?] = { + guard let values = childrenValues else { return [] } + // Use flatMap to flatten the array but preserve nil values + return values.flatMap { innerArray -> [P.T?] in + // Each inner array element is preserved, including nil values + return innerArray.map { $0 } + } + }() + return [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T] + flattenedChildrenValues + } + } + + public func reduce(property: P.Type, values: [P.T?]) -> P.T where P.T: AdditiveArithmetic { + let droppedNil = values.compactMap { $0 } + return droppedNil.reduce(P.T.zero, +) } } - + @available(FoundationPreview 6.2, *) // Hashable & Equatable Conformance extension ProgressReporter: Hashable, Equatable { diff --git a/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift b/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift index 6e8b1f540..a64ed57d3 100644 --- a/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift +++ b/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift @@ -44,8 +44,9 @@ public struct Subprogress: ~Copyable, Sendable { // Set interop child of ghost reporter so ghost reporter reads from here ghostReporter?.setInteropChild(interopChild: childReporter) } else { - // Add child to parent's _children list - parent.addToChildren(childReporter: childReporter) + // Add child to parent's _children list & Store in child children's position in parent + let childPositionInParent = parent.addToChildren(childReporter: childReporter) + childReporter.setPositionInParent(to: childPositionInParent) } return childReporter From 1783e047b5dd97412856473dc54c8229b832276e Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 2 May 2025 12:43:08 -0700 Subject: [PATCH 28/85] v3 fix to always preserve nil values in tree --- .../ProgressReporter/ProgressReporter.swift | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index ed0f98ab0..1cef3be0a 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -421,6 +421,26 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { children.append(childReporter) return children.count - 1 } + // Resizing childrenOtherProperties for all types need to happen here for entry of nil to exist + state.withLock { state in + for (metatype, _) in state.childrenOtherProperties { + state.childrenOtherProperties[metatype] = resizeArray(array: &state.childrenOtherProperties[metatype], to: childPosition + 1) + // Propagate my value + my flattened children up to parent + var valueForParent: [(any Sendable)?] = [state.otherProperties[metatype]] + let newChildrenValues = state.childrenOtherProperties[metatype] + let flattenedChildrenValues: [(any Sendable)?] = { + guard let values = newChildrenValues else { return [] } + // Use flatMap to flatten the array but preserve nil values + return values.flatMap { innerArray -> [(any Sendable)?] in + // Each inner array element is preserved, including nil values + return innerArray.map { $0 } + } + }() + valueForParent += flattenedChildrenValues + parent?.updateChildrenOtherPropertiesAnyValue(property: metatype, idx: state.positionInParent!, value: valueForParent) + + } + } return childPosition } @@ -430,6 +450,41 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } + internal func updateChildrenOtherPropertiesAnyValue(property metatype: AnyMetatypeWrapper, idx: Int, value: [any Sendable]) { + state.withLock { state in + let myEntries: [[(any Sendable)?]]? = state.childrenOtherProperties[metatype] + if let entries = myEntries { + // If entries is not nil, make sure it is a valid index, then update my entry of children values + let entriesLength = entries.count + // Check if entries need resizing + if idx >= entriesLength { + // Entries need resizing + state.childrenOtherProperties[metatype] = resizeArray(array: &state.childrenOtherProperties[metatype], to: idx+1) + state.childrenOtherProperties[metatype]![idx] = value + } else { + // Entries don't need resizing + state.childrenOtherProperties[metatype]![idx] = value + } + } else { + // If entries is nil + state.childrenOtherProperties[metatype] = resizeArray(array: &state.childrenOtherProperties[metatype], to: idx+1) + state.childrenOtherProperties[metatype]![idx] = value + } + // Ask parent to update their entry with my value + new children value + let newChildrenValues: [[(any Sendable)?]]? = state.childrenOtherProperties[metatype] + let flattenedChildrenValues: [(any Sendable)?] = { + guard let values = newChildrenValues else { return [] } + // Use flatMap to flatten the array but preserve nil values + return values.flatMap { innerArray -> [(any Sendable)?] in + // Each inner array element is preserved, including nil values + return innerArray.map { $0 } + } + }() + let newValueForParent: [(any Sendable)?] = [state.otherProperties[metatype]] + flattenedChildrenValues + parent?.updateChildrenOtherPropertiesAnyValue(property: metatype, idx: state.positionInParent!, value: newValueForParent) + } + } + internal func updateChildrenOtherProperties(property metatype: P.Type, idx: Int, value: [P.T?]) { state.withLock { state in let myEntries: [[P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [[P.T?]] @@ -468,7 +523,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Copy elements of array to new array with the correct size private func resizeArray(array: inout [[any Sendable]]?, to size: Int) -> [[any Sendable]] { - var newArray: [[any Sendable]] = Array(repeating: [], count: size) + var newArray: [[any Sendable]] = Array(repeating: [nil], count: size) if array == nil && size == 1 { return newArray } From 28d5718487bbe23313cc6ca8901424c2fbd5127b Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 5 May 2025 18:59:33 -0700 Subject: [PATCH 29/85] deinit implementation for cancellation handling + getAllValues update to use defaultValue when nil to keep consistency with subscript method --- .../ProgressReporter/ProgressReporter.swift | 14 ++++++++++++-- .../ProgressReporter/Subprogress.swift | 6 ++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index 1cef3be0a..8ba6fe0de 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -544,10 +544,10 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Use flatMap to flatten the array but preserve nil values return values.flatMap { innerArray -> [P.T?] in // Each inner array element is preserved, including nil values - return innerArray.map { $0 } + return innerArray.map { $0 ?? P.defaultValue } } }() - return [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T] + flattenedChildrenValues + return [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T ?? P.defaultValue] + flattenedChildrenValues } } @@ -555,6 +555,16 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { let droppedNil = values.compactMap { $0 } return droppedNil.reduce(P.T.zero, +) } + + deinit { + if !isFinished { + self.withProperties { properties in + if let totalCount = properties.totalCount { + properties.completedCount = totalCount + } + } + } + } } @available(FoundationPreview 6.2, *) diff --git a/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift b/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift index a64ed57d3..02afc8a8e 100644 --- a/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift +++ b/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift @@ -51,5 +51,11 @@ public struct Subprogress: ~Copyable, Sendable { return childReporter } + + deinit { + if !self.isInitializedToProgressReporter { + parent.complete(count: portionOfParent) + } + } } From d95a1a98ea916938e932c0ddc8f9a1fb4c2a8cdf Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 6 May 2025 18:33:34 -0700 Subject: [PATCH 30/85] change parent and portionInParent storage to be an array --- .../ProgressReporter/ProgressMonitor.swift | 23 ++++++++++ .../ProgressReporter/ProgressReporter.swift | 46 +++++++++++++++---- .../ProgressReporter/Subprogress.swift | 3 +- 3 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 Sources/FoundationEssentials/ProgressReporter/ProgressMonitor.swift diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressMonitor.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressMonitor.swift new file mode 100644 index 000000000..a0e5963ce --- /dev/null +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressMonitor.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@available(FoundationPreview 6.2, *) +extension ProgressReporter { + /// ProgressMonitor is just a wrapper that carries information about ProgressReporter. It is read-only and can be added as a child of something else. + public struct ProgressMonitor: Sendable { + internal let reporter: ProgressReporter + + internal init(reporter: ProgressReporter) { + self.reporter = reporter + } + } +} diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index 8ba6fe0de..505b9875f 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -41,7 +41,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Stores all the state of properties internal struct State { - var positionInParent: Int? +// var positionInParent: Int? var fractionState: FractionState var otherProperties: [AnyMetatypeWrapper: (any Sendable)] // Type: Array of Array @@ -108,6 +108,10 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } + public var monitor: ProgressMonitor { + return .init(reporter: self) + } + /// A type that conveys task-specific information on progress. public protocol Property { @@ -189,14 +193,19 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } - private let portionOfParent: Int - internal let parent: ProgressReporter? +// private let portionOfParent: Int +// internal let parent: ProgressReporter? + + // Parents dictionary maps parent to portionOfParent - my parent to my portion inside that parent & my position in parent's children list + + internal let parents: LockedState<[ProgressReporter: Int]> private let children: LockedState<[ProgressReporter?]> private let state: LockedState - internal init(total: Int?, parent: ProgressReporter?, portionOfParent: Int, ghostReporter: ProgressReporter?, interopObservation: (any Sendable)?) { - self.portionOfParent = portionOfParent - self.parent = parent + internal init(total: Int?, ghostReporter: ProgressReporter?, interopObservation: (any Sendable)?) { +// self.portionOfParent = portionOfParent +// self.parent = parent + self.parents = .init(initialState: [:]) self.children = .init(initialState: []) let fractionState = FractionState( indeterminate: total == nil ? true : false, @@ -215,7 +224,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// If `totalCount` is set to `nil`, `self` is indeterminate. /// - Parameter totalCount: Total units of work. public convenience init(totalCount: Int?) { - self.init(total: totalCount, parent: nil, portionOfParent: 0, ghostReporter: nil, interopObservation: nil) + self.init(total: totalCount, ghostReporter: nil, interopObservation: nil) } @@ -241,7 +250,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } - /// Returns a `Subprogress` representing a portion of `self`which can be passed to any method that reports progress. /// /// - Parameter count: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. @@ -346,7 +354,12 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { private func updateFractionCompleted(from: _ProgressFraction, to: _ProgressFraction) { _$observationRegistrar.withMutation(of: self, keyPath: \.fractionCompleted) { if from != to { - parent?.updateChildFraction(from: from, to: to, portion: portionOfParent) + parents.withLock { parents in + for (parent, portionOfParent) in parents { + parent.updateChildFraction(from: from, to: to, portion: portionOfParent) + } + } +// parent?.updateChildFraction(from: from, to: to, portion: portionOfParent) } } } @@ -444,6 +457,12 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { return childPosition } + internal func addToParents(parentReporter: ProgressReporter, portionOfParent: Int) { + parents.withLock { parents in + parents[parentReporter] = portionOfParent + } + } + internal func setPositionInParent(to position: Int) { state.withLock { state in state.positionInParent = position @@ -556,6 +575,15 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { return droppedNil.reduce(P.T.zero, +) } + // Adds Progress Monitor as a child - a monitor can be added to multiple parents + public func addChild(_ monitor: ProgressMonitor, assignedCount portionOfParent: Int) { + // get the actual progress from within the monitor, then add as children + let actualReporter = monitor.reporter + self.addToChildren(childReporter: actualReporter) + actualReporter.addToParents(parentReporter: self, portionOfParent: portionOfParent) + + } + deinit { if !isFinished { self.withProperties { properties in diff --git a/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift b/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift index 02afc8a8e..98c30f942 100644 --- a/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift +++ b/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift @@ -38,7 +38,7 @@ public struct Subprogress: ~Copyable, Sendable { public consuming func reporter(totalCount: Int?) -> ProgressReporter { isInitializedToProgressReporter = true - let childReporter = ProgressReporter(total: totalCount, parent: parent, portionOfParent: portionOfParent, ghostReporter: ghostReporter, interopObservation: observation) + let childReporter = ProgressReporter(total: totalCount, ghostReporter: ghostReporter, interopObservation: observation) if interopWithProgressParent { // Set interop child of ghost reporter so ghost reporter reads from here @@ -47,6 +47,7 @@ public struct Subprogress: ~Copyable, Sendable { // Add child to parent's _children list & Store in child children's position in parent let childPositionInParent = parent.addToChildren(childReporter: childReporter) childReporter.setPositionInParent(to: childPositionInParent) + childReporter.addToParents(parentReporter: parent, portionOfParent: portionOfParent) } return childReporter From 62f982eb22e43f7d7f34c5ab270d8d956c587741 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 6 May 2025 19:14:50 -0700 Subject: [PATCH 31/85] update type-safe metadata implementation for multi-parents --- .../ProgressReporter/ProgressReporter.swift | 149 ++++++++---------- 1 file changed, 67 insertions(+), 82 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index 505b9875f..1bd98a7af 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -44,8 +44,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // var positionInParent: Int? var fractionState: FractionState var otherProperties: [AnyMetatypeWrapper: (any Sendable)] - // Type: Array of Array - var childrenOtherProperties: [AnyMetatypeWrapper: [[(any Sendable)]]] + // Type: Metatype maps to dictionary of child to value + var childrenOtherProperties: [AnyMetatypeWrapper: [ProgressReporter: [(any Sendable)]]] } // Interop states @@ -178,17 +178,21 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = newValue // Flatten myself + myChildren to be sent to parent var updateValueForParent: [P.T?] = [newValue] - let childrenValues: [[P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] as? [[P.T?]] + let childrenValues: [ProgressReporter: [P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] as? [ProgressReporter: [P.T?]] let flattenedChildrenValues: [P.T?] = { guard let values = childrenValues else { return [] } // Use flatMap to flatten the array but preserve nil values - return values.flatMap { innerArray -> [P.T?] in + return values.flatMap { // Each inner array element is preserved, including nil values - return innerArray.map { $0 } + $0.value } }() updateValueForParent += flattenedChildrenValues - reporter.parent?.updateChildrenOtherProperties(property: P.self, idx: state.positionInParent!, value: updateValueForParent) + reporter.parents.withLock { parents in + for (parent, _) in parents { + parent.updateChildrenOtherProperties(property: P.self, child: reporter, value: updateValueForParent) + } + } } } } @@ -199,14 +203,14 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Parents dictionary maps parent to portionOfParent - my parent to my portion inside that parent & my position in parent's children list internal let parents: LockedState<[ProgressReporter: Int]> - private let children: LockedState<[ProgressReporter?]> + private let children: LockedState> private let state: LockedState internal init(total: Int?, ghostReporter: ProgressReporter?, interopObservation: (any Sendable)?) { // self.portionOfParent = portionOfParent // self.parent = parent self.parents = .init(initialState: [:]) - self.children = .init(initialState: []) + self.children = .init(initialState: Set()) let fractionState = FractionState( indeterminate: total == nil ? true : false, selfFraction: _ProgressFraction(completed: 0, total: total ?? 0), @@ -429,32 +433,31 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } // Returns position of child in parent - internal func addToChildren(childReporter: ProgressReporter) -> Int { - let childPosition = children.withLock { children in - children.append(childReporter) - return children.count - 1 + internal func addToChildren(childReporter: ProgressReporter) { + _ = children.withLock { children in + children.insert(childReporter) } // Resizing childrenOtherProperties for all types need to happen here for entry of nil to exist state.withLock { state in for (metatype, _) in state.childrenOtherProperties { - state.childrenOtherProperties[metatype] = resizeArray(array: &state.childrenOtherProperties[metatype], to: childPosition + 1) + state.childrenOtherProperties[metatype] = [self: [nil as (any Sendable)?]] // Propagate my value + my flattened children up to parent var valueForParent: [(any Sendable)?] = [state.otherProperties[metatype]] let newChildrenValues = state.childrenOtherProperties[metatype] let flattenedChildrenValues: [(any Sendable)?] = { guard let values = newChildrenValues else { return [] } - // Use flatMap to flatten the array but preserve nil values - return values.flatMap { innerArray -> [(any Sendable)?] in - // Each inner array element is preserved, including nil values - return innerArray.map { $0 } - } + return values.flatMap { $0.value } }() + valueForParent += flattenedChildrenValues - parent?.updateChildrenOtherPropertiesAnyValue(property: metatype, idx: state.positionInParent!, value: valueForParent) + parents.withLock { parents in + for (parent, _) in parents { + parent.updateChildrenOtherPropertiesAnyValue(property: metatype, child: self, value: valueForParent) + } + } } } - return childPosition } internal func addToParents(parentReporter: ProgressReporter, portionOfParent: Int) { @@ -463,97 +466,79 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } - internal func setPositionInParent(to position: Int) { - state.withLock { state in - state.positionInParent = position - } - } +// internal func setPositionInParent(to position: Int) { +// state.withLock { state in +// state.positionInParent = position +// } +// } - internal func updateChildrenOtherPropertiesAnyValue(property metatype: AnyMetatypeWrapper, idx: Int, value: [any Sendable]) { + internal func updateChildrenOtherPropertiesAnyValue(property metatype: AnyMetatypeWrapper, child: ProgressReporter, value: [any Sendable]) { state.withLock { state in - let myEntries: [[(any Sendable)?]]? = state.childrenOtherProperties[metatype] + let myEntries: [ProgressReporter: [(any Sendable)?]]? = state.childrenOtherProperties[metatype] if let entries = myEntries { - // If entries is not nil, make sure it is a valid index, then update my entry of children values - let entriesLength = entries.count - // Check if entries need resizing - if idx >= entriesLength { - // Entries need resizing - state.childrenOtherProperties[metatype] = resizeArray(array: &state.childrenOtherProperties[metatype], to: idx+1) - state.childrenOtherProperties[metatype]![idx] = value - } else { - // Entries don't need resizing - state.childrenOtherProperties[metatype]![idx] = value - } + // If entries is not nil, then update my entry of children values + state.childrenOtherProperties[metatype]![child] = value } else { // If entries is nil - state.childrenOtherProperties[metatype] = resizeArray(array: &state.childrenOtherProperties[metatype], to: idx+1) - state.childrenOtherProperties[metatype]![idx] = value + state.childrenOtherProperties[metatype] = [:] + state.childrenOtherProperties[metatype]![child] = value } // Ask parent to update their entry with my value + new children value - let newChildrenValues: [[(any Sendable)?]]? = state.childrenOtherProperties[metatype] + let newChildrenValues: [ProgressReporter: [(any Sendable)?]]? = state.childrenOtherProperties[metatype] let flattenedChildrenValues: [(any Sendable)?] = { guard let values = newChildrenValues else { return [] } - // Use flatMap to flatten the array but preserve nil values - return values.flatMap { innerArray -> [(any Sendable)?] in - // Each inner array element is preserved, including nil values - return innerArray.map { $0 } - } + return values.flatMap { $0.value } }() let newValueForParent: [(any Sendable)?] = [state.otherProperties[metatype]] + flattenedChildrenValues - parent?.updateChildrenOtherPropertiesAnyValue(property: metatype, idx: state.positionInParent!, value: newValueForParent) + parents.withLock { parents in + for (parent, _) in parents { + parent.updateChildrenOtherPropertiesAnyValue(property: metatype, child: child, value: newValueForParent) + } + } } } - internal func updateChildrenOtherProperties(property metatype: P.Type, idx: Int, value: [P.T?]) { + internal func updateChildrenOtherProperties(property metatype: P.Type, child: ProgressReporter, value: [P.T?]) { state.withLock { state in - let myEntries: [[P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [[P.T?]] + let myEntries: [ProgressReporter: [P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [ProgressReporter: [P.T?]] if let entries = myEntries { - // If entries is not nil, make sure it is a valid index, then update my entry of children values - let entriesLength = entries.count - // Check if entries need resizing - if idx >= entriesLength { - // Entries need resizing - state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] = resizeArray(array: &state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)], to: idx+1) - state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)]![idx] = value - } else { - // Entries don't need resizing - state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)]![idx] = value - } - + // If entries is not nil, then update my entry of children values + state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)]![child] = value } else { // If entries is nil - state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] = resizeArray(array: &state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)], to: idx+1) - state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)]![idx] = value + state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] = [:] + state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)]![child] = value } // Ask parent to update their entry with my value + new children value - let newChildrenValues: [[P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [[P.T?]] + let newChildrenValues: [ProgressReporter: [P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [ProgressReporter: [P.T?]] let flattenedChildrenValues: [P.T?] = { guard let values = newChildrenValues else { return [] } // Use flatMap to flatten the array but preserve nil values - return values.flatMap { innerArray -> [P.T?] in - // Each inner array element is preserved, including nil values - return innerArray.map { $0 } - } + return values.flatMap { $0.value } }() let newValueForParent: [P.T?] = [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T] + flattenedChildrenValues - parent?.updateChildrenOtherProperties(property: metatype, idx: state.positionInParent!, value: newValueForParent) + parents.withLock { parents in + for (parent, _) in parents { + parent.updateChildrenOtherProperties(property: metatype, child: self, value: newValueForParent) + } + } } } // Copy elements of array to new array with the correct size - private func resizeArray(array: inout [[any Sendable]]?, to size: Int) -> [[any Sendable]] { - var newArray: [[any Sendable]] = Array(repeating: [nil], count: size) - if array == nil && size == 1 { - return newArray - } - if let oldArray = array { - // Use array.count to avoid invalid index - for idx in 0.. [[any Sendable]] { +// var newArray: [[any Sendable]] = Array(repeating: [nil], count: size) +// if array == nil && size == 1 { +// return newArray +// } +// if let oldArray = array { +// // Use array.count to avoid invalid index +// for idx in 0..(property metatype: P.Type) -> [P.T?] { return state.withLock { state in From ca7698a91095480ab0108a09431ff84d94997757 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 6 May 2025 19:16:38 -0700 Subject: [PATCH 32/85] remove positionInParent implementation --- .../FoundationEssentials/ProgressReporter/Subprogress.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift b/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift index 98c30f942..78eecedb5 100644 --- a/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift +++ b/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift @@ -45,8 +45,7 @@ public struct Subprogress: ~Copyable, Sendable { ghostReporter?.setInteropChild(interopChild: childReporter) } else { // Add child to parent's _children list & Store in child children's position in parent - let childPositionInParent = parent.addToChildren(childReporter: childReporter) - childReporter.setPositionInParent(to: childPositionInParent) + parent.addToChildren(childReporter: childReporter) childReporter.addToParents(parentReporter: parent, portionOfParent: portionOfParent) } From 16c43d53726bddaeac49edea808254009d07a5ec Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 7 May 2025 09:59:40 -0700 Subject: [PATCH 33/85] fix custom properties implementation for multi-parent --- .../ProgressReporter/ProgressReporter.swift | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index 1bd98a7af..c2b861dca 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -525,31 +525,12 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } - // Copy elements of array to new array with the correct size -// private func resizeArray(array: inout [[any Sendable]]?, to size: Int) -> [[any Sendable]] { -// var newArray: [[any Sendable]] = Array(repeating: [nil], count: size) -// if array == nil && size == 1 { -// return newArray -// } -// if let oldArray = array { -// // Use array.count to avoid invalid index -// for idx in 0..(property metatype: P.Type) -> [P.T?] { return state.withLock { state in - let childrenValues: [[P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [[P.T?]] + let childrenValues: [ProgressReporter: [P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [ProgressReporter: [P.T?]] let flattenedChildrenValues: [P.T?] = { guard let values = childrenValues else { return [] } - // Use flatMap to flatten the array but preserve nil values - return values.flatMap { innerArray -> [P.T?] in - // Each inner array element is preserved, including nil values - return innerArray.map { $0 ?? P.defaultValue } - } + return values.flatMap { $0.value } }() return [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T ?? P.defaultValue] + flattenedChildrenValues } From 9e5693e8cc703e27ac042e9284cc6c2cc8d67070 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 7 May 2025 13:38:35 -0700 Subject: [PATCH 34/85] fix propagation of in-progress value of children to parent in multi-parent context --- .../ProgressReporter/ProgressReporter.swift | 26 ++++++++++--------- .../ProgressReporter/Subprogress.swift | 2 +- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index c2b861dca..70ac33265 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -437,7 +437,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { _ = children.withLock { children in children.insert(childReporter) } - // Resizing childrenOtherProperties for all types need to happen here for entry of nil to exist state.withLock { state in for (metatype, _) in state.childrenOtherProperties { state.childrenOtherProperties[metatype] = [self: [nil as (any Sendable)?]] @@ -460,22 +459,24 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } - internal func addToParents(parentReporter: ProgressReporter, portionOfParent: Int) { + internal func addParent(parentReporter: ProgressReporter, portionOfParent: Int) { parents.withLock { parents in parents[parentReporter] = portionOfParent } + + let updates = state.withLock { state in + let original = _ProgressFraction(completed: 0, total: 0) + let updated = state.fractionState.overallFraction + return (original, updated) + } + + parentReporter.updateChildFraction(from: updates.0, to: updates.1, portion: portionOfParent) } -// internal func setPositionInParent(to position: Int) { -// state.withLock { state in -// state.positionInParent = position -// } -// } - internal func updateChildrenOtherPropertiesAnyValue(property metatype: AnyMetatypeWrapper, child: ProgressReporter, value: [any Sendable]) { state.withLock { state in let myEntries: [ProgressReporter: [(any Sendable)?]]? = state.childrenOtherProperties[metatype] - if let entries = myEntries { + if myEntries != nil { // If entries is not nil, then update my entry of children values state.childrenOtherProperties[metatype]![child] = value } else { @@ -501,7 +502,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { internal func updateChildrenOtherProperties(property metatype: P.Type, child: ProgressReporter, value: [P.T?]) { state.withLock { state in let myEntries: [ProgressReporter: [P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [ProgressReporter: [P.T?]] - if let entries = myEntries { + if myEntries != nil { // If entries is not nil, then update my entry of children values state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)]![child] = value } else { @@ -546,8 +547,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // get the actual progress from within the monitor, then add as children let actualReporter = monitor.reporter self.addToChildren(childReporter: actualReporter) - actualReporter.addToParents(parentReporter: self, portionOfParent: portionOfParent) - + actualReporter.addParent(parentReporter: self, portionOfParent: portionOfParent) +// print("self.children \(children.withLock { $0 })") +// print("child's parents list \(actualReporter.parents.withLock { $0 })") } deinit { diff --git a/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift b/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift index 78eecedb5..c76a0f0dd 100644 --- a/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift +++ b/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift @@ -46,7 +46,7 @@ public struct Subprogress: ~Copyable, Sendable { } else { // Add child to parent's _children list & Store in child children's position in parent parent.addToChildren(childReporter: childReporter) - childReporter.addToParents(parentReporter: parent, portionOfParent: portionOfParent) + childReporter.addParent(parentReporter: parent, portionOfParent: portionOfParent) } return childReporter From 5fc0c0ab5e0ee5430abd5bcc1bef219f77ad3c67 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 8 May 2025 13:31:22 -0700 Subject: [PATCH 35/85] fix propagation of metadata in multi-parent --- .../ProgressReporter/ProgressReporter.swift | 73 ++++++++----------- 1 file changed, 30 insertions(+), 43 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index 70ac33265..8bb61004a 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -176,7 +176,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { set { // Update my own other properties entry state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = newValue - // Flatten myself + myChildren to be sent to parent + + // Generate an array of myself + children values of the property var updateValueForParent: [P.T?] = [newValue] let childrenValues: [ProgressReporter: [P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] as? [ProgressReporter: [P.T?]] let flattenedChildrenValues: [P.T?] = { @@ -188,6 +189,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } }() updateValueForParent += flattenedChildrenValues + + // Send the array for that property to parents reporter.parents.withLock { parents in for (parent, _) in parents { parent.updateChildrenOtherProperties(property: P.self, child: reporter, value: updateValueForParent) @@ -197,18 +200,11 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } -// private let portionOfParent: Int -// internal let parent: ProgressReporter? - - // Parents dictionary maps parent to portionOfParent - my parent to my portion inside that parent & my position in parent's children list - internal let parents: LockedState<[ProgressReporter: Int]> - private let children: LockedState> + private let children: LockedState> private let state: LockedState internal init(total: Int?, ghostReporter: ProgressReporter?, interopObservation: (any Sendable)?) { -// self.portionOfParent = portionOfParent -// self.parent = parent self.parents = .init(initialState: [:]) self.children = .init(initialState: Set()) let fractionState = FractionState( @@ -432,31 +428,10 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } - // Returns position of child in parent internal func addToChildren(childReporter: ProgressReporter) { _ = children.withLock { children in children.insert(childReporter) } - state.withLock { state in - for (metatype, _) in state.childrenOtherProperties { - state.childrenOtherProperties[metatype] = [self: [nil as (any Sendable)?]] - // Propagate my value + my flattened children up to parent - var valueForParent: [(any Sendable)?] = [state.otherProperties[metatype]] - let newChildrenValues = state.childrenOtherProperties[metatype] - let flattenedChildrenValues: [(any Sendable)?] = { - guard let values = newChildrenValues else { return [] } - return values.flatMap { $0.value } - }() - - valueForParent += flattenedChildrenValues - parents.withLock { parents in - for (parent, _) in parents { - parent.updateChildrenOtherPropertiesAnyValue(property: metatype, child: self, value: valueForParent) - } - } - - } - } } internal func addParent(parentReporter: ProgressReporter, portionOfParent: Int) { @@ -467,20 +442,33 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { let updates = state.withLock { state in let original = _ProgressFraction(completed: 0, total: 0) let updated = state.fractionState.overallFraction + + // Update metatype entry in parent + for (metatype, value) in state.otherProperties { + let newChildrenValues: [ProgressReporter: [(any Sendable)?]]? = state.childrenOtherProperties[metatype] + let flattenedChildrenValues: [(any Sendable)?] = { + guard let values = newChildrenValues else { return [] } + return values.flatMap { $0.value } + }() + let newValueForParent: [(any Sendable)?] = [value] + flattenedChildrenValues + parentReporter.updateChildrenOtherPropertiesAnyValue(property: metatype, child: self, value: newValueForParent) + } + return (original, updated) } + // Update parent's total & completed to include self's values parentReporter.updateChildFraction(from: updates.0, to: updates.1, portion: portionOfParent) } - internal func updateChildrenOtherPropertiesAnyValue(property metatype: AnyMetatypeWrapper, child: ProgressReporter, value: [any Sendable]) { + internal func updateChildrenOtherPropertiesAnyValue(property metatype: AnyMetatypeWrapper, child: ProgressReporter, value: [(any Sendable)?]) { state.withLock { state in let myEntries: [ProgressReporter: [(any Sendable)?]]? = state.childrenOtherProperties[metatype] if myEntries != nil { // If entries is not nil, then update my entry of children values state.childrenOtherProperties[metatype]![child] = value } else { - // If entries is nil + // If entries is nil, initialize then update my entry of children values state.childrenOtherProperties[metatype] = [:] state.childrenOtherProperties[metatype]![child] = value } @@ -506,7 +494,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // If entries is not nil, then update my entry of children values state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)]![child] = value } else { - // If entries is nil + // If entries is nil, initialize then update my entry of children values state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] = [:] state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)]![child] = value } @@ -514,7 +502,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { let newChildrenValues: [ProgressReporter: [P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [ProgressReporter: [P.T?]] let flattenedChildrenValues: [P.T?] = { guard let values = newChildrenValues else { return [] } - // Use flatMap to flatten the array but preserve nil values return values.flatMap { $0.value } }() let newValueForParent: [P.T?] = [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T] + flattenedChildrenValues @@ -546,10 +533,10 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public func addChild(_ monitor: ProgressMonitor, assignedCount portionOfParent: Int) { // get the actual progress from within the monitor, then add as children let actualReporter = monitor.reporter + + // Add monitor as child + Add self as parent self.addToChildren(childReporter: actualReporter) actualReporter.addParent(parentReporter: self, portionOfParent: portionOfParent) -// print("self.children \(children.withLock { $0 })") -// print("child's parents list \(actualReporter.parents.withLock { $0 })") } deinit { @@ -576,10 +563,10 @@ extension ProgressReporter: Hashable, Equatable { } } -@available(FoundationPreview 6.2, *) -extension ProgressReporter: CustomDebugStringConvertible { - /// The description for `completedCount` and `totalCount`. - public var debugDescription: String { - return "\(completedCount) / \(totalCount ?? 0)" - } -} +//@available(FoundationPreview 6.2, *) +//extension ProgressReporter: CustomDebugStringConvertible { +// /// The description for `completedCount` and `totalCount`. +// public var debugDescription: String { +// return "\(completedCount) / \(totalCount ?? 0)" +// } +//} From 5f0c4a9be34585383b661b742a88e83b5fea0b46 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 8 May 2025 14:44:10 -0700 Subject: [PATCH 36/85] updated implementation to use OrderedDictionary to store metadata throughout tree --- .../ProgressReporter/ProgressReporter.swift | 179 ++++++++++-------- 1 file changed, 104 insertions(+), 75 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index 8bb61004a..d72b5c984 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -12,6 +12,14 @@ import Observation +#if canImport(CollectionsInternal) +internal import CollectionsInternal +#elseif canImport(OrderedCollections) +internal import OrderedCollections +#elseif canImport(_FoundationCollections) +internal import _FoundationCollections +#endif + internal struct FractionState { var indeterminate: Bool var selfFraction: _ProgressFraction @@ -45,7 +53,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { var fractionState: FractionState var otherProperties: [AnyMetatypeWrapper: (any Sendable)] // Type: Metatype maps to dictionary of child to value - var childrenOtherProperties: [AnyMetatypeWrapper: [ProgressReporter: [(any Sendable)]]] + var childrenOtherProperties: [AnyMetatypeWrapper: OrderedDictionary] } // Interop states @@ -179,13 +187,13 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Generate an array of myself + children values of the property var updateValueForParent: [P.T?] = [newValue] - let childrenValues: [ProgressReporter: [P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] as? [ProgressReporter: [P.T?]] + let childrenValues: OrderedDictionary? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] let flattenedChildrenValues: [P.T?] = { guard let values = childrenValues else { return [] } // Use flatMap to flatten the array but preserve nil values return values.flatMap { // Each inner array element is preserved, including nil values - $0.value + $0.value as? P.T } }() updateValueForParent += flattenedChildrenValues @@ -250,7 +258,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } - /// Returns a `Subprogress` representing a portion of `self`which can be passed to any method that reports progress. + /// Returns a `Subprogress` representing a portion of `self` which can be passed to any method that reports progress. /// /// - Parameter count: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. /// - Returns: A `Subprogress` instance. @@ -260,6 +268,20 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { return subprogress } + + /// Adds a `ProgressMonitor` as a child, with its progress representing a portion of `self`'s progress. + /// - Parameters: + /// - monitor: A `ProgressMonitor` instance. + /// - portionOfParent: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. + public func addChild(_ monitor: ProgressMonitor, assignedCount portionOfParent: Int) { + // get the actual progress from within the monitor, then add as children + let actualReporter = monitor.reporter + + // Add monitor as child + Add self as parent + self.addToChildren(childReporter: actualReporter) + actualReporter.addParent(parentReporter: self, portionOfParent: portionOfParent) + } + /// Increases `completedCount` by `count`. /// - Parameter count: Units of work. public func complete(count: Int) { @@ -268,19 +290,25 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { ghostReporter?.notifyObservers(with: .fractionUpdated) } - private struct UpdateState { - let previous: _ProgressFraction - let current: _ProgressFraction - } - private func updateCompletedCount(count: Int) -> UpdateState { - // Acquire and release child's lock - let (previous, current) = state.withLock { state in - let prev = state.fractionState.overallFraction - state.fractionState.selfFraction.completed += count - return (prev, state.fractionState.overallFraction) + /// Returns an array of values for specified property in subtree. + /// - Parameter metatype: Type of property. + /// - Returns: Array of values for property. + public func values(property metatype: P.Type) -> [P.T?] { + return state.withLock { state in + let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) + return [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T ?? P.defaultValue] + childrenValues } - return UpdateState(previous: previous, current: current) + } + + + /// Returns the aggregated result of values. + /// - Parameters: + /// - property: Type of property. + /// - values:Sum of values. + public func total(property: P.Type, values: [P.T?]) -> P.T where P.T: AdditiveArithmetic { + let droppedNil = values.compactMap { $0 } + return droppedNil.reduce(P.T.zero, +) } /// Mutates any settable properties that convey information about progress. @@ -351,6 +379,21 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } //MARK: FractionCompleted Calculation methods + private struct UpdateState { + let previous: _ProgressFraction + let current: _ProgressFraction + } + + private func updateCompletedCount(count: Int) -> UpdateState { + // Acquire and release child's lock + let (previous, current) = state.withLock { state in + let prev = state.fractionState.overallFraction + state.fractionState.selfFraction.completed += count + return (prev, state.fractionState.overallFraction) + } + return UpdateState(previous: previous, current: current) + } + private func updateFractionCompleted(from: _ProgressFraction, to: _ProgressFraction) { _$observationRegistrar.withMutation(of: self, keyPath: \.fractionCompleted) { if from != to { @@ -359,7 +402,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { parent.updateChildFraction(from: from, to: to, portion: portionOfParent) } } -// parent?.updateChildFraction(from: from, to: to, portion: portionOfParent) } } } @@ -445,25 +487,46 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Update metatype entry in parent for (metatype, value) in state.otherProperties { - let newChildrenValues: [ProgressReporter: [(any Sendable)?]]? = state.childrenOtherProperties[metatype] - let flattenedChildrenValues: [(any Sendable)?] = { - guard let values = newChildrenValues else { return [] } - return values.flatMap { $0.value } - }() - let newValueForParent: [(any Sendable)?] = [value] + flattenedChildrenValues - parentReporter.updateChildrenOtherPropertiesAnyValue(property: metatype, child: self, value: newValueForParent) + let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) + let updatedParentEntry: [(any Sendable)?] = [value] + childrenValues + parentReporter.updateChildrenOtherPropertiesAnyValue(property: metatype, child: self, value: updatedParentEntry) } return (original, updated) } - // Update parent's total & completed to include self's values + // Update childFraction entry in parent parentReporter.updateChildFraction(from: updates.0, to: updates.1, portion: portionOfParent) } - internal func updateChildrenOtherPropertiesAnyValue(property metatype: AnyMetatypeWrapper, child: ProgressReporter, value: [(any Sendable)?]) { + // MARK: Propagation of Additional Properties Methods (Dual Mode of Operations) + private func getFlattenedChildrenValues(property metatype: P.Type, state: inout State) -> [P.T?] { + let childrenDictionary = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] + var childrenValues: [P.T?] = [] + if let dictionary = childrenDictionary { + for (_, value) in dictionary { + if let value = value as? [P.T] { + childrenValues.append(contentsOf: value) + } + } + } + return childrenValues + } + + private func getFlattenedChildrenValues(property metatype: AnyMetatypeWrapper, state: inout State) -> [(any Sendable)?] { + let childrenDictionary = state.childrenOtherProperties[metatype] + var childrenValues: [(any Sendable)?] = [] + if let dictionary = childrenDictionary { + for (_, value) in dictionary { + childrenValues.append(contentsOf: value) + } + } + return childrenValues + } + + private func updateChildrenOtherPropertiesAnyValue(property metatype: AnyMetatypeWrapper, child: ProgressReporter, value: [(any Sendable)?]) { state.withLock { state in - let myEntries: [ProgressReporter: [(any Sendable)?]]? = state.childrenOtherProperties[metatype] + let myEntries = state.childrenOtherProperties[metatype] if myEntries != nil { // If entries is not nil, then update my entry of children values state.childrenOtherProperties[metatype]![child] = value @@ -473,23 +536,19 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { state.childrenOtherProperties[metatype]![child] = value } // Ask parent to update their entry with my value + new children value - let newChildrenValues: [ProgressReporter: [(any Sendable)?]]? = state.childrenOtherProperties[metatype] - let flattenedChildrenValues: [(any Sendable)?] = { - guard let values = newChildrenValues else { return [] } - return values.flatMap { $0.value } - }() - let newValueForParent: [(any Sendable)?] = [state.otherProperties[metatype]] + flattenedChildrenValues + let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) + let updatedParentEntry: [(any Sendable)?] = [state.otherProperties[metatype]] + childrenValues parents.withLock { parents in for (parent, _) in parents { - parent.updateChildrenOtherPropertiesAnyValue(property: metatype, child: child, value: newValueForParent) + parent.updateChildrenOtherPropertiesAnyValue(property: metatype, child: child, value: updatedParentEntry) } } } } - internal func updateChildrenOtherProperties(property metatype: P.Type, child: ProgressReporter, value: [P.T?]) { + private func updateChildrenOtherProperties(property metatype: P.Type, child: ProgressReporter, value: [P.T?]) { state.withLock { state in - let myEntries: [ProgressReporter: [P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [ProgressReporter: [P.T?]] + let myEntries = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] if myEntries != nil { // If entries is not nil, then update my entry of children values state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)]![child] = value @@ -499,46 +558,16 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)]![child] = value } // Ask parent to update their entry with my value + new children value - let newChildrenValues: [ProgressReporter: [P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [ProgressReporter: [P.T?]] - let flattenedChildrenValues: [P.T?] = { - guard let values = newChildrenValues else { return [] } - return values.flatMap { $0.value } - }() - let newValueForParent: [P.T?] = [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T] + flattenedChildrenValues + let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) + let updatedParentEntry: [P.T?] = [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T] + childrenValues parents.withLock { parents in for (parent, _) in parents { - parent.updateChildrenOtherProperties(property: metatype, child: self, value: newValueForParent) + parent.updateChildrenOtherProperties(property: metatype, child: self, value: updatedParentEntry) } } } } - public func getAllValues(property metatype: P.Type) -> [P.T?] { - return state.withLock { state in - let childrenValues: [ProgressReporter: [P.T?]]? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] as? [ProgressReporter: [P.T?]] - let flattenedChildrenValues: [P.T?] = { - guard let values = childrenValues else { return [] } - return values.flatMap { $0.value } - }() - return [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T ?? P.defaultValue] + flattenedChildrenValues - } - } - - public func reduce(property: P.Type, values: [P.T?]) -> P.T where P.T: AdditiveArithmetic { - let droppedNil = values.compactMap { $0 } - return droppedNil.reduce(P.T.zero, +) - } - - // Adds Progress Monitor as a child - a monitor can be added to multiple parents - public func addChild(_ monitor: ProgressMonitor, assignedCount portionOfParent: Int) { - // get the actual progress from within the monitor, then add as children - let actualReporter = monitor.reporter - - // Add monitor as child + Add self as parent - self.addToChildren(childReporter: actualReporter) - actualReporter.addParent(parentReporter: self, portionOfParent: portionOfParent) - } - deinit { if !isFinished { self.withProperties { properties in @@ -563,10 +592,10 @@ extension ProgressReporter: Hashable, Equatable { } } -//@available(FoundationPreview 6.2, *) -//extension ProgressReporter: CustomDebugStringConvertible { -// /// The description for `completedCount` and `totalCount`. -// public var debugDescription: String { -// return "\(completedCount) / \(totalCount ?? 0)" -// } -//} +@available(FoundationPreview 6.2, *) +extension ProgressReporter: CustomDebugStringConvertible { + /// The description for `completedCount` and `totalCount`. + public var debugDescription: String { + return "\(completedCount) / \(totalCount ?? 0)" + } +} From ecd71eac2910f35bc6e9c220a69c8fac3c24cb77 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 8 May 2025 16:20:56 -0700 Subject: [PATCH 37/85] add convenience for observing a ProgressMonitor --- .../ProgressReporter/ProgressReporter.swift | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index d72b5c984..cdf68dc68 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -186,20 +186,19 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = newValue // Generate an array of myself + children values of the property - var updateValueForParent: [P.T?] = [newValue] let childrenValues: OrderedDictionary? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] let flattenedChildrenValues: [P.T?] = { guard let values = childrenValues else { return [] } - // Use flatMap to flatten the array but preserve nil values - return values.flatMap { + // Use compactMap to flatten the array but preserve nil values + return values.compactMap { // Each inner array element is preserved, including nil values $0.value as? P.T } }() - updateValueForParent += flattenedChildrenValues + let updateValueForParent: [P.T?] = [newValue] + flattenedChildrenValues - // Send the array for that property to parents - reporter.parents.withLock { parents in + // Send the array of myself + children values of property to parents + reporter.parents.withLock { [reporter] parents in for (parent, _) in parents { parent.updateChildrenOtherProperties(property: P.self, child: reporter, value: updateValueForParent) } @@ -235,6 +234,11 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { self.init(total: totalCount, ghostReporter: nil, interopObservation: nil) } + public convenience init(from monitor: ProgressMonitor) { + self.init(total: 1, ghostReporter: nil, interopObservation: nil) + self.addChild(monitor, assignedCount: 1) + monitor.reporter.addParent(parentReporter: self, portionOfParent: 1) + } /// Sets `totalCount`. /// - Parameter newTotal: Total units of work. From 29ad39a768867cddff7796f11d45d18ce4b0ec6f Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 9 May 2025 19:48:34 -0700 Subject: [PATCH 38/85] fix additional property propagation for more than 2 levels: typecast matters a lot --- .../ProgressReporter/ProgressReporter.swift | 66 +++++++++++++++---- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index cdf68dc68..1fc9f0a63 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -64,6 +64,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Interop properties - Just kept alive internal let interopObservation: (any Sendable)? // set at init +// internal let interopObservationForMonitor: LockedState<(any Sendable)?> = LockedState(initialState: nil) +// internal let monitorInterop: LockedState = LockedState(initialState: false) + #if FOUNDATION_FRAMEWORK internal let parentBridge: LockedState = LockedState(initialState: nil) // dummy, set upon calling setParentBridge #endif @@ -157,6 +160,11 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { //TODO: rdar://149015734 Check throttling reporter.updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) reporter.ghostReporter?.notifyObservers(with: .totalCountUpdated) +// reporter.monitorInterop.withLock { [reporter] interop in +// if interop == true { +// reporter.notifyObservers(with: .totalCountUpdated) +// } +// } } } @@ -172,6 +180,12 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { state.fractionState.selfFraction.completed = newValue reporter.updateFractionCompleted(from: prev, to: state.fractionState.overallFraction) reporter.ghostReporter?.notifyObservers(with: .fractionUpdated) + +// reporter.monitorInterop.withLock { [reporter] interop in +// if interop == true { +// reporter.notifyObservers(with: .fractionUpdated) +// } +// } } } @@ -186,18 +200,21 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = newValue // Generate an array of myself + children values of the property - let childrenValues: OrderedDictionary? = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] let flattenedChildrenValues: [P.T?] = { - guard let values = childrenValues else { return [] } - // Use compactMap to flatten the array but preserve nil values - return values.compactMap { - // Each inner array element is preserved, including nil values - $0.value as? P.T + let childrenDictionary = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] + var childrenValues: [P.T?] = [] + if let dictionary = childrenDictionary { + for (_, value) in dictionary { + if let value = value as? [P.T?] { + childrenValues.append(contentsOf: value) + } + } } + return childrenValues }() - let updateValueForParent: [P.T?] = [newValue] + flattenedChildrenValues // Send the array of myself + children values of property to parents + let updateValueForParent: [P.T?] = [newValue] + flattenedChildrenValues reporter.parents.withLock { [reporter] parents in for (parent, _) in parents { parent.updateChildrenOtherProperties(property: P.self, child: reporter, value: updateValueForParent) @@ -259,6 +276,13 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) ghostReporter?.notifyObservers(with: .totalCountUpdated) + +// monitorInterop.withLock { [self] interop in +// if interop == true { +// print("notifying observers") +// notifyObservers(with: .totalCountUpdated) +// } +// } } } @@ -292,6 +316,13 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { let updateState = updateCompletedCount(count: count) updateFractionCompleted(from: updateState.previous, to: updateState.current) ghostReporter?.notifyObservers(with: .fractionUpdated) + +// monitorInterop.withLock { [self] interop in +// if interop == true { +// print("notifying observers") +// notifyObservers(with: .fractionUpdated) +// } +// } } @@ -301,7 +332,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public func values(property metatype: P.Type) -> [P.T?] { return state.withLock { state in let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) - return [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T ?? P.defaultValue] + childrenValues + return [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T ?? P.defaultValue] + childrenValues.map { $0 ?? P.defaultValue } } } @@ -459,6 +490,18 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } +// internal func setInteropObservationForMonitor(observation monitorObservation: (any Sendable)) { +// interopObservationForMonitor.withLock { observation in +// observation = monitorObservation +// } +// } +// +// internal func setMonitorInterop(to value: Bool) { +// monitorInterop.withLock { monitorInterop in +// monitorInterop = value +// } +// } + //MARK: Internal methods to mutate locked context #if FOUNDATION_FRAMEWORK internal func setParentBridge(parentBridge: Foundation.Progress) { @@ -486,9 +529,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } let updates = state.withLock { state in - let original = _ProgressFraction(completed: 0, total: 0) - let updated = state.fractionState.overallFraction - // Update metatype entry in parent for (metatype, value) in state.otherProperties { let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) @@ -496,6 +536,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { parentReporter.updateChildrenOtherPropertiesAnyValue(property: metatype, child: self, value: updatedParentEntry) } + let original = _ProgressFraction(completed: 0, total: 0) + let updated = state.fractionState.overallFraction return (original, updated) } @@ -509,7 +551,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { var childrenValues: [P.T?] = [] if let dictionary = childrenDictionary { for (_, value) in dictionary { - if let value = value as? [P.T] { + if let value = value as? [P.T?] { childrenValues.append(contentsOf: value) } } From fa37634e353b1990fff7334a826ca2a7a245fd4a Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 9 May 2025 19:49:45 -0700 Subject: [PATCH 39/85] draft monitor interop implementation + reorganize tests --- .../ProgressReporter+Interop.swift | 17 ++ .../ProgressReporterTests.swift | 248 +++++++++++------- 2 files changed, 163 insertions(+), 102 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift index 36c12a7fd..268cc30d0 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift @@ -48,6 +48,21 @@ extension Progress { actualProgress.interopWithProgressParent = true return actualProgress } + +// public func addChild(_ monitor: ProgressReporter.ProgressMonitor, withPendingUnitCount count: Int) { +// +// // Make ghost parent & add it to actual parent's children list +// let ghostProgressParent = Progress(totalUnitCount: Int64(monitor.reporter.totalCount ?? 0)) +// ghostProgressParent.completedUnitCount = Int64(monitor.reporter.completedCount) +// print("ghost progress parent \(ghostProgressParent)") +// self.addChild(ghostProgressParent, withPendingUnitCount: Int64(count)) +// +// // Make observation instance +// let observation = _ProgressParentProgressReporterChild(ghostParent: ghostProgressParent, ghostChild: monitor.reporter) +// +// monitor.reporter.setInteropObservationForMonitor(observation: observation) +// monitor.reporter.setMonitorInterop(to: true) +// } } private final class _ProgressParentProgressReporterChild: Sendable { @@ -67,9 +82,11 @@ private final class _ProgressParentProgressReporterChild: Sendable { switch observerState { case .totalCountUpdated: +// print("received totalCountUpdated") self.ghostParent.totalUnitCount = Int64(self.ghostChild.totalCount ?? 0) case .fractionUpdated: +// print("received fractionUpdated") let count = self.ghostChild.withProperties { p in return (p.completedCount, p.totalCount) } diff --git a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift index a6c21acb1..0b4645608 100644 --- a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift @@ -47,25 +47,6 @@ class TestProgressReporter: XCTestCase { } } - func doFileOperation(reportTo progress: consuming Subprogress) async { - let reporter = progress.reporter(totalCount: 100) - reporter.withProperties { properties in - properties.totalFileCount = 100 - } - - XCTAssertEqual(reporter.withProperties(\.totalFileCount), 100) - - reporter.complete(count: 100) - XCTAssertEqual(reporter.fractionCompleted, 1.0) - XCTAssertTrue(reporter.isFinished) - - reporter.withProperties { properties in - properties.completedFileCount = 100 - } - XCTAssertEqual(reporter.withProperties(\.completedFileCount), 100) - XCTAssertEqual(reporter.withProperties(\.totalFileCount), 100) - } - /// MARK: Tests calculations based on change in totalCount func testTotalCountNil() async throws { let overall = ProgressReporter(totalCount: nil) @@ -163,14 +144,6 @@ class TestProgressReporter: XCTestCase { XCTAssertTrue(reporter.isFinished) } - func testDiscreteReporterWithFileProperties() async throws { - let fileReporter = ProgressReporter(totalCount: 3) - await doFileOperation(reportTo: fileReporter.subprogress(assigningCount: 3)) - XCTAssertEqual(fileReporter.fractionCompleted, 1.0) - XCTAssertEqual(fileReporter.completedCount, 3) - XCTAssertTrue(fileReporter.isFinished) - } - /// MARK: Tests multiple-level trees func testEmptyDiscreteReporter() async throws { let reporter = ProgressReporter(totalCount: nil) @@ -188,80 +161,6 @@ class TestProgressReporter: XCTestCase { XCTAssertTrue(reporter.isFinished) } - func testTwoLevelTreeWithOneChildWithFileProperties() async throws { - let overall = ProgressReporter(totalCount: 2) - - let progress1 = overall.subprogress(assigningCount: 1) - let reporter1 = progress1.reporter(totalCount: 10) - reporter1.withProperties { properties in - properties.totalFileCount = 10 - properties.completedFileCount = 0 - } - reporter1.complete(count: 10) - - XCTAssertEqual(overall.fractionCompleted, 0.5) - // This should call reduce and get 10 - XCTAssertEqual(overall.withProperties(\.totalFileCount), 10) - } - - func testTwoLevelTreeWithTwoChildrenWithFileProperties() async throws { - let overall = ProgressReporter(totalCount: 2) - - let progress1 = overall.subprogress(assigningCount: 1) - let reporter1 = progress1.reporter(totalCount: 10) - - reporter1.withProperties { properties in - properties.totalFileCount = 11 - properties.completedFileCount = 0 - } - - let progress2 = overall.subprogress(assigningCount: 1) - let reporter2 = progress2.reporter(totalCount: 10) - - reporter2.withProperties { properties in - properties.totalFileCount = 9 - properties.completedFileCount = 0 - } - - XCTAssertEqual(overall.fractionCompleted, 0.0) - XCTAssertEqual(overall.withProperties(\.totalFileCount), 20) - - // Update FileCounts - reporter1.withProperties { properties in - properties.completedFileCount = 1 - } - - reporter2.withProperties { properties in - properties.completedFileCount = 1 - } - - XCTAssertEqual(overall.withProperties(\.completedFileCount), 2) - } - - func testThreeLevelTreeWithFileProperties() async throws { - let overall = ProgressReporter(totalCount: 1) - - let progress1 = overall.subprogress(assigningCount: 1) - let reporter1 = progress1.reporter(totalCount: 5) - - let childProgress1 = reporter1.subprogress(assigningCount: 3) - let childReporter1 = childProgress1.reporter(totalCount: nil) - childReporter1.withProperties { properties in - properties.totalFileCount += 10 - } - - let childProgress2 = reporter1.subprogress(assigningCount: 2) - let childReporter2 = childProgress2.reporter(totalCount: nil) - childReporter2.withProperties { properties in - properties.totalFileCount += 10 - } - - XCTAssertEqual(reporter1.withProperties(\.totalFileCount), 20) - - // Tests that totalFileCount propagates to root level - XCTAssertEqual(overall.withProperties(\.totalFileCount), 20) - } - func testTwoLevelTreeWithTwoChildren() async throws { let overall = ProgressReporter(totalCount: 2) @@ -293,7 +192,7 @@ class TestProgressReporter: XCTestCase { XCTAssertEqual(overall.fractionCompleted, 0.5) // Parent is expected to get totalFileCount from one of the children with a totalFileCount - XCTAssertEqual(overall.withProperties(\.totalFileCount), 10) + XCTAssertEqual(overall.withProperties(\.totalFileCount), 0) } func testTwoLevelTreeWithMultipleChildren() async throws { @@ -366,6 +265,151 @@ class TestProgressReporter: XCTestCase { } } +/// Unit tests for propagation of type-safe metadata in ProgressReporter tree. +class TestProgressReporterAdditionalProperties: XCTestCase { + func doFileOperation(reportTo progress: consuming Subprogress) async { + let reporter = progress.reporter(totalCount: 100) + reporter.withProperties { properties in + properties.totalFileCount = 100 + } + + XCTAssertEqual(reporter.withProperties(\.totalFileCount), 100) + + reporter.complete(count: 100) + XCTAssertEqual(reporter.fractionCompleted, 1.0) + XCTAssertTrue(reporter.isFinished) + + reporter.withProperties { properties in + properties.completedFileCount = 100 + } + XCTAssertEqual(reporter.withProperties(\.completedFileCount), 100) + XCTAssertEqual(reporter.withProperties(\.totalFileCount), 100) + } + + func testDiscreteReporterWithFileProperties() async throws { + let fileReporter = ProgressReporter(totalCount: 3) + await doFileOperation(reportTo: fileReporter.subprogress(assigningCount: 3)) + XCTAssertEqual(fileReporter.fractionCompleted, 1.0) + XCTAssertEqual(fileReporter.completedCount, 3) + XCTAssertTrue(fileReporter.isFinished) + XCTAssertEqual(fileReporter.withProperties(\.totalFileCount), 0) + XCTAssertEqual(fileReporter.withProperties(\.completedFileCount), 0) + + let totalFileValues = fileReporter.values(property: ProgressReporter.Properties.TotalFileCount.self) + XCTAssertEqual(totalFileValues, [0, 100]) + + let reducedTotalFileValue = fileReporter.total(property: ProgressReporter.Properties.TotalFileCount.self, values: totalFileValues) + XCTAssertEqual(reducedTotalFileValue, 100) + + let completedFileValues = fileReporter.values(property: ProgressReporter.Properties.CompletedFileCount.self) + XCTAssertEqual(completedFileValues, [0, 100]) + + let reducedCompletedFileValue = fileReporter.total(property: ProgressReporter.Properties.CompletedFileCount.self, values: completedFileValues) + XCTAssertEqual(reducedCompletedFileValue, 100) + } + + func testTwoLevelTreeWithOneChildWithFileProperties() async throws { + let overall = ProgressReporter(totalCount: 2) + + let progress1 = overall.subprogress(assigningCount: 1) + let reporter1 = progress1.reporter(totalCount: 10) + reporter1.withProperties { properties in + properties.totalFileCount = 10 + properties.completedFileCount = 0 + } + reporter1.complete(count: 10) + + XCTAssertEqual(overall.fractionCompleted, 0.5) + + XCTAssertEqual(overall.withProperties(\.totalFileCount), 0) + XCTAssertEqual(reporter1.withProperties(\.totalFileCount), 10) + XCTAssertEqual(reporter1.withProperties(\.completedFileCount), 0) + + let totalFileValues = overall.values(property: ProgressReporter.Properties.TotalFileCount.self) + XCTAssertEqual(totalFileValues, [0, 10]) + + let completedFileValues = overall.values(property: ProgressReporter.Properties.CompletedFileCount.self) + XCTAssertEqual(completedFileValues, [0, 0]) + } + + func testTwoLevelTreeWithTwoChildrenWithFileProperties() async throws { + let overall = ProgressReporter(totalCount: 2) + + let progress1 = overall.subprogress(assigningCount: 1) + let reporter1 = progress1.reporter(totalCount: 10) + + reporter1.withProperties { properties in + properties.totalFileCount = 11 + properties.completedFileCount = 0 + } + + let progress2 = overall.subprogress(assigningCount: 1) + let reporter2 = progress2.reporter(totalCount: 10) + + reporter2.withProperties { properties in + properties.totalFileCount = 9 + properties.completedFileCount = 0 + } + + XCTAssertEqual(overall.fractionCompleted, 0.0) + XCTAssertEqual(overall.withProperties(\.totalFileCount), 0) + XCTAssertEqual(overall.withProperties(\.completedFileCount), 0) + let totalFileValues = overall.values(property: ProgressReporter.Properties.TotalFileCount.self) + XCTAssertEqual(totalFileValues, [0, 11, 9]) + let completedFileValues = overall.values(property: ProgressReporter.Properties.CompletedFileCount.self) + XCTAssertEqual(completedFileValues, [0, 0, 0]) + + // Update FileCounts + reporter1.withProperties { properties in + properties.completedFileCount = 1 + } + + reporter2.withProperties { properties in + properties.completedFileCount = 1 + } + + XCTAssertEqual(overall.withProperties(\.completedFileCount), 0) + let updatedCompletedFileValues = overall.values(property: ProgressReporter.Properties.CompletedFileCount.self) + XCTAssertEqual(updatedCompletedFileValues, [0, 1, 1]) + } + + func testThreeLevelTreeWithFileProperties() async throws { + let overall = ProgressReporter(totalCount: 1) + + let progress1 = overall.subprogress(assigningCount: 1) + let reporter1 = progress1.reporter(totalCount: 5) + + + let childProgress1 = reporter1.subprogress(assigningCount: 3) + let childReporter1 = childProgress1.reporter(totalCount: nil) + childReporter1.withProperties { properties in + properties.totalFileCount += 10 + } + XCTAssertEqual(childReporter1.withProperties(\.totalFileCount), 10) + + let preTotalFileValues = overall.values(property: ProgressReporter.Properties.TotalFileCount.self) + XCTAssertEqual(preTotalFileValues, [0, 0, 10]) + + let childProgress2 = reporter1.subprogress(assigningCount: 2) + let childReporter2 = childProgress2.reporter(totalCount: nil) + childReporter2.withProperties { properties in + properties.totalFileCount += 10 + } + XCTAssertEqual(childReporter2.withProperties(\.totalFileCount), 10) + + // Tests that totalFileCount propagates to root level + XCTAssertEqual(overall.withProperties(\.totalFileCount), 0) + let totalFileValues = overall.values(property: ProgressReporter.Properties.TotalFileCount.self) + XCTAssertEqual(totalFileValues, [0, 0, 10, 10]) + + reporter1.withProperties { properties in + properties.totalFileCount += 999 + } + let totalUpdatedFileValues = overall.values(property: ProgressReporter.Properties.TotalFileCount.self) + XCTAssertEqual(totalUpdatedFileValues, [0, 999, 10, 10]) + } +} + #if FOUNDATION_FRAMEWORK /// Unit tests for interop methods that support building Progress trees with both Progress and ProgressReporter class TestProgressReporterInterop: XCTestCase { From 3a1500e7790e93736daae71c106b1f8438f1c578 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 12 May 2025 13:57:40 -0700 Subject: [PATCH 40/85] interop with ProgressMonitor implementation + unit tests --- .../ProgressReporter+Interop.swift | 58 +++++++++++----- .../ProgressReporter/ProgressReporter.swift | 69 +++++++++---------- .../ProgressReporterTests.swift | 58 ++++++++++++++++ 3 files changed, 133 insertions(+), 52 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift index 268cc30d0..403c3832e 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift @@ -49,20 +49,19 @@ extension Progress { return actualProgress } -// public func addChild(_ monitor: ProgressReporter.ProgressMonitor, withPendingUnitCount count: Int) { -// -// // Make ghost parent & add it to actual parent's children list -// let ghostProgressParent = Progress(totalUnitCount: Int64(monitor.reporter.totalCount ?? 0)) -// ghostProgressParent.completedUnitCount = Int64(monitor.reporter.completedCount) -// print("ghost progress parent \(ghostProgressParent)") -// self.addChild(ghostProgressParent, withPendingUnitCount: Int64(count)) -// -// // Make observation instance -// let observation = _ProgressParentProgressReporterChild(ghostParent: ghostProgressParent, ghostChild: monitor.reporter) -// -// monitor.reporter.setInteropObservationForMonitor(observation: observation) -// monitor.reporter.setMonitorInterop(to: true) -// } + public func addChild(_ monitor: ProgressReporter.ProgressMonitor, withPendingUnitCount count: Int) { + + // Make intermediary & add it to NSProgress parent's children list + let ghostProgressParent = Progress(totalUnitCount: Int64(monitor.reporter.totalCount ?? 0)) + ghostProgressParent.completedUnitCount = Int64(monitor.reporter.completedCount) + self.addChild(ghostProgressParent, withPendingUnitCount: Int64(count)) + + // Make observation instance + let observation = _ProgressParentProgressMonitorChild(intermediary: ghostProgressParent, monitorChild: monitor) + + monitor.reporter.setInteropObservationForMonitor(observation: observation) + monitor.reporter.setMonitorInterop(to: true) + } } private final class _ProgressParentProgressReporterChild: Sendable { @@ -82,11 +81,9 @@ private final class _ProgressParentProgressReporterChild: Sendable { switch observerState { case .totalCountUpdated: -// print("received totalCountUpdated") self.ghostParent.totalUnitCount = Int64(self.ghostChild.totalCount ?? 0) case .fractionUpdated: -// print("received fractionUpdated") let count = self.ghostChild.withProperties { p in return (p.completedCount, p.totalCount) } @@ -97,6 +94,35 @@ private final class _ProgressParentProgressReporterChild: Sendable { } } +private final class _ProgressParentProgressMonitorChild: Sendable { + private let intermediary: Progress + private let monitorChild: ProgressReporter.ProgressMonitor + + fileprivate init(intermediary: Progress, monitorChild: ProgressReporter.ProgressMonitor) { + self.intermediary = intermediary + self.monitorChild = monitorChild + + monitorChild.reporter.addObserver { [weak self] observerState in + guard let self else { + return + } + + switch observerState { + case .totalCountUpdated: + self.intermediary.totalUnitCount = Int64(self.monitorChild.reporter.totalCount ?? 0) + + case .fractionUpdated: + let count = self.monitorChild.reporter.withProperties { p in + return (p.completedCount, p.totalCount) + } + self.intermediary.completedUnitCount = Int64(count.0) + self.intermediary.totalUnitCount = Int64(count.1 ?? 0) + } + } + } + +} + @available(FoundationPreview 6.2, *) //MARK: ProgressReporter Parent - Progress Child Interop extension ProgressReporter { diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index 1fc9f0a63..e4617610f 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -49,7 +49,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Stores all the state of properties internal struct State { -// var positionInParent: Int? var fractionState: FractionState var otherProperties: [AnyMetatypeWrapper: (any Sendable)] // Type: Metatype maps to dictionary of child to value @@ -64,8 +63,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Interop properties - Just kept alive internal let interopObservation: (any Sendable)? // set at init -// internal let interopObservationForMonitor: LockedState<(any Sendable)?> = LockedState(initialState: nil) -// internal let monitorInterop: LockedState = LockedState(initialState: false) + internal let interopObservationForMonitor: LockedState<(any Sendable)?> = LockedState(initialState: nil) + internal let monitorInterop: LockedState = LockedState(initialState: false) #if FOUNDATION_FRAMEWORK internal let parentBridge: LockedState = LockedState(initialState: nil) // dummy, set upon calling setParentBridge @@ -160,11 +159,11 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { //TODO: rdar://149015734 Check throttling reporter.updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) reporter.ghostReporter?.notifyObservers(with: .totalCountUpdated) -// reporter.monitorInterop.withLock { [reporter] interop in -// if interop == true { -// reporter.notifyObservers(with: .totalCountUpdated) -// } -// } + reporter.monitorInterop.withLock { [reporter] interop in + if interop == true { + reporter.notifyObservers(with: .totalCountUpdated) + } + } } } @@ -181,11 +180,11 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { reporter.updateFractionCompleted(from: prev, to: state.fractionState.overallFraction) reporter.ghostReporter?.notifyObservers(with: .fractionUpdated) -// reporter.monitorInterop.withLock { [reporter] interop in -// if interop == true { -// reporter.notifyObservers(with: .fractionUpdated) -// } -// } + reporter.monitorInterop.withLock { [reporter] interop in + if interop == true { + reporter.notifyObservers(with: .fractionUpdated) + } + } } } @@ -277,12 +276,11 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { ghostReporter?.notifyObservers(with: .totalCountUpdated) -// monitorInterop.withLock { [self] interop in -// if interop == true { -// print("notifying observers") -// notifyObservers(with: .totalCountUpdated) -// } -// } + monitorInterop.withLock { [self] interop in + if interop == true { + notifyObservers(with: .totalCountUpdated) + } + } } } @@ -317,12 +315,11 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { updateFractionCompleted(from: updateState.previous, to: updateState.current) ghostReporter?.notifyObservers(with: .fractionUpdated) -// monitorInterop.withLock { [self] interop in -// if interop == true { -// print("notifying observers") -// notifyObservers(with: .fractionUpdated) -// } -// } + monitorInterop.withLock { [self] interop in + if interop == true { + notifyObservers(with: .fractionUpdated) + } + } } @@ -490,17 +487,17 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } -// internal func setInteropObservationForMonitor(observation monitorObservation: (any Sendable)) { -// interopObservationForMonitor.withLock { observation in -// observation = monitorObservation -// } -// } -// -// internal func setMonitorInterop(to value: Bool) { -// monitorInterop.withLock { monitorInterop in -// monitorInterop = value -// } -// } + internal func setInteropObservationForMonitor(observation monitorObservation: (any Sendable)) { + interopObservationForMonitor.withLock { observation in + observation = monitorObservation + } + } + + internal func setMonitorInterop(to value: Bool) { + monitorInterop.withLock { monitorInterop in + monitorInterop = value + } + } //MARK: Internal methods to mutate locked context #if FOUNDATION_FRAMEWORK diff --git a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift index 0b4645608..c45eb16df 100644 --- a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift @@ -455,6 +455,63 @@ class TestProgressReporterInterop: XCTestCase { XCTAssertEqual(overall.completedUnitCount, 10) } + func testInteropProgressParentProgressMonitorChildWithEmptyProgress() async throws { + // Initialize a Progress parent + let overall = Progress.discreteProgress(totalUnitCount: 10) + + // Add Progress as Child + let expectation1 = XCTestExpectation(description: "Set completed unit count to 1") + let expectation2 = XCTestExpectation(description: "Set completed unit count to 2") + let p1 = await doSomethingWithProgress(expectation1: expectation1, expectation2: expectation2) + overall.addChild(p1, withPendingUnitCount: 5) + + await fulfillment(of: [expectation1, expectation2], timeout: 10.0) + + // Check if ProgressReporter values propagate to Progress parent + XCTAssertEqual(overall.fractionCompleted, 0.5) + XCTAssertEqual(overall.completedUnitCount, 5) + + // Add ProgressMonitor as Child + let p2 = ProgressReporter(totalCount: 10) + let p2Monitor = p2.monitor + overall.addChild(p2Monitor, withPendingUnitCount: 5) + + p2.complete(count: 10) + + // Check if Progress values propagate to Progress parent + XCTAssertEqual(overall.fractionCompleted, 1.0) + XCTAssertEqual(overall.completedUnitCount, 10) + } + + func testInteropProgressParentProgressMonitorChildWithExistingProgress() async throws { + // Initialize a Progress parent + let overall = Progress.discreteProgress(totalUnitCount: 10) + + // Add Progress as Child + let expectation1 = XCTestExpectation(description: "Set completed unit count to 1") + let expectation2 = XCTestExpectation(description: "Set completed unit count to 2") + let p1 = await doSomethingWithProgress(expectation1: expectation1, expectation2: expectation2) + overall.addChild(p1, withPendingUnitCount: 5) + + await fulfillment(of: [expectation1, expectation2], timeout: 10.0) + + // Check if ProgressReporter values propagate to Progress parent + XCTAssertEqual(overall.fractionCompleted, 0.5) + XCTAssertEqual(overall.completedUnitCount, 5) + + // Add ProgressMonitor with CompletedCount 3 as Child + let p2 = ProgressReporter(totalCount: 10) + p2.complete(count: 3) + let p2Monitor = p2.monitor + overall.addChild(p2Monitor, withPendingUnitCount: 5) + + p2.complete(count: 7) + + // Check if Progress values propagate to Progress parent + XCTAssertEqual(overall.fractionCompleted, 1.0) + XCTAssertEqual(overall.completedUnitCount, 10) + } + func testInteropProgressReporterParentProgressChild() async throws { // Initialize ProgressReporter parent let overallReporter = ProgressReporter(totalCount: 10) @@ -477,6 +534,7 @@ class TestProgressReporterInterop: XCTestCase { // Check if Progress values propagate to ProgressRerpoter parent XCTAssertEqual(overallReporter.completedCount, 10) XCTAssertEqual(overallReporter.totalCount, 10) + //TODO: Somehow this sometimes gets updated to 1.25 instead of just 1.0 XCTAssertEqual(overallReporter.fractionCompleted, 1.0) } From 8a67e855c1abf8f10abbf4135fa51e78b4fc0bf5 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 13 May 2025 17:57:20 -0700 Subject: [PATCH 41/85] renaming + fix implementation --- .../ProgressReporter/ProgressMonitor.swift | 14 ++++++-------- .../ProgressReporter/ProgressReporter.swift | 5 ++--- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressMonitor.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressMonitor.swift index a0e5963ce..c5f2d0557 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressMonitor.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressMonitor.swift @@ -11,13 +11,11 @@ //===----------------------------------------------------------------------===// @available(FoundationPreview 6.2, *) -extension ProgressReporter { - /// ProgressMonitor is just a wrapper that carries information about ProgressReporter. It is read-only and can be added as a child of something else. - public struct ProgressMonitor: Sendable { - internal let reporter: ProgressReporter - - internal init(reporter: ProgressReporter) { - self.reporter = reporter - } +/// ProgressMonitor is just a wrapper that carries information about ProgressReporter. It is read-only and can be added as a child of something else. +public final class ProgressMonitor: Sendable { + internal let reporter: ProgressReporter + + internal init(reporter: ProgressReporter) { + self.reporter = reporter } } diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index e4617610f..f465607ab 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -252,8 +252,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public convenience init(from monitor: ProgressMonitor) { self.init(total: 1, ghostReporter: nil, interopObservation: nil) - self.addChild(monitor, assignedCount: 1) - monitor.reporter.addParent(parentReporter: self, portionOfParent: 1) + self.assign(count: 1, to: monitor) } /// Sets `totalCount`. @@ -299,7 +298,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// - Parameters: /// - monitor: A `ProgressMonitor` instance. /// - portionOfParent: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. - public func addChild(_ monitor: ProgressMonitor, assignedCount portionOfParent: Int) { + public func assign(count portionOfParent: Int, to monitor: ProgressMonitor) { // get the actual progress from within the monitor, then add as children let actualReporter = monitor.reporter From c2673c9c0a7dbf0a4ff8bbdb64847bfafd9a8a8b Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 13 May 2025 17:59:31 -0700 Subject: [PATCH 42/85] rename monitor --- .../ProgressReporter/ProgressReporter+Interop.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift index 403c3832e..08575a96a 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift @@ -49,7 +49,7 @@ extension Progress { return actualProgress } - public func addChild(_ monitor: ProgressReporter.ProgressMonitor, withPendingUnitCount count: Int) { + public func addChild(_ monitor: ProgressMonitor, withPendingUnitCount count: Int) { // Make intermediary & add it to NSProgress parent's children list let ghostProgressParent = Progress(totalUnitCount: Int64(monitor.reporter.totalCount ?? 0)) @@ -96,9 +96,9 @@ private final class _ProgressParentProgressReporterChild: Sendable { private final class _ProgressParentProgressMonitorChild: Sendable { private let intermediary: Progress - private let monitorChild: ProgressReporter.ProgressMonitor + private let monitorChild: ProgressMonitor - fileprivate init(intermediary: Progress, monitorChild: ProgressReporter.ProgressMonitor) { + fileprivate init(intermediary: Progress, monitorChild: ProgressMonitor) { self.intermediary = intermediary self.monitorChild = monitorChild From 8b3769e18296d1cc610b193a7389378b0a1f5798 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 13 May 2025 18:21:36 -0700 Subject: [PATCH 43/85] renaming Subprogress and ProgressMonitor --- ...{Subprogress.swift => ProgressInput.swift} | 8 +++--- ...ressMonitor.swift => ProgressOutput.swift} | 2 +- .../ProgressReporter+Interop.swift | 28 +++++++++---------- .../ProgressReporter/ProgressReporter.swift | 16 +++++------ .../ProgressReporterTests.swift | 16 +++++------ 5 files changed, 35 insertions(+), 35 deletions(-) rename Sources/FoundationEssentials/ProgressReporter/{Subprogress.swift => ProgressInput.swift} (88%) rename Sources/FoundationEssentials/ProgressReporter/{ProgressMonitor.swift => ProgressOutput.swift} (94%) diff --git a/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressInput.swift similarity index 88% rename from Sources/FoundationEssentials/ProgressReporter/Subprogress.swift rename to Sources/FoundationEssentials/ProgressReporter/ProgressInput.swift index c76a0f0dd..0e2156e03 100644 --- a/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressInput.swift @@ -11,11 +11,11 @@ //===----------------------------------------------------------------------===// @available(FoundationPreview 6.2, *) -/// Subprogress is a nested ~Copyable struct used to establish parent-child relationship between two instances of ProgressReporter. +/// ProgressInput is a nested ~Copyable struct used to establish parent-child relationship between two instances of ProgressReporter. /// -/// Subprogress is returned from a call to `subprogress(assigningCount:)` by a parent ProgressReporter. -/// A child ProgressReporter is then returned by calling`reporter(totalCount:)` on a Subprogress. -public struct Subprogress: ~Copyable, Sendable { +/// ProgressInput is returned from a call to `subprogress(assigningCount:)` by a parent ProgressReporter. +/// A child ProgressReporter is then returned by calling`reporter(totalCount:)` on a ProgressInput. +public struct ProgressInput: ~Copyable, Sendable { internal var parent: ProgressReporter internal var portionOfParent: Int internal var isInitializedToProgressReporter: Bool diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressMonitor.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressOutput.swift similarity index 94% rename from Sources/FoundationEssentials/ProgressReporter/ProgressMonitor.swift rename to Sources/FoundationEssentials/ProgressReporter/ProgressOutput.swift index c5f2d0557..e87bf3fb6 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressMonitor.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressOutput.swift @@ -12,7 +12,7 @@ @available(FoundationPreview 6.2, *) /// ProgressMonitor is just a wrapper that carries information about ProgressReporter. It is read-only and can be added as a child of something else. -public final class ProgressMonitor: Sendable { +public final class ProgressOutput: Sendable { internal let reporter: ProgressReporter internal init(reporter: ProgressReporter) { diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift index 08575a96a..ca8b1d6a8 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift @@ -29,7 +29,7 @@ extension Progress { /// - Parameter count: Number of units delegated to a child instance of `ProgressReporter` /// which may be instantiated by `Subprogress` later when `reporter(totalCount:)` is called. /// - Returns: A `Subprogress` instance. - public func makeChild(withPendingUnitCount count: Int) -> Subprogress { + public func makeChild(withPendingUnitCount count: Int) -> ProgressInput { // Make ghost parent & add it to actual parent's children list let ghostProgressParent = Progress(totalUnitCount: Int64(count)) @@ -49,18 +49,18 @@ extension Progress { return actualProgress } - public func addChild(_ monitor: ProgressMonitor, withPendingUnitCount count: Int) { + public func addChild(_ output: ProgressOutput, withPendingUnitCount count: Int) { // Make intermediary & add it to NSProgress parent's children list - let ghostProgressParent = Progress(totalUnitCount: Int64(monitor.reporter.totalCount ?? 0)) - ghostProgressParent.completedUnitCount = Int64(monitor.reporter.completedCount) + let ghostProgressParent = Progress(totalUnitCount: Int64(output.reporter.totalCount ?? 0)) + ghostProgressParent.completedUnitCount = Int64(output.reporter.completedCount) self.addChild(ghostProgressParent, withPendingUnitCount: Int64(count)) // Make observation instance - let observation = _ProgressParentProgressMonitorChild(intermediary: ghostProgressParent, monitorChild: monitor) + let observation = _ProgressParentProgressOutputChild(intermediary: ghostProgressParent, progressOutput: output) - monitor.reporter.setInteropObservationForMonitor(observation: observation) - monitor.reporter.setMonitorInterop(to: true) + output.reporter.setInteropObservationForMonitor(observation: observation) + output.reporter.setMonitorInterop(to: true) } } @@ -94,25 +94,25 @@ private final class _ProgressParentProgressReporterChild: Sendable { } } -private final class _ProgressParentProgressMonitorChild: Sendable { +private final class _ProgressParentProgressOutputChild: Sendable { private let intermediary: Progress - private let monitorChild: ProgressMonitor + private let progressOutput: ProgressOutput - fileprivate init(intermediary: Progress, monitorChild: ProgressMonitor) { + fileprivate init(intermediary: Progress, progressOutput: ProgressOutput) { self.intermediary = intermediary - self.monitorChild = monitorChild + self.progressOutput = progressOutput - monitorChild.reporter.addObserver { [weak self] observerState in + progressOutput.reporter.addObserver { [weak self] observerState in guard let self else { return } switch observerState { case .totalCountUpdated: - self.intermediary.totalUnitCount = Int64(self.monitorChild.reporter.totalCount ?? 0) + self.intermediary.totalUnitCount = Int64(self.progressOutput.reporter.totalCount ?? 0) case .fractionUpdated: - let count = self.monitorChild.reporter.withProperties { p in + let count = self.progressOutput.reporter.withProperties { p in return (p.completedCount, p.totalCount) } self.intermediary.completedUnitCount = Int64(count.0) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index f465607ab..acc2011fa 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -118,7 +118,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } - public var monitor: ProgressMonitor { + public var output: ProgressOutput { return .init(reporter: self) } @@ -250,9 +250,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { self.init(total: totalCount, ghostReporter: nil, interopObservation: nil) } - public convenience init(from monitor: ProgressMonitor) { + public convenience init(from progress: ProgressOutput) { self.init(total: 1, ghostReporter: nil, interopObservation: nil) - self.assign(count: 1, to: monitor) + self.assign(count: 1, to: progress) } /// Sets `totalCount`. @@ -287,18 +287,18 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// /// - Parameter count: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. /// - Returns: A `Subprogress` instance. - public func subprogress(assigningCount portionOfParent: Int) -> Subprogress { + public func subprogress(assigningCount portionOfParent: Int) -> ProgressInput { precondition(portionOfParent > 0, "Giving out zero units is not a valid operation.") - let subprogress = Subprogress(parent: self, portionOfParent: portionOfParent) + let subprogress = ProgressInput(parent: self, portionOfParent: portionOfParent) return subprogress } - /// Adds a `ProgressMonitor` as a child, with its progress representing a portion of `self`'s progress. + /// Adds a `ProgressOutput` as a child, with its progress representing a portion of `self`'s progress. /// - Parameters: - /// - monitor: A `ProgressMonitor` instance. + /// - monitor: A `ProgressOutput` instance. /// - portionOfParent: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. - public func assign(count portionOfParent: Int, to monitor: ProgressMonitor) { + public func assign(count portionOfParent: Int, to monitor: ProgressOutput) { // get the actual progress from within the monitor, then add as children let actualReporter = monitor.reporter diff --git a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift index c45eb16df..9310eb104 100644 --- a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift @@ -20,7 +20,7 @@ import XCTest /// Unit tests for basic functionalities of ProgressReporter class TestProgressReporter: XCTestCase { /// MARK: Helper methods that report progress - func doBasicOperationV1(reportTo progress: consuming Subprogress) async { + func doBasicOperationV1(reportTo progress: consuming ProgressInput) async { let reporter = progress.reporter(totalCount: 8) for i in 1...8 { reporter.complete(count: 1) @@ -29,7 +29,7 @@ class TestProgressReporter: XCTestCase { } } - func doBasicOperationV2(reportTo progress: consuming Subprogress) async { + func doBasicOperationV2(reportTo progress: consuming ProgressInput) async { let reporter = progress.reporter(totalCount: 7) for i in 1...7 { reporter.complete(count: 1) @@ -38,7 +38,7 @@ class TestProgressReporter: XCTestCase { } } - func doBasicOperationV3(reportTo progress: consuming Subprogress) async { + func doBasicOperationV3(reportTo progress: consuming ProgressInput) async { let reporter = progress.reporter(totalCount: 11) for i in 1...11 { reporter.complete(count: 1) @@ -267,7 +267,7 @@ class TestProgressReporter: XCTestCase { /// Unit tests for propagation of type-safe metadata in ProgressReporter tree. class TestProgressReporterAdditionalProperties: XCTestCase { - func doFileOperation(reportTo progress: consuming Subprogress) async { + func doFileOperation(reportTo progress: consuming ProgressInput) async { let reporter = progress.reporter(totalCount: 100) reporter.withProperties { properties in properties.totalFileCount = 100 @@ -424,7 +424,7 @@ class TestProgressReporterInterop: XCTestCase { return p } - func doSomethingWithReporter(progress: consuming Subprogress?) async { + func doSomethingWithReporter(progress: consuming ProgressInput?) async { let reporter = progress?.reporter(totalCount: 4) reporter?.complete(count: 2) reporter?.complete(count: 2) @@ -473,7 +473,7 @@ class TestProgressReporterInterop: XCTestCase { // Add ProgressMonitor as Child let p2 = ProgressReporter(totalCount: 10) - let p2Monitor = p2.monitor + let p2Monitor = p2.output overall.addChild(p2Monitor, withPendingUnitCount: 5) p2.complete(count: 10) @@ -502,7 +502,7 @@ class TestProgressReporterInterop: XCTestCase { // Add ProgressMonitor with CompletedCount 3 as Child let p2 = ProgressReporter(totalCount: 10) p2.complete(count: 3) - let p2Monitor = p2.monitor + let p2Monitor = p2.output overall.addChild(p2Monitor, withPendingUnitCount: 5) p2.complete(count: 7) @@ -542,7 +542,7 @@ class TestProgressReporterInterop: XCTestCase { return Progress(totalUnitCount: 5) } - func receiveProgress(progress: consuming Subprogress) { + func receiveProgress(progress: consuming ProgressInput) { let _ = progress.reporter(totalCount: 5) } From ba499c1d392afa696057fdc997de5f024645c32e Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 14 May 2025 17:19:21 -0700 Subject: [PATCH 44/85] add to ProgressOutput to have read-only properties --- .../ProgressReporter/ProgressOutput.swift | 30 +++++++++++++++++++ .../ProgressReporter/ProgressReporter.swift | 5 ---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressOutput.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressOutput.swift index e87bf3fb6..a8c69aed1 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressOutput.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressOutput.swift @@ -12,7 +12,37 @@ @available(FoundationPreview 6.2, *) /// ProgressMonitor is just a wrapper that carries information about ProgressReporter. It is read-only and can be added as a child of something else. +@Observable public final class ProgressOutput: Sendable { + + var totalCount: Int? { + reporter.totalCount + } + + var completedCount: Int { + reporter.completedCount + } + + var fractionCompleted: Double { + reporter.fractionCompleted + } + + var isIndeterminate: Bool { + reporter.isIndeterminate + } + + var isFinished: Bool { + reporter.isFinished + } + + // TODO: Need to figure out how to expose properties such as totalFileCount and completedFileCount + var properties: ProgressReporter.Values { + reporter.withProperties { properties in + return properties + } + } + + internal let reporter: ProgressReporter internal init(reporter: ProgressReporter) { diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index acc2011fa..ca58657ad 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -250,11 +250,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { self.init(total: totalCount, ghostReporter: nil, interopObservation: nil) } - public convenience init(from progress: ProgressOutput) { - self.init(total: 1, ghostReporter: nil, interopObservation: nil) - self.assign(count: 1, to: progress) - } - /// Sets `totalCount`. /// - Parameter newTotal: Total units of work. public func setTotalCount(_ newTotal: Int?) { From d658037e2ee6982c0e6b9c7c96c9c9f48af94f59 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 16 May 2025 13:01:40 -0700 Subject: [PATCH 45/85] rename ProgressReporter -> ProgressManager, ProgressInput -> Subprogress, ProgressOutput -> ProgressReporter --- .../ProgressReporter/ProgressInput.swift | 61 -- ...op.swift => ProgressManager+Interop.swift} | 72 +- ...swift => ProgressManager+Properties.swift} | 9 +- .../ProgressReporter/ProgressManager.swift | 638 ++++++++++++++++++ .../ProgressReporter/ProgressOutput.swift | 51 -- .../ProgressReporter/ProgressReporter.swift | 627 +---------------- .../ProgressReporter/Subprogress.swift | 61 ++ ... => ProgressManager+FileFormatStyle.swift} | 28 +- ...wift => ProgressManager+FormatStyle.swift} | 30 +- ...Tests.swift => ProgressManagerTests.swift} | 326 ++++----- 10 files changed, 959 insertions(+), 944 deletions(-) delete mode 100644 Sources/FoundationEssentials/ProgressReporter/ProgressInput.swift rename Sources/FoundationEssentials/ProgressReporter/{ProgressReporter+Interop.swift => ProgressManager+Interop.swift} (69%) rename Sources/FoundationEssentials/ProgressReporter/{ProgressReporter+Properties.swift => ProgressManager+Properties.swift} (88%) create mode 100644 Sources/FoundationEssentials/ProgressReporter/ProgressManager.swift delete mode 100644 Sources/FoundationEssentials/ProgressReporter/ProgressOutput.swift create mode 100644 Sources/FoundationEssentials/ProgressReporter/Subprogress.swift rename Sources/FoundationInternationalization/ProgressReporter/{ProgressReporter+FileFormatStyle.swift => ProgressManager+FileFormatStyle.swift} (87%) rename Sources/FoundationInternationalization/ProgressReporter/{ProgressReporter+FormatStyle.swift => ProgressManager+FormatStyle.swift} (84%) rename Tests/FoundationEssentialsTests/ProgressReporter/{ProgressReporterTests.swift => ProgressManagerTests.swift} (60%) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressInput.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressInput.swift deleted file mode 100644 index 0e2156e03..000000000 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressInput.swift +++ /dev/null @@ -1,61 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -@available(FoundationPreview 6.2, *) -/// ProgressInput is a nested ~Copyable struct used to establish parent-child relationship between two instances of ProgressReporter. -/// -/// ProgressInput is returned from a call to `subprogress(assigningCount:)` by a parent ProgressReporter. -/// A child ProgressReporter is then returned by calling`reporter(totalCount:)` on a ProgressInput. -public struct ProgressInput: ~Copyable, Sendable { - internal var parent: ProgressReporter - internal var portionOfParent: Int - internal var isInitializedToProgressReporter: Bool - - // Interop variables for Progress - ProgressReporter Interop - internal var interopWithProgressParent: Bool = false - // To be kept alive in ProgressReporter - internal var observation: (any Sendable)? - internal var ghostReporter: ProgressReporter? - - internal init(parent: ProgressReporter, portionOfParent: Int) { - self.parent = parent - self.portionOfParent = portionOfParent - self.isInitializedToProgressReporter = false - } - - /// Instantiates a ProgressReporter which is a child to the parent from which `self` is returned. - /// - Parameter totalCount: Total count of returned child `ProgressReporter` instance. - /// - Returns: A `ProgressReporter` instance. - public consuming func reporter(totalCount: Int?) -> ProgressReporter { - isInitializedToProgressReporter = true - - let childReporter = ProgressReporter(total: totalCount, ghostReporter: ghostReporter, interopObservation: observation) - - if interopWithProgressParent { - // Set interop child of ghost reporter so ghost reporter reads from here - ghostReporter?.setInteropChild(interopChild: childReporter) - } else { - // Add child to parent's _children list & Store in child children's position in parent - parent.addToChildren(childReporter: childReporter) - childReporter.addParent(parentReporter: parent, portionOfParent: portionOfParent) - } - - return childReporter - } - - deinit { - if !self.isInitializedToProgressReporter { - parent.complete(count: portionOfParent) - } - } -} - diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressManager+Interop.swift similarity index 69% rename from Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift rename to Sources/FoundationEssentials/ProgressReporter/ProgressManager+Interop.swift index ca8b1d6a8..251881534 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressManager+Interop.swift @@ -14,32 +14,32 @@ internal import _ForSwiftFoundation @available(FoundationPreview 6.2, *) -//MARK: Progress Parent - ProgressReporter Child Interop +//MARK: Progress Parent - ProgressManager Child Interop // Actual Progress Parent // Ghost Progress Parent -// Ghost ProgressReporter Child -// Actual ProgressReporter Child +// Ghost ProgressManager Child +// Actual ProgressManager Child extension Progress { /// Returns a Subprogress which can be passed to any method that reports progress - /// and can be initialized into a child `ProgressReporter` to the `self`. + /// and can be initialized into a child `ProgressManager` to the `self`. /// - /// Delegates a portion of totalUnitCount to a future child `ProgressReporter` instance. + /// Delegates a portion of totalUnitCount to a future child `ProgressManager` instance. /// - /// - Parameter count: Number of units delegated to a child instance of `ProgressReporter` + /// - Parameter count: Number of units delegated to a child instance of `ProgressManager` /// which may be instantiated by `Subprogress` later when `reporter(totalCount:)` is called. /// - Returns: A `Subprogress` instance. - public func makeChild(withPendingUnitCount count: Int) -> ProgressInput { + public func makeChild(withPendingUnitCount count: Int) -> Subprogress { // Make ghost parent & add it to actual parent's children list let ghostProgressParent = Progress(totalUnitCount: Int64(count)) self.addChild(ghostProgressParent, withPendingUnitCount: Int64(count)) // Make ghost child - let ghostReporterChild = ProgressReporter(totalCount: count) + let ghostReporterChild = ProgressManager(totalCount: count) // Make observation instance - let observation = _ProgressParentProgressReporterChild(ghostParent: ghostProgressParent, ghostChild: ghostReporterChild) + let observation = _ProgressParentProgressManagerChild(ghostParent: ghostProgressParent, ghostChild: ghostReporterChild) // Make actual child with ghost child being parent var actualProgress = ghostReporterChild.subprogress(assigningCount: count) @@ -49,26 +49,32 @@ extension Progress { return actualProgress } - public func addChild(_ output: ProgressOutput, withPendingUnitCount count: Int) { + + /// Adds a ProgressReporter as a child to a ProgressManager, which constitutes a portion of ProgressManager's totalUnitCount. + /// + /// - Parameters: + /// - reporter: A `ProgressReporter` instance. + /// - count: Number of units delegated from `self`'s `totalCount`. + public func addChild(_ reporter: ProgressReporter, withPendingUnitCount count: Int) { // Make intermediary & add it to NSProgress parent's children list - let ghostProgressParent = Progress(totalUnitCount: Int64(output.reporter.totalCount ?? 0)) - ghostProgressParent.completedUnitCount = Int64(output.reporter.completedCount) + let ghostProgressParent = Progress(totalUnitCount: Int64(reporter.manager.totalCount ?? 0)) + ghostProgressParent.completedUnitCount = Int64(reporter.manager.completedCount) self.addChild(ghostProgressParent, withPendingUnitCount: Int64(count)) // Make observation instance - let observation = _ProgressParentProgressOutputChild(intermediary: ghostProgressParent, progressOutput: output) + let observation = _ProgressParentProgressReporterChild(intermediary: ghostProgressParent, reporter: reporter) - output.reporter.setInteropObservationForMonitor(observation: observation) - output.reporter.setMonitorInterop(to: true) + reporter.manager.setInteropObservationForMonitor(observation: observation) + reporter.manager.setMonitorInterop(to: true) } } -private final class _ProgressParentProgressReporterChild: Sendable { +private final class _ProgressParentProgressManagerChild: Sendable { private let ghostParent: Progress - private let ghostChild: ProgressReporter + private let ghostChild: ProgressManager - fileprivate init(ghostParent: Progress, ghostChild: ProgressReporter) { + fileprivate init(ghostParent: Progress, ghostChild: ProgressManager) { self.ghostParent = ghostParent self.ghostChild = ghostChild @@ -94,25 +100,25 @@ private final class _ProgressParentProgressReporterChild: Sendable { } } -private final class _ProgressParentProgressOutputChild: Sendable { +private final class _ProgressParentProgressReporterChild: Sendable { private let intermediary: Progress - private let progressOutput: ProgressOutput + private let reporter: ProgressReporter - fileprivate init(intermediary: Progress, progressOutput: ProgressOutput) { + fileprivate init(intermediary: Progress, reporter: ProgressReporter) { self.intermediary = intermediary - self.progressOutput = progressOutput + self.reporter = reporter - progressOutput.reporter.addObserver { [weak self] observerState in + reporter.manager.addObserver { [weak self] observerState in guard let self else { return } switch observerState { case .totalCountUpdated: - self.intermediary.totalUnitCount = Int64(self.progressOutput.reporter.totalCount ?? 0) + self.intermediary.totalUnitCount = Int64(self.reporter.manager.totalCount ?? 0) case .fractionUpdated: - let count = self.progressOutput.reporter.withProperties { p in + let count = self.reporter.manager.withProperties { p in return (p.completedCount, p.totalCount) } self.intermediary.completedUnitCount = Int64(count.0) @@ -124,18 +130,18 @@ private final class _ProgressParentProgressOutputChild: Sendable { } @available(FoundationPreview 6.2, *) -//MARK: ProgressReporter Parent - Progress Child Interop -extension ProgressReporter { +//MARK: ProgressManager Parent - Progress Child Interop +extension ProgressManager { /// Adds a Foundation's `Progress` instance as a child which constitutes a certain `count` of `self`'s `totalCount`. /// - Parameters: /// - count: Number of units delegated from `self`'s `totalCount`. /// - progress: `Progress` which receives the delegated `count`. public func subprogress(assigningCount count: Int, to progress: Foundation.Progress) { - let parentBridge = _NSProgressParentBridge(reporterParent: self) + let parentBridge = _NSProgressParentBridge(managerParent: self) progress._setParent(parentBridge, portion: Int64(count)) - // Save ghost parent in ProgressReporter so it doesn't go out of scope after assign method ends + // Save ghost parent in ProgressManager so it doesn't go out of scope after assign method ends // So that when NSProgress increases completedUnitCount and queries for parent there is still a reference to ghostParent and parent doesn't show 0x0 (portion: 5) self.setParentBridge(parentBridge: parentBridge) } @@ -144,15 +150,15 @@ extension ProgressReporter { // Subclass of Foundation.Progress internal final class _NSProgressParentBridge: Progress, @unchecked Sendable { - let actualParent: ProgressReporter + let actualParent: ProgressManager - init(reporterParent: ProgressReporter) { - self.actualParent = reporterParent + init(managerParent: ProgressManager) { + self.actualParent = managerParent super.init(parent: nil, userInfo: nil) } // Overrides the _updateChild func that Foundation.Progress calls to update parent - // so that the parent that gets updated is the ProgressReporter parent + // so that the parent that gets updated is the ProgressManager parent override func _updateChild(_ child: Foundation.Progress, fraction: _NSProgressFractionTuple, portion: Int64) { actualParent.updateChildFraction(from: _ProgressFraction(nsProgressFraction: fraction.previous), to: _ProgressFraction(nsProgressFraction: fraction.next), portion: Int(portion)) } diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressManager+Properties.swift similarity index 88% rename from Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift rename to Sources/FoundationEssentials/ProgressReporter/ProgressManager+Properties.swift index 8f449cc54..f6de74baa 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressManager+Properties.swift @@ -10,9 +10,11 @@ // //===----------------------------------------------------------------------===// @available(FoundationPreview 6.2, *) -extension ProgressReporter { +extension ProgressManager { // Namespace for properties specific to operations reported on public struct Properties: Sendable { + + /// The total number of files. public var totalFileCount: TotalFileCount.Type { TotalFileCount.self } public struct TotalFileCount: Sendable, Property { public static var defaultValue: Int { return 0 } @@ -20,6 +22,7 @@ extension ProgressReporter { public typealias T = Int } + /// The number of completed files. public var completedFileCount: CompletedFileCount.Type { CompletedFileCount.self } public struct CompletedFileCount: Sendable, Property { public static var defaultValue: Int { return 0 } @@ -27,6 +30,7 @@ extension ProgressReporter { public typealias T = Int } + /// The total number of bytes. public var totalByteCount: TotalByteCount.Type { TotalByteCount.self } public struct TotalByteCount: Sendable, Property { public static var defaultValue: Int64 { return 0 } @@ -34,6 +38,7 @@ extension ProgressReporter { public typealias T = Int64 } + /// The number of completed bytes. public var completedByteCount: CompletedByteCount.Type { CompletedByteCount.self } public struct CompletedByteCount: Sendable, Property { public static var defaultValue: Int64 { return 0 } @@ -41,6 +46,7 @@ extension ProgressReporter { public typealias T = Int64 } + /// The throughput, in bytes per second. public var throughput: Throughput.Type { Throughput.self } public struct Throughput: Sendable, Property { public static var defaultValue: Int64 { return 0 } @@ -48,6 +54,7 @@ extension ProgressReporter { public typealias T = Int64 } + /// The amount of time remaining in the processing of files. public var estimatedTimeRemaining: EstimatedTimeRemaining.Type { EstimatedTimeRemaining.self } public struct EstimatedTimeRemaining: Sendable, Property { public static var defaultValue: Duration { return Duration.seconds(0) } diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressManager.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressManager.swift new file mode 100644 index 000000000..6991663fe --- /dev/null +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressManager.swift @@ -0,0 +1,638 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Observation + +#if canImport(CollectionsInternal) +internal import CollectionsInternal +#elseif canImport(OrderedCollections) +internal import OrderedCollections +#elseif canImport(_FoundationCollections) +internal import _FoundationCollections +#endif + +internal struct FractionState { + var indeterminate: Bool + var selfFraction: _ProgressFraction + var childFraction: _ProgressFraction + var overallFraction: _ProgressFraction { + selfFraction + childFraction + } + var interopChild: ProgressManager? // read from this if self is actually an interop ghost +} + +internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { + let metatype: Any.Type + + internal static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.metatype == rhs.metatype + } + + internal func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(metatype)) + } +} + +@available(FoundationPreview 6.2, *) +// ProgressManager +/// An object that conveys ongoing progress to the user for a specified task. +@Observable public final class ProgressManager: Sendable { + + // Stores all the state of properties + internal struct State { + var fractionState: FractionState + var otherProperties: [AnyMetatypeWrapper: (any Sendable)] + // Type: Metatype maps to dictionary of child to value + var childrenOtherProperties: [AnyMetatypeWrapper: OrderedDictionary] + } + + // Interop states + internal enum ObserverState { + case fractionUpdated + case totalCountUpdated + } + + // Interop properties - Just kept alive + internal let interopObservation: (any Sendable)? // set at init + internal let interopObservationForMonitor: LockedState<(any Sendable)?> = LockedState(initialState: nil) + internal let monitorInterop: LockedState = LockedState(initialState: false) + + #if FOUNDATION_FRAMEWORK + internal let parentBridge: LockedState = LockedState(initialState: nil) // dummy, set upon calling setParentBridge + #endif + // Interop properties - Actually set and called + internal let ghostReporter: ProgressManager? // set at init, used to call notify observers + internal let observers: LockedState<[@Sendable (ObserverState) -> Void]> = LockedState(initialState: [])// storage for all observers, set upon calling addObservers + + /// The total units of work. + public var totalCount: Int? { + _$observationRegistrar.access(self, keyPath: \.totalCount) + return state.withLock { state in + getTotalCount(fractionState: &state.fractionState) + } + } + + /// The completed units of work. + /// If `self` is indeterminate, the value will be 0. + public var completedCount: Int { + _$observationRegistrar.access(self, keyPath: \.completedCount) + return state.withLock { state in + getCompletedCount(fractionState: &state.fractionState) + } + } + + /// The proportion of work completed. + /// This takes into account the fraction completed in its children instances if children are present. + /// If `self` is indeterminate, the value will be 0. + public var fractionCompleted: Double { + _$observationRegistrar.access(self, keyPath: \.fractionCompleted) + return state.withLock { state in + getFractionCompleted(fractionState: &state.fractionState) + } + } + + /// The state of initialization of `totalCount`. + /// If `totalCount` is `nil`, the value will be `true`. + public var isIndeterminate: Bool { + _$observationRegistrar.access(self, keyPath: \.isIndeterminate) + return state.withLock { state in + getIsIndeterminate(fractionState: &state.fractionState) + } + } + + /// The state of completion of work. + /// If `completedCount` >= `totalCount`, the value will be `true`. + public var isFinished: Bool { + _$observationRegistrar.access(self, keyPath: \.isFinished) + return state.withLock { state in + getIsFinished(fractionState: &state.fractionState) + } + } + + public var reporter: ProgressReporter { + return .init(manager: self) + } + + /// A type that conveys task-specific information on progress. + public protocol Property { + + associatedtype T: Sendable, Hashable, Equatable + + static var defaultValue: T { get } + } + + /// A container that holds values for properties that specify information on progress. + @dynamicMemberLookup + public struct Values : Sendable { + //TODO: rdar://149225947 Non-escapable conformance + let manager: ProgressManager + var state: State + + /// The total units of work. + public var totalCount: Int? { + mutating get { + manager.getTotalCount(fractionState: &state.fractionState) + } + + set { + let previous = state.fractionState.overallFraction + if state.fractionState.selfFraction.total != newValue && state.fractionState.selfFraction.total > 0 { + state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: newValue ?? 1) + } + state.fractionState.selfFraction.total = newValue ?? 0 + + // if newValue is nil, reset indeterminate to true + if newValue != nil { + state.fractionState.indeterminate = false + } else { + state.fractionState.indeterminate = true + } + //TODO: rdar://149015734 Check throttling + manager.updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) + manager.ghostReporter?.notifyObservers(with: .totalCountUpdated) + manager.monitorInterop.withLock { [manager] interop in + if interop == true { + manager.notifyObservers(with: .totalCountUpdated) + } + } + } + } + + + /// The completed units of work. + public var completedCount: Int { + mutating get { + manager.getCompletedCount(fractionState: &state.fractionState) + } + + set { + let prev = state.fractionState.overallFraction + state.fractionState.selfFraction.completed = newValue + manager.updateFractionCompleted(from: prev, to: state.fractionState.overallFraction) + manager.ghostReporter?.notifyObservers(with: .fractionUpdated) + + manager.monitorInterop.withLock { [manager] interop in + if interop == true { + manager.notifyObservers(with: .fractionUpdated) + } + } + } + } + + /// Returns a property value that a key path indicates. If value is not defined, returns property's `defaultValue`. + public subscript(dynamicMember key: KeyPath) -> P.T { + get { + return state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T ?? P.self.defaultValue + } + + set { + // Update my own other properties entry + state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = newValue + + // Generate an array of myself + children values of the property + let flattenedChildrenValues: [P.T?] = { + let childrenDictionary = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] + var childrenValues: [P.T?] = [] + if let dictionary = childrenDictionary { + for (_, value) in dictionary { + if let value = value as? [P.T?] { + childrenValues.append(contentsOf: value) + } + } + } + return childrenValues + }() + + // Send the array of myself + children values of property to parents + let updateValueForParent: [P.T?] = [newValue] + flattenedChildrenValues + manager.parents.withLock { [manager] parents in + for (parent, _) in parents { + parent.updateChildrenOtherProperties(property: P.self, child: manager, value: updateValueForParent) + } + } + } + } + } + + internal let parents: LockedState<[ProgressManager: Int]> + private let children: LockedState> + private let state: LockedState + + internal init(total: Int?, ghostReporter: ProgressManager?, interopObservation: (any Sendable)?) { + self.parents = .init(initialState: [:]) + self.children = .init(initialState: Set()) + let fractionState = FractionState( + indeterminate: total == nil ? true : false, + selfFraction: _ProgressFraction(completed: 0, total: total ?? 0), + childFraction: _ProgressFraction(completed: 0, total: 1), + interopChild: nil + ) + let state = State(fractionState: fractionState, otherProperties: [:], childrenOtherProperties: [:]) + self.state = LockedState(initialState: state) + self.interopObservation = interopObservation + self.ghostReporter = ghostReporter + } + + /// Initializes `self` with `totalCount`. + /// + /// If `totalCount` is set to `nil`, `self` is indeterminate. + /// - Parameter totalCount: Total units of work. + public convenience init(totalCount: Int?) { + self.init(total: totalCount, ghostReporter: nil, interopObservation: nil) + } + + /// Sets `totalCount`. + /// - Parameter newTotal: Total units of work. + public func setTotalCount(_ newTotal: Int?) { + state.withLock { state in + let previous = state.fractionState.overallFraction + if state.fractionState.selfFraction.total != newTotal && state.fractionState.selfFraction.total > 0 { + state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: newTotal ?? 1) + } + state.fractionState.selfFraction.total = newTotal ?? 0 + + // if newValue is nil, reset indeterminate to true + if newTotal != nil { + state.fractionState.indeterminate = false + } else { + state.fractionState.indeterminate = true + } + updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) + + ghostReporter?.notifyObservers(with: .totalCountUpdated) + + monitorInterop.withLock { [self] interop in + if interop == true { + notifyObservers(with: .totalCountUpdated) + } + } + } + } + + /// Returns a `Subprogress` representing a portion of `self` which can be passed to any method that reports progress. + /// + /// - Parameter count: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. + /// - Returns: A `Subprogress` instance. + public func subprogress(assigningCount portionOfParent: Int) -> Subprogress { + precondition(portionOfParent > 0, "Giving out zero units is not a valid operation.") + let subprogress = Subprogress(parent: self, portionOfParent: portionOfParent) + return subprogress + } + + + /// Adds a `ProgressReporter` as a child, with its progress representing a portion of `self`'s progress. + /// - Parameters: + /// - reporter: A `ProgressReporter` instance. + /// - portionOfParent: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. + public func assign(count portionOfParent: Int, to reporter: ProgressReporter) { + // get the actual progress from within the reporter, then add as children + let actualManager = reporter.manager + + // Add reporter as child + Add self as parent + self.addToChildren(childManager: actualManager) + actualManager.addParent(parentReporter: self, portionOfParent: portionOfParent) + } + + /// Increases `completedCount` by `count`. + /// - Parameter count: Units of work. + public func complete(count: Int) { + let updateState = updateCompletedCount(count: count) + updateFractionCompleted(from: updateState.previous, to: updateState.current) + ghostReporter?.notifyObservers(with: .fractionUpdated) + + monitorInterop.withLock { [self] interop in + if interop == true { + notifyObservers(with: .fractionUpdated) + } + } + } + + + /// Returns an array of values for specified property in subtree. + /// - Parameter metatype: Type of property. + /// - Returns: Array of values for property. + public func values(property metatype: P.Type) -> [P.T?] { + return state.withLock { state in + let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) + return [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T ?? P.defaultValue] + childrenValues.map { $0 ?? P.defaultValue } + } + } + + + /// Returns the aggregated result of values. + /// - Parameters: + /// - property: Type of property. + /// - values:Sum of values. + public func total(property: P.Type, values: [P.T?]) -> P.T where P.T: AdditiveArithmetic { + let droppedNil = values.compactMap { $0 } + return droppedNil.reduce(P.T.zero, +) + } + + /// Mutates any settable properties that convey information about progress. + public func withProperties(_ closure: @Sendable (inout Values) throws -> T) rethrows -> T { + return try state.withLock { state in + var values = Values(manager: self, state: state) + // This is done to avoid copy on write later + state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction()), otherProperties: [:], childrenOtherProperties: [:]) + let result = try closure(&values) + state = values.state + return result + } + } + + //MARK: ProgressManager Properties getters + /// Returns nil if `self` was instantiated without total units; + /// returns a `Int` value otherwise. + private func getTotalCount(fractionState: inout FractionState) -> Int? { + if let interopChild = fractionState.interopChild { + return interopChild.totalCount + } + if fractionState.indeterminate { + return nil + } else { + return fractionState.selfFraction.total + } + } + + /// Returns nil if `self` has `nil` total units; + /// returns a `Int` value otherwise. + private func getCompletedCount(fractionState: inout FractionState) -> Int { + if let interopChild = fractionState.interopChild { + return interopChild.completedCount + } + return fractionState.selfFraction.completed + } + + /// Returns 0.0 if `self` has `nil` total units; + /// returns a `Double` otherwise. + /// If `indeterminate`, return 0.0. + /// + /// The calculation of fraction completed for a ProgressManager instance that has children + /// will take into account children's fraction completed as well. + private func getFractionCompleted(fractionState: inout FractionState) -> Double { + if let interopChild = fractionState.interopChild { + return interopChild.fractionCompleted + } + if fractionState.indeterminate { + return 0.0 + } + guard fractionState.selfFraction.total > 0 else { + return fractionState.selfFraction.fractionCompleted + } + return (fractionState.selfFraction + fractionState.childFraction).fractionCompleted + } + + + /// Returns `true` if completed and total units are not `nil` and completed units is greater than or equal to total units; + /// returns `false` otherwise. + private func getIsFinished(fractionState: inout FractionState) -> Bool { + return fractionState.selfFraction.isFinished + } + + + /// Returns `true` if `self` has `nil` total units. + private func getIsIndeterminate(fractionState: inout FractionState) -> Bool { + return fractionState.indeterminate + } + + //MARK: FractionCompleted Calculation methods + private struct UpdateState { + let previous: _ProgressFraction + let current: _ProgressFraction + } + + private func updateCompletedCount(count: Int) -> UpdateState { + // Acquire and release child's lock + let (previous, current) = state.withLock { state in + let prev = state.fractionState.overallFraction + state.fractionState.selfFraction.completed += count + return (prev, state.fractionState.overallFraction) + } + return UpdateState(previous: previous, current: current) + } + + private func updateFractionCompleted(from: _ProgressFraction, to: _ProgressFraction) { + _$observationRegistrar.withMutation(of: self, keyPath: \.fractionCompleted) { + if from != to { + parents.withLock { parents in + for (parent, portionOfParent) in parents { + parent.updateChildFraction(from: from, to: to, portion: portionOfParent) + } + } + } + } + } + + /// A child progress has been updated, which changes our own fraction completed. + internal func updateChildFraction(from previous: _ProgressFraction, to next: _ProgressFraction, portion: Int) { + let updateState = state.withLock { state in + let previousOverallFraction = state.fractionState.overallFraction + let multiple = _ProgressFraction(completed: portion, total: state.fractionState.selfFraction.total) + let oldFractionOfParent = previous * multiple + + if previous.total != 0 { + state.fractionState.childFraction = state.fractionState.childFraction - oldFractionOfParent + } + + if next.total != 0 { + state.fractionState.childFraction = state.fractionState.childFraction + (next * multiple) + + if next.isFinished { + // Remove from children list +// _ = children.withLock { $0.remove(self) } + + if portion != 0 { + // Update our self completed units + state.fractionState.selfFraction.completed += portion + } + + // Subtract the (child's fraction completed * multiple) from our child fraction + state.fractionState.childFraction = state.fractionState.childFraction - (multiple * next) + } + } + return UpdateState(previous: previousOverallFraction, current: state.fractionState.overallFraction) + } + updateFractionCompleted(from: updateState.previous, to: updateState.current) + } + + //MARK: Interop-related internal methods + /// Adds `observer` to list of `_observers` in `self`. + internal func addObserver(observer: @escaping @Sendable (ObserverState) -> Void) { + observers.withLock { observers in + observers.append(observer) + } + } + + /// Notifies all `_observers` of `self` when `state` changes. + private func notifyObservers(with state: ObserverState) { + observers.withLock { observers in + for observer in observers { + observer(state) + } + } + } + + internal func setInteropObservationForMonitor(observation monitorObservation: (any Sendable)) { + interopObservationForMonitor.withLock { observation in + observation = monitorObservation + } + } + + internal func setMonitorInterop(to value: Bool) { + monitorInterop.withLock { monitorInterop in + monitorInterop = value + } + } + + //MARK: Internal methods to mutate locked context +#if FOUNDATION_FRAMEWORK + internal func setParentBridge(parentBridge: Foundation.Progress) { + self.parentBridge.withLock { bridge in + bridge = parentBridge + } + } +#endif + + internal func setInteropChild(interopChild: ProgressManager) { + state.withLock { state in + state.fractionState.interopChild = interopChild + } + } + + internal func addToChildren(childManager: ProgressManager) { + _ = children.withLock { children in + children.insert(childManager) + } + } + + internal func addParent(parentReporter: ProgressManager, portionOfParent: Int) { + parents.withLock { parents in + parents[parentReporter] = portionOfParent + } + + let updates = state.withLock { state in + // Update metatype entry in parent + for (metatype, value) in state.otherProperties { + let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) + let updatedParentEntry: [(any Sendable)?] = [value] + childrenValues + parentReporter.updateChildrenOtherPropertiesAnyValue(property: metatype, child: self, value: updatedParentEntry) + } + + let original = _ProgressFraction(completed: 0, total: 0) + let updated = state.fractionState.overallFraction + return (original, updated) + } + + // Update childFraction entry in parent + parentReporter.updateChildFraction(from: updates.0, to: updates.1, portion: portionOfParent) + } + + // MARK: Propagation of Additional Properties Methods (Dual Mode of Operations) + private func getFlattenedChildrenValues(property metatype: P.Type, state: inout State) -> [P.T?] { + let childrenDictionary = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] + var childrenValues: [P.T?] = [] + if let dictionary = childrenDictionary { + for (_, value) in dictionary { + if let value = value as? [P.T?] { + childrenValues.append(contentsOf: value) + } + } + } + return childrenValues + } + + private func getFlattenedChildrenValues(property metatype: AnyMetatypeWrapper, state: inout State) -> [(any Sendable)?] { + let childrenDictionary = state.childrenOtherProperties[metatype] + var childrenValues: [(any Sendable)?] = [] + if let dictionary = childrenDictionary { + for (_, value) in dictionary { + childrenValues.append(contentsOf: value) + } + } + return childrenValues + } + + private func updateChildrenOtherPropertiesAnyValue(property metatype: AnyMetatypeWrapper, child: ProgressManager, value: [(any Sendable)?]) { + state.withLock { state in + let myEntries = state.childrenOtherProperties[metatype] + if myEntries != nil { + // If entries is not nil, then update my entry of children values + state.childrenOtherProperties[metatype]![child] = value + } else { + // If entries is nil, initialize then update my entry of children values + state.childrenOtherProperties[metatype] = [:] + state.childrenOtherProperties[metatype]![child] = value + } + // Ask parent to update their entry with my value + new children value + let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) + let updatedParentEntry: [(any Sendable)?] = [state.otherProperties[metatype]] + childrenValues + parents.withLock { parents in + for (parent, _) in parents { + parent.updateChildrenOtherPropertiesAnyValue(property: metatype, child: child, value: updatedParentEntry) + } + } + } + } + + private func updateChildrenOtherProperties(property metatype: P.Type, child: ProgressManager, value: [P.T?]) { + state.withLock { state in + let myEntries = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] + if myEntries != nil { + // If entries is not nil, then update my entry of children values + state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)]![child] = value + } else { + // If entries is nil, initialize then update my entry of children values + state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] = [:] + state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)]![child] = value + } + // Ask parent to update their entry with my value + new children value + let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) + let updatedParentEntry: [P.T?] = [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T] + childrenValues + parents.withLock { parents in + for (parent, _) in parents { + parent.updateChildrenOtherProperties(property: metatype, child: self, value: updatedParentEntry) + } + } + } + } + + deinit { + if !isFinished { + self.withProperties { properties in + if let totalCount = properties.totalCount { + properties.completedCount = totalCount + } + } + } + } +} + +@available(FoundationPreview 6.2, *) +// Hashable & Equatable Conformance +extension ProgressManager: Hashable, Equatable { + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } + + /// Returns `true` if pointer of `lhs` is equal to pointer of `rhs`. + public static func ==(lhs: ProgressManager, rhs: ProgressManager) -> Bool { + return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } +} + +@available(FoundationPreview 6.2, *) +extension ProgressManager: CustomDebugStringConvertible { + /// The description for `completedCount` and `totalCount`. + public var debugDescription: String { + return "\(completedCount) / \(totalCount ?? 0)" + } +} diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressOutput.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressOutput.swift deleted file mode 100644 index a8c69aed1..000000000 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressOutput.swift +++ /dev/null @@ -1,51 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -@available(FoundationPreview 6.2, *) -/// ProgressMonitor is just a wrapper that carries information about ProgressReporter. It is read-only and can be added as a child of something else. -@Observable -public final class ProgressOutput: Sendable { - - var totalCount: Int? { - reporter.totalCount - } - - var completedCount: Int { - reporter.completedCount - } - - var fractionCompleted: Double { - reporter.fractionCompleted - } - - var isIndeterminate: Bool { - reporter.isIndeterminate - } - - var isFinished: Bool { - reporter.isFinished - } - - // TODO: Need to figure out how to expose properties such as totalFileCount and completedFileCount - var properties: ProgressReporter.Values { - reporter.withProperties { properties in - return properties - } - } - - - internal let reporter: ProgressReporter - - internal init(reporter: ProgressReporter) { - self.reporter = reporter - } -} diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift index ca58657ad..83fe99a13 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -10,629 +10,44 @@ // //===----------------------------------------------------------------------===// -import Observation - -#if canImport(CollectionsInternal) -internal import CollectionsInternal -#elseif canImport(OrderedCollections) -internal import OrderedCollections -#elseif canImport(_FoundationCollections) -internal import _FoundationCollections -#endif - -internal struct FractionState { - var indeterminate: Bool - var selfFraction: _ProgressFraction - var childFraction: _ProgressFraction - var overallFraction: _ProgressFraction { - selfFraction + childFraction - } - var interopChild: ProgressReporter? // read from this if self is actually an interop ghost -} - -internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { - let metatype: Any.Type - - internal static func ==(lhs: Self, rhs: Self) -> Bool { - lhs.metatype == rhs.metatype - } - - internal func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(metatype)) - } -} - @available(FoundationPreview 6.2, *) -// ProgressReporter -/// An object that conveys ongoing progress to the user for a specified task. -@Observable public final class ProgressReporter: Sendable { - - // Stores all the state of properties - internal struct State { - var fractionState: FractionState - var otherProperties: [AnyMetatypeWrapper: (any Sendable)] - // Type: Metatype maps to dictionary of child to value - var childrenOtherProperties: [AnyMetatypeWrapper: OrderedDictionary] - } - - // Interop states - internal enum ObserverState { - case fractionUpdated - case totalCountUpdated - } - - // Interop properties - Just kept alive - internal let interopObservation: (any Sendable)? // set at init - internal let interopObservationForMonitor: LockedState<(any Sendable)?> = LockedState(initialState: nil) - internal let monitorInterop: LockedState = LockedState(initialState: false) - - #if FOUNDATION_FRAMEWORK - internal let parentBridge: LockedState = LockedState(initialState: nil) // dummy, set upon calling setParentBridge - #endif - // Interop properties - Actually set and called - internal let ghostReporter: ProgressReporter? // set at init, used to call notify observers - internal let observers: LockedState<[@Sendable (ObserverState) -> Void]> = LockedState(initialState: [])// storage for all observers, set upon calling addObservers - - /// The total units of work. - public var totalCount: Int? { - _$observationRegistrar.access(self, keyPath: \.totalCount) - return state.withLock { state in - getTotalCount(fractionState: &state.fractionState) - } - } - - /// The completed units of work. - /// If `self` is indeterminate, the value will be 0. - public var completedCount: Int { - _$observationRegistrar.access(self, keyPath: \.completedCount) - return state.withLock { state in - getCompletedCount(fractionState: &state.fractionState) - } - } - - /// The proportion of work completed. - /// This takes into account the fraction completed in its children instances if children are present. - /// If `self` is indeterminate, the value will be 0. - public var fractionCompleted: Double { - _$observationRegistrar.access(self, keyPath: \.fractionCompleted) - return state.withLock { state in - getFractionCompleted(fractionState: &state.fractionState) - } - } - - /// The state of initialization of `totalCount`. - /// If `totalCount` is `nil`, the value will be `true`. - public var isIndeterminate: Bool { - _$observationRegistrar.access(self, keyPath: \.isIndeterminate) - return state.withLock { state in - getIsIndeterminate(fractionState: &state.fractionState) - } - } - - /// The state of completion of work. - /// If `completedCount` >= `totalCount`, the value will be `true`. - public var isFinished: Bool { - _$observationRegistrar.access(self, keyPath: \.isFinished) - return state.withLock { state in - getIsFinished(fractionState: &state.fractionState) - } - } - - public var output: ProgressOutput { - return .init(reporter: self) - } - - /// A type that conveys task-specific information on progress. - public protocol Property { - - associatedtype T: Sendable, Hashable, Equatable - - static var defaultValue: T { get } - } - - /// A container that holds values for properties that specify information on progress. - @dynamicMemberLookup - public struct Values : Sendable { - //TODO: rdar://149225947 Non-escapable conformance - let reporter: ProgressReporter - var state: State - - /// The total units of work. - public var totalCount: Int? { - mutating get { - reporter.getTotalCount(fractionState: &state.fractionState) - } - - set { - let previous = state.fractionState.overallFraction - if state.fractionState.selfFraction.total != newValue && state.fractionState.selfFraction.total > 0 { - state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: newValue ?? 1) - } - state.fractionState.selfFraction.total = newValue ?? 0 - - // if newValue is nil, reset indeterminate to true - if newValue != nil { - state.fractionState.indeterminate = false - } else { - state.fractionState.indeterminate = true - } - //TODO: rdar://149015734 Check throttling - reporter.updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) - reporter.ghostReporter?.notifyObservers(with: .totalCountUpdated) - reporter.monitorInterop.withLock { [reporter] interop in - if interop == true { - reporter.notifyObservers(with: .totalCountUpdated) - } - } - } - } - - - /// The completed units of work. - public var completedCount: Int { - mutating get { - reporter.getCompletedCount(fractionState: &state.fractionState) - } - - set { - let prev = state.fractionState.overallFraction - state.fractionState.selfFraction.completed = newValue - reporter.updateFractionCompleted(from: prev, to: state.fractionState.overallFraction) - reporter.ghostReporter?.notifyObservers(with: .fractionUpdated) - - reporter.monitorInterop.withLock { [reporter] interop in - if interop == true { - reporter.notifyObservers(with: .fractionUpdated) - } - } - } - } - - /// Returns a property value that a key path indicates. - public subscript(dynamicMember key: KeyPath) -> P.T { - get { - return state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T ?? P.self.defaultValue - } - - set { - // Update my own other properties entry - state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = newValue - - // Generate an array of myself + children values of the property - let flattenedChildrenValues: [P.T?] = { - let childrenDictionary = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] - var childrenValues: [P.T?] = [] - if let dictionary = childrenDictionary { - for (_, value) in dictionary { - if let value = value as? [P.T?] { - childrenValues.append(contentsOf: value) - } - } - } - return childrenValues - }() - - // Send the array of myself + children values of property to parents - let updateValueForParent: [P.T?] = [newValue] + flattenedChildrenValues - reporter.parents.withLock { [reporter] parents in - for (parent, _) in parents { - parent.updateChildrenOtherProperties(property: P.self, child: reporter, value: updateValueForParent) - } - } - } - } - } - - internal let parents: LockedState<[ProgressReporter: Int]> - private let children: LockedState> - private let state: LockedState - - internal init(total: Int?, ghostReporter: ProgressReporter?, interopObservation: (any Sendable)?) { - self.parents = .init(initialState: [:]) - self.children = .init(initialState: Set()) - let fractionState = FractionState( - indeterminate: total == nil ? true : false, - selfFraction: _ProgressFraction(completed: 0, total: total ?? 0), - childFraction: _ProgressFraction(completed: 0, total: 1), - interopChild: nil - ) - let state = State(fractionState: fractionState, otherProperties: [:], childrenOtherProperties: [:]) - self.state = LockedState(initialState: state) - self.interopObservation = interopObservation - self.ghostReporter = ghostReporter - } - - /// Initializes `self` with `totalCount`. - /// - /// If `totalCount` is set to `nil`, `self` is indeterminate. - /// - Parameter totalCount: Total units of work. - public convenience init(totalCount: Int?) { - self.init(total: totalCount, ghostReporter: nil, interopObservation: nil) - } - - /// Sets `totalCount`. - /// - Parameter newTotal: Total units of work. - public func setTotalCount(_ newTotal: Int?) { - state.withLock { state in - let previous = state.fractionState.overallFraction - if state.fractionState.selfFraction.total != newTotal && state.fractionState.selfFraction.total > 0 { - state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: newTotal ?? 1) - } - state.fractionState.selfFraction.total = newTotal ?? 0 - - // if newValue is nil, reset indeterminate to true - if newTotal != nil { - state.fractionState.indeterminate = false - } else { - state.fractionState.indeterminate = true - } - updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) - - ghostReporter?.notifyObservers(with: .totalCountUpdated) - - monitorInterop.withLock { [self] interop in - if interop == true { - notifyObservers(with: .totalCountUpdated) - } - } - } - } - - /// Returns a `Subprogress` representing a portion of `self` which can be passed to any method that reports progress. - /// - /// - Parameter count: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. - /// - Returns: A `Subprogress` instance. - public func subprogress(assigningCount portionOfParent: Int) -> ProgressInput { - precondition(portionOfParent > 0, "Giving out zero units is not a valid operation.") - let subprogress = ProgressInput(parent: self, portionOfParent: portionOfParent) - return subprogress - } - - - /// Adds a `ProgressOutput` as a child, with its progress representing a portion of `self`'s progress. - /// - Parameters: - /// - monitor: A `ProgressOutput` instance. - /// - portionOfParent: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. - public func assign(count portionOfParent: Int, to monitor: ProgressOutput) { - // get the actual progress from within the monitor, then add as children - let actualReporter = monitor.reporter - - // Add monitor as child + Add self as parent - self.addToChildren(childReporter: actualReporter) - actualReporter.addParent(parentReporter: self, portionOfParent: portionOfParent) - } - - /// Increases `completedCount` by `count`. - /// - Parameter count: Units of work. - public func complete(count: Int) { - let updateState = updateCompletedCount(count: count) - updateFractionCompleted(from: updateState.previous, to: updateState.current) - ghostReporter?.notifyObservers(with: .fractionUpdated) - - monitorInterop.withLock { [self] interop in - if interop == true { - notifyObservers(with: .fractionUpdated) - } - } - } - - - /// Returns an array of values for specified property in subtree. - /// - Parameter metatype: Type of property. - /// - Returns: Array of values for property. - public func values(property metatype: P.Type) -> [P.T?] { - return state.withLock { state in - let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) - return [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T ?? P.defaultValue] + childrenValues.map { $0 ?? P.defaultValue } - } - } - - - /// Returns the aggregated result of values. - /// - Parameters: - /// - property: Type of property. - /// - values:Sum of values. - public func total(property: P.Type, values: [P.T?]) -> P.T where P.T: AdditiveArithmetic { - let droppedNil = values.compactMap { $0 } - return droppedNil.reduce(P.T.zero, +) - } - - /// Mutates any settable properties that convey information about progress. - public func withProperties(_ closure: @Sendable (inout Values) throws -> T) rethrows -> T { - return try state.withLock { state in - var values = Values(reporter: self, state: state) - // This is done to avoid copy on write later - state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction()), otherProperties: [:], childrenOtherProperties: [:]) - let result = try closure(&values) - state = values.state - return result - } - } - - //MARK: ProgressReporter Properties getters - /// Returns nil if `self` was instantiated without total units; - /// returns a `Int` value otherwise. - private func getTotalCount(fractionState: inout FractionState) -> Int? { - if let interopChild = fractionState.interopChild { - return interopChild.totalCount - } - if fractionState.indeterminate { - return nil - } else { - return fractionState.selfFraction.total - } - } - - /// Returns nil if `self` has `nil` total units; - /// returns a `Int` value otherwise. - private func getCompletedCount(fractionState: inout FractionState) -> Int { - if let interopChild = fractionState.interopChild { - return interopChild.completedCount - } - return fractionState.selfFraction.completed - } - - /// Returns 0.0 if `self` has `nil` total units; - /// returns a `Double` otherwise. - /// If `indeterminate`, return 0.0. - /// - /// The calculation of fraction completed for a ProgressReporter instance that has children - /// will take into account children's fraction completed as well. - private func getFractionCompleted(fractionState: inout FractionState) -> Double { - if let interopChild = fractionState.interopChild { - return interopChild.fractionCompleted - } - if fractionState.indeterminate { - return 0.0 - } - guard fractionState.selfFraction.total > 0 else { - return fractionState.selfFraction.fractionCompleted - } - return (fractionState.selfFraction + fractionState.childFraction).fractionCompleted - } - - - /// Returns `true` if completed and total units are not `nil` and completed units is greater than or equal to total units; - /// returns `false` otherwise. - private func getIsFinished(fractionState: inout FractionState) -> Bool { - return fractionState.selfFraction.isFinished - } - - - /// Returns `true` if `self` has `nil` total units. - private func getIsIndeterminate(fractionState: inout FractionState) -> Bool { - return fractionState.indeterminate - } - - //MARK: FractionCompleted Calculation methods - private struct UpdateState { - let previous: _ProgressFraction - let current: _ProgressFraction - } - - private func updateCompletedCount(count: Int) -> UpdateState { - // Acquire and release child's lock - let (previous, current) = state.withLock { state in - let prev = state.fractionState.overallFraction - state.fractionState.selfFraction.completed += count - return (prev, state.fractionState.overallFraction) - } - return UpdateState(previous: previous, current: current) - } - - private func updateFractionCompleted(from: _ProgressFraction, to: _ProgressFraction) { - _$observationRegistrar.withMutation(of: self, keyPath: \.fractionCompleted) { - if from != to { - parents.withLock { parents in - for (parent, portionOfParent) in parents { - parent.updateChildFraction(from: from, to: to, portion: portionOfParent) - } - } - } - } - } - - /// A child progress has been updated, which changes our own fraction completed. - internal func updateChildFraction(from previous: _ProgressFraction, to next: _ProgressFraction, portion: Int) { - let updateState = state.withLock { state in - let previousOverallFraction = state.fractionState.overallFraction - let multiple = _ProgressFraction(completed: portion, total: state.fractionState.selfFraction.total) - let oldFractionOfParent = previous * multiple - - if previous.total != 0 { - state.fractionState.childFraction = state.fractionState.childFraction - oldFractionOfParent - } - - if next.total != 0 { - state.fractionState.childFraction = state.fractionState.childFraction + (next * multiple) - - if next.isFinished { - // Remove from children list -// _ = children.withLock { $0.remove(self) } - - if portion != 0 { - // Update our self completed units - state.fractionState.selfFraction.completed += portion - } - - // Subtract the (child's fraction completed * multiple) from our child fraction - state.fractionState.childFraction = state.fractionState.childFraction - (multiple * next) - } - } - return UpdateState(previous: previousOverallFraction, current: state.fractionState.overallFraction) - } - updateFractionCompleted(from: updateState.previous, to: updateState.current) - } - - //MARK: Interop-related internal methods - /// Adds `observer` to list of `_observers` in `self`. - internal func addObserver(observer: @escaping @Sendable (ObserverState) -> Void) { - observers.withLock { observers in - observers.append(observer) - } - } - - /// Notifies all `_observers` of `self` when `state` changes. - private func notifyObservers(with state: ObserverState) { - observers.withLock { observers in - for observer in observers { - observer(state) - } - } - } - - internal func setInteropObservationForMonitor(observation monitorObservation: (any Sendable)) { - interopObservationForMonitor.withLock { observation in - observation = monitorObservation - } - } - internal func setMonitorInterop(to value: Bool) { - monitorInterop.withLock { monitorInterop in - monitorInterop = value - } - } - - //MARK: Internal methods to mutate locked context -#if FOUNDATION_FRAMEWORK - internal func setParentBridge(parentBridge: Foundation.Progress) { - self.parentBridge.withLock { bridge in - bridge = parentBridge - } - } -#endif - - internal func setInteropChild(interopChild: ProgressReporter) { - state.withLock { state in - state.fractionState.interopChild = interopChild - } - } +/// ProgressReporter is a wrapper for ProgressManager that carries information about ProgressManager. +/// +/// It is read-only and can be added as a child of another ProgressManager. +@Observable public final class ProgressReporter: Sendable { - internal func addToChildren(childReporter: ProgressReporter) { - _ = children.withLock { children in - children.insert(childReporter) - } + var totalCount: Int? { + manager.totalCount } - internal func addParent(parentReporter: ProgressReporter, portionOfParent: Int) { - parents.withLock { parents in - parents[parentReporter] = portionOfParent - } - - let updates = state.withLock { state in - // Update metatype entry in parent - for (metatype, value) in state.otherProperties { - let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) - let updatedParentEntry: [(any Sendable)?] = [value] + childrenValues - parentReporter.updateChildrenOtherPropertiesAnyValue(property: metatype, child: self, value: updatedParentEntry) - } - - let original = _ProgressFraction(completed: 0, total: 0) - let updated = state.fractionState.overallFraction - return (original, updated) - } - - // Update childFraction entry in parent - parentReporter.updateChildFraction(from: updates.0, to: updates.1, portion: portionOfParent) + var completedCount: Int { + manager.completedCount } - // MARK: Propagation of Additional Properties Methods (Dual Mode of Operations) - private func getFlattenedChildrenValues(property metatype: P.Type, state: inout State) -> [P.T?] { - let childrenDictionary = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] - var childrenValues: [P.T?] = [] - if let dictionary = childrenDictionary { - for (_, value) in dictionary { - if let value = value as? [P.T?] { - childrenValues.append(contentsOf: value) - } - } - } - return childrenValues + var fractionCompleted: Double { + manager.fractionCompleted } - private func getFlattenedChildrenValues(property metatype: AnyMetatypeWrapper, state: inout State) -> [(any Sendable)?] { - let childrenDictionary = state.childrenOtherProperties[metatype] - var childrenValues: [(any Sendable)?] = [] - if let dictionary = childrenDictionary { - for (_, value) in dictionary { - childrenValues.append(contentsOf: value) - } - } - return childrenValues + var isIndeterminate: Bool { + manager.isIndeterminate } - private func updateChildrenOtherPropertiesAnyValue(property metatype: AnyMetatypeWrapper, child: ProgressReporter, value: [(any Sendable)?]) { - state.withLock { state in - let myEntries = state.childrenOtherProperties[metatype] - if myEntries != nil { - // If entries is not nil, then update my entry of children values - state.childrenOtherProperties[metatype]![child] = value - } else { - // If entries is nil, initialize then update my entry of children values - state.childrenOtherProperties[metatype] = [:] - state.childrenOtherProperties[metatype]![child] = value - } - // Ask parent to update their entry with my value + new children value - let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) - let updatedParentEntry: [(any Sendable)?] = [state.otherProperties[metatype]] + childrenValues - parents.withLock { parents in - for (parent, _) in parents { - parent.updateChildrenOtherPropertiesAnyValue(property: metatype, child: child, value: updatedParentEntry) - } - } - } + var isFinished: Bool { + manager.isFinished } - private func updateChildrenOtherProperties(property metatype: P.Type, child: ProgressReporter, value: [P.T?]) { - state.withLock { state in - let myEntries = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] - if myEntries != nil { - // If entries is not nil, then update my entry of children values - state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)]![child] = value - } else { - // If entries is nil, initialize then update my entry of children values - state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] = [:] - state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)]![child] = value - } - // Ask parent to update their entry with my value + new children value - let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) - let updatedParentEntry: [P.T?] = [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T] + childrenValues - parents.withLock { parents in - for (parent, _) in parents { - parent.updateChildrenOtherProperties(property: metatype, child: self, value: updatedParentEntry) - } - } + // TODO: Need to figure out how to expose properties such as totalFileCount and completedFileCount + var properties: ProgressManager.Values { + manager.withProperties { properties in + return properties } } - deinit { - if !isFinished { - self.withProperties { properties in - if let totalCount = properties.totalCount { - properties.completedCount = totalCount - } - } - } - } -} -@available(FoundationPreview 6.2, *) -// Hashable & Equatable Conformance -extension ProgressReporter: Hashable, Equatable { - public func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(self)) - } + internal let manager: ProgressManager - /// Returns `true` if pointer of `lhs` is equal to pointer of `rhs`. - public static func ==(lhs: ProgressReporter, rhs: ProgressReporter) -> Bool { - return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) - } -} - -@available(FoundationPreview 6.2, *) -extension ProgressReporter: CustomDebugStringConvertible { - /// The description for `completedCount` and `totalCount`. - public var debugDescription: String { - return "\(completedCount) / \(totalCount ?? 0)" + internal init(manager: ProgressManager) { + self.manager = manager } } diff --git a/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift b/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift new file mode 100644 index 000000000..358b7642f --- /dev/null +++ b/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@available(FoundationPreview 6.2, *) +/// Subprogress is a nested ~Copyable struct used to establish parent-child relationship between two instances of ProgressManager. +/// +/// Subprogress is returned from a call to `subprogress(assigningCount:)` by a parent ProgressManager. +/// A child ProgressManager is then returned by calling`manager(totalCount:)` on a Subprogress. +public struct Subprogress: ~Copyable, Sendable { + internal var parent: ProgressManager + internal var portionOfParent: Int + internal var isInitializedToProgressReporter: Bool + + // Interop variables for Progress - ProgressManager Interop + internal var interopWithProgressParent: Bool = false + // To be kept alive in ProgressManager + internal var observation: (any Sendable)? + internal var ghostReporter: ProgressManager? + + internal init(parent: ProgressManager, portionOfParent: Int) { + self.parent = parent + self.portionOfParent = portionOfParent + self.isInitializedToProgressReporter = false + } + + /// Instantiates a ProgressManager which is a child to the parent from which `self` is returned. + /// - Parameter totalCount: Total count of returned child `ProgressManager` instance. + /// - Returns: A `ProgressManager` instance. + public consuming func manager(totalCount: Int?) -> ProgressManager { + isInitializedToProgressReporter = true + + let childManager = ProgressManager(total: totalCount, ghostReporter: ghostReporter, interopObservation: observation) + + if interopWithProgressParent { + // Set interop child of ghost manager so ghost manager reads from here + ghostReporter?.setInteropChild(interopChild: childManager) + } else { + // Add child to parent's _children list & Store in child children's position in parent + parent.addToChildren(childManager: childManager) + childManager.addParent(parentReporter: parent, portionOfParent: portionOfParent) + } + + return childManager + } + + deinit { + if !self.isInitializedToProgressReporter { + parent.complete(count: portionOfParent) + } + } +} + diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressManager+FileFormatStyle.swift similarity index 87% rename from Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift rename to Sources/FoundationInternationalization/ProgressReporter/ProgressManager+FileFormatStyle.swift index 0e2b15f81..bd68d4d6f 100644 --- a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressManager+FileFormatStyle.swift @@ -14,7 +14,7 @@ import FoundationEssentials #endif @available(FoundationPreview 6.2, *) -extension ProgressReporter { +extension ProgressManager { //TODO: rdar://149092406 Manual Codable Conformance public struct FileFormatStyle: Sendable, Codable, Equatable, Hashable { @@ -76,13 +76,13 @@ extension ProgressReporter { @available(FoundationPreview 6.2, *) -extension ProgressReporter.FileFormatStyle: FormatStyle { +extension ProgressManager.FileFormatStyle: FormatStyle { - public func locale(_ locale: Locale) -> ProgressReporter.FileFormatStyle { + public func locale(_ locale: Locale) -> ProgressManager.FileFormatStyle { .init(self.option, locale: locale) } - public func format(_ reporter: ProgressReporter) -> String { + public func format(_ manager: ProgressManager) -> String { switch self.option.rawOption { case .file: @@ -92,17 +92,17 @@ extension ProgressReporter.FileFormatStyle: FormatStyle { var throughputLSR: LocalizedStringResource? var timeRemainingLSR: LocalizedStringResource? - let properties = reporter.withProperties(\.self) + let properties = manager.withProperties(\.self) - fileCountLSR = LocalizedStringResource("\(properties.completedFileCount, format: IntegerFormatStyle()) of \(properties.totalFileCount, format: IntegerFormatStyle()) files", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + fileCountLSR = LocalizedStringResource("\(properties.completedFileCount, format: IntegerFormatStyle()) of \(properties.totalFileCount, format: IntegerFormatStyle()) files", locale: self.locale, bundle: .forClass(ProgressManager.self)) - byteCountLSR = LocalizedStringResource("\(properties.completedByteCount, format: ByteCountFormatStyle()) of \(properties.totalByteCount, format: ByteCountFormatStyle())", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + byteCountLSR = LocalizedStringResource("\(properties.completedByteCount, format: ByteCountFormatStyle()) of \(properties.totalByteCount, format: ByteCountFormatStyle())", locale: self.locale, bundle: .forClass(ProgressManager.self)) - throughputLSR = LocalizedStringResource("\(properties.throughput, format: ByteCountFormatStyle())/s", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + throughputLSR = LocalizedStringResource("\(properties.throughput, format: ByteCountFormatStyle())/s", locale: self.locale, bundle: .forClass(ProgressManager.self)) - timeRemainingLSR = LocalizedStringResource("\(properties.estimatedTimeRemaining, format: Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .wide)) remaining", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + timeRemainingLSR = LocalizedStringResource("\(properties.estimatedTimeRemaining, format: Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .wide)) remaining", locale: self.locale, bundle: .forClass(ProgressManager.self)) return """ \(String(localized: fileCountLSR ?? "")) @@ -117,7 +117,7 @@ extension ProgressReporter.FileFormatStyle: FormatStyle { var throughputString: String? var timeRemainingString: String? - let properties = reporter.withProperties(\.self) + let properties = manager.withProperties(\.self) fileCountString = "\(properties.completedFileCount.formatted(IntegerFormatStyle(locale: self.locale))) / \(properties.totalFileCount.formatted(IntegerFormatStyle(locale: self.locale)))" @@ -143,15 +143,15 @@ extension ProgressReporter.FileFormatStyle: FormatStyle { } @available(FoundationPreview 6.2, *) -// Make access easier to format ProgressReporter -extension ProgressReporter { - public func formatted(_ style: ProgressReporter.FileFormatStyle) -> String { +// Make access easier to format ProgressManager +extension ProgressManager { + public func formatted(_ style: ProgressManager.FileFormatStyle) -> String { style.format(self) } } @available(FoundationPreview 6.2, *) -extension FormatStyle where Self == ProgressReporter.FileFormatStyle { +extension FormatStyle where Self == ProgressManager.FileFormatStyle { public static var file: Self { .init(.file) } diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressManager+FormatStyle.swift similarity index 84% rename from Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift rename to Sources/FoundationInternationalization/ProgressReporter/ProgressManager+FormatStyle.swift index d44dcd6ce..271b3b37b 100644 --- a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressManager+FormatStyle.swift @@ -12,13 +12,13 @@ #if canImport(FoundationEssentials) import FoundationEssentials #endif -// Outlines the options available to format ProgressReporter +// Outlines the options available to format ProgressManager @available(FoundationPreview 6.2, *) -extension ProgressReporter { +extension ProgressManager { public struct FormatStyle: Sendable, Codable, Equatable, Hashable { - // Outlines the options available to format ProgressReporter + // Outlines the options available to format ProgressManager internal struct Option: Sendable, Codable, Hashable, Equatable { init(from decoder: any Decoder) throws { @@ -95,20 +95,20 @@ extension ProgressReporter { } @available(FoundationPreview 6.2, *) -extension ProgressReporter.FormatStyle: FormatStyle { +extension ProgressManager.FormatStyle: FormatStyle { - public func locale(_ locale: Locale) -> ProgressReporter.FormatStyle { + public func locale(_ locale: Locale) -> ProgressManager.FormatStyle { .init(self.option, locale: locale) } - public func format(_ reporter: ProgressReporter) -> String { + public func format(_ manager: ProgressManager) -> String { switch self.option.rawOption { case .count(let countStyle): - let count = reporter.withProperties { p in + let count = manager.withProperties { p in return (p.completedCount, p.totalCount) } #if FOUNDATION_FRAMEWORK - let countLSR = LocalizedStringResource("\(count.0, format: countStyle) of \(count.1 ?? 0, format: countStyle)", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + let countLSR = LocalizedStringResource("\(count.0, format: countStyle) of \(count.1 ?? 0, format: countStyle)", locale: self.locale, bundle: .forClass(ProgressManager.self)) return String(localized: countLSR) #else return "\(count.0.formatted(countStyle.locale(self.locale))) / \((count.1 ?? 0).formatted(countStyle.locale(self.locale)))" @@ -116,25 +116,25 @@ extension ProgressReporter.FormatStyle: FormatStyle { case .fractionCompleted(let fractionStyle): #if FOUNDATION_FRAMEWORK - let fractionLSR = LocalizedStringResource("\(reporter.fractionCompleted, format: fractionStyle) completed", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + let fractionLSR = LocalizedStringResource("\(manager.fractionCompleted, format: fractionStyle) completed", locale: self.locale, bundle: .forClass(ProgressManager.self)) return String(localized: fractionLSR) #else - return "\(reporter.fractionCompleted.formatted(fractionStyle.locale(self.locale)))" + return "\(manager.fractionCompleted.formatted(fractionStyle.locale(self.locale)))" #endif } } } @available(FoundationPreview 6.2, *) -// Make access easier to format ProgressReporter -extension ProgressReporter { +// Make access easier to format ProgressManager +extension ProgressManager { #if FOUNDATION_FRAMEWORK - public func formatted(_ style: F) -> F.FormatOutput where F.FormatInput == ProgressReporter { + public func formatted(_ style: F) -> F.FormatOutput where F.FormatInput == ProgressManager { style.format(self) } #else - public func formatted(_ style: F) -> F.FormatOutput where F.FormatInput == ProgressReporter { + public func formatted(_ style: F) -> F.FormatOutput where F.FormatInput == ProgressManager { style.format(self) } #endif // FOUNDATION_FRAMEWORK @@ -146,7 +146,7 @@ extension ProgressReporter { } @available(FoundationPreview 6.2, *) -extension FormatStyle where Self == ProgressReporter.FormatStyle { +extension FormatStyle where Self == ProgressManager.FormatStyle { public static func fractionCompleted( format: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent() diff --git a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressManagerTests.swift similarity index 60% rename from Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift rename to Tests/FoundationEssentialsTests/ProgressReporter/ProgressManagerTests.swift index 9310eb104..4f7fdc541 100644 --- a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressManagerTests.swift @@ -17,39 +17,39 @@ import XCTest @testable import FoundationEssentials #endif // FOUNDATION_FRAMEWORK -/// Unit tests for basic functionalities of ProgressReporter -class TestProgressReporter: XCTestCase { +/// Unit tests for basic functionalities of ProgressManager +class TestProgressManager: XCTestCase { /// MARK: Helper methods that report progress - func doBasicOperationV1(reportTo progress: consuming ProgressInput) async { - let reporter = progress.reporter(totalCount: 8) + func doBasicOperationV1(reportTo subprogress: consuming Subprogress) async { + let manager = subprogress.manager(totalCount: 8) for i in 1...8 { - reporter.complete(count: 1) - XCTAssertEqual(reporter.completedCount, i) - XCTAssertEqual(reporter.fractionCompleted, Double(i) / Double(8)) + manager.complete(count: 1) + XCTAssertEqual(manager.completedCount, i) + XCTAssertEqual(manager.fractionCompleted, Double(i) / Double(8)) } } - func doBasicOperationV2(reportTo progress: consuming ProgressInput) async { - let reporter = progress.reporter(totalCount: 7) + func doBasicOperationV2(reportTo subprogress: consuming Subprogress) async { + let manager = subprogress.manager(totalCount: 7) for i in 1...7 { - reporter.complete(count: 1) - XCTAssertEqual(reporter.completedCount, i) - XCTAssertEqual(reporter.fractionCompleted,Double(i) / Double(7)) + manager.complete(count: 1) + XCTAssertEqual(manager.completedCount, i) + XCTAssertEqual(manager.fractionCompleted,Double(i) / Double(7)) } } - func doBasicOperationV3(reportTo progress: consuming ProgressInput) async { - let reporter = progress.reporter(totalCount: 11) + func doBasicOperationV3(reportTo subprogress: consuming Subprogress) async { + let manager = subprogress.manager(totalCount: 11) for i in 1...11 { - reporter.complete(count: 1) - XCTAssertEqual(reporter.completedCount, i) - XCTAssertEqual(reporter.fractionCompleted, Double(i) / Double(11)) + manager.complete(count: 1) + XCTAssertEqual(manager.completedCount, i) + XCTAssertEqual(manager.fractionCompleted, Double(i) / Double(11)) } } /// MARK: Tests calculations based on change in totalCount func testTotalCountNil() async throws { - let overall = ProgressReporter(totalCount: nil) + let overall = ProgressManager(totalCount: nil) overall.complete(count: 10) XCTAssertEqual(overall.completedCount, 10) XCTAssertEqual(overall.fractionCompleted, 0.0) @@ -58,7 +58,7 @@ class TestProgressReporter: XCTestCase { } func testTotalCountReset() async throws { - let overall = ProgressReporter(totalCount: 10) + let overall = ProgressManager(totalCount: 10) overall.complete(count: 5) XCTAssertEqual(overall.completedCount, 5) XCTAssertEqual(overall.totalCount, 10) @@ -87,7 +87,7 @@ class TestProgressReporter: XCTestCase { } func testTotalCountNilWithChild() async throws { - let overall = ProgressReporter(totalCount: nil) + let overall = ProgressManager(totalCount: nil) XCTAssertEqual(overall.completedCount, 0) XCTAssertNil(overall.totalCount) XCTAssertEqual(overall.fractionCompleted, 0.0) @@ -95,14 +95,14 @@ class TestProgressReporter: XCTestCase { XCTAssertFalse(overall.isFinished) let progress1 = overall.subprogress(assigningCount: 2) - let reporter1 = progress1.reporter(totalCount: 1) + let manager1 = progress1.manager(totalCount: 1) - reporter1.complete(count: 1) - XCTAssertEqual(reporter1.totalCount, 1) - XCTAssertEqual(reporter1.completedCount, 1) - XCTAssertEqual(reporter1.fractionCompleted, 1.0) - XCTAssertFalse(reporter1.isIndeterminate) - XCTAssertTrue(reporter1.isFinished) + manager1.complete(count: 1) + XCTAssertEqual(manager1.totalCount, 1) + XCTAssertEqual(manager1.completedCount, 1) + XCTAssertEqual(manager1.fractionCompleted, 1.0) + XCTAssertFalse(manager1.isIndeterminate) + XCTAssertTrue(manager1.isFinished) XCTAssertEqual(overall.completedCount, 2) XCTAssertEqual(overall.totalCount, nil) @@ -121,12 +121,12 @@ class TestProgressReporter: XCTestCase { } func testTotalCountFinishesWithLessCompletedCount() async throws { - let overall = ProgressReporter(totalCount: 10) + let overall = ProgressManager(totalCount: 10) overall.complete(count: 5) let progress1 = overall.subprogress(assigningCount: 8) - let reporter1 = progress1.reporter(totalCount: 1) - reporter1.complete(count: 1) + let manager1 = progress1.manager(totalCount: 1) + manager1.complete(count: 1) XCTAssertEqual(overall.completedCount, 13) XCTAssertEqual(overall.totalCount, 10) @@ -137,32 +137,32 @@ class TestProgressReporter: XCTestCase { /// MARK: Tests single-level tree func testDiscreteReporter() async throws { - let reporter = ProgressReporter(totalCount: 3) - await doBasicOperationV1(reportTo: reporter.subprogress(assigningCount: 3)) - XCTAssertEqual(reporter.fractionCompleted, 1.0) - XCTAssertEqual(reporter.completedCount, 3) - XCTAssertTrue(reporter.isFinished) + let manager = ProgressManager(totalCount: 3) + await doBasicOperationV1(reportTo: manager.subprogress(assigningCount: 3)) + XCTAssertEqual(manager.fractionCompleted, 1.0) + XCTAssertEqual(manager.completedCount, 3) + XCTAssertTrue(manager.isFinished) } /// MARK: Tests multiple-level trees func testEmptyDiscreteReporter() async throws { - let reporter = ProgressReporter(totalCount: nil) - XCTAssertTrue(reporter.isIndeterminate) + let manager = ProgressManager(totalCount: nil) + XCTAssertTrue(manager.isIndeterminate) - reporter.withProperties { p in + manager.withProperties { p in p.totalCount = 10 } - XCTAssertFalse(reporter.isIndeterminate) - XCTAssertEqual(reporter.totalCount, 10) + XCTAssertFalse(manager.isIndeterminate) + XCTAssertEqual(manager.totalCount, 10) - await doBasicOperationV1(reportTo: reporter.subprogress(assigningCount: 10)) - XCTAssertEqual(reporter.fractionCompleted, 1.0) - XCTAssertEqual(reporter.completedCount, 10) - XCTAssertTrue(reporter.isFinished) + await doBasicOperationV1(reportTo: manager.subprogress(assigningCount: 10)) + XCTAssertEqual(manager.fractionCompleted, 1.0) + XCTAssertEqual(manager.completedCount, 10) + XCTAssertTrue(manager.isFinished) } func testTwoLevelTreeWithTwoChildren() async throws { - let overall = ProgressReporter(totalCount: 2) + let overall = ProgressManager(totalCount: 2) await doBasicOperationV1(reportTo: overall.subprogress(assigningCount: 1)) XCTAssertEqual(overall.fractionCompleted, 0.5) @@ -178,15 +178,15 @@ class TestProgressReporter: XCTestCase { } func testTwoLevelTreeWithTwoChildrenWithOneFileProperty() async throws { - let overall = ProgressReporter(totalCount: 2) + let overall = ProgressManager(totalCount: 2) let progress1 = overall.subprogress(assigningCount: 1) - let reporter1 = progress1.reporter(totalCount: 5) - reporter1.complete(count: 5) + let manager1 = progress1.manager(totalCount: 5) + manager1.complete(count: 5) let progress2 = overall.subprogress(assigningCount: 1) - let reporter2 = progress2.reporter(totalCount: 5) - reporter2.withProperties { properties in + let manager2 = progress2.manager(totalCount: 5) + manager2.withProperties { properties in properties.totalFileCount = 10 } @@ -196,7 +196,7 @@ class TestProgressReporter: XCTestCase { } func testTwoLevelTreeWithMultipleChildren() async throws { - let overall = ProgressReporter(totalCount: 3) + let overall = ProgressManager(totalCount: 3) await doBasicOperationV1(reportTo: overall.subprogress(assigningCount:1)) XCTAssertEqual(overall.fractionCompleted, Double(1) / Double(3)) @@ -212,141 +212,141 @@ class TestProgressReporter: XCTestCase { } func testThreeLevelTree() async throws { - let overall = ProgressReporter(totalCount: 100) + let overall = ProgressManager(totalCount: 100) XCTAssertEqual(overall.fractionCompleted, 0.0) let child1 = overall.subprogress(assigningCount: 100) - let reporter1 = child1.reporter(totalCount: 100) + let manager1 = child1.manager(totalCount: 100) - let grandchild1 = reporter1.subprogress(assigningCount: 100) - let grandchildReporter1 = grandchild1.reporter(totalCount: 100) + let grandchild1 = manager1.subprogress(assigningCount: 100) + let grandchildManager1 = grandchild1.manager(totalCount: 100) XCTAssertEqual(overall.fractionCompleted, 0.0) - grandchildReporter1.complete(count: 50) - XCTAssertEqual(reporter1.fractionCompleted, 0.5) + grandchildManager1.complete(count: 50) + XCTAssertEqual(manager1.fractionCompleted, 0.5) XCTAssertEqual(overall.fractionCompleted, 0.5) - grandchildReporter1.complete(count: 50) - XCTAssertEqual(reporter1.fractionCompleted, 1.0) + grandchildManager1.complete(count: 50) + XCTAssertEqual(manager1.fractionCompleted, 1.0) XCTAssertEqual(overall.fractionCompleted, 1.0) - XCTAssertTrue(grandchildReporter1.isFinished) - XCTAssertTrue(reporter1.isFinished) + XCTAssertTrue(grandchildManager1.isFinished) + XCTAssertTrue(manager1.isFinished) XCTAssertTrue(overall.isFinished) } func testFourLevelTree() async throws { - let overall = ProgressReporter(totalCount: 100) + let overall = ProgressManager(totalCount: 100) XCTAssertEqual(overall.fractionCompleted, 0.0) let child1 = overall.subprogress(assigningCount: 100) - let reporter1 = child1.reporter(totalCount: 100) + let manager1 = child1.manager(totalCount: 100) - let grandchild1 = reporter1.subprogress(assigningCount: 100) - let grandchildReporter1 = grandchild1.reporter(totalCount: 100) + let grandchild1 = manager1.subprogress(assigningCount: 100) + let grandchildManager1 = grandchild1.manager(totalCount: 100) XCTAssertEqual(overall.fractionCompleted, 0.0) - let greatGrandchild1 = grandchildReporter1.subprogress(assigningCount: 100) - let greatGrandchildReporter1 = greatGrandchild1.reporter(totalCount: 100) + let greatGrandchild1 = grandchildManager1.subprogress(assigningCount: 100) + let greatGrandchildManager1 = greatGrandchild1.manager(totalCount: 100) - greatGrandchildReporter1.complete(count: 50) + greatGrandchildManager1.complete(count: 50) XCTAssertEqual(overall.fractionCompleted, 0.5) - greatGrandchildReporter1.complete(count: 50) + greatGrandchildManager1.complete(count: 50) XCTAssertEqual(overall.fractionCompleted, 1.0) - XCTAssertTrue(greatGrandchildReporter1.isFinished) - XCTAssertTrue(grandchildReporter1.isFinished) - XCTAssertTrue(reporter1.isFinished) + XCTAssertTrue(greatGrandchildManager1.isFinished) + XCTAssertTrue(grandchildManager1.isFinished) + XCTAssertTrue(manager1.isFinished) XCTAssertTrue(overall.isFinished) } } -/// Unit tests for propagation of type-safe metadata in ProgressReporter tree. -class TestProgressReporterAdditionalProperties: XCTestCase { - func doFileOperation(reportTo progress: consuming ProgressInput) async { - let reporter = progress.reporter(totalCount: 100) - reporter.withProperties { properties in +/// Unit tests for propagation of type-safe metadata in ProgressManager tree. +class TestProgressManagerAdditionalProperties: XCTestCase { + func doFileOperation(reportTo subprogress: consuming Subprogress) async { + let manager = subprogress.manager(totalCount: 100) + manager.withProperties { properties in properties.totalFileCount = 100 } - XCTAssertEqual(reporter.withProperties(\.totalFileCount), 100) + XCTAssertEqual(manager.withProperties(\.totalFileCount), 100) - reporter.complete(count: 100) - XCTAssertEqual(reporter.fractionCompleted, 1.0) - XCTAssertTrue(reporter.isFinished) + manager.complete(count: 100) + XCTAssertEqual(manager.fractionCompleted, 1.0) + XCTAssertTrue(manager.isFinished) - reporter.withProperties { properties in + manager.withProperties { properties in properties.completedFileCount = 100 } - XCTAssertEqual(reporter.withProperties(\.completedFileCount), 100) - XCTAssertEqual(reporter.withProperties(\.totalFileCount), 100) + XCTAssertEqual(manager.withProperties(\.completedFileCount), 100) + XCTAssertEqual(manager.withProperties(\.totalFileCount), 100) } func testDiscreteReporterWithFileProperties() async throws { - let fileReporter = ProgressReporter(totalCount: 3) - await doFileOperation(reportTo: fileReporter.subprogress(assigningCount: 3)) - XCTAssertEqual(fileReporter.fractionCompleted, 1.0) - XCTAssertEqual(fileReporter.completedCount, 3) - XCTAssertTrue(fileReporter.isFinished) - XCTAssertEqual(fileReporter.withProperties(\.totalFileCount), 0) - XCTAssertEqual(fileReporter.withProperties(\.completedFileCount), 0) + let fileProgressManager = ProgressManager(totalCount: 3) + await doFileOperation(reportTo: fileProgressManager.subprogress(assigningCount: 3)) + XCTAssertEqual(fileProgressManager.fractionCompleted, 1.0) + XCTAssertEqual(fileProgressManager.completedCount, 3) + XCTAssertTrue(fileProgressManager.isFinished) + XCTAssertEqual(fileProgressManager.withProperties(\.totalFileCount), 0) + XCTAssertEqual(fileProgressManager.withProperties(\.completedFileCount), 0) - let totalFileValues = fileReporter.values(property: ProgressReporter.Properties.TotalFileCount.self) + let totalFileValues = fileProgressManager.values(property: ProgressManager.Properties.TotalFileCount.self) XCTAssertEqual(totalFileValues, [0, 100]) - let reducedTotalFileValue = fileReporter.total(property: ProgressReporter.Properties.TotalFileCount.self, values: totalFileValues) + let reducedTotalFileValue = fileProgressManager.total(property: ProgressManager.Properties.TotalFileCount.self, values: totalFileValues) XCTAssertEqual(reducedTotalFileValue, 100) - let completedFileValues = fileReporter.values(property: ProgressReporter.Properties.CompletedFileCount.self) + let completedFileValues = fileProgressManager.values(property: ProgressManager.Properties.CompletedFileCount.self) XCTAssertEqual(completedFileValues, [0, 100]) - let reducedCompletedFileValue = fileReporter.total(property: ProgressReporter.Properties.CompletedFileCount.self, values: completedFileValues) + let reducedCompletedFileValue = fileProgressManager.total(property: ProgressManager.Properties.CompletedFileCount.self, values: completedFileValues) XCTAssertEqual(reducedCompletedFileValue, 100) } func testTwoLevelTreeWithOneChildWithFileProperties() async throws { - let overall = ProgressReporter(totalCount: 2) + let overall = ProgressManager(totalCount: 2) let progress1 = overall.subprogress(assigningCount: 1) - let reporter1 = progress1.reporter(totalCount: 10) - reporter1.withProperties { properties in + let manager1 = progress1.manager(totalCount: 10) + manager1.withProperties { properties in properties.totalFileCount = 10 properties.completedFileCount = 0 } - reporter1.complete(count: 10) + manager1.complete(count: 10) XCTAssertEqual(overall.fractionCompleted, 0.5) XCTAssertEqual(overall.withProperties(\.totalFileCount), 0) - XCTAssertEqual(reporter1.withProperties(\.totalFileCount), 10) - XCTAssertEqual(reporter1.withProperties(\.completedFileCount), 0) + XCTAssertEqual(manager1.withProperties(\.totalFileCount), 10) + XCTAssertEqual(manager1.withProperties(\.completedFileCount), 0) - let totalFileValues = overall.values(property: ProgressReporter.Properties.TotalFileCount.self) + let totalFileValues = overall.values(property: ProgressManager.Properties.TotalFileCount.self) XCTAssertEqual(totalFileValues, [0, 10]) - let completedFileValues = overall.values(property: ProgressReporter.Properties.CompletedFileCount.self) + let completedFileValues = overall.values(property: ProgressManager.Properties.CompletedFileCount.self) XCTAssertEqual(completedFileValues, [0, 0]) } func testTwoLevelTreeWithTwoChildrenWithFileProperties() async throws { - let overall = ProgressReporter(totalCount: 2) + let overall = ProgressManager(totalCount: 2) let progress1 = overall.subprogress(assigningCount: 1) - let reporter1 = progress1.reporter(totalCount: 10) + let manager1 = progress1.manager(totalCount: 10) - reporter1.withProperties { properties in + manager1.withProperties { properties in properties.totalFileCount = 11 properties.completedFileCount = 0 } let progress2 = overall.subprogress(assigningCount: 1) - let reporter2 = progress2.reporter(totalCount: 10) + let manager2 = progress2.manager(totalCount: 10) - reporter2.withProperties { properties in + manager2.withProperties { properties in properties.totalFileCount = 9 properties.completedFileCount = 0 } @@ -354,65 +354,65 @@ class TestProgressReporterAdditionalProperties: XCTestCase { XCTAssertEqual(overall.fractionCompleted, 0.0) XCTAssertEqual(overall.withProperties(\.totalFileCount), 0) XCTAssertEqual(overall.withProperties(\.completedFileCount), 0) - let totalFileValues = overall.values(property: ProgressReporter.Properties.TotalFileCount.self) + let totalFileValues = overall.values(property: ProgressManager.Properties.TotalFileCount.self) XCTAssertEqual(totalFileValues, [0, 11, 9]) - let completedFileValues = overall.values(property: ProgressReporter.Properties.CompletedFileCount.self) + let completedFileValues = overall.values(property: ProgressManager.Properties.CompletedFileCount.self) XCTAssertEqual(completedFileValues, [0, 0, 0]) // Update FileCounts - reporter1.withProperties { properties in + manager1.withProperties { properties in properties.completedFileCount = 1 } - reporter2.withProperties { properties in + manager2.withProperties { properties in properties.completedFileCount = 1 } XCTAssertEqual(overall.withProperties(\.completedFileCount), 0) - let updatedCompletedFileValues = overall.values(property: ProgressReporter.Properties.CompletedFileCount.self) + let updatedCompletedFileValues = overall.values(property: ProgressManager.Properties.CompletedFileCount.self) XCTAssertEqual(updatedCompletedFileValues, [0, 1, 1]) } func testThreeLevelTreeWithFileProperties() async throws { - let overall = ProgressReporter(totalCount: 1) + let overall = ProgressManager(totalCount: 1) let progress1 = overall.subprogress(assigningCount: 1) - let reporter1 = progress1.reporter(totalCount: 5) + let manager1 = progress1.manager(totalCount: 5) - let childProgress1 = reporter1.subprogress(assigningCount: 3) - let childReporter1 = childProgress1.reporter(totalCount: nil) - childReporter1.withProperties { properties in + let childProgress1 = manager1.subprogress(assigningCount: 3) + let childManager1 = childProgress1.manager(totalCount: nil) + childManager1.withProperties { properties in properties.totalFileCount += 10 } - XCTAssertEqual(childReporter1.withProperties(\.totalFileCount), 10) + XCTAssertEqual(childManager1.withProperties(\.totalFileCount), 10) - let preTotalFileValues = overall.values(property: ProgressReporter.Properties.TotalFileCount.self) + let preTotalFileValues = overall.values(property: ProgressManager.Properties.TotalFileCount.self) XCTAssertEqual(preTotalFileValues, [0, 0, 10]) - let childProgress2 = reporter1.subprogress(assigningCount: 2) - let childReporter2 = childProgress2.reporter(totalCount: nil) - childReporter2.withProperties { properties in + let childProgress2 = manager1.subprogress(assigningCount: 2) + let childManager2 = childProgress2.manager(totalCount: nil) + childManager2.withProperties { properties in properties.totalFileCount += 10 } - XCTAssertEqual(childReporter2.withProperties(\.totalFileCount), 10) + XCTAssertEqual(childManager2.withProperties(\.totalFileCount), 10) // Tests that totalFileCount propagates to root level XCTAssertEqual(overall.withProperties(\.totalFileCount), 0) - let totalFileValues = overall.values(property: ProgressReporter.Properties.TotalFileCount.self) + let totalFileValues = overall.values(property: ProgressManager.Properties.TotalFileCount.self) XCTAssertEqual(totalFileValues, [0, 0, 10, 10]) - reporter1.withProperties { properties in + manager1.withProperties { properties in properties.totalFileCount += 999 } - let totalUpdatedFileValues = overall.values(property: ProgressReporter.Properties.TotalFileCount.self) + let totalUpdatedFileValues = overall.values(property: ProgressManager.Properties.TotalFileCount.self) XCTAssertEqual(totalUpdatedFileValues, [0, 999, 10, 10]) } } #if FOUNDATION_FRAMEWORK -/// Unit tests for interop methods that support building Progress trees with both Progress and ProgressReporter -class TestProgressReporterInterop: XCTestCase { +/// Unit tests for interop methods that support building Progress trees with both Progress and ProgressManager +class TestProgressManagerInterop: XCTestCase { func doSomethingWithProgress(expectation1: XCTestExpectation, expectation2: XCTestExpectation) async -> Progress { let p = Progress(totalUnitCount: 2) Task.detached { @@ -424,13 +424,13 @@ class TestProgressReporterInterop: XCTestCase { return p } - func doSomethingWithReporter(progress: consuming ProgressInput?) async { - let reporter = progress?.reporter(totalCount: 4) - reporter?.complete(count: 2) - reporter?.complete(count: 2) + func doSomethingWithReporter(subprogress: consuming Subprogress?) async { + let manager = subprogress?.manager(totalCount: 4) + manager?.complete(count: 2) + manager?.complete(count: 2) } - func testInteropProgressParentProgressReporterChild() async throws { + func testInteropProgressParentProgressManagerChild() async throws { // Initialize a Progress Parent let overall = Progress.discreteProgress(totalUnitCount: 10) @@ -442,13 +442,13 @@ class TestProgressReporterInterop: XCTestCase { await fulfillment(of: [expectation1, expectation2], timeout: 10.0) - // Check if ProgressReporter values propagate to Progress parent + // Check if ProgressManager values propagate to Progress parent XCTAssertEqual(overall.fractionCompleted, 0.5) XCTAssertEqual(overall.completedUnitCount, 5) - // Add ProgressReporter as Child + // Add ProgressManager as Child let p2 = overall.makeChild(withPendingUnitCount: 5) - await doSomethingWithReporter(progress: p2) + await doSomethingWithReporter(subprogress: p2) // Check if Progress values propagate to Progress parent XCTAssertEqual(overall.fractionCompleted, 1.0) @@ -467,14 +467,14 @@ class TestProgressReporterInterop: XCTestCase { await fulfillment(of: [expectation1, expectation2], timeout: 10.0) - // Check if ProgressReporter values propagate to Progress parent + // Check if ProgressManager values propagate to Progress parent XCTAssertEqual(overall.fractionCompleted, 0.5) XCTAssertEqual(overall.completedUnitCount, 5) // Add ProgressMonitor as Child - let p2 = ProgressReporter(totalCount: 10) - let p2Monitor = p2.output - overall.addChild(p2Monitor, withPendingUnitCount: 5) + let p2 = ProgressManager(totalCount: 10) + let p2Reporter = p2.reporter + overall.addChild(p2Reporter, withPendingUnitCount: 5) p2.complete(count: 10) @@ -495,15 +495,15 @@ class TestProgressReporterInterop: XCTestCase { await fulfillment(of: [expectation1, expectation2], timeout: 10.0) - // Check if ProgressReporter values propagate to Progress parent + // Check if ProgressManager values propagate to Progress parent XCTAssertEqual(overall.fractionCompleted, 0.5) XCTAssertEqual(overall.completedUnitCount, 5) // Add ProgressMonitor with CompletedCount 3 as Child - let p2 = ProgressReporter(totalCount: 10) + let p2 = ProgressManager(totalCount: 10) p2.complete(count: 3) - let p2Monitor = p2.output - overall.addChild(p2Monitor, withPendingUnitCount: 5) + let p2Reporter = p2.reporter + overall.addChild(p2Reporter, withPendingUnitCount: 5) p2.complete(count: 7) @@ -512,53 +512,53 @@ class TestProgressReporterInterop: XCTestCase { XCTAssertEqual(overall.completedUnitCount, 10) } - func testInteropProgressReporterParentProgressChild() async throws { - // Initialize ProgressReporter parent - let overallReporter = ProgressReporter(totalCount: 10) + func testInteropProgressManagerParentProgressChild() async throws { + // Initialize ProgressManager parent + let overallManager = ProgressManager(totalCount: 10) - // Add ProgressReporter as Child - await doSomethingWithReporter(progress: overallReporter.subprogress(assigningCount: 5)) + // Add ProgressManager as Child + await doSomethingWithReporter(subprogress: overallManager.subprogress(assigningCount: 5)) - // Check if ProgressReporter values propagate to ProgressReporter parent - XCTAssertEqual(overallReporter.fractionCompleted, 0.5) - XCTAssertEqual(overallReporter.completedCount, 5) + // Check if ProgressManager values propagate to ProgressManager parent + XCTAssertEqual(overallManager.fractionCompleted, 0.5) + XCTAssertEqual(overallManager.completedCount, 5) // Interop: Add Progress as Child let expectation1 = XCTestExpectation(description: "Set completed unit count to 1") let expectation2 = XCTestExpectation(description: "Set completed unit count to 2") let p2 = await doSomethingWithProgress(expectation1: expectation1, expectation2: expectation2) - overallReporter.subprogress(assigningCount: 5, to: p2) + overallManager.subprogress(assigningCount: 5, to: p2) await fulfillment(of: [expectation1, expectation2], timeout: 10.0) // Check if Progress values propagate to ProgressRerpoter parent - XCTAssertEqual(overallReporter.completedCount, 10) - XCTAssertEqual(overallReporter.totalCount, 10) + XCTAssertEqual(overallManager.completedCount, 10) + XCTAssertEqual(overallManager.totalCount, 10) //TODO: Somehow this sometimes gets updated to 1.25 instead of just 1.0 - XCTAssertEqual(overallReporter.fractionCompleted, 1.0) + XCTAssertEqual(overallManager.fractionCompleted, 1.0) } func getProgressWithTotalCountInitialized() -> Progress { return Progress(totalUnitCount: 5) } - func receiveProgress(progress: consuming ProgressInput) { - let _ = progress.reporter(totalCount: 5) + func receiveProgress(progress: consuming Subprogress) { + let _ = progress.manager(totalCount: 5) } - func testInteropProgressReporterParentProgressChildConsistency() async throws { - let overallReporter = ProgressReporter(totalCount: nil) + func testInteropProgressManagerParentProgressChildConsistency() async throws { + let overallReporter = ProgressManager(totalCount: nil) let child = overallReporter.subprogress(assigningCount: 5) receiveProgress(progress: child) XCTAssertNil(overallReporter.totalCount) - let overallReporter2 = ProgressReporter(totalCount: nil) + let overallReporter2 = ProgressManager(totalCount: nil) let interopChild = getProgressWithTotalCountInitialized() overallReporter2.subprogress(assigningCount: 5, to: interopChild) XCTAssertNil(overallReporter2.totalCount) } - func testInteropProgressParentProgressReporterChildConsistency() async throws { + func testInteropProgressParentProgressManagerChildConsistency() async throws { let overallProgress = Progress() let child = Progress(totalUnitCount: 5) overallProgress.addChild(child, withPendingUnitCount: 5) From 8f3d6c0547815f2bb5fcd5c568791be7a8c0a8b6 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 19 May 2025 11:13:37 -0700 Subject: [PATCH 46/85] renamed files + updated CMake files --- Sources/FoundationEssentials/CMakeLists.txt | 2 +- .../{ProgressReporter => ProgressManager}/CMakeLists.txt | 5 +++-- .../ProgressFraction.swift | 0 .../ProgressManager+Interop.swift | 0 .../ProgressManager+Properties.swift | 0 .../ProgressManager.swift | 0 .../ProgressReporter.swift | 7 +++---- .../Subprogress.swift | 0 Sources/FoundationInternationalization/CMakeLists.txt | 2 +- .../{ProgressReporter => ProgressManager}/CMakeLists.txt | 4 ++-- .../ProgressManager+FileFormatStyle.swift | 0 .../ProgressManager+FormatStyle.swift | 0 12 files changed, 10 insertions(+), 10 deletions(-) rename Sources/FoundationEssentials/{ProgressReporter => ProgressManager}/CMakeLists.txt (86%) rename Sources/FoundationEssentials/{ProgressReporter => ProgressManager}/ProgressFraction.swift (100%) rename Sources/FoundationEssentials/{ProgressReporter => ProgressManager}/ProgressManager+Interop.swift (100%) rename Sources/FoundationEssentials/{ProgressReporter => ProgressManager}/ProgressManager+Properties.swift (100%) rename Sources/FoundationEssentials/{ProgressReporter => ProgressManager}/ProgressManager.swift (100%) rename Sources/FoundationEssentials/{ProgressReporter => ProgressManager}/ProgressReporter.swift (87%) rename Sources/FoundationEssentials/{ProgressReporter => ProgressManager}/Subprogress.swift (100%) rename Sources/FoundationInternationalization/{ProgressReporter => ProgressManager}/CMakeLists.txt (87%) rename Sources/FoundationInternationalization/{ProgressReporter => ProgressManager}/ProgressManager+FileFormatStyle.swift (100%) rename Sources/FoundationInternationalization/{ProgressReporter => ProgressManager}/ProgressManager+FormatStyle.swift (100%) diff --git a/Sources/FoundationEssentials/CMakeLists.txt b/Sources/FoundationEssentials/CMakeLists.txt index 8feb8e85b..b9a493cd4 100644 --- a/Sources/FoundationEssentials/CMakeLists.txt +++ b/Sources/FoundationEssentials/CMakeLists.txt @@ -44,7 +44,7 @@ add_subdirectory(JSON) add_subdirectory(Locale) add_subdirectory(Predicate) add_subdirectory(ProcessInfo) -add_subdirectory(ProgressReporter) +add_subdirectory(ProgressManager) add_subdirectory(PropertyList) add_subdirectory(String) add_subdirectory(TimeZone) diff --git a/Sources/FoundationEssentials/ProgressReporter/CMakeLists.txt b/Sources/FoundationEssentials/ProgressManager/CMakeLists.txt similarity index 86% rename from Sources/FoundationEssentials/ProgressReporter/CMakeLists.txt rename to Sources/FoundationEssentials/ProgressManager/CMakeLists.txt index 73004105d..a3f1e920b 100644 --- a/Sources/FoundationEssentials/ProgressReporter/CMakeLists.txt +++ b/Sources/FoundationEssentials/ProgressManager/CMakeLists.txt @@ -13,7 +13,8 @@ ##===----------------------------------------------------------------------===## target_sources(FoundationEssentials PRIVATE ProgressFraction.swift + ProgressManager.swift + ProgressManager+Interop.swift + ProgressManager+Properties.swift ProgressReporter.swift - ProgressReporter+Interop.swift - ProgressReporter+Properties.swift Subprogress.swift) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressFraction.swift b/Sources/FoundationEssentials/ProgressManager/ProgressFraction.swift similarity index 100% rename from Sources/FoundationEssentials/ProgressReporter/ProgressFraction.swift rename to Sources/FoundationEssentials/ProgressManager/ProgressFraction.swift diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressManager+Interop.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift similarity index 100% rename from Sources/FoundationEssentials/ProgressReporter/ProgressManager+Interop.swift rename to Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressManager+Properties.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties.swift similarity index 100% rename from Sources/FoundationEssentials/ProgressReporter/ProgressManager+Properties.swift rename to Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties.swift diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift similarity index 100% rename from Sources/FoundationEssentials/ProgressReporter/ProgressManager.swift rename to Sources/FoundationEssentials/ProgressManager/ProgressManager.swift diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift similarity index 87% rename from Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift rename to Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift index 83fe99a13..aa24c19fb 100644 --- a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift @@ -15,6 +15,7 @@ /// ProgressReporter is a wrapper for ProgressManager that carries information about ProgressManager. /// /// It is read-only and can be added as a child of another ProgressManager. +@dynamicMemberLookup @Observable public final class ProgressReporter: Sendable { var totalCount: Int? { @@ -37,14 +38,12 @@ manager.isFinished } - // TODO: Need to figure out how to expose properties such as totalFileCount and completedFileCount - var properties: ProgressManager.Values { + public subscript(dynamicMember key: KeyPath) -> P.T { manager.withProperties { properties in - return properties + properties[dynamicMember: key] } } - internal let manager: ProgressManager internal init(manager: ProgressManager) { diff --git a/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift b/Sources/FoundationEssentials/ProgressManager/Subprogress.swift similarity index 100% rename from Sources/FoundationEssentials/ProgressReporter/Subprogress.swift rename to Sources/FoundationEssentials/ProgressManager/Subprogress.swift diff --git a/Sources/FoundationInternationalization/CMakeLists.txt b/Sources/FoundationInternationalization/CMakeLists.txt index 6c9ace4a0..52b5af368 100644 --- a/Sources/FoundationInternationalization/CMakeLists.txt +++ b/Sources/FoundationInternationalization/CMakeLists.txt @@ -25,7 +25,7 @@ add_subdirectory(Formatting) add_subdirectory(ICU) add_subdirectory(Locale) add_subdirectory(Predicate) -add_subdirectory(ProgressReporter) +add_subdirectory(ProgressManager) add_subdirectory(String) add_subdirectory(TimeZone) diff --git a/Sources/FoundationInternationalization/ProgressReporter/CMakeLists.txt b/Sources/FoundationInternationalization/ProgressManager/CMakeLists.txt similarity index 87% rename from Sources/FoundationInternationalization/ProgressReporter/CMakeLists.txt rename to Sources/FoundationInternationalization/ProgressManager/CMakeLists.txt index 5cc2cc948..8113765bf 100644 --- a/Sources/FoundationInternationalization/ProgressReporter/CMakeLists.txt +++ b/Sources/FoundationInternationalization/ProgressManager/CMakeLists.txt @@ -14,5 +14,5 @@ target_include_directories(FoundationInternationalization PRIVATE .) target_sources(FoundationInternationalization PRIVATE - ProgressReporter+FileFormatStyle.swift - ProgressReporter+FormatStyle.swift) + ProgressManager+FileFormatStyle.swift + ProgressManager+FormatStyle.swift) diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressManager+FileFormatStyle.swift b/Sources/FoundationInternationalization/ProgressManager/ProgressManager+FileFormatStyle.swift similarity index 100% rename from Sources/FoundationInternationalization/ProgressReporter/ProgressManager+FileFormatStyle.swift rename to Sources/FoundationInternationalization/ProgressManager/ProgressManager+FileFormatStyle.swift diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressManager+FormatStyle.swift b/Sources/FoundationInternationalization/ProgressManager/ProgressManager+FormatStyle.swift similarity index 100% rename from Sources/FoundationInternationalization/ProgressReporter/ProgressManager+FormatStyle.swift rename to Sources/FoundationInternationalization/ProgressManager/ProgressManager+FormatStyle.swift From 01f8dd2dd0610432ae37b435683fb1a446aca4f5 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 19 May 2025 11:34:42 -0700 Subject: [PATCH 47/85] resolve xcconfig conflict --- .../ProgressManager/ProgressFormatting.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift diff --git a/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift b/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift new file mode 100644 index 000000000..88a0d91a9 --- /dev/null +++ b/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@available(FoundationPreview 6.2, *) +public protocol ProgressFormatting {} + +@available(FoundationPreview 6.2, *) +extension ProgressManager: ProgressFormatting {} + +@available(FoundationPreview 6.2, *) +extension ProgressReporter: ProgressFormatting {} + From 8dc20aab08b4341e0156f93d458ae293bafff30e Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 19 May 2025 12:22:54 -0700 Subject: [PATCH 48/85] add minimally working format style to progress reporter + draft format protocol --- .../ProgressManager/CMakeLists.txt | 5 +- .../ProgressManager/ProgressFormatting.swift | 11 ++ .../ProgressReporter+FileFormatStyle.swift | 158 +++++++++++++++++ .../ProgressReporter+FormatStyle.swift | 162 ++++++++++++++++++ 4 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FileFormatStyle.swift create mode 100644 Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FormatStyle.swift diff --git a/Sources/FoundationInternationalization/ProgressManager/CMakeLists.txt b/Sources/FoundationInternationalization/ProgressManager/CMakeLists.txt index 8113765bf..988db2502 100644 --- a/Sources/FoundationInternationalization/ProgressManager/CMakeLists.txt +++ b/Sources/FoundationInternationalization/ProgressManager/CMakeLists.txt @@ -14,5 +14,8 @@ target_include_directories(FoundationInternationalization PRIVATE .) target_sources(FoundationInternationalization PRIVATE + ProgressFormatting.swift ProgressManager+FileFormatStyle.swift - ProgressManager+FormatStyle.swift) + ProgressManager+FormatStyle.swift + ProgressReporter+FileFormatStyle.swift + ProgressReporter+FormatStyle.swift) diff --git a/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift b/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift index 88a0d91a9..f27b82ad5 100644 --- a/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift +++ b/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift @@ -19,3 +19,14 @@ extension ProgressManager: ProgressFormatting {} @available(FoundationPreview 6.2, *) extension ProgressReporter: ProgressFormatting {} +protocol ProgressFormatStyle: FormatStyle { + + associatedtype Option: Sendable, Codable, Hashable, Equatable + + var option: Option { get } + + func format(_ value: any ProgressFormatting) -> String + + func locale(_ locale: Locale) -> any ProgressFormatStyle +} + diff --git a/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FileFormatStyle.swift b/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FileFormatStyle.swift new file mode 100644 index 000000000..8089caa11 --- /dev/null +++ b/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FileFormatStyle.swift @@ -0,0 +1,158 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +#if canImport(FoundationEssentials) +import FoundationEssentials +#endif + +@available(FoundationPreview 6.2, *) +extension ProgressReporter { + //TODO: rdar://149092406 Manual Codable Conformance + public struct FileFormatStyle: Sendable, Codable, Equatable, Hashable { + + internal struct Option: Sendable, Codable, Equatable, Hashable { + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + self.rawOption = try container.decode(RawOption.self) + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawOption) + } + + internal static var file: Option { Option(.file) } + + fileprivate enum RawOption: Codable, Equatable, Hashable { + case file + } + + fileprivate var rawOption: RawOption + + private init(_ rawOption: RawOption) { + self.rawOption = rawOption + } + } + + struct CodableRepresentation: Codable { + let locale: Locale + let option: Option + } + + var codableRepresentation: CodableRepresentation { + .init(locale: self.locale, option: self.option) + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(CodableRepresentation.self) + self.locale = rawValue.locale + self.option = rawValue.option + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(codableRepresentation) + } + + public var locale: Locale + let option: Option + + internal init(_ option: Option, locale: Locale = .autoupdatingCurrent) { + self.locale = locale + self.option = option + } + } +} + + +@available(FoundationPreview 6.2, *) +extension ProgressReporter.FileFormatStyle: FormatStyle { + + public func locale(_ locale: Locale) -> ProgressReporter.FileFormatStyle { + .init(self.option, locale: locale) + } + + public func format(_ reporter: ProgressReporter) -> String { + switch self.option.rawOption { + + case .file: + #if FOUNDATION_FRAMEWORK + var fileCountLSR: LocalizedStringResource? + var byteCountLSR: LocalizedStringResource? + var throughputLSR: LocalizedStringResource? + var timeRemainingLSR: LocalizedStringResource? + + let properties = reporter.manager.withProperties(\.self) + + fileCountLSR = LocalizedStringResource("\(properties.completedFileCount, format: IntegerFormatStyle()) of \(properties.totalFileCount, format: IntegerFormatStyle()) files", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + + + byteCountLSR = LocalizedStringResource("\(properties.completedByteCount, format: ByteCountFormatStyle()) of \(properties.totalByteCount, format: ByteCountFormatStyle())", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + + + throughputLSR = LocalizedStringResource("\(properties.throughput, format: ByteCountFormatStyle())/s", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + + timeRemainingLSR = LocalizedStringResource("\(properties.estimatedTimeRemaining, format: Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .wide)) remaining", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + + return """ + \(String(localized: fileCountLSR ?? "")) + \(String(localized: byteCountLSR ?? "")) + \(String(localized: throughputLSR ?? "")) + \(String(localized: timeRemainingLSR ?? "")) + """ + #else + + var fileCountString: String? + var byteCountString: String? + var throughputString: String? + var timeRemainingString: String? + + let properties = manager.withProperties(\.self) + + + fileCountString = "\(properties.completedFileCount.formatted(IntegerFormatStyle(locale: self.locale))) / \(properties.totalFileCount.formatted(IntegerFormatStyle(locale: self.locale)))" + + + byteCountString = "\(properties.completedByteCount.formatted(ByteCountFormatStyle(locale: self.locale))) / \(properties.totalByteCount.formatted(ByteCountFormatStyle(locale: self.locale)))" + + throughputString = "\(properties.throughput.formatted(ByteCountFormatStyle(locale: self.locale)))/s" + + var formatStyle = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .wide) + formatStyle.locale = self.locale + timeRemainingString = "\(properties.estimatedTimeRemaining.formatted(formatStyle)) remaining" + + return """ + \(fileCountString ?? "") + \(byteCountString ?? "") + \(throughputString ?? "") + \(timeRemainingString ?? "") + """ + #endif + } + } +} + +@available(FoundationPreview 6.2, *) +// Make access easier to format ProgressReporter +extension ProgressReporter { + public func formatted(_ style: ProgressReporter.FileFormatStyle) -> String { + style.format(self) + } +} + +@available(FoundationPreview 6.2, *) +extension FormatStyle where Self == ProgressReporter.FileFormatStyle { + public static var file: Self { + .init(.file) + } +} diff --git a/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FormatStyle.swift b/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FormatStyle.swift new file mode 100644 index 000000000..59220075a --- /dev/null +++ b/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FormatStyle.swift @@ -0,0 +1,162 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +#if canImport(FoundationEssentials) +import FoundationEssentials +#endif +// Outlines the options available to format ProgressReporter +@available(FoundationPreview 6.2, *) +extension ProgressReporter { + + public struct FormatStyle: Sendable, Codable, Equatable, Hashable { + + // Outlines the options available to format ProgressReporter + internal struct Option: Sendable, Codable, Hashable, Equatable { + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + rawOption = try container.decode(RawOption.self) + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawOption) + } + + /// Option specifying `fractionCompleted`. + /// + /// For example, 20% completed. + /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance that should be used to format `fractionCompleted`. + /// - Returns: A `LocalizedStringResource` for formatted `fractionCompleted`. + internal static func fractionCompleted(format style: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent() + ) -> Option { + return Option(.fractionCompleted(style)) + } + + /// Option specifying `completedCount` / `totalCount`. + /// + /// For example, 5 of 10. + /// - Parameter style: An `IntegerFormatStyle` instance that should be used to format `completedCount` and `totalCount`. + /// - Returns: A `LocalizedStringResource` for formatted `completedCount` / `totalCount`. + internal static func count(format style: IntegerFormatStyle = IntegerFormatStyle() + ) -> Option { + return Option(.count(style)) + } + + fileprivate enum RawOption: Codable, Hashable, Equatable { + case count(IntegerFormatStyle) + case fractionCompleted(FloatingPointFormatStyle.Percent) + } + + fileprivate var rawOption: RawOption + + private init(_ rawOption: RawOption) { + self.rawOption = rawOption + } + } + + struct CodableRepresentation: Codable { + let locale: Locale + let option: Option + } + + var codableRepresentation: CodableRepresentation { + .init(locale: self.locale, option: self.option) + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(CodableRepresentation.self) + self.locale = rawValue.locale + self.option = rawValue.option + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(codableRepresentation) + } + + public var locale: Locale + let option: Option + + internal init(_ option: Option, locale: Locale = .autoupdatingCurrent) { + self.locale = locale + self.option = option + } + } +} + +@available(FoundationPreview 6.2, *) +extension ProgressReporter.FormatStyle: FormatStyle { + + public func locale(_ locale: Locale) -> ProgressReporter.FormatStyle { + .init(self.option, locale: locale) + } + + public func format(_ reporter: ProgressReporter) -> String { + switch self.option.rawOption { + case .count(let countStyle): + let count = reporter.manager.withProperties { p in + return (p.completedCount, p.totalCount) + } + #if FOUNDATION_FRAMEWORK + let countLSR = LocalizedStringResource("\(count.0, format: countStyle) of \(count.1 ?? 0, format: countStyle)", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + return String(localized: countLSR) + #else + return "\(count.0.formatted(countStyle.locale(self.locale))) / \((count.1 ?? 0).formatted(countStyle.locale(self.locale)))" + #endif + + case .fractionCompleted(let fractionStyle): + #if FOUNDATION_FRAMEWORK + let fractionLSR = LocalizedStringResource("\(reporter.fractionCompleted, format: fractionStyle) completed", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + return String(localized: fractionLSR) + #else + return "\(manager.fractionCompleted.formatted(fractionStyle.locale(self.locale)))" + #endif + } + } +} + +@available(FoundationPreview 6.2, *) +// Make access easier to format ProgressReporter +extension ProgressReporter { + +#if FOUNDATION_FRAMEWORK + public func formatted(_ style: F) -> F.FormatOutput where F.FormatInput == ProgressReporter { + style.format(self) + } +#else + public func formatted(_ style: F) -> F.FormatOutput where F.FormatInput == ProgressReporter { + style.format(self) + } +#endif // FOUNDATION_FRAMEWORK + + public func formatted() -> String { + self.formatted(.fractionCompleted()) + } + +} + +@available(FoundationPreview 6.2, *) +extension FormatStyle where Self == ProgressReporter.FormatStyle { + + public static func fractionCompleted( + format: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent() + ) -> Self { + .init(.fractionCompleted(format: format)) + } + + public static func count( + format: IntegerFormatStyle = IntegerFormatStyle() + ) -> Self { + .init(.count(format: format)) + } +} From 0be8f6cb9a1e5f12a3b959f76b6ed0304f95d4c4 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 19 May 2025 12:24:24 -0700 Subject: [PATCH 49/85] import observation in file --- .../ProgressManager/ProgressReporter.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift index aa24c19fb..39d318bdc 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift @@ -10,8 +10,9 @@ // //===----------------------------------------------------------------------===// -@available(FoundationPreview 6.2, *) +import Observation +@available(FoundationPreview 6.2, *) /// ProgressReporter is a wrapper for ProgressManager that carries information about ProgressManager. /// /// It is read-only and can be added as a child of another ProgressManager. From c4088f6fa78958ce08cd3c758e53e6bdf647a60b Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 19 May 2025 12:40:13 -0700 Subject: [PATCH 50/85] replace dynamicMemberLookup with withProperties in ProgressReporter --- .../ProgressManager/ProgressManager.swift | 10 ++++++++++ .../ProgressManager/ProgressReporter.swift | 8 +++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 6991663fe..4781a3fff 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -536,6 +536,16 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { parentReporter.updateChildFraction(from: updates.0, to: updates.1, portion: portionOfParent) } + internal func getAdditionalProperties(_ closure: @Sendable (Values) throws -> T) rethrows -> T { + try state.withLock { state in + let values = Values(manager: self, state: state) + // No need to modify state since this is read-only + let result = try closure(values) + // No state update after closure execution + return result + } + } + // MARK: Propagation of Additional Properties Methods (Dual Mode of Operations) private func getFlattenedChildrenValues(property metatype: P.Type, state: inout State) -> [P.T?] { let childrenDictionary = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift index 39d318bdc..411bc643a 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift @@ -16,7 +16,6 @@ import Observation /// ProgressReporter is a wrapper for ProgressManager that carries information about ProgressManager. /// /// It is read-only and can be added as a child of another ProgressManager. -@dynamicMemberLookup @Observable public final class ProgressReporter: Sendable { var totalCount: Int? { @@ -39,10 +38,9 @@ import Observation manager.isFinished } - public subscript(dynamicMember key: KeyPath) -> P.T { - manager.withProperties { properties in - properties[dynamicMember: key] - } + /// Reads properties that convey additional information about progress. + public func withProperties(_ closure: @Sendable (ProgressManager.Values) throws -> T) rethrows -> T { + return try manager.getAdditionalProperties(closure) } internal let manager: ProgressManager From 08d4b0e69d8d6fd430a435d52addb43c7fc31028 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 19 May 2025 22:16:50 -0700 Subject: [PATCH 51/85] comment out formatting --- .../ProgressManager/ProgressFormatting.swift | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift b/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift index f27b82ad5..25ff11840 100644 --- a/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift +++ b/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift @@ -10,23 +10,23 @@ // //===----------------------------------------------------------------------===// -@available(FoundationPreview 6.2, *) -public protocol ProgressFormatting {} - -@available(FoundationPreview 6.2, *) -extension ProgressManager: ProgressFormatting {} - -@available(FoundationPreview 6.2, *) -extension ProgressReporter: ProgressFormatting {} - -protocol ProgressFormatStyle: FormatStyle { - - associatedtype Option: Sendable, Codable, Hashable, Equatable - - var option: Option { get } - - func format(_ value: any ProgressFormatting) -> String - - func locale(_ locale: Locale) -> any ProgressFormatStyle -} +//@available(FoundationPreview 6.2, *) +//public protocol ProgressFormatting {} +// +//@available(FoundationPreview 6.2, *) +//extension ProgressManager: ProgressFormatting {} +// +//@available(FoundationPreview 6.2, *) +//extension ProgressReporter: ProgressFormatting {} +// +//protocol ProgressFormatStyle: FormatStyle { +// +// associatedtype Option: Sendable, Codable, Hashable, Equatable +// +// var option: Option { get } +// +// func format(_ value: any ProgressFormatting) -> String +// +// func locale(_ locale: Locale) -> any ProgressFormatStyle +//} From 630857949c59195510b74a452b0c01ee7060b953 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 20 May 2025 13:59:51 -0700 Subject: [PATCH 52/85] rename T to Valeu --- .../ProgressManager/ProgressManager.swift | 58 ++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 4781a3fff..7fadf5124 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -125,9 +125,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// A type that conveys task-specific information on progress. public protocol Property { - associatedtype T: Sendable, Hashable, Equatable + associatedtype Value: Sendable, Hashable, Equatable - static var defaultValue: T { get } + static var defaultValue: Value { get } } /// A container that holds values for properties that specify information on progress. @@ -189,9 +189,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } /// Returns a property value that a key path indicates. If value is not defined, returns property's `defaultValue`. - public subscript(dynamicMember key: KeyPath) -> P.T { + public subscript(dynamicMember key: KeyPath) -> P.Value { get { - return state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T ?? P.self.defaultValue + return state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.Value ?? P.self.defaultValue } set { @@ -199,12 +199,12 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = newValue // Generate an array of myself + children values of the property - let flattenedChildrenValues: [P.T?] = { + let flattenedChildrenValues: [P.Value?] = { let childrenDictionary = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] - var childrenValues: [P.T?] = [] + var childrenValues: [P.Value?] = [] if let dictionary = childrenDictionary { for (_, value) in dictionary { - if let value = value as? [P.T?] { + if let value = value as? [P.Value?] { childrenValues.append(contentsOf: value) } } @@ -213,7 +213,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { }() // Send the array of myself + children values of property to parents - let updateValueForParent: [P.T?] = [newValue] + flattenedChildrenValues + let updateValueForParent: [P.Value?] = [newValue] + flattenedChildrenValues manager.parents.withLock { [manager] parents in for (parent, _) in parents { parent.updateChildrenOtherProperties(property: P.self, child: manager, value: updateValueForParent) @@ -320,10 +320,10 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// Returns an array of values for specified property in subtree. /// - Parameter metatype: Type of property. /// - Returns: Array of values for property. - public func values(property metatype: P.Type) -> [P.T?] { + public func values(of property: P.Type) -> [P.Value?] { return state.withLock { state in - let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) - return [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T ?? P.defaultValue] + childrenValues.map { $0 ?? P.defaultValue } + let childrenValues = getFlattenedChildrenValues(property: property, state: &state) + return [state.otherProperties[AnyMetatypeWrapper(metatype: property)] as? P.Value ?? P.defaultValue] + childrenValues.map { $0 ?? P.defaultValue } } } @@ -331,13 +331,13 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// Returns the aggregated result of values. /// - Parameters: /// - property: Type of property. - /// - values:Sum of values. - public func total(property: P.Type, values: [P.T?]) -> P.T where P.T: AdditiveArithmetic { - let droppedNil = values.compactMap { $0 } - return droppedNil.reduce(P.T.zero, +) + public func total(of property: P.Type) -> P.Value where P.Value: AdditiveArithmetic { + let droppedNil = values(of: property).compactMap { $0 } + return droppedNil.reduce(P.Value.zero, +) } /// Mutates any settable properties that convey information about progress. + /// public func withProperties(_ closure: @Sendable (inout Values) throws -> T) rethrows -> T { return try state.withLock { state in var values = Values(manager: self, state: state) @@ -349,6 +349,24 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } +// public func withProperties( +// _ closure: (inout sending Values) throws(E) -> sending T +// ) throws(E) -> sending T { +// return try state.withLock { state in +// var values = Values(manager: self, state: state) +// // This is done to avoid copy on write later +// state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction()), otherProperties: [:], childrenOtherProperties: [:]) +// +// do { +// let result = try closure(&values) +// state = values.state +// return result +// } catch let localError { +// throw localError as! E +// } +// } +// } + //MARK: ProgressManager Properties getters /// Returns nil if `self` was instantiated without total units; /// returns a `Int` value otherwise. @@ -547,12 +565,12 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } // MARK: Propagation of Additional Properties Methods (Dual Mode of Operations) - private func getFlattenedChildrenValues(property metatype: P.Type, state: inout State) -> [P.T?] { + private func getFlattenedChildrenValues(property metatype: P.Type, state: inout State) -> [P.Value?] { let childrenDictionary = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] - var childrenValues: [P.T?] = [] + var childrenValues: [P.Value?] = [] if let dictionary = childrenDictionary { for (_, value) in dictionary { - if let value = value as? [P.T?] { + if let value = value as? [P.Value?] { childrenValues.append(contentsOf: value) } } @@ -593,7 +611,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } - private func updateChildrenOtherProperties(property metatype: P.Type, child: ProgressManager, value: [P.T?]) { + private func updateChildrenOtherProperties(property metatype: P.Type, child: ProgressManager, value: [P.Value?]) { state.withLock { state in let myEntries = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: metatype)] if myEntries != nil { @@ -606,7 +624,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } // Ask parent to update their entry with my value + new children value let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) - let updatedParentEntry: [P.T?] = [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T] + childrenValues + let updatedParentEntry: [P.Value?] = [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.Value] + childrenValues parents.withLock { parents in for (parent, _) in parents { parent.updateChildrenOtherProperties(property: metatype, child: self, value: updatedParentEntry) From 7f1fa05dd71d2245adfb894c1e2b15e22cd6ceac Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 20 May 2025 14:00:30 -0700 Subject: [PATCH 53/85] draft formatstyle stuff --- .../ProgressManager/ProgressFormatting.swift | 8 ++++++-- .../ProgressReporter+FileFormatStyle.swift | 4 ++-- .../ProgressManager/ProgressReporter+FormatStyle.swift | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift b/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift index 25ff11840..61c67a1cd 100644 --- a/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift +++ b/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift @@ -11,7 +11,10 @@ //===----------------------------------------------------------------------===// //@available(FoundationPreview 6.2, *) -//public protocol ProgressFormatting {} +//public protocol ProgressFormatting { +// +//var fractionCompleted: Double +//} // //@available(FoundationPreview 6.2, *) //extension ProgressManager: ProgressFormatting {} @@ -19,7 +22,7 @@ //@available(FoundationPreview 6.2, *) //extension ProgressReporter: ProgressFormatting {} // -//protocol ProgressFormatStyle: FormatStyle { +//struct GenericProgressFormatStyle: FormatStyle { // // associatedtype Option: Sendable, Codable, Hashable, Equatable // @@ -30,3 +33,4 @@ // func locale(_ locale: Locale) -> any ProgressFormatStyle //} +//struct FileProgressFormatStyle: FormatStyle diff --git a/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FileFormatStyle.swift b/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FileFormatStyle.swift index 8089caa11..0e2b15f81 100644 --- a/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FileFormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FileFormatStyle.swift @@ -92,7 +92,7 @@ extension ProgressReporter.FileFormatStyle: FormatStyle { var throughputLSR: LocalizedStringResource? var timeRemainingLSR: LocalizedStringResource? - let properties = reporter.manager.withProperties(\.self) + let properties = reporter.withProperties(\.self) fileCountLSR = LocalizedStringResource("\(properties.completedFileCount, format: IntegerFormatStyle()) of \(properties.totalFileCount, format: IntegerFormatStyle()) files", locale: self.locale, bundle: .forClass(ProgressReporter.self)) @@ -117,7 +117,7 @@ extension ProgressReporter.FileFormatStyle: FormatStyle { var throughputString: String? var timeRemainingString: String? - let properties = manager.withProperties(\.self) + let properties = reporter.withProperties(\.self) fileCountString = "\(properties.completedFileCount.formatted(IntegerFormatStyle(locale: self.locale))) / \(properties.totalFileCount.formatted(IntegerFormatStyle(locale: self.locale)))" diff --git a/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FormatStyle.swift b/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FormatStyle.swift index 59220075a..869399aba 100644 --- a/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FormatStyle.swift +++ b/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FormatStyle.swift @@ -119,7 +119,7 @@ extension ProgressReporter.FormatStyle: FormatStyle { let fractionLSR = LocalizedStringResource("\(reporter.fractionCompleted, format: fractionStyle) completed", locale: self.locale, bundle: .forClass(ProgressReporter.self)) return String(localized: fractionLSR) #else - return "\(manager.fractionCompleted.formatted(fractionStyle.locale(self.locale)))" + return "\(reporter.fractionCompleted.formatted(fractionStyle.locale(self.locale)))" #endif } } From 50024183c376b0c6cf34eeade5f4eed300c1a4b0 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 27 May 2025 16:23:58 -0700 Subject: [PATCH 54/85] cycle detection --- .../ProgressManager/ProgressManager.swift | 22 +++++ .../ProgressFractionTests.swift | 2 +- .../ProgressManagerTests.swift | 89 ++++++++++++++++--- 3 files changed, 100 insertions(+), 13 deletions(-) rename Tests/FoundationEssentialsTests/{ProgressReporter => ProgressManager}/ProgressFractionTests.swift (98%) rename Tests/FoundationEssentialsTests/{ProgressReporter => ProgressManager}/ProgressManagerTests.swift (86%) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 7fadf5124..cf0b7e1fb 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -294,6 +294,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// - reporter: A `ProgressReporter` instance. /// - portionOfParent: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. public func assign(count portionOfParent: Int, to reporter: ProgressReporter) { + precondition(isCycle(reporter: reporter) == false, "Creating a cycle is not allowed.") + // get the actual progress from within the reporter, then add as children let actualManager = reporter.manager @@ -633,6 +635,26 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } + // MARK: Cycle detection + func isCycle(reporter: ProgressReporter, visited: Set = []) -> Bool { + if reporter.manager === self { + return true + } + + let updatedVisited = visited.union([self]) + + return parents.withLock { parents in + for (parent, _) in parents { + if !updatedVisited.contains(parent) { + if parent.isCycle(reporter: reporter, visited: updatedVisited) { + return true + } + } + } + return false + } + } + deinit { if !isFinished { self.withProperties { properties in diff --git a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressFractionTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressFractionTests.swift similarity index 98% rename from Tests/FoundationEssentialsTests/ProgressReporter/ProgressFractionTests.swift rename to Tests/FoundationEssentialsTests/ProgressManager/ProgressFractionTests.swift index 89dc77ec7..42e5211e7 100644 --- a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressFractionTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressFractionTests.swift @@ -43,7 +43,7 @@ final class ProgressFractionTests: XCTestCase { func test_addDifferent() { let f1 = _ProgressFraction(completed: 5, total: 10) - let f2 = _ProgressFraction(completed: 300, total: 1000) + let f2 = _ProgressFraction(completed : 300, total: 1000) let r = f1 + f2 XCTAssertEqual(r.completed, 800) diff --git a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressManagerTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift similarity index 86% rename from Tests/FoundationEssentialsTests/ProgressReporter/ProgressManagerTests.swift rename to Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift index 4f7fdc541..9391be1da 100644 --- a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressManagerTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift @@ -295,16 +295,16 @@ class TestProgressManagerAdditionalProperties: XCTestCase { XCTAssertEqual(fileProgressManager.withProperties(\.totalFileCount), 0) XCTAssertEqual(fileProgressManager.withProperties(\.completedFileCount), 0) - let totalFileValues = fileProgressManager.values(property: ProgressManager.Properties.TotalFileCount.self) + let totalFileValues = fileProgressManager.values(of: ProgressManager.Properties.TotalFileCount.self) XCTAssertEqual(totalFileValues, [0, 100]) - let reducedTotalFileValue = fileProgressManager.total(property: ProgressManager.Properties.TotalFileCount.self, values: totalFileValues) + let reducedTotalFileValue = fileProgressManager.total(of: ProgressManager.Properties.TotalFileCount.self) XCTAssertEqual(reducedTotalFileValue, 100) - let completedFileValues = fileProgressManager.values(property: ProgressManager.Properties.CompletedFileCount.self) + let completedFileValues = fileProgressManager.values(of: ProgressManager.Properties.CompletedFileCount.self) XCTAssertEqual(completedFileValues, [0, 100]) - let reducedCompletedFileValue = fileProgressManager.total(property: ProgressManager.Properties.CompletedFileCount.self, values: completedFileValues) + let reducedCompletedFileValue = fileProgressManager.total(of: ProgressManager.Properties.CompletedFileCount.self) XCTAssertEqual(reducedCompletedFileValue, 100) } @@ -325,10 +325,10 @@ class TestProgressManagerAdditionalProperties: XCTestCase { XCTAssertEqual(manager1.withProperties(\.totalFileCount), 10) XCTAssertEqual(manager1.withProperties(\.completedFileCount), 0) - let totalFileValues = overall.values(property: ProgressManager.Properties.TotalFileCount.self) + let totalFileValues = overall.values(of: ProgressManager.Properties.TotalFileCount.self) XCTAssertEqual(totalFileValues, [0, 10]) - let completedFileValues = overall.values(property: ProgressManager.Properties.CompletedFileCount.self) + let completedFileValues = overall.values(of: ProgressManager.Properties.CompletedFileCount.self) XCTAssertEqual(completedFileValues, [0, 0]) } @@ -354,9 +354,9 @@ class TestProgressManagerAdditionalProperties: XCTestCase { XCTAssertEqual(overall.fractionCompleted, 0.0) XCTAssertEqual(overall.withProperties(\.totalFileCount), 0) XCTAssertEqual(overall.withProperties(\.completedFileCount), 0) - let totalFileValues = overall.values(property: ProgressManager.Properties.TotalFileCount.self) + let totalFileValues = overall.values(of: ProgressManager.Properties.TotalFileCount.self) XCTAssertEqual(totalFileValues, [0, 11, 9]) - let completedFileValues = overall.values(property: ProgressManager.Properties.CompletedFileCount.self) + let completedFileValues = overall.values(of: ProgressManager.Properties.CompletedFileCount.self) XCTAssertEqual(completedFileValues, [0, 0, 0]) // Update FileCounts @@ -369,7 +369,7 @@ class TestProgressManagerAdditionalProperties: XCTestCase { } XCTAssertEqual(overall.withProperties(\.completedFileCount), 0) - let updatedCompletedFileValues = overall.values(property: ProgressManager.Properties.CompletedFileCount.self) + let updatedCompletedFileValues = overall.values(of: ProgressManager.Properties.CompletedFileCount.self) XCTAssertEqual(updatedCompletedFileValues, [0, 1, 1]) } @@ -387,7 +387,7 @@ class TestProgressManagerAdditionalProperties: XCTestCase { } XCTAssertEqual(childManager1.withProperties(\.totalFileCount), 10) - let preTotalFileValues = overall.values(property: ProgressManager.Properties.TotalFileCount.self) + let preTotalFileValues = overall.values(of: ProgressManager.Properties.TotalFileCount.self) XCTAssertEqual(preTotalFileValues, [0, 0, 10]) let childProgress2 = manager1.subprogress(assigningCount: 2) @@ -399,13 +399,13 @@ class TestProgressManagerAdditionalProperties: XCTestCase { // Tests that totalFileCount propagates to root level XCTAssertEqual(overall.withProperties(\.totalFileCount), 0) - let totalFileValues = overall.values(property: ProgressManager.Properties.TotalFileCount.self) + let totalFileValues = overall.values(of: ProgressManager.Properties.TotalFileCount.self) XCTAssertEqual(totalFileValues, [0, 0, 10, 10]) manager1.withProperties { properties in properties.totalFileCount += 999 } - let totalUpdatedFileValues = overall.values(property: ProgressManager.Properties.TotalFileCount.self) + let totalUpdatedFileValues = overall.values(of: ProgressManager.Properties.TotalFileCount.self) XCTAssertEqual(totalUpdatedFileValues, [0, 999, 10, 10]) } } @@ -571,3 +571,68 @@ class TestProgressManagerInterop: XCTestCase { } } #endif + +class TestProgressReporter: XCTestCase { + func testObserveProgressReporter() { + let manager = ProgressManager(totalCount: 3) + + let reporter = manager.reporter + + manager.complete(count: 1) + XCTAssertEqual(reporter.completedCount, 1) + + manager.complete(count: 1) + XCTAssertEqual(reporter.completedCount, 2) + + manager.complete(count: 1) + XCTAssertEqual(reporter.completedCount, 3) + } + + func testAddProgressReporterAsChild() { + let manager = ProgressManager(totalCount: 2) + + let reporter = manager.reporter + + let altManager1 = ProgressManager(totalCount: 4) + altManager1.assign(count: 1, to: reporter) + + let altManager2 = ProgressManager(totalCount: 5) + altManager2.assign(count: 2, to: reporter) + + manager.complete(count: 1) + XCTAssertEqual(altManager1.fractionCompleted, 0.125) + XCTAssertEqual(altManager2.fractionCompleted, 0.2) + + manager.complete(count: 1) + XCTAssertEqual(altManager1.fractionCompleted, 0.25) + XCTAssertEqual(altManager2.fractionCompleted, 0.4) + } + + /// All of these test cases hit the precondition for cycle detection, but currently there's no way to check for hitting precondition in xctest. +// func testProgressReporterDirectCycleDetection() { +// let manager = ProgressManager(totalCount: 2) +// +// manager.assign(count: 1, to: manager.reporter) +// } +// +// func testProgressReporterIndirectCycleDetection() { +// let manager = ProgressManager(totalCount: 2) +// +// let altManager = ProgressManager(totalCount: 1) +// altManager.assign(count: 1, to: manager.reporter) +// +// manager.assign(count: 1, to: altManager.reporter) +// } +// +// func testProgressReporterNestedCycleDetection() { +// let manager1 = ProgressManager(totalCount: 1) +// +// let manager2 = ProgressManager(totalCount: 2) +// manager1.assign(count: 1, to: manager2.reporter) +// +// let manager3 = ProgressManager(totalCount: 3) +// manager2.assign(count: 1, to: manager3.reporter) +// +// manager3.assign(count: 1, to: manager1.reporter) +// } +} From bb6a5b10e11a718025a107c0ea974152d4c03581 Mon Sep 17 00:00:00 2001 From: Chloe Yeo Date: Wed, 28 May 2025 13:01:36 -0700 Subject: [PATCH 55/85] Update Package.swift to say ProgressManager --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 75545f0ee..5e521d6cb 100644 --- a/Package.swift +++ b/Package.swift @@ -134,7 +134,7 @@ let package = Package( "ProcessInfo/CMakeLists.txt", "FileManager/CMakeLists.txt", "URL/CMakeLists.txt", - "ProgressReporter/CMakeLists.txt", + "ProgressManager/CMakeLists.txt", ], cSettings: [ .define("_GNU_SOURCE", .when(platforms: [.linux])) @@ -197,7 +197,7 @@ let package = Package( "Calendar/CMakeLists.txt", "CMakeLists.txt", "Predicate/CMakeLists.txt", - "ProgressReporter/CMakeLists.txt", + "ProgressManager/CMakeLists.txt", ], cSettings: wasiLibcCSettings, swiftSettings: [ From 59cea410a584c25d61d8b5a650d13da07b524d66 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 28 May 2025 13:45:11 -0700 Subject: [PATCH 56/85] enforce Progress having single parent + add test for Progress indirect participation in acyclic graph --- .../ProgressManager+Interop.swift | 4 ++- .../ProgressManagerTests.swift | 27 ++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift index 251881534..3a69b3de7 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift @@ -50,7 +50,7 @@ extension Progress { } - /// Adds a ProgressReporter as a child to a ProgressManager, which constitutes a portion of ProgressManager's totalUnitCount. + /// Adds a ProgressReporter as a child to a Progress, which constitutes a portion of Progress's totalUnitCount. /// /// - Parameters: /// - reporter: A `ProgressReporter` instance. @@ -138,6 +138,8 @@ extension ProgressManager { /// - count: Number of units delegated from `self`'s `totalCount`. /// - progress: `Progress` which receives the delegated `count`. public func subprogress(assigningCount count: Int, to progress: Foundation.Progress) { + precondition(progress._parent() == nil, "Cannot assign a progress to more than one parent.") + let parentBridge = _NSProgressParentBridge(managerParent: self) progress._setParent(parentBridge, portion: Int64(count)) diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift index 9391be1da..ad172ad17 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift @@ -455,7 +455,7 @@ class TestProgressManagerInterop: XCTestCase { XCTAssertEqual(overall.completedUnitCount, 10) } - func testInteropProgressParentProgressMonitorChildWithEmptyProgress() async throws { + func testInteropProgressParentProgressReporterChildWithEmptyProgress() async throws { // Initialize a Progress parent let overall = Progress.discreteProgress(totalUnitCount: 10) @@ -471,7 +471,7 @@ class TestProgressManagerInterop: XCTestCase { XCTAssertEqual(overall.fractionCompleted, 0.5) XCTAssertEqual(overall.completedUnitCount, 5) - // Add ProgressMonitor as Child + // Add ProgressReporter as Child let p2 = ProgressManager(totalCount: 10) let p2Reporter = p2.reporter overall.addChild(p2Reporter, withPendingUnitCount: 5) @@ -483,7 +483,7 @@ class TestProgressManagerInterop: XCTestCase { XCTAssertEqual(overall.completedUnitCount, 10) } - func testInteropProgressParentProgressMonitorChildWithExistingProgress() async throws { + func testInteropProgressParentProgressReporterChildWithExistingProgress() async throws { // Initialize a Progress parent let overall = Progress.discreteProgress(totalUnitCount: 10) @@ -499,7 +499,7 @@ class TestProgressManagerInterop: XCTestCase { XCTAssertEqual(overall.fractionCompleted, 0.5) XCTAssertEqual(overall.completedUnitCount, 5) - // Add ProgressMonitor with CompletedCount 3 as Child + // Add ProgressReporter with CompletedCount 3 as Child let p2 = ProgressManager(totalCount: 10) p2.complete(count: 3) let p2Reporter = p2.reporter @@ -569,6 +569,25 @@ class TestProgressManagerInterop: XCTestCase { receiveProgress(progress: interopChild) XCTAssertEqual(overallProgress2.totalUnitCount, 0) } + + func testIndirectParticipationOfProgressInAcyclicGraph() async throws { + let manager = ProgressManager(totalCount: 2) + + let parentManager1 = ProgressManager(totalCount: 1) + parentManager1.assign(count: 1, to: manager.reporter) + + let parentManager2 = ProgressManager(totalCount: 1) + parentManager2.assign(count: 1, to: manager.reporter) + + let progress = Progress.discreteProgress(totalUnitCount: 4) + manager.subprogress(assigningCount: 1, to: progress) + + progress.completedUnitCount = 2 + XCTAssertEqual(progress.fractionCompleted, 0.5) + XCTAssertEqual(manager.fractionCompleted, 0.25) + XCTAssertEqual(parentManager1.fractionCompleted, 0.25) + XCTAssertEqual(parentManager2.fractionCompleted, 0.25) + } } #endif From 3305aefcc44e4c5860c0c699163589285d16871a Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 28 May 2025 16:31:23 -0700 Subject: [PATCH 57/85] add cycle detection to interop --- .../ProgressManager+Interop.swift | 40 +++++++++++++++++-- .../ProgressManagerTests.swift | 2 + 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift index 3a69b3de7..f1dffb954 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift @@ -56,7 +56,10 @@ extension Progress { /// - reporter: A `ProgressReporter` instance. /// - count: Number of units delegated from `self`'s `totalCount`. public func addChild(_ reporter: ProgressReporter, withPendingUnitCount count: Int) { - + + // Need to detect cycle here + precondition(self.isCycle(reporter: reporter) == false, "Creating a cycle is not allowed.") + // Make intermediary & add it to NSProgress parent's children list let ghostProgressParent = Progress(totalUnitCount: Int64(reporter.manager.totalCount ?? 0)) ghostProgressParent.completedUnitCount = Int64(reporter.manager.completedCount) @@ -64,10 +67,41 @@ extension Progress { // Make observation instance let observation = _ProgressParentProgressReporterChild(intermediary: ghostProgressParent, reporter: reporter) - + reporter.manager.setInteropObservationForMonitor(observation: observation) reporter.manager.setMonitorInterop(to: true) } + + // MARK: Cycle detection + func isCycle(reporter: ProgressReporter, visited: Set = []) -> Bool { + if self._parent() == nil { + return false + } + + if !(self._parent() is _NSProgressParentBridge) { + return self._parent().isCycle(reporter: reporter) + } + + // then check against ProgressManager + let unwrappedParent = (self._parent() as? _NSProgressParentBridge)?.actualParent + if let unwrappedParent = unwrappedParent { + if unwrappedParent === reporter.manager { + return true + } + let updatedVisited = visited.union([unwrappedParent]) + return unwrappedParent.parents.withLock { parents in + for (parent, _) in parents { + if !updatedVisited.contains(parent) { + if parent.isCycle(reporter: reporter, visited: updatedVisited) { + return true + } + } + } + return false + } + } + return false + } } private final class _ProgressParentProgressManagerChild: Sendable { @@ -152,7 +186,7 @@ extension ProgressManager { // Subclass of Foundation.Progress internal final class _NSProgressParentBridge: Progress, @unchecked Sendable { - let actualParent: ProgressManager + internal let actualParent: ProgressManager init(managerParent: ProgressManager) { self.actualParent = managerParent diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift index ad172ad17..fe8b65af0 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift @@ -587,6 +587,8 @@ class TestProgressManagerInterop: XCTestCase { XCTAssertEqual(manager.fractionCompleted, 0.25) XCTAssertEqual(parentManager1.fractionCompleted, 0.25) XCTAssertEqual(parentManager2.fractionCompleted, 0.25) + +// progress.addChild(parentManager1.reporter, withPendingUnitCount: 1) // this should trigger cycle detection } } #endif From c175d00df9d2f46933b39454585f0a4e5edf3882 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 28 May 2025 16:31:34 -0700 Subject: [PATCH 58/85] add cycle detection to interop --- .../ProgressManager/ProgressManager+Interop.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift index f1dffb954..f1f01f5ef 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift @@ -73,7 +73,7 @@ extension Progress { } // MARK: Cycle detection - func isCycle(reporter: ProgressReporter, visited: Set = []) -> Bool { + private func isCycle(reporter: ProgressReporter, visited: Set = []) -> Bool { if self._parent() == nil { return false } From 322a7df7714a25552161dba838c03541477d2d70 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 29 May 2025 11:15:16 -0700 Subject: [PATCH 59/85] add dirty flag and update documentation --- .../BenchmarkProgressReporting.swift | 0 .../ProgressManager/ProgressManager.swift | 11 ++++++----- 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 Benchmarks/Benchmarks/Internationalization/ProgressReporting/BenchmarkProgressReporting.swift diff --git a/Benchmarks/Benchmarks/Internationalization/ProgressReporting/BenchmarkProgressReporting.swift b/Benchmarks/Benchmarks/Internationalization/ProgressReporting/BenchmarkProgressReporting.swift new file mode 100644 index 000000000..e69de29bb diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index cf0b7e1fb..082941cb7 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -53,6 +53,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { var otherProperties: [AnyMetatypeWrapper: (any Sendable)] // Type: Metatype maps to dictionary of child to value var childrenOtherProperties: [AnyMetatypeWrapper: OrderedDictionary] + var isDirty: Bool } // Interop states @@ -236,7 +237,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { childFraction: _ProgressFraction(completed: 0, total: 1), interopChild: nil ) - let state = State(fractionState: fractionState, otherProperties: [:], childrenOtherProperties: [:]) + let state = State(fractionState: fractionState, otherProperties: [:], childrenOtherProperties: [:], isDirty: true) self.state = LockedState(initialState: state) self.interopObservation = interopObservation self.ghostReporter = ghostReporter @@ -309,8 +310,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public func complete(count: Int) { let updateState = updateCompletedCount(count: count) updateFractionCompleted(from: updateState.previous, to: updateState.current) - ghostReporter?.notifyObservers(with: .fractionUpdated) + // Interop updates stuff + ghostReporter?.notifyObservers(with: .fractionUpdated) monitorInterop.withLock { [self] interop in if interop == true { notifyObservers(with: .fractionUpdated) @@ -339,12 +341,11 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } /// Mutates any settable properties that convey information about progress. - /// public func withProperties(_ closure: @Sendable (inout Values) throws -> T) rethrows -> T { return try state.withLock { state in var values = Values(manager: self, state: state) // This is done to avoid copy on write later - state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction()), otherProperties: [:], childrenOtherProperties: [:]) + state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction()), otherProperties: [:], childrenOtherProperties: [:], isDirty: state.isDirty) let result = try closure(&values) state = values.state return result @@ -383,7 +384,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } - /// Returns nil if `self` has `nil` total units; + /// Returns 0 if `self` has `nil` total units; /// returns a `Int` value otherwise. private func getCompletedCount(fractionState: inout FractionState) -> Int { if let interopChild = fractionState.interopChild { From c81e334fadd88ea544cdd20a87e61dca72d124c2 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 29 May 2025 11:33:33 -0700 Subject: [PATCH 60/85] totalCount + completedCount updates swap to isDirty --- .../ProgressManager/ProgressManager.swift | 88 ++++++++++++------- 1 file changed, 55 insertions(+), 33 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 082941cb7..1f9bdd976 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -145,7 +145,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } set { - let previous = state.fractionState.overallFraction +// let previous = state.fractionState.overallFraction if state.fractionState.selfFraction.total != newValue && state.fractionState.selfFraction.total > 0 { state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: newValue ?? 1) } @@ -158,7 +158,10 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { state.fractionState.indeterminate = true } //TODO: rdar://149015734 Check throttling - manager.updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) + manager.markDirty() +// manager.updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) + + // Interop updates stuff manager.ghostReporter?.notifyObservers(with: .totalCountUpdated) manager.monitorInterop.withLock { [manager] interop in if interop == true { @@ -176,9 +179,10 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } set { - let prev = state.fractionState.overallFraction +// let prev = state.fractionState.overallFraction state.fractionState.selfFraction.completed = newValue - manager.updateFractionCompleted(from: prev, to: state.fractionState.overallFraction) + manager.markDirty() +// manager.updateFractionCompleted(from: prev, to: state.fractionState.overallFraction) manager.ghostReporter?.notifyObservers(with: .fractionUpdated) manager.monitorInterop.withLock { [manager] interop in @@ -251,33 +255,33 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { self.init(total: totalCount, ghostReporter: nil, interopObservation: nil) } - /// Sets `totalCount`. - /// - Parameter newTotal: Total units of work. - public func setTotalCount(_ newTotal: Int?) { - state.withLock { state in - let previous = state.fractionState.overallFraction - if state.fractionState.selfFraction.total != newTotal && state.fractionState.selfFraction.total > 0 { - state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: newTotal ?? 1) - } - state.fractionState.selfFraction.total = newTotal ?? 0 - - // if newValue is nil, reset indeterminate to true - if newTotal != nil { - state.fractionState.indeterminate = false - } else { - state.fractionState.indeterminate = true - } - updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) - - ghostReporter?.notifyObservers(with: .totalCountUpdated) - - monitorInterop.withLock { [self] interop in - if interop == true { - notifyObservers(with: .totalCountUpdated) - } - } - } - } +// /// Sets `totalCount`. +// /// - Parameter newTotal: Total units of work. +// public func setTotalCount(_ newTotal: Int?) { +// state.withLock { state in +// let previous = state.fractionState.overallFraction +// if state.fractionState.selfFraction.total != newTotal && state.fractionState.selfFraction.total > 0 { +// state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: newTotal ?? 1) +// } +// state.fractionState.selfFraction.total = newTotal ?? 0 +// +// // if newValue is nil, reset indeterminate to true +// if newTotal != nil { +// state.fractionState.indeterminate = false +// } else { +// state.fractionState.indeterminate = true +// } +// updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) +// +// ghostReporter?.notifyObservers(with: .totalCountUpdated) +// +// monitorInterop.withLock { [self] interop in +// if interop == true { +// notifyObservers(with: .totalCountUpdated) +// } +// } +// } +// } /// Returns a `Subprogress` representing a portion of `self` which can be passed to any method that reports progress. /// @@ -308,8 +312,12 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// Increases `completedCount` by `count`. /// - Parameter count: Units of work. public func complete(count: Int) { - let updateState = updateCompletedCount(count: count) - updateFractionCompleted(from: updateState.previous, to: updateState.current) + state.withLock { state in + state.fractionState.selfFraction.completed += count + } + markDirty() +// let updateState = updateCompletedCount(count: count) +// updateFractionCompleted(from: updateState.previous, to: updateState.current) // Interop updates stuff ghostReporter?.notifyObservers(with: .fractionUpdated) @@ -485,6 +493,20 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { updateFractionCompleted(from: updateState.previous, to: updateState.current) } + + /// Update completedCount and mark all ancestors as dirty. + private func markDirty() { + state.withLock { state in + state.isDirty = true + } + // recursively mark all ancestors as dirty + parents.withLock { parents in + for (parent, _) in parents { + parent.markDirty() + } + } + } + //MARK: Interop-related internal methods /// Adds `observer` to list of `_observers` in `self`. internal func addObserver(observer: @escaping @Sendable (ObserverState) -> Void) { From 834ead57148840358211efa32ace5ad9dad65ac6 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 29 May 2025 17:35:50 -0700 Subject: [PATCH 61/85] debugging: using dirty bit for values update --- .../ProgressManager/ProgressManager.swift | 241 ++++++++++++------ 1 file changed, 163 insertions(+), 78 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 1f9bdd976..6fcef227a 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -53,7 +53,16 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { var otherProperties: [AnyMetatypeWrapper: (any Sendable)] // Type: Metatype maps to dictionary of child to value var childrenOtherProperties: [AnyMetatypeWrapper: OrderedDictionary] - var isDirty: Bool + + // dirty bit for completed count + var isDirtyCompleted: Bool + var updatedCompletedCount: Int + + // dirty bit for total count + var isDirtyTotal: Bool + var updatedTotalCount: Int? + + var children: Set } // Interop states @@ -78,7 +87,10 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var totalCount: Int? { _$observationRegistrar.access(self, keyPath: \.totalCount) return state.withLock { state in - getTotalCount(fractionState: &state.fractionState) + if state.isDirtyTotal { + updateDirtiedValues(state: &state) + } + return getTotalCount(fractionState: &state.fractionState) } } @@ -87,7 +99,10 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var completedCount: Int { _$observationRegistrar.access(self, keyPath: \.completedCount) return state.withLock { state in - getCompletedCount(fractionState: &state.fractionState) + if state.isDirtyCompleted { + updateDirtiedValues(state: &state) + } + return getCompletedCount(fractionState: &state.fractionState) } } @@ -97,7 +112,10 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var fractionCompleted: Double { _$observationRegistrar.access(self, keyPath: \.fractionCompleted) return state.withLock { state in - getFractionCompleted(fractionState: &state.fractionState) + if state.isDirtyTotal || state.isDirtyCompleted { + updateDirtiedValues(state: &state) + } + return getFractionCompleted(fractionState: &state.fractionState) } } @@ -106,7 +124,12 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var isIndeterminate: Bool { _$observationRegistrar.access(self, keyPath: \.isIndeterminate) return state.withLock { state in - getIsIndeterminate(fractionState: &state.fractionState) + if state.isDirtyTotal { + if state.isDirtyTotal || state.isDirtyCompleted { + updateDirtiedValues(state: &state) + } + } + return getIsIndeterminate(fractionState: &state.fractionState) } } @@ -115,7 +138,47 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var isFinished: Bool { _$observationRegistrar.access(self, keyPath: \.isFinished) return state.withLock { state in - getIsFinished(fractionState: &state.fractionState) + if state.isDirtyTotal || state.isDirtyCompleted { + updateDirtiedValues(state: &state) + } + return getIsFinished(fractionState: &state.fractionState) + } + } + + /// This is called if the total dirty bit is set + private func updateDirtiedValues(state: inout State) { + if state.children.isEmpty { + // If there are no children, update value directly and don't traverse down anymore + let updateState = getPreviousAndCurrentState(state: &state) + updateFractionCompleted(from: updateState.previous, to: updateState.current) + + // mark dirty bit false + state.isDirtyTotal = false + state.isDirtyCompleted = false + } else { + // If there are children, traverse down + for child in state.children { + child.updateDirtiedValues() + } + } + } + + private func updateDirtiedValues() { + state.withLock { state in + if state.children.isEmpty { + // If there are no children, update value directly and don't traverse down anymore + let updateState = getPreviousAndCurrentState(state: &state) + updateFractionCompleted(from: updateState.previous, to: updateState.current) + + // mark dirty bit false + state.isDirtyTotal = false + state.isDirtyCompleted = false + } else { + // If there are children, traverse down + for child in state.children { + child.updateDirtiedValues() + } + } } } @@ -141,26 +204,16 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// The total units of work. public var totalCount: Int? { mutating get { - manager.getTotalCount(fractionState: &state.fractionState) + if state.isDirtyTotal { + manager.updateDirtiedValues(state: &state) + } + return manager.getTotalCount(fractionState: &state.fractionState) } set { -// let previous = state.fractionState.overallFraction - if state.fractionState.selfFraction.total != newValue && state.fractionState.selfFraction.total > 0 { - state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: newValue ?? 1) - } - state.fractionState.selfFraction.total = newValue ?? 0 - - // if newValue is nil, reset indeterminate to true - if newValue != nil { - state.fractionState.indeterminate = false - } else { - state.fractionState.indeterminate = true - } - //TODO: rdar://149015734 Check throttling - manager.markDirty() -// manager.updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) - + state.updatedTotalCount = newValue + manager.markTotalDirty(state: &state) + // Interop updates stuff manager.ghostReporter?.notifyObservers(with: .totalCountUpdated) manager.monitorInterop.withLock { [manager] interop in @@ -175,16 +228,17 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// The completed units of work. public var completedCount: Int { mutating get { - manager.getCompletedCount(fractionState: &state.fractionState) + if state.isDirtyCompleted { + manager.updateDirtiedValues(state: &state) + } + return manager.getCompletedCount(fractionState: &state.fractionState) } set { -// let prev = state.fractionState.overallFraction - state.fractionState.selfFraction.completed = newValue - manager.markDirty() -// manager.updateFractionCompleted(from: prev, to: state.fractionState.overallFraction) + state.updatedCompletedCount = newValue + manager.markCompletedDirty(state: &state) + manager.ghostReporter?.notifyObservers(with: .fractionUpdated) - manager.monitorInterop.withLock { [manager] interop in if interop == true { manager.notifyObservers(with: .fractionUpdated) @@ -229,19 +283,24 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } internal let parents: LockedState<[ProgressManager: Int]> - private let children: LockedState> private let state: LockedState internal init(total: Int?, ghostReporter: ProgressManager?, interopObservation: (any Sendable)?) { self.parents = .init(initialState: [:]) - self.children = .init(initialState: Set()) let fractionState = FractionState( indeterminate: total == nil ? true : false, selfFraction: _ProgressFraction(completed: 0, total: total ?? 0), childFraction: _ProgressFraction(completed: 0, total: 1), interopChild: nil ) - let state = State(fractionState: fractionState, otherProperties: [:], childrenOtherProperties: [:], isDirty: true) + let state = State(fractionState: fractionState, + otherProperties: [:], + childrenOtherProperties: [:], + isDirtyCompleted: false, + updatedCompletedCount: 0, + isDirtyTotal: false, + updatedTotalCount: 0, + children: Set()) self.state = LockedState(initialState: state) self.interopObservation = interopObservation self.ghostReporter = ghostReporter @@ -255,34 +314,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { self.init(total: totalCount, ghostReporter: nil, interopObservation: nil) } -// /// Sets `totalCount`. -// /// - Parameter newTotal: Total units of work. -// public func setTotalCount(_ newTotal: Int?) { -// state.withLock { state in -// let previous = state.fractionState.overallFraction -// if state.fractionState.selfFraction.total != newTotal && state.fractionState.selfFraction.total > 0 { -// state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: newTotal ?? 1) -// } -// state.fractionState.selfFraction.total = newTotal ?? 0 -// -// // if newValue is nil, reset indeterminate to true -// if newTotal != nil { -// state.fractionState.indeterminate = false -// } else { -// state.fractionState.indeterminate = true -// } -// updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) -// -// ghostReporter?.notifyObservers(with: .totalCountUpdated) -// -// monitorInterop.withLock { [self] interop in -// if interop == true { -// notifyObservers(with: .totalCountUpdated) -// } -// } -// } -// } - /// Returns a `Subprogress` representing a portion of `self` which can be passed to any method that reports progress. /// /// - Parameter count: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. @@ -313,11 +344,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// - Parameter count: Units of work. public func complete(count: Int) { state.withLock { state in - state.fractionState.selfFraction.completed += count + state.updatedCompletedCount = state.fractionState.selfFraction.completed + count + markCompletedDirty(state: &state) } - markDirty() -// let updateState = updateCompletedCount(count: count) -// updateFractionCompleted(from: updateState.previous, to: updateState.current) // Interop updates stuff ghostReporter?.notifyObservers(with: .fractionUpdated) @@ -353,7 +382,16 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { return try state.withLock { state in var values = Values(manager: self, state: state) // This is done to avoid copy on write later - state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction()), otherProperties: [:], childrenOtherProperties: [:], isDirty: state.isDirty) + state = State(fractionState: FractionState(indeterminate: true, + selfFraction: _ProgressFraction(), + childFraction: _ProgressFraction()), + otherProperties: [:], + childrenOtherProperties: [:], + isDirtyCompleted: false, + updatedCompletedCount: 0, + isDirtyTotal: false, + updatedTotalCount: 0, + children: Set()) let result = try closure(&values) state = values.state return result @@ -439,14 +477,26 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { let current: _ProgressFraction } - private func updateCompletedCount(count: Int) -> UpdateState { - // Acquire and release child's lock - let (previous, current) = state.withLock { state in - let prev = state.fractionState.overallFraction - state.fractionState.selfFraction.completed += count - return (prev, state.fractionState.overallFraction) + private func getPreviousAndCurrentState(state: inout State) -> UpdateState { + let prev = state.fractionState.overallFraction + if state.isDirtyTotal { + if state.updatedTotalCount == nil { + print("updated total count is nil") + state.fractionState.indeterminate = true + } else { + state.fractionState.indeterminate = false + } + // actually update totalCount + if (state.fractionState.selfFraction.total != (state.updatedTotalCount ?? 0)) && (state.fractionState.selfFraction.total > 0) { + state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: state.updatedTotalCount ?? 1) + } + state.fractionState.selfFraction.total = state.updatedTotalCount ?? 0 + } + if state.isDirtyCompleted { + // actually update completedCount + state.fractionState.selfFraction.completed = state.updatedCompletedCount } - return UpdateState(previous: previous, current: current) + return UpdateState(previous: prev, current: state.fractionState.overallFraction) } private func updateFractionCompleted(from: _ProgressFraction, to: _ProgressFraction) { @@ -488,6 +538,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { state.fractionState.childFraction = state.fractionState.childFraction - (multiple * next) } } + state.isDirtyTotal = false + state.isDirtyCompleted = false return UpdateState(previous: previousOverallFraction, current: state.fractionState.overallFraction) } updateFractionCompleted(from: updateState.previous, to: updateState.current) @@ -495,14 +547,47 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// Update completedCount and mark all ancestors as dirty. - private func markDirty() { + private func markCompletedDirty(state: inout State) { + state.isDirtyCompleted = true + + // recursively mark all ancestors as dirty + parents.withLock { parents in + for (parent, _) in parents { + parent.markCompletedDirty() + } + } + } + + private func markCompletedDirty() { state.withLock { state in - state.isDirty = true + state.isDirtyCompleted = true } - // recursively mark all ancestors as dirty + + parents.withLock { parents in + for (parent, _) in parents { + parent.markCompletedDirty() + } + } + } + + private func markTotalDirty(state: inout State) { + state.isDirtyTotal = true + + parents.withLock { parents in + for (parent, _) in parents { + parent.markTotalDirty() + } + } + } + + private func markTotalDirty() { + state.withLock { state in + state.isDirtyTotal = true + } + parents.withLock { parents in for (parent, _) in parents { - parent.markDirty() + parent.markTotalDirty() } } } @@ -552,8 +637,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } internal func addToChildren(childManager: ProgressManager) { - _ = children.withLock { children in - children.insert(childManager) + _ = state.withLock { state in + state.children.insert(childManager) } } From 803103a9224468c68cd3c9e4c16938b217d54950 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 30 May 2025 16:04:53 -0700 Subject: [PATCH 62/85] temporary revert using dirty bit --- .../ProgressManager/ProgressManager.swift | 239 +++++------------- 1 file changed, 65 insertions(+), 174 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 6fcef227a..2069b2d1f 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -53,15 +53,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { var otherProperties: [AnyMetatypeWrapper: (any Sendable)] // Type: Metatype maps to dictionary of child to value var childrenOtherProperties: [AnyMetatypeWrapper: OrderedDictionary] - - // dirty bit for completed count - var isDirtyCompleted: Bool - var updatedCompletedCount: Int - - // dirty bit for total count - var isDirtyTotal: Bool - var updatedTotalCount: Int? - var children: Set } @@ -87,10 +78,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var totalCount: Int? { _$observationRegistrar.access(self, keyPath: \.totalCount) return state.withLock { state in - if state.isDirtyTotal { - updateDirtiedValues(state: &state) - } - return getTotalCount(fractionState: &state.fractionState) + getTotalCount(fractionState: &state.fractionState) } } @@ -99,10 +87,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var completedCount: Int { _$observationRegistrar.access(self, keyPath: \.completedCount) return state.withLock { state in - if state.isDirtyCompleted { - updateDirtiedValues(state: &state) - } - return getCompletedCount(fractionState: &state.fractionState) + getCompletedCount(fractionState: &state.fractionState) } } @@ -112,10 +97,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var fractionCompleted: Double { _$observationRegistrar.access(self, keyPath: \.fractionCompleted) return state.withLock { state in - if state.isDirtyTotal || state.isDirtyCompleted { - updateDirtiedValues(state: &state) - } - return getFractionCompleted(fractionState: &state.fractionState) + getFractionCompleted(fractionState: &state.fractionState) } } @@ -124,12 +106,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var isIndeterminate: Bool { _$observationRegistrar.access(self, keyPath: \.isIndeterminate) return state.withLock { state in - if state.isDirtyTotal { - if state.isDirtyTotal || state.isDirtyCompleted { - updateDirtiedValues(state: &state) - } - } - return getIsIndeterminate(fractionState: &state.fractionState) + getIsIndeterminate(fractionState: &state.fractionState) } } @@ -138,47 +115,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var isFinished: Bool { _$observationRegistrar.access(self, keyPath: \.isFinished) return state.withLock { state in - if state.isDirtyTotal || state.isDirtyCompleted { - updateDirtiedValues(state: &state) - } - return getIsFinished(fractionState: &state.fractionState) - } - } - - /// This is called if the total dirty bit is set - private func updateDirtiedValues(state: inout State) { - if state.children.isEmpty { - // If there are no children, update value directly and don't traverse down anymore - let updateState = getPreviousAndCurrentState(state: &state) - updateFractionCompleted(from: updateState.previous, to: updateState.current) - - // mark dirty bit false - state.isDirtyTotal = false - state.isDirtyCompleted = false - } else { - // If there are children, traverse down - for child in state.children { - child.updateDirtiedValues() - } - } - } - - private func updateDirtiedValues() { - state.withLock { state in - if state.children.isEmpty { - // If there are no children, update value directly and don't traverse down anymore - let updateState = getPreviousAndCurrentState(state: &state) - updateFractionCompleted(from: updateState.previous, to: updateState.current) - - // mark dirty bit false - state.isDirtyTotal = false - state.isDirtyCompleted = false - } else { - // If there are children, traverse down - for child in state.children { - child.updateDirtiedValues() - } - } + getIsFinished(fractionState: &state.fractionState) } } @@ -204,17 +141,24 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// The total units of work. public var totalCount: Int? { mutating get { - if state.isDirtyTotal { - manager.updateDirtiedValues(state: &state) - } - return manager.getTotalCount(fractionState: &state.fractionState) + manager.getTotalCount(fractionState: &state.fractionState) } set { - state.updatedTotalCount = newValue - manager.markTotalDirty(state: &state) - - // Interop updates stuff + let previous = state.fractionState.overallFraction + if state.fractionState.selfFraction.total != newValue && state.fractionState.selfFraction.total > 0 { + state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: newValue ?? 1) + } + state.fractionState.selfFraction.total = newValue ?? 0 + + // if newValue is nil, reset indeterminate to true + if newValue != nil { + state.fractionState.indeterminate = false + } else { + state.fractionState.indeterminate = true + } + //TODO: rdar://149015734 Check throttling + manager.updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) manager.ghostReporter?.notifyObservers(with: .totalCountUpdated) manager.monitorInterop.withLock { [manager] interop in if interop == true { @@ -228,17 +172,15 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// The completed units of work. public var completedCount: Int { mutating get { - if state.isDirtyCompleted { - manager.updateDirtiedValues(state: &state) - } - return manager.getCompletedCount(fractionState: &state.fractionState) + manager.getCompletedCount(fractionState: &state.fractionState) } set { - state.updatedCompletedCount = newValue - manager.markCompletedDirty(state: &state) - + let prev = state.fractionState.overallFraction + state.fractionState.selfFraction.completed = newValue + manager.updateFractionCompleted(from: prev, to: state.fractionState.overallFraction) manager.ghostReporter?.notifyObservers(with: .fractionUpdated) + manager.monitorInterop.withLock { [manager] interop in if interop == true { manager.notifyObservers(with: .fractionUpdated) @@ -293,14 +235,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { childFraction: _ProgressFraction(completed: 0, total: 1), interopChild: nil ) - let state = State(fractionState: fractionState, - otherProperties: [:], - childrenOtherProperties: [:], - isDirtyCompleted: false, - updatedCompletedCount: 0, - isDirtyTotal: false, - updatedTotalCount: 0, - children: Set()) + let state = State(fractionState: fractionState, otherProperties: [:], childrenOtherProperties: [:], children: Set()) self.state = LockedState(initialState: state) self.interopObservation = interopObservation self.ghostReporter = ghostReporter @@ -314,6 +249,34 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { self.init(total: totalCount, ghostReporter: nil, interopObservation: nil) } + /// Sets `totalCount`. + /// - Parameter newTotal: Total units of work. + public func setTotalCount(_ newTotal: Int?) { + state.withLock { state in + let previous = state.fractionState.overallFraction + if state.fractionState.selfFraction.total != newTotal && state.fractionState.selfFraction.total > 0 { + state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: newTotal ?? 1) + } + state.fractionState.selfFraction.total = newTotal ?? 0 + + // if newValue is nil, reset indeterminate to true + if newTotal != nil { + state.fractionState.indeterminate = false + } else { + state.fractionState.indeterminate = true + } + updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) + + ghostReporter?.notifyObservers(with: .totalCountUpdated) + + monitorInterop.withLock { [self] interop in + if interop == true { + notifyObservers(with: .totalCountUpdated) + } + } + } + } + /// Returns a `Subprogress` representing a portion of `self` which can be passed to any method that reports progress. /// /// - Parameter count: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. @@ -343,10 +306,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// Increases `completedCount` by `count`. /// - Parameter count: Units of work. public func complete(count: Int) { - state.withLock { state in - state.updatedCompletedCount = state.fractionState.selfFraction.completed + count - markCompletedDirty(state: &state) - } + let updateState = updateCompletedCount(count: count) + updateFractionCompleted(from: updateState.previous, to: updateState.current) // Interop updates stuff ghostReporter?.notifyObservers(with: .fractionUpdated) @@ -382,16 +343,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { return try state.withLock { state in var values = Values(manager: self, state: state) // This is done to avoid copy on write later - state = State(fractionState: FractionState(indeterminate: true, - selfFraction: _ProgressFraction(), - childFraction: _ProgressFraction()), - otherProperties: [:], - childrenOtherProperties: [:], - isDirtyCompleted: false, - updatedCompletedCount: 0, - isDirtyTotal: false, - updatedTotalCount: 0, - children: Set()) + state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction()), otherProperties: [:], childrenOtherProperties: [:], children: Set()) let result = try closure(&values) state = values.state return result @@ -405,7 +357,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // var values = Values(manager: self, state: state) // // This is done to avoid copy on write later // state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction()), otherProperties: [:], childrenOtherProperties: [:]) -// +// // do { // let result = try closure(&values) // state = values.state @@ -477,26 +429,14 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { let current: _ProgressFraction } - private func getPreviousAndCurrentState(state: inout State) -> UpdateState { - let prev = state.fractionState.overallFraction - if state.isDirtyTotal { - if state.updatedTotalCount == nil { - print("updated total count is nil") - state.fractionState.indeterminate = true - } else { - state.fractionState.indeterminate = false - } - // actually update totalCount - if (state.fractionState.selfFraction.total != (state.updatedTotalCount ?? 0)) && (state.fractionState.selfFraction.total > 0) { - state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: state.updatedTotalCount ?? 1) - } - state.fractionState.selfFraction.total = state.updatedTotalCount ?? 0 + private func updateCompletedCount(count: Int) -> UpdateState { + // Acquire and release child's lock + let (previous, current) = state.withLock { state in + let prev = state.fractionState.overallFraction + state.fractionState.selfFraction.completed += count + return (prev, state.fractionState.overallFraction) } - if state.isDirtyCompleted { - // actually update completedCount - state.fractionState.selfFraction.completed = state.updatedCompletedCount - } - return UpdateState(previous: prev, current: state.fractionState.overallFraction) + return UpdateState(previous: previous, current: current) } private func updateFractionCompleted(from: _ProgressFraction, to: _ProgressFraction) { @@ -538,60 +478,11 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { state.fractionState.childFraction = state.fractionState.childFraction - (multiple * next) } } - state.isDirtyTotal = false - state.isDirtyCompleted = false return UpdateState(previous: previousOverallFraction, current: state.fractionState.overallFraction) } updateFractionCompleted(from: updateState.previous, to: updateState.current) } - - /// Update completedCount and mark all ancestors as dirty. - private func markCompletedDirty(state: inout State) { - state.isDirtyCompleted = true - - // recursively mark all ancestors as dirty - parents.withLock { parents in - for (parent, _) in parents { - parent.markCompletedDirty() - } - } - } - - private func markCompletedDirty() { - state.withLock { state in - state.isDirtyCompleted = true - } - - parents.withLock { parents in - for (parent, _) in parents { - parent.markCompletedDirty() - } - } - } - - private func markTotalDirty(state: inout State) { - state.isDirtyTotal = true - - parents.withLock { parents in - for (parent, _) in parents { - parent.markTotalDirty() - } - } - } - - private func markTotalDirty() { - state.withLock { state in - state.isDirtyTotal = true - } - - parents.withLock { parents in - for (parent, _) in parents { - parent.markTotalDirty() - } - } - } - //MARK: Interop-related internal methods /// Adds `observer` to list of `_observers` in `self`. internal func addObserver(observer: @escaping @Sendable (ObserverState) -> Void) { From a60bdfbe4faa577e2b85c4c017b451d09458a0bd Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 2 Jun 2025 13:00:52 -0700 Subject: [PATCH 63/85] attempt at dirty flag - need to try again --- .../ProgressManager/ProgressManager.swift | 251 +++++++++++++++--- 1 file changed, 215 insertions(+), 36 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 2069b2d1f..aa78f0ad8 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -28,6 +28,11 @@ internal struct FractionState { selfFraction + childFraction } var interopChild: ProgressManager? // read from this if self is actually an interop ghost + + var isDirty: Bool = false // Flag to indicate fraction computation needs update + var isProcessing: Bool = false + var pendingCompletedCount: Int? + var dirtyChildren: Set = Set() // Track which children are dirty } internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { @@ -78,7 +83,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var totalCount: Int? { _$observationRegistrar.access(self, keyPath: \.totalCount) return state.withLock { state in - getTotalCount(fractionState: &state.fractionState) + return getTotalCount(state: &state) } } @@ -87,8 +92,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var completedCount: Int { _$observationRegistrar.access(self, keyPath: \.completedCount) return state.withLock { state in - getCompletedCount(fractionState: &state.fractionState) + return getCompletedCount(state: &state) } + } /// The proportion of work completed. @@ -97,7 +103,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var fractionCompleted: Double { _$observationRegistrar.access(self, keyPath: \.fractionCompleted) return state.withLock { state in - getFractionCompleted(fractionState: &state.fractionState) + getFractionCompleted(state: &state) } } @@ -106,7 +112,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var isIndeterminate: Bool { _$observationRegistrar.access(self, keyPath: \.isIndeterminate) return state.withLock { state in - getIsIndeterminate(fractionState: &state.fractionState) + getIsIndeterminate(state: &state) } } @@ -115,7 +121,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var isFinished: Bool { _$observationRegistrar.access(self, keyPath: \.isFinished) return state.withLock { state in - getIsFinished(fractionState: &state.fractionState) + getIsFinished(state: &state) } } @@ -141,7 +147,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// The total units of work. public var totalCount: Int? { mutating get { - manager.getTotalCount(fractionState: &state.fractionState) + manager.getTotalCount(state: &state) } set { @@ -172,15 +178,15 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// The completed units of work. public var completedCount: Int { mutating get { - manager.getCompletedCount(fractionState: &state.fractionState) + manager.getCompletedCount(state: &state) } set { - let prev = state.fractionState.overallFraction - state.fractionState.selfFraction.completed = newValue - manager.updateFractionCompleted(from: prev, to: state.fractionState.overallFraction) - manager.ghostReporter?.notifyObservers(with: .fractionUpdated) + //TODO: I am scared, this might introduce recursive lock again +// let existingCompleted = state.fractionState.selfFraction.completed +// manager.complete(count: newValue - existingCompleted) + manager.ghostReporter?.notifyObservers(with: .fractionUpdated) manager.monitorInterop.withLock { [manager] interop in if interop == true { manager.notifyObservers(with: .fractionUpdated) @@ -306,8 +312,20 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// Increases `completedCount` by `count`. /// - Parameter count: Units of work. public func complete(count: Int) { - let updateState = updateCompletedCount(count: count) - updateFractionCompleted(from: updateState.previous, to: updateState.current) + // First update pendingCompletedCount + updatePendingCompletedCount(increment: count) + + // Then mark self to be dirty + state.withLock { state in + state.fractionState.isDirty = true + } + + // Add self to all parent's dirtyChildren list & mark parents as dirty recursively + parents.withLock { parents in + for (parent, _) in parents { + parent.addDirtyChild(self) + } + } // Interop updates stuff ghostReporter?.notifyObservers(with: .fractionUpdated) @@ -371,24 +389,28 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { //MARK: ProgressManager Properties getters /// Returns nil if `self` was instantiated without total units; /// returns a `Int` value otherwise. - private func getTotalCount(fractionState: inout FractionState) -> Int? { - if let interopChild = fractionState.interopChild { + private func getTotalCount(state: inout State) -> Int? { + if let interopChild = state.fractionState.interopChild { return interopChild.totalCount } - if fractionState.indeterminate { + if state.fractionState.indeterminate { return nil } else { - return fractionState.selfFraction.total + return state.fractionState.selfFraction.total } } /// Returns 0 if `self` has `nil` total units; /// returns a `Int` value otherwise. - private func getCompletedCount(fractionState: inout FractionState) -> Int { - if let interopChild = fractionState.interopChild { + private func getCompletedCount(state: inout State) -> Int { + if let interopChild = state.fractionState.interopChild { return interopChild.completedCount } - return fractionState.selfFraction.completed + + // Trigger updates all the way from leaf + processDirtyStates(state: &state) + + return state.fractionState.selfFraction.completed } /// Returns 0.0 if `self` has `nil` total units; @@ -397,30 +419,30 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// /// The calculation of fraction completed for a ProgressManager instance that has children /// will take into account children's fraction completed as well. - private func getFractionCompleted(fractionState: inout FractionState) -> Double { - if let interopChild = fractionState.interopChild { + private func getFractionCompleted(state: inout State) -> Double { + if let interopChild = state.fractionState.interopChild { return interopChild.fractionCompleted } - if fractionState.indeterminate { + if state.fractionState.indeterminate { return 0.0 } - guard fractionState.selfFraction.total > 0 else { - return fractionState.selfFraction.fractionCompleted + guard state.fractionState.selfFraction.total > 0 else { + return state.fractionState.selfFraction.fractionCompleted } - return (fractionState.selfFraction + fractionState.childFraction).fractionCompleted + return (state.fractionState.selfFraction + state.fractionState.childFraction).fractionCompleted } /// Returns `true` if completed and total units are not `nil` and completed units is greater than or equal to total units; /// returns `false` otherwise. - private func getIsFinished(fractionState: inout FractionState) -> Bool { - return fractionState.selfFraction.isFinished + private func getIsFinished(state: inout State) -> Bool { + return state.fractionState.selfFraction.isFinished } /// Returns `true` if `self` has `nil` total units. - private func getIsIndeterminate(fractionState: inout FractionState) -> Bool { - return fractionState.indeterminate + private func getIsIndeterminate(state: inout State) -> Bool { + return state.fractionState.indeterminate } //MARK: FractionCompleted Calculation methods @@ -429,16 +451,173 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { let current: _ProgressFraction } - private func updateCompletedCount(count: Int) -> UpdateState { + // Called only by complete(count:) + private func updatePendingCompletedCount(increment count: Int) { + print("called update pending completed count") // Acquire and release child's lock - let (previous, current) = state.withLock { state in - let prev = state.fractionState.overallFraction - state.fractionState.selfFraction.completed += count - return (prev, state.fractionState.overallFraction) + state.withLock { state in + // If there was a pending update before + if let updatedCompletedCount = state.fractionState.pendingCompletedCount { + state.fractionState.pendingCompletedCount = updatedCompletedCount + count + } else { + // If this is the first pending update + state.fractionState.pendingCompletedCount = state.fractionState.selfFraction.completed + count + } + } + } + + private func addDirtyChild(_ child: ProgressManager) { + print("called dirty child") + state.withLock { state in + // If already dirty, don't continue adding + if state.fractionState.dirtyChildren.contains(child) { + return + } else { + state.fractionState.dirtyChildren.insert(child) + state.fractionState.isDirty = true + } + } + + parents.withLock { parents in + for (parent, _) in parents { + parent.addDirtyChild(child) + } + } + } + + private func processDirtyStates(state: inout State) { + if state.fractionState.isProcessing { return } + + if !state.fractionState.isDirty { return } + + state.fractionState.isProcessing = true + + + // Collect dirty nodes in DFS order + let nodesToProcess = collectAllDirtyNodes(state: &state) + + // Process in bottom-up order + for node in nodesToProcess.reversed() { + node.processLocalState() + } + + state.fractionState.isProcessing = false + + } + + private func collectAllDirtyNodes(state: inout State) -> [ProgressManager] { + var result = [ProgressManager]() + var visited = Set() + collectDirtyNodesRecursive(&result, visited: &visited, state: &state) + return result.reversed() + } + + private func collectDirtyNodesRecursive(_ result: inout [ProgressManager], visited: inout Set, state: inout State) { + if visited.contains(self) { return } + visited.insert(self) + + let dirtyChildren = state.fractionState.dirtyChildren + + // TODO: Any danger here because children can be concurrently added? + + for dirtyChild in dirtyChildren { + dirtyChild.state.withLock { state in + dirtyChild.collectDirtyNodesRecursive(&result, visited: &visited, state: &state) + } + } + +// if state.fractionState.isDirty { + result.append(self) +// } + } + + private func processLocalState() { + // 1. Apply our own pending completed count first + + + state.withLock { state in + // Update our own self fraction + if let updatedCompletedCount = state.fractionState.pendingCompletedCount { + state.fractionState.selfFraction.completed = updatedCompletedCount + } + // IMPORTANT: We don't need to update our parent here + // Parents will call updateFractionForChild on us later + } + + + // 2. Get dirty children before clearing the set + let dirtyChildren = state.withLock { state in + let children = Array(state.fractionState.dirtyChildren) + state.fractionState.dirtyChildren.removeAll() + return children + } + + // 3. Reset our child fraction since we'll recalculate it + state.withLock { state in + state.fractionState.childFraction = _ProgressFraction(completed: 0, total: 0) + } + + // 4. Update for each dirty child + for child in dirtyChildren { + // THIS is where we update our childFraction based on each child + updateFractionForChild(child) + } + + // 5. Recalculate overall fraction +// recalculateOverallFraction() + + // 6. Clear dirty flag + state.withLock { state in + state.fractionState.isDirty = false + } + } + + // THIS is the key method where parent updates its childFraction based on a child + private func updateFractionForChild(_ child: ProgressManager) { + // Get child's CURRENT fraction (which includes its updated selfFraction) + let childFraction = child.getCurrentFraction() + + // Get the portion assigned to this child + let childPortion = getChildPortion(child) + + state.withLock { state in + // Calculate the child's contribution to our progress + let multiplier = _ProgressFraction(completed: childPortion, total: state.fractionState.selfFraction.total) + + // Add child's contribution to our childFraction + // This is how our childFraction gets updated based on the child's state + state.fractionState.childFraction = state.fractionState.childFraction + (childFraction * multiplier) + } + } + + // Helper to get a child's current fraction WITHOUT triggering processing + internal func getCurrentFraction() -> _ProgressFraction { + return state.withLock { state in + // Direct access to fraction state without processing + return state.fractionState.overallFraction } - return UpdateState(previous: previous, current: current) } + private func getChildPortion(_ child: ProgressManager) -> Int { + return child.parents.withLock { parents in + for (parent, portionOfParent) in parents { + if parent == self { + return portionOfParent + } + } + return 0 + } + } + + // Recalculate overall fraction +// private func recalculateOverallFraction() { +// state.withLock { state in +// // Combine self fraction and child fraction +// // This should make the "0" is not equal to "13" test pass +// state.fractionState.overallFraction = state.fractionState.selfFraction + state.fractionState.childFraction +// } +// } + private func updateFractionCompleted(from: _ProgressFraction, to: _ProgressFraction) { _$observationRegistrar.withMutation(of: self, keyPath: \.fractionCompleted) { if from != to { From e40c904ee335054a4974373a81ffcda7c5a295d4 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 2 Jun 2025 13:58:30 -0700 Subject: [PATCH 64/85] remove format style from PR --- .../ProgressManager/CMakeLists.txt | 21 --- .../ProgressManager/ProgressFormatting.swift | 36 ---- .../ProgressManager+FileFormatStyle.swift | 158 ----------------- .../ProgressManager+FormatStyle.swift | 162 ------------------ .../ProgressReporter+FileFormatStyle.swift | 158 ----------------- .../ProgressReporter+FormatStyle.swift | 162 ------------------ 6 files changed, 697 deletions(-) delete mode 100644 Sources/FoundationInternationalization/ProgressManager/CMakeLists.txt delete mode 100644 Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift delete mode 100644 Sources/FoundationInternationalization/ProgressManager/ProgressManager+FileFormatStyle.swift delete mode 100644 Sources/FoundationInternationalization/ProgressManager/ProgressManager+FormatStyle.swift delete mode 100644 Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FileFormatStyle.swift delete mode 100644 Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FormatStyle.swift diff --git a/Sources/FoundationInternationalization/ProgressManager/CMakeLists.txt b/Sources/FoundationInternationalization/ProgressManager/CMakeLists.txt deleted file mode 100644 index 988db2502..000000000 --- a/Sources/FoundationInternationalization/ProgressManager/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -##===----------------------------------------------------------------------===## -## -## This source file is part of the Swift open source project -## -## Copyright (c) 2025 Apple Inc. and the Swift project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.md for the list of Swift project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -target_include_directories(FoundationInternationalization PRIVATE .) -target_sources(FoundationInternationalization PRIVATE - ProgressFormatting.swift - ProgressManager+FileFormatStyle.swift - ProgressManager+FormatStyle.swift - ProgressReporter+FileFormatStyle.swift - ProgressReporter+FormatStyle.swift) diff --git a/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift b/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift deleted file mode 100644 index 61c67a1cd..000000000 --- a/Sources/FoundationInternationalization/ProgressManager/ProgressFormatting.swift +++ /dev/null @@ -1,36 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -//@available(FoundationPreview 6.2, *) -//public protocol ProgressFormatting { -// -//var fractionCompleted: Double -//} -// -//@available(FoundationPreview 6.2, *) -//extension ProgressManager: ProgressFormatting {} -// -//@available(FoundationPreview 6.2, *) -//extension ProgressReporter: ProgressFormatting {} -// -//struct GenericProgressFormatStyle: FormatStyle { -// -// associatedtype Option: Sendable, Codable, Hashable, Equatable -// -// var option: Option { get } -// -// func format(_ value: any ProgressFormatting) -> String -// -// func locale(_ locale: Locale) -> any ProgressFormatStyle -//} - -//struct FileProgressFormatStyle: FormatStyle diff --git a/Sources/FoundationInternationalization/ProgressManager/ProgressManager+FileFormatStyle.swift b/Sources/FoundationInternationalization/ProgressManager/ProgressManager+FileFormatStyle.swift deleted file mode 100644 index bd68d4d6f..000000000 --- a/Sources/FoundationInternationalization/ProgressManager/ProgressManager+FileFormatStyle.swift +++ /dev/null @@ -1,158 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// -#if canImport(FoundationEssentials) -import FoundationEssentials -#endif - -@available(FoundationPreview 6.2, *) -extension ProgressManager { - //TODO: rdar://149092406 Manual Codable Conformance - public struct FileFormatStyle: Sendable, Codable, Equatable, Hashable { - - internal struct Option: Sendable, Codable, Equatable, Hashable { - - init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - self.rawOption = try container.decode(RawOption.self) - } - - func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(rawOption) - } - - internal static var file: Option { Option(.file) } - - fileprivate enum RawOption: Codable, Equatable, Hashable { - case file - } - - fileprivate var rawOption: RawOption - - private init(_ rawOption: RawOption) { - self.rawOption = rawOption - } - } - - struct CodableRepresentation: Codable { - let locale: Locale - let option: Option - } - - var codableRepresentation: CodableRepresentation { - .init(locale: self.locale, option: self.option) - } - - public init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let rawValue = try container.decode(CodableRepresentation.self) - self.locale = rawValue.locale - self.option = rawValue.option - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(codableRepresentation) - } - - public var locale: Locale - let option: Option - - internal init(_ option: Option, locale: Locale = .autoupdatingCurrent) { - self.locale = locale - self.option = option - } - } -} - - -@available(FoundationPreview 6.2, *) -extension ProgressManager.FileFormatStyle: FormatStyle { - - public func locale(_ locale: Locale) -> ProgressManager.FileFormatStyle { - .init(self.option, locale: locale) - } - - public func format(_ manager: ProgressManager) -> String { - switch self.option.rawOption { - - case .file: - #if FOUNDATION_FRAMEWORK - var fileCountLSR: LocalizedStringResource? - var byteCountLSR: LocalizedStringResource? - var throughputLSR: LocalizedStringResource? - var timeRemainingLSR: LocalizedStringResource? - - let properties = manager.withProperties(\.self) - - fileCountLSR = LocalizedStringResource("\(properties.completedFileCount, format: IntegerFormatStyle()) of \(properties.totalFileCount, format: IntegerFormatStyle()) files", locale: self.locale, bundle: .forClass(ProgressManager.self)) - - - byteCountLSR = LocalizedStringResource("\(properties.completedByteCount, format: ByteCountFormatStyle()) of \(properties.totalByteCount, format: ByteCountFormatStyle())", locale: self.locale, bundle: .forClass(ProgressManager.self)) - - - throughputLSR = LocalizedStringResource("\(properties.throughput, format: ByteCountFormatStyle())/s", locale: self.locale, bundle: .forClass(ProgressManager.self)) - - timeRemainingLSR = LocalizedStringResource("\(properties.estimatedTimeRemaining, format: Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .wide)) remaining", locale: self.locale, bundle: .forClass(ProgressManager.self)) - - return """ - \(String(localized: fileCountLSR ?? "")) - \(String(localized: byteCountLSR ?? "")) - \(String(localized: throughputLSR ?? "")) - \(String(localized: timeRemainingLSR ?? "")) - """ - #else - - var fileCountString: String? - var byteCountString: String? - var throughputString: String? - var timeRemainingString: String? - - let properties = manager.withProperties(\.self) - - - fileCountString = "\(properties.completedFileCount.formatted(IntegerFormatStyle(locale: self.locale))) / \(properties.totalFileCount.formatted(IntegerFormatStyle(locale: self.locale)))" - - - byteCountString = "\(properties.completedByteCount.formatted(ByteCountFormatStyle(locale: self.locale))) / \(properties.totalByteCount.formatted(ByteCountFormatStyle(locale: self.locale)))" - - throughputString = "\(properties.throughput.formatted(ByteCountFormatStyle(locale: self.locale)))/s" - - var formatStyle = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .wide) - formatStyle.locale = self.locale - timeRemainingString = "\(properties.estimatedTimeRemaining.formatted(formatStyle)) remaining" - - return """ - \(fileCountString ?? "") - \(byteCountString ?? "") - \(throughputString ?? "") - \(timeRemainingString ?? "") - """ - #endif - } - } -} - -@available(FoundationPreview 6.2, *) -// Make access easier to format ProgressManager -extension ProgressManager { - public func formatted(_ style: ProgressManager.FileFormatStyle) -> String { - style.format(self) - } -} - -@available(FoundationPreview 6.2, *) -extension FormatStyle where Self == ProgressManager.FileFormatStyle { - public static var file: Self { - .init(.file) - } -} diff --git a/Sources/FoundationInternationalization/ProgressManager/ProgressManager+FormatStyle.swift b/Sources/FoundationInternationalization/ProgressManager/ProgressManager+FormatStyle.swift deleted file mode 100644 index 271b3b37b..000000000 --- a/Sources/FoundationInternationalization/ProgressManager/ProgressManager+FormatStyle.swift +++ /dev/null @@ -1,162 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// -#if canImport(FoundationEssentials) -import FoundationEssentials -#endif -// Outlines the options available to format ProgressManager -@available(FoundationPreview 6.2, *) -extension ProgressManager { - - public struct FormatStyle: Sendable, Codable, Equatable, Hashable { - - // Outlines the options available to format ProgressManager - internal struct Option: Sendable, Codable, Hashable, Equatable { - - init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - rawOption = try container.decode(RawOption.self) - } - - func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(rawOption) - } - - /// Option specifying `fractionCompleted`. - /// - /// For example, 20% completed. - /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance that should be used to format `fractionCompleted`. - /// - Returns: A `LocalizedStringResource` for formatted `fractionCompleted`. - internal static func fractionCompleted(format style: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent() - ) -> Option { - return Option(.fractionCompleted(style)) - } - - /// Option specifying `completedCount` / `totalCount`. - /// - /// For example, 5 of 10. - /// - Parameter style: An `IntegerFormatStyle` instance that should be used to format `completedCount` and `totalCount`. - /// - Returns: A `LocalizedStringResource` for formatted `completedCount` / `totalCount`. - internal static func count(format style: IntegerFormatStyle = IntegerFormatStyle() - ) -> Option { - return Option(.count(style)) - } - - fileprivate enum RawOption: Codable, Hashable, Equatable { - case count(IntegerFormatStyle) - case fractionCompleted(FloatingPointFormatStyle.Percent) - } - - fileprivate var rawOption: RawOption - - private init(_ rawOption: RawOption) { - self.rawOption = rawOption - } - } - - struct CodableRepresentation: Codable { - let locale: Locale - let option: Option - } - - var codableRepresentation: CodableRepresentation { - .init(locale: self.locale, option: self.option) - } - - public init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let rawValue = try container.decode(CodableRepresentation.self) - self.locale = rawValue.locale - self.option = rawValue.option - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(codableRepresentation) - } - - public var locale: Locale - let option: Option - - internal init(_ option: Option, locale: Locale = .autoupdatingCurrent) { - self.locale = locale - self.option = option - } - } -} - -@available(FoundationPreview 6.2, *) -extension ProgressManager.FormatStyle: FormatStyle { - - public func locale(_ locale: Locale) -> ProgressManager.FormatStyle { - .init(self.option, locale: locale) - } - - public func format(_ manager: ProgressManager) -> String { - switch self.option.rawOption { - case .count(let countStyle): - let count = manager.withProperties { p in - return (p.completedCount, p.totalCount) - } - #if FOUNDATION_FRAMEWORK - let countLSR = LocalizedStringResource("\(count.0, format: countStyle) of \(count.1 ?? 0, format: countStyle)", locale: self.locale, bundle: .forClass(ProgressManager.self)) - return String(localized: countLSR) - #else - return "\(count.0.formatted(countStyle.locale(self.locale))) / \((count.1 ?? 0).formatted(countStyle.locale(self.locale)))" - #endif - - case .fractionCompleted(let fractionStyle): - #if FOUNDATION_FRAMEWORK - let fractionLSR = LocalizedStringResource("\(manager.fractionCompleted, format: fractionStyle) completed", locale: self.locale, bundle: .forClass(ProgressManager.self)) - return String(localized: fractionLSR) - #else - return "\(manager.fractionCompleted.formatted(fractionStyle.locale(self.locale)))" - #endif - } - } -} - -@available(FoundationPreview 6.2, *) -// Make access easier to format ProgressManager -extension ProgressManager { - -#if FOUNDATION_FRAMEWORK - public func formatted(_ style: F) -> F.FormatOutput where F.FormatInput == ProgressManager { - style.format(self) - } -#else - public func formatted(_ style: F) -> F.FormatOutput where F.FormatInput == ProgressManager { - style.format(self) - } -#endif // FOUNDATION_FRAMEWORK - - public func formatted() -> String { - self.formatted(.fractionCompleted()) - } - -} - -@available(FoundationPreview 6.2, *) -extension FormatStyle where Self == ProgressManager.FormatStyle { - - public static func fractionCompleted( - format: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent() - ) -> Self { - .init(.fractionCompleted(format: format)) - } - - public static func count( - format: IntegerFormatStyle = IntegerFormatStyle() - ) -> Self { - .init(.count(format: format)) - } -} diff --git a/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FileFormatStyle.swift b/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FileFormatStyle.swift deleted file mode 100644 index 0e2b15f81..000000000 --- a/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FileFormatStyle.swift +++ /dev/null @@ -1,158 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// -#if canImport(FoundationEssentials) -import FoundationEssentials -#endif - -@available(FoundationPreview 6.2, *) -extension ProgressReporter { - //TODO: rdar://149092406 Manual Codable Conformance - public struct FileFormatStyle: Sendable, Codable, Equatable, Hashable { - - internal struct Option: Sendable, Codable, Equatable, Hashable { - - init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - self.rawOption = try container.decode(RawOption.self) - } - - func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(rawOption) - } - - internal static var file: Option { Option(.file) } - - fileprivate enum RawOption: Codable, Equatable, Hashable { - case file - } - - fileprivate var rawOption: RawOption - - private init(_ rawOption: RawOption) { - self.rawOption = rawOption - } - } - - struct CodableRepresentation: Codable { - let locale: Locale - let option: Option - } - - var codableRepresentation: CodableRepresentation { - .init(locale: self.locale, option: self.option) - } - - public init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let rawValue = try container.decode(CodableRepresentation.self) - self.locale = rawValue.locale - self.option = rawValue.option - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(codableRepresentation) - } - - public var locale: Locale - let option: Option - - internal init(_ option: Option, locale: Locale = .autoupdatingCurrent) { - self.locale = locale - self.option = option - } - } -} - - -@available(FoundationPreview 6.2, *) -extension ProgressReporter.FileFormatStyle: FormatStyle { - - public func locale(_ locale: Locale) -> ProgressReporter.FileFormatStyle { - .init(self.option, locale: locale) - } - - public func format(_ reporter: ProgressReporter) -> String { - switch self.option.rawOption { - - case .file: - #if FOUNDATION_FRAMEWORK - var fileCountLSR: LocalizedStringResource? - var byteCountLSR: LocalizedStringResource? - var throughputLSR: LocalizedStringResource? - var timeRemainingLSR: LocalizedStringResource? - - let properties = reporter.withProperties(\.self) - - fileCountLSR = LocalizedStringResource("\(properties.completedFileCount, format: IntegerFormatStyle()) of \(properties.totalFileCount, format: IntegerFormatStyle()) files", locale: self.locale, bundle: .forClass(ProgressReporter.self)) - - - byteCountLSR = LocalizedStringResource("\(properties.completedByteCount, format: ByteCountFormatStyle()) of \(properties.totalByteCount, format: ByteCountFormatStyle())", locale: self.locale, bundle: .forClass(ProgressReporter.self)) - - - throughputLSR = LocalizedStringResource("\(properties.throughput, format: ByteCountFormatStyle())/s", locale: self.locale, bundle: .forClass(ProgressReporter.self)) - - timeRemainingLSR = LocalizedStringResource("\(properties.estimatedTimeRemaining, format: Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .wide)) remaining", locale: self.locale, bundle: .forClass(ProgressReporter.self)) - - return """ - \(String(localized: fileCountLSR ?? "")) - \(String(localized: byteCountLSR ?? "")) - \(String(localized: throughputLSR ?? "")) - \(String(localized: timeRemainingLSR ?? "")) - """ - #else - - var fileCountString: String? - var byteCountString: String? - var throughputString: String? - var timeRemainingString: String? - - let properties = reporter.withProperties(\.self) - - - fileCountString = "\(properties.completedFileCount.formatted(IntegerFormatStyle(locale: self.locale))) / \(properties.totalFileCount.formatted(IntegerFormatStyle(locale: self.locale)))" - - - byteCountString = "\(properties.completedByteCount.formatted(ByteCountFormatStyle(locale: self.locale))) / \(properties.totalByteCount.formatted(ByteCountFormatStyle(locale: self.locale)))" - - throughputString = "\(properties.throughput.formatted(ByteCountFormatStyle(locale: self.locale)))/s" - - var formatStyle = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .wide) - formatStyle.locale = self.locale - timeRemainingString = "\(properties.estimatedTimeRemaining.formatted(formatStyle)) remaining" - - return """ - \(fileCountString ?? "") - \(byteCountString ?? "") - \(throughputString ?? "") - \(timeRemainingString ?? "") - """ - #endif - } - } -} - -@available(FoundationPreview 6.2, *) -// Make access easier to format ProgressReporter -extension ProgressReporter { - public func formatted(_ style: ProgressReporter.FileFormatStyle) -> String { - style.format(self) - } -} - -@available(FoundationPreview 6.2, *) -extension FormatStyle where Self == ProgressReporter.FileFormatStyle { - public static var file: Self { - .init(.file) - } -} diff --git a/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FormatStyle.swift b/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FormatStyle.swift deleted file mode 100644 index 869399aba..000000000 --- a/Sources/FoundationInternationalization/ProgressManager/ProgressReporter+FormatStyle.swift +++ /dev/null @@ -1,162 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// -#if canImport(FoundationEssentials) -import FoundationEssentials -#endif -// Outlines the options available to format ProgressReporter -@available(FoundationPreview 6.2, *) -extension ProgressReporter { - - public struct FormatStyle: Sendable, Codable, Equatable, Hashable { - - // Outlines the options available to format ProgressReporter - internal struct Option: Sendable, Codable, Hashable, Equatable { - - init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - rawOption = try container.decode(RawOption.self) - } - - func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(rawOption) - } - - /// Option specifying `fractionCompleted`. - /// - /// For example, 20% completed. - /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance that should be used to format `fractionCompleted`. - /// - Returns: A `LocalizedStringResource` for formatted `fractionCompleted`. - internal static func fractionCompleted(format style: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent() - ) -> Option { - return Option(.fractionCompleted(style)) - } - - /// Option specifying `completedCount` / `totalCount`. - /// - /// For example, 5 of 10. - /// - Parameter style: An `IntegerFormatStyle` instance that should be used to format `completedCount` and `totalCount`. - /// - Returns: A `LocalizedStringResource` for formatted `completedCount` / `totalCount`. - internal static func count(format style: IntegerFormatStyle = IntegerFormatStyle() - ) -> Option { - return Option(.count(style)) - } - - fileprivate enum RawOption: Codable, Hashable, Equatable { - case count(IntegerFormatStyle) - case fractionCompleted(FloatingPointFormatStyle.Percent) - } - - fileprivate var rawOption: RawOption - - private init(_ rawOption: RawOption) { - self.rawOption = rawOption - } - } - - struct CodableRepresentation: Codable { - let locale: Locale - let option: Option - } - - var codableRepresentation: CodableRepresentation { - .init(locale: self.locale, option: self.option) - } - - public init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let rawValue = try container.decode(CodableRepresentation.self) - self.locale = rawValue.locale - self.option = rawValue.option - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(codableRepresentation) - } - - public var locale: Locale - let option: Option - - internal init(_ option: Option, locale: Locale = .autoupdatingCurrent) { - self.locale = locale - self.option = option - } - } -} - -@available(FoundationPreview 6.2, *) -extension ProgressReporter.FormatStyle: FormatStyle { - - public func locale(_ locale: Locale) -> ProgressReporter.FormatStyle { - .init(self.option, locale: locale) - } - - public func format(_ reporter: ProgressReporter) -> String { - switch self.option.rawOption { - case .count(let countStyle): - let count = reporter.manager.withProperties { p in - return (p.completedCount, p.totalCount) - } - #if FOUNDATION_FRAMEWORK - let countLSR = LocalizedStringResource("\(count.0, format: countStyle) of \(count.1 ?? 0, format: countStyle)", locale: self.locale, bundle: .forClass(ProgressReporter.self)) - return String(localized: countLSR) - #else - return "\(count.0.formatted(countStyle.locale(self.locale))) / \((count.1 ?? 0).formatted(countStyle.locale(self.locale)))" - #endif - - case .fractionCompleted(let fractionStyle): - #if FOUNDATION_FRAMEWORK - let fractionLSR = LocalizedStringResource("\(reporter.fractionCompleted, format: fractionStyle) completed", locale: self.locale, bundle: .forClass(ProgressReporter.self)) - return String(localized: fractionLSR) - #else - return "\(reporter.fractionCompleted.formatted(fractionStyle.locale(self.locale)))" - #endif - } - } -} - -@available(FoundationPreview 6.2, *) -// Make access easier to format ProgressReporter -extension ProgressReporter { - -#if FOUNDATION_FRAMEWORK - public func formatted(_ style: F) -> F.FormatOutput where F.FormatInput == ProgressReporter { - style.format(self) - } -#else - public func formatted(_ style: F) -> F.FormatOutput where F.FormatInput == ProgressReporter { - style.format(self) - } -#endif // FOUNDATION_FRAMEWORK - - public func formatted() -> String { - self.formatted(.fractionCompleted()) - } - -} - -@available(FoundationPreview 6.2, *) -extension FormatStyle where Self == ProgressReporter.FormatStyle { - - public static func fractionCompleted( - format: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent() - ) -> Self { - .init(.fractionCompleted(format: format)) - } - - public static func count( - format: IntegerFormatStyle = IntegerFormatStyle() - ) -> Self { - .init(.count(format: format)) - } -} From d5665bb20606d797770259fa9c5f6235db493b53 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 2 Jun 2025 13:58:58 -0700 Subject: [PATCH 65/85] remove source from Internationalization CMake --- Sources/FoundationInternationalization/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/FoundationInternationalization/CMakeLists.txt b/Sources/FoundationInternationalization/CMakeLists.txt index 52b5af368..6cf0629ef 100644 --- a/Sources/FoundationInternationalization/CMakeLists.txt +++ b/Sources/FoundationInternationalization/CMakeLists.txt @@ -25,7 +25,6 @@ add_subdirectory(Formatting) add_subdirectory(ICU) add_subdirectory(Locale) add_subdirectory(Predicate) -add_subdirectory(ProgressManager) add_subdirectory(String) add_subdirectory(TimeZone) From a2540a78fc86e7f742288e79608728bdbf309048 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 2 Jun 2025 14:23:31 -0700 Subject: [PATCH 66/85] revert to recursive implementation --- .../ProgressManager/ProgressManager.swift | 262 +++--------------- 1 file changed, 42 insertions(+), 220 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index aa78f0ad8..e41cd8d37 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -28,11 +28,6 @@ internal struct FractionState { selfFraction + childFraction } var interopChild: ProgressManager? // read from this if self is actually an interop ghost - - var isDirty: Bool = false // Flag to indicate fraction computation needs update - var isProcessing: Bool = false - var pendingCompletedCount: Int? - var dirtyChildren: Set = Set() // Track which children are dirty } internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { @@ -58,7 +53,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { var otherProperties: [AnyMetatypeWrapper: (any Sendable)] // Type: Metatype maps to dictionary of child to value var childrenOtherProperties: [AnyMetatypeWrapper: OrderedDictionary] - var children: Set } // Interop states @@ -83,7 +77,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var totalCount: Int? { _$observationRegistrar.access(self, keyPath: \.totalCount) return state.withLock { state in - return getTotalCount(state: &state) + getTotalCount(fractionState: &state.fractionState) } } @@ -92,9 +86,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var completedCount: Int { _$observationRegistrar.access(self, keyPath: \.completedCount) return state.withLock { state in - return getCompletedCount(state: &state) + getCompletedCount(fractionState: &state.fractionState) } - } /// The proportion of work completed. @@ -103,7 +96,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var fractionCompleted: Double { _$observationRegistrar.access(self, keyPath: \.fractionCompleted) return state.withLock { state in - getFractionCompleted(state: &state) + getFractionCompleted(fractionState: &state.fractionState) } } @@ -112,7 +105,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var isIndeterminate: Bool { _$observationRegistrar.access(self, keyPath: \.isIndeterminate) return state.withLock { state in - getIsIndeterminate(state: &state) + getIsIndeterminate(fractionState: &state.fractionState) } } @@ -121,7 +114,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var isFinished: Bool { _$observationRegistrar.access(self, keyPath: \.isFinished) return state.withLock { state in - getIsFinished(state: &state) + getIsFinished(fractionState: &state.fractionState) } } @@ -147,7 +140,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// The total units of work. public var totalCount: Int? { mutating get { - manager.getTotalCount(state: &state) + manager.getTotalCount(fractionState: &state.fractionState) } set { @@ -178,15 +171,15 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// The completed units of work. public var completedCount: Int { mutating get { - manager.getCompletedCount(state: &state) + manager.getCompletedCount(fractionState: &state.fractionState) } set { - //TODO: I am scared, this might introduce recursive lock again -// let existingCompleted = state.fractionState.selfFraction.completed -// manager.complete(count: newValue - existingCompleted) - + let prev = state.fractionState.overallFraction + state.fractionState.selfFraction.completed = newValue + manager.updateFractionCompleted(from: prev, to: state.fractionState.overallFraction) manager.ghostReporter?.notifyObservers(with: .fractionUpdated) + manager.monitorInterop.withLock { [manager] interop in if interop == true { manager.notifyObservers(with: .fractionUpdated) @@ -231,17 +224,19 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } internal let parents: LockedState<[ProgressManager: Int]> + private let children: LockedState> private let state: LockedState internal init(total: Int?, ghostReporter: ProgressManager?, interopObservation: (any Sendable)?) { self.parents = .init(initialState: [:]) + self.children = .init(initialState: Set()) let fractionState = FractionState( indeterminate: total == nil ? true : false, selfFraction: _ProgressFraction(completed: 0, total: total ?? 0), childFraction: _ProgressFraction(completed: 0, total: 1), interopChild: nil ) - let state = State(fractionState: fractionState, otherProperties: [:], childrenOtherProperties: [:], children: Set()) + let state = State(fractionState: fractionState, otherProperties: [:], childrenOtherProperties: [:]) self.state = LockedState(initialState: state) self.interopObservation = interopObservation self.ghostReporter = ghostReporter @@ -312,20 +307,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// Increases `completedCount` by `count`. /// - Parameter count: Units of work. public func complete(count: Int) { - // First update pendingCompletedCount - updatePendingCompletedCount(increment: count) - - // Then mark self to be dirty - state.withLock { state in - state.fractionState.isDirty = true - } - - // Add self to all parent's dirtyChildren list & mark parents as dirty recursively - parents.withLock { parents in - for (parent, _) in parents { - parent.addDirtyChild(self) - } - } + let updateState = updateCompletedCount(count: count) + updateFractionCompleted(from: updateState.previous, to: updateState.current) // Interop updates stuff ghostReporter?.notifyObservers(with: .fractionUpdated) @@ -361,7 +344,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { return try state.withLock { state in var values = Values(manager: self, state: state) // This is done to avoid copy on write later - state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction()), otherProperties: [:], childrenOtherProperties: [:], children: Set()) + state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction()), otherProperties: [:], childrenOtherProperties: [:]) let result = try closure(&values) state = values.state return result @@ -389,28 +372,24 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { //MARK: ProgressManager Properties getters /// Returns nil if `self` was instantiated without total units; /// returns a `Int` value otherwise. - private func getTotalCount(state: inout State) -> Int? { - if let interopChild = state.fractionState.interopChild { + private func getTotalCount(fractionState: inout FractionState) -> Int? { + if let interopChild = fractionState.interopChild { return interopChild.totalCount } - if state.fractionState.indeterminate { + if fractionState.indeterminate { return nil } else { - return state.fractionState.selfFraction.total + return fractionState.selfFraction.total } } /// Returns 0 if `self` has `nil` total units; /// returns a `Int` value otherwise. - private func getCompletedCount(state: inout State) -> Int { - if let interopChild = state.fractionState.interopChild { + private func getCompletedCount(fractionState: inout FractionState) -> Int { + if let interopChild = fractionState.interopChild { return interopChild.completedCount } - - // Trigger updates all the way from leaf - processDirtyStates(state: &state) - - return state.fractionState.selfFraction.completed + return fractionState.selfFraction.completed } /// Returns 0.0 if `self` has `nil` total units; @@ -419,30 +398,30 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// /// The calculation of fraction completed for a ProgressManager instance that has children /// will take into account children's fraction completed as well. - private func getFractionCompleted(state: inout State) -> Double { - if let interopChild = state.fractionState.interopChild { + private func getFractionCompleted(fractionState: inout FractionState) -> Double { + if let interopChild = fractionState.interopChild { return interopChild.fractionCompleted } - if state.fractionState.indeterminate { + if fractionState.indeterminate { return 0.0 } - guard state.fractionState.selfFraction.total > 0 else { - return state.fractionState.selfFraction.fractionCompleted + guard fractionState.selfFraction.total > 0 else { + return fractionState.selfFraction.fractionCompleted } - return (state.fractionState.selfFraction + state.fractionState.childFraction).fractionCompleted + return (fractionState.selfFraction + fractionState.childFraction).fractionCompleted } /// Returns `true` if completed and total units are not `nil` and completed units is greater than or equal to total units; /// returns `false` otherwise. - private func getIsFinished(state: inout State) -> Bool { - return state.fractionState.selfFraction.isFinished + private func getIsFinished(fractionState: inout FractionState) -> Bool { + return fractionState.selfFraction.isFinished } /// Returns `true` if `self` has `nil` total units. - private func getIsIndeterminate(state: inout State) -> Bool { - return state.fractionState.indeterminate + private func getIsIndeterminate(fractionState: inout FractionState) -> Bool { + return fractionState.indeterminate } //MARK: FractionCompleted Calculation methods @@ -451,173 +430,16 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { let current: _ProgressFraction } - // Called only by complete(count:) - private func updatePendingCompletedCount(increment count: Int) { - print("called update pending completed count") + private func updateCompletedCount(count: Int) -> UpdateState { // Acquire and release child's lock - state.withLock { state in - // If there was a pending update before - if let updatedCompletedCount = state.fractionState.pendingCompletedCount { - state.fractionState.pendingCompletedCount = updatedCompletedCount + count - } else { - // If this is the first pending update - state.fractionState.pendingCompletedCount = state.fractionState.selfFraction.completed + count - } - } - } - - private func addDirtyChild(_ child: ProgressManager) { - print("called dirty child") - state.withLock { state in - // If already dirty, don't continue adding - if state.fractionState.dirtyChildren.contains(child) { - return - } else { - state.fractionState.dirtyChildren.insert(child) - state.fractionState.isDirty = true - } - } - - parents.withLock { parents in - for (parent, _) in parents { - parent.addDirtyChild(child) - } - } - } - - private func processDirtyStates(state: inout State) { - if state.fractionState.isProcessing { return } - - if !state.fractionState.isDirty { return } - - state.fractionState.isProcessing = true - - - // Collect dirty nodes in DFS order - let nodesToProcess = collectAllDirtyNodes(state: &state) - - // Process in bottom-up order - for node in nodesToProcess.reversed() { - node.processLocalState() - } - - state.fractionState.isProcessing = false - - } - - private func collectAllDirtyNodes(state: inout State) -> [ProgressManager] { - var result = [ProgressManager]() - var visited = Set() - collectDirtyNodesRecursive(&result, visited: &visited, state: &state) - return result.reversed() - } - - private func collectDirtyNodesRecursive(_ result: inout [ProgressManager], visited: inout Set, state: inout State) { - if visited.contains(self) { return } - visited.insert(self) - - let dirtyChildren = state.fractionState.dirtyChildren - - // TODO: Any danger here because children can be concurrently added? - - for dirtyChild in dirtyChildren { - dirtyChild.state.withLock { state in - dirtyChild.collectDirtyNodesRecursive(&result, visited: &visited, state: &state) - } - } - -// if state.fractionState.isDirty { - result.append(self) -// } - } - - private func processLocalState() { - // 1. Apply our own pending completed count first - - - state.withLock { state in - // Update our own self fraction - if let updatedCompletedCount = state.fractionState.pendingCompletedCount { - state.fractionState.selfFraction.completed = updatedCompletedCount - } - // IMPORTANT: We don't need to update our parent here - // Parents will call updateFractionForChild on us later - } - - - // 2. Get dirty children before clearing the set - let dirtyChildren = state.withLock { state in - let children = Array(state.fractionState.dirtyChildren) - state.fractionState.dirtyChildren.removeAll() - return children - } - - // 3. Reset our child fraction since we'll recalculate it - state.withLock { state in - state.fractionState.childFraction = _ProgressFraction(completed: 0, total: 0) - } - - // 4. Update for each dirty child - for child in dirtyChildren { - // THIS is where we update our childFraction based on each child - updateFractionForChild(child) - } - - // 5. Recalculate overall fraction -// recalculateOverallFraction() - - // 6. Clear dirty flag - state.withLock { state in - state.fractionState.isDirty = false - } - } - - // THIS is the key method where parent updates its childFraction based on a child - private func updateFractionForChild(_ child: ProgressManager) { - // Get child's CURRENT fraction (which includes its updated selfFraction) - let childFraction = child.getCurrentFraction() - - // Get the portion assigned to this child - let childPortion = getChildPortion(child) - - state.withLock { state in - // Calculate the child's contribution to our progress - let multiplier = _ProgressFraction(completed: childPortion, total: state.fractionState.selfFraction.total) - - // Add child's contribution to our childFraction - // This is how our childFraction gets updated based on the child's state - state.fractionState.childFraction = state.fractionState.childFraction + (childFraction * multiplier) - } - } - - // Helper to get a child's current fraction WITHOUT triggering processing - internal func getCurrentFraction() -> _ProgressFraction { - return state.withLock { state in - // Direct access to fraction state without processing - return state.fractionState.overallFraction + let (previous, current) = state.withLock { state in + let prev = state.fractionState.overallFraction + state.fractionState.selfFraction.completed += count + return (prev, state.fractionState.overallFraction) } + return UpdateState(previous: previous, current: current) } - private func getChildPortion(_ child: ProgressManager) -> Int { - return child.parents.withLock { parents in - for (parent, portionOfParent) in parents { - if parent == self { - return portionOfParent - } - } - return 0 - } - } - - // Recalculate overall fraction -// private func recalculateOverallFraction() { -// state.withLock { state in -// // Combine self fraction and child fraction -// // This should make the "0" is not equal to "13" test pass -// state.fractionState.overallFraction = state.fractionState.selfFraction + state.fractionState.childFraction -// } -// } - private func updateFractionCompleted(from: _ProgressFraction, to: _ProgressFraction) { _$observationRegistrar.withMutation(of: self, keyPath: \.fractionCompleted) { if from != to { @@ -707,8 +529,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } internal func addToChildren(childManager: ProgressManager) { - _ = state.withLock { state in - state.children.insert(childManager) + _ = children.withLock { children in + children.insert(childManager) } } From a28d0b7e144c303838948113f9ae956a31297c9d Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 2 Jun 2025 14:56:31 -0700 Subject: [PATCH 67/85] convert to use typed throws --- .../FoundationEssentials/LockedState.swift | 14 ++++--- .../ProgressManager/ProgressManager.swift | 38 ++++++------------- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/Sources/FoundationEssentials/LockedState.swift b/Sources/FoundationEssentials/LockedState.swift index 2316fb955..5a3953dae 100644 --- a/Sources/FoundationEssentials/LockedState.swift +++ b/Sources/FoundationEssentials/LockedState.swift @@ -113,13 +113,17 @@ package struct LockedState { return initialState }) } - - package func withLock(_ body: @Sendable (inout State) throws -> T) rethrows -> T { + + package func withLock( + _ body: (inout sending State) throws(E) -> sending T + ) throws(E) -> sending T { try withLockUnchecked(body) } - - package func withLockUnchecked(_ body: (inout State) throws -> T) rethrows -> T { - try _buffer.withUnsafeMutablePointers { state, lock in + + package func withLockUnchecked( + _ body: (inout sending State) throws(E) -> sending T + ) throws(E) -> sending T { + try _buffer.withUnsafeMutablePointers { (state, lock) throws(E) in _Lock.lock(lock) defer { _Lock.unlock(lock) } return try body(&state.pointee) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index e41cd8d37..40a1abd58 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -66,9 +66,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { internal let interopObservationForMonitor: LockedState<(any Sendable)?> = LockedState(initialState: nil) internal let monitorInterop: LockedState = LockedState(initialState: false) - #if FOUNDATION_FRAMEWORK +#if FOUNDATION_FRAMEWORK internal let parentBridge: LockedState = LockedState(initialState: nil) // dummy, set upon calling setParentBridge - #endif +#endif // Interop properties - Actually set and called internal let ghostReporter: ProgressManager? // set at init, used to call notify observers internal let observers: LockedState<[@Sendable (ObserverState) -> Void]> = LockedState(initialState: [])// storage for all observers, set upon calling addObservers @@ -340,8 +340,10 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } /// Mutates any settable properties that convey information about progress. - public func withProperties(_ closure: @Sendable (inout Values) throws -> T) rethrows -> T { - return try state.withLock { state in + public func withProperties( + _ closure: (inout sending Values) throws(E) -> sending T + ) throws(E) -> sending T { + return try state.withLock { (state) throws(E) -> T in var values = Values(manager: self, state: state) // This is done to avoid copy on write later state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction()), otherProperties: [:], childrenOtherProperties: [:]) @@ -351,24 +353,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } -// public func withProperties( -// _ closure: (inout sending Values) throws(E) -> sending T -// ) throws(E) -> sending T { -// return try state.withLock { state in -// var values = Values(manager: self, state: state) -// // This is done to avoid copy on write later -// state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction()), otherProperties: [:], childrenOtherProperties: [:]) -// -// do { -// let result = try closure(&values) -// state = values.state -// return result -// } catch let localError { -// throw localError as! E -// } -// } -// } - //MARK: ProgressManager Properties getters /// Returns nil if `self` was instantiated without total units; /// returns a `Int` value otherwise. @@ -468,7 +452,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { if next.isFinished { // Remove from children list -// _ = children.withLock { $0.remove(self) } + // _ = children.withLock { $0.remove(self) } if portion != 0 { // Update our self completed units @@ -506,7 +490,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { observation = monitorObservation } } - + internal func setMonitorInterop(to value: Bool) { monitorInterop.withLock { monitorInterop in monitorInterop = value @@ -556,8 +540,10 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { parentReporter.updateChildFraction(from: updates.0, to: updates.1, portion: portionOfParent) } - internal func getAdditionalProperties(_ closure: @Sendable (Values) throws -> T) rethrows -> T { - try state.withLock { state in + internal func getAdditionalProperties( + _ closure: (sending Values) throws(E) -> sending T + ) throws(E) -> sending T { + try state.withLock { state throws(E) -> T in let values = Values(manager: self, state: state) // No need to modify state since this is read-only let result = try closure(values) From e65e5d756a3416a8c880f92e167b02bde4f70f25 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 2 Jun 2025 15:16:28 -0700 Subject: [PATCH 68/85] remove setTotalCount method --- .../ProgressManager/ProgressManager.swift | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 40a1abd58..ba5ea12d7 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -250,34 +250,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { self.init(total: totalCount, ghostReporter: nil, interopObservation: nil) } - /// Sets `totalCount`. - /// - Parameter newTotal: Total units of work. - public func setTotalCount(_ newTotal: Int?) { - state.withLock { state in - let previous = state.fractionState.overallFraction - if state.fractionState.selfFraction.total != newTotal && state.fractionState.selfFraction.total > 0 { - state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: newTotal ?? 1) - } - state.fractionState.selfFraction.total = newTotal ?? 0 - - // if newValue is nil, reset indeterminate to true - if newTotal != nil { - state.fractionState.indeterminate = false - } else { - state.fractionState.indeterminate = true - } - updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) - - ghostReporter?.notifyObservers(with: .totalCountUpdated) - - monitorInterop.withLock { [self] interop in - if interop == true { - notifyObservers(with: .totalCountUpdated) - } - } - } - } - /// Returns a `Subprogress` representing a portion of `self` which can be passed to any method that reports progress. /// /// - Parameter count: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. From f11ba3e9f70709249806325df98e0f57209d1666 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 2 Jun 2025 15:20:23 -0700 Subject: [PATCH 69/85] rename manager to start --- .../ProgressManager/Subprogress.swift | 2 +- .../ProgressManagerTests.swift | 42 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/Subprogress.swift b/Sources/FoundationEssentials/ProgressManager/Subprogress.swift index 358b7642f..234b32ee4 100644 --- a/Sources/FoundationEssentials/ProgressManager/Subprogress.swift +++ b/Sources/FoundationEssentials/ProgressManager/Subprogress.swift @@ -35,7 +35,7 @@ public struct Subprogress: ~Copyable, Sendable { /// Instantiates a ProgressManager which is a child to the parent from which `self` is returned. /// - Parameter totalCount: Total count of returned child `ProgressManager` instance. /// - Returns: A `ProgressManager` instance. - public consuming func manager(totalCount: Int?) -> ProgressManager { + public consuming func start(totalCount: Int?) -> ProgressManager { isInitializedToProgressReporter = true let childManager = ProgressManager(total: totalCount, ghostReporter: ghostReporter, interopObservation: observation) diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift index fe8b65af0..9e9838749 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift @@ -21,7 +21,7 @@ import XCTest class TestProgressManager: XCTestCase { /// MARK: Helper methods that report progress func doBasicOperationV1(reportTo subprogress: consuming Subprogress) async { - let manager = subprogress.manager(totalCount: 8) + let manager = subprogress.start(totalCount: 8) for i in 1...8 { manager.complete(count: 1) XCTAssertEqual(manager.completedCount, i) @@ -30,7 +30,7 @@ class TestProgressManager: XCTestCase { } func doBasicOperationV2(reportTo subprogress: consuming Subprogress) async { - let manager = subprogress.manager(totalCount: 7) + let manager = subprogress.start(totalCount: 7) for i in 1...7 { manager.complete(count: 1) XCTAssertEqual(manager.completedCount, i) @@ -39,7 +39,7 @@ class TestProgressManager: XCTestCase { } func doBasicOperationV3(reportTo subprogress: consuming Subprogress) async { - let manager = subprogress.manager(totalCount: 11) + let manager = subprogress.start(totalCount: 11) for i in 1...11 { manager.complete(count: 1) XCTAssertEqual(manager.completedCount, i) @@ -95,7 +95,7 @@ class TestProgressManager: XCTestCase { XCTAssertFalse(overall.isFinished) let progress1 = overall.subprogress(assigningCount: 2) - let manager1 = progress1.manager(totalCount: 1) + let manager1 = progress1.start(totalCount: 1) manager1.complete(count: 1) XCTAssertEqual(manager1.totalCount, 1) @@ -125,7 +125,7 @@ class TestProgressManager: XCTestCase { overall.complete(count: 5) let progress1 = overall.subprogress(assigningCount: 8) - let manager1 = progress1.manager(totalCount: 1) + let manager1 = progress1.start(totalCount: 1) manager1.complete(count: 1) XCTAssertEqual(overall.completedCount, 13) @@ -181,11 +181,11 @@ class TestProgressManager: XCTestCase { let overall = ProgressManager(totalCount: 2) let progress1 = overall.subprogress(assigningCount: 1) - let manager1 = progress1.manager(totalCount: 5) + let manager1 = progress1.start(totalCount: 5) manager1.complete(count: 5) let progress2 = overall.subprogress(assigningCount: 1) - let manager2 = progress2.manager(totalCount: 5) + let manager2 = progress2.start(totalCount: 5) manager2.withProperties { properties in properties.totalFileCount = 10 } @@ -216,10 +216,10 @@ class TestProgressManager: XCTestCase { XCTAssertEqual(overall.fractionCompleted, 0.0) let child1 = overall.subprogress(assigningCount: 100) - let manager1 = child1.manager(totalCount: 100) + let manager1 = child1.start(totalCount: 100) let grandchild1 = manager1.subprogress(assigningCount: 100) - let grandchildManager1 = grandchild1.manager(totalCount: 100) + let grandchildManager1 = grandchild1.start(totalCount: 100) XCTAssertEqual(overall.fractionCompleted, 0.0) @@ -241,16 +241,16 @@ class TestProgressManager: XCTestCase { XCTAssertEqual(overall.fractionCompleted, 0.0) let child1 = overall.subprogress(assigningCount: 100) - let manager1 = child1.manager(totalCount: 100) + let manager1 = child1.start(totalCount: 100) let grandchild1 = manager1.subprogress(assigningCount: 100) - let grandchildManager1 = grandchild1.manager(totalCount: 100) + let grandchildManager1 = grandchild1.start(totalCount: 100) XCTAssertEqual(overall.fractionCompleted, 0.0) let greatGrandchild1 = grandchildManager1.subprogress(assigningCount: 100) - let greatGrandchildManager1 = greatGrandchild1.manager(totalCount: 100) + let greatGrandchildManager1 = greatGrandchild1.start(totalCount: 100) greatGrandchildManager1.complete(count: 50) XCTAssertEqual(overall.fractionCompleted, 0.5) @@ -268,7 +268,7 @@ class TestProgressManager: XCTestCase { /// Unit tests for propagation of type-safe metadata in ProgressManager tree. class TestProgressManagerAdditionalProperties: XCTestCase { func doFileOperation(reportTo subprogress: consuming Subprogress) async { - let manager = subprogress.manager(totalCount: 100) + let manager = subprogress.start(totalCount: 100) manager.withProperties { properties in properties.totalFileCount = 100 } @@ -312,7 +312,7 @@ class TestProgressManagerAdditionalProperties: XCTestCase { let overall = ProgressManager(totalCount: 2) let progress1 = overall.subprogress(assigningCount: 1) - let manager1 = progress1.manager(totalCount: 10) + let manager1 = progress1.start(totalCount: 10) manager1.withProperties { properties in properties.totalFileCount = 10 properties.completedFileCount = 0 @@ -336,7 +336,7 @@ class TestProgressManagerAdditionalProperties: XCTestCase { let overall = ProgressManager(totalCount: 2) let progress1 = overall.subprogress(assigningCount: 1) - let manager1 = progress1.manager(totalCount: 10) + let manager1 = progress1.start(totalCount: 10) manager1.withProperties { properties in properties.totalFileCount = 11 @@ -344,7 +344,7 @@ class TestProgressManagerAdditionalProperties: XCTestCase { } let progress2 = overall.subprogress(assigningCount: 1) - let manager2 = progress2.manager(totalCount: 10) + let manager2 = progress2.start(totalCount: 10) manager2.withProperties { properties in properties.totalFileCount = 9 @@ -377,11 +377,11 @@ class TestProgressManagerAdditionalProperties: XCTestCase { let overall = ProgressManager(totalCount: 1) let progress1 = overall.subprogress(assigningCount: 1) - let manager1 = progress1.manager(totalCount: 5) + let manager1 = progress1.start(totalCount: 5) let childProgress1 = manager1.subprogress(assigningCount: 3) - let childManager1 = childProgress1.manager(totalCount: nil) + let childManager1 = childProgress1.start(totalCount: nil) childManager1.withProperties { properties in properties.totalFileCount += 10 } @@ -391,7 +391,7 @@ class TestProgressManagerAdditionalProperties: XCTestCase { XCTAssertEqual(preTotalFileValues, [0, 0, 10]) let childProgress2 = manager1.subprogress(assigningCount: 2) - let childManager2 = childProgress2.manager(totalCount: nil) + let childManager2 = childProgress2.start(totalCount: nil) childManager2.withProperties { properties in properties.totalFileCount += 10 } @@ -425,7 +425,7 @@ class TestProgressManagerInterop: XCTestCase { } func doSomethingWithReporter(subprogress: consuming Subprogress?) async { - let manager = subprogress?.manager(totalCount: 4) + let manager = subprogress?.start(totalCount: 4) manager?.complete(count: 2) manager?.complete(count: 2) } @@ -543,7 +543,7 @@ class TestProgressManagerInterop: XCTestCase { } func receiveProgress(progress: consuming Subprogress) { - let _ = progress.manager(totalCount: 5) + let _ = progress.start(totalCount: 5) } func testInteropProgressManagerParentProgressChildConsistency() async throws { From 97b723f36cae88e64ee01f48264935fc9efb6e3f Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 2 Jun 2025 15:30:35 -0700 Subject: [PATCH 70/85] remove benchmark stuff --- .../ProgressReporting/BenchmarkProgressReporting.swift | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Benchmarks/Benchmarks/Internationalization/ProgressReporting/BenchmarkProgressReporting.swift diff --git a/Benchmarks/Benchmarks/Internationalization/ProgressReporting/BenchmarkProgressReporting.swift b/Benchmarks/Benchmarks/Internationalization/ProgressReporting/BenchmarkProgressReporting.swift deleted file mode 100644 index e69de29bb..000000000 From 126e650c6edbd4f1ceeef27cfc461da8946b967e Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 3 Jun 2025 10:26:08 -0700 Subject: [PATCH 71/85] add code documentation --- .../ProgressManager/ProgressManager.swift | 12 +++++++++--- .../ProgressManager/ProgressReporter.swift | 14 +++++++++++++- .../ProgressManager/ProgressManagerTests.swift | 10 ++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index ba5ea12d7..2878d39d6 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -118,6 +118,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } + + /// A `ProgressReporter` instance, used for providing read-only observation of progress updates or composing into other `ProgressManager`s. public var reporter: ProgressReporter { return .init(manager: self) } @@ -127,6 +129,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { associatedtype Value: Sendable, Hashable, Equatable + /// The default value to return when property is not set to a specific value. static var defaultValue: Value { get } } @@ -252,6 +255,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// Returns a `Subprogress` representing a portion of `self` which can be passed to any method that reports progress. /// + /// If the `Subprogress` is not converted into a `ProgressManager` (for example, due to an error or early return), + /// then the assigned count is marked as completed in the parent `ProgressManager`. + /// /// - Parameter count: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. /// - Returns: A `Subprogress` instance. public func subprogress(assigningCount portionOfParent: Int) -> Subprogress { @@ -264,8 +270,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// Adds a `ProgressReporter` as a child, with its progress representing a portion of `self`'s progress. /// - Parameters: /// - reporter: A `ProgressReporter` instance. - /// - portionOfParent: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. - public func assign(count portionOfParent: Int, to reporter: ProgressReporter) { + /// - count: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. + public func assign(count: Int, to reporter: ProgressReporter) { precondition(isCycle(reporter: reporter) == false, "Creating a cycle is not allowed.") // get the actual progress from within the reporter, then add as children @@ -273,7 +279,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Add reporter as child + Add self as parent self.addToChildren(childManager: actualManager) - actualManager.addParent(parentReporter: self, portionOfParent: portionOfParent) + actualManager.addParent(parentReporter: self, portionOfParent: count) } /// Increases `completedCount` by `count`. diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift index 411bc643a..2d9526a43 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift @@ -18,28 +18,40 @@ import Observation /// It is read-only and can be added as a child of another ProgressManager. @Observable public final class ProgressReporter: Sendable { + /// The total units of work. var totalCount: Int? { manager.totalCount } + /// The completed units of work. + /// If `self` is indeterminate, the value will be 0. var completedCount: Int { manager.completedCount } + /// The proportion of work completed. + /// This takes into account the fraction completed in its children instances if children are present. + /// If `self` is indeterminate, the value will be 0. var fractionCompleted: Double { manager.fractionCompleted } + /// The state of initialization of `totalCount`. + /// If `totalCount` is `nil`, the value will be `true`. var isIndeterminate: Bool { manager.isIndeterminate } + /// The state of completion of work. + /// If `completedCount` >= `totalCount`, the value will be `true`. var isFinished: Bool { manager.isFinished } /// Reads properties that convey additional information about progress. - public func withProperties(_ closure: @Sendable (ProgressManager.Values) throws -> T) rethrows -> T { + public func withProperties( + _ closure: (sending ProgressManager.Values) throws(E) -> sending T + ) throws(E) -> T { return try manager.getAdditionalProperties(closure) } diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift index 9e9838749..abb0d0206 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift @@ -607,6 +607,16 @@ class TestProgressReporter: XCTestCase { manager.complete(count: 1) XCTAssertEqual(reporter.completedCount, 3) + + let fileCount = reporter.withProperties { properties in + properties.totalFileCount + } + XCTAssertEqual(fileCount, 0) + + manager.withProperties { properties in + properties.totalFileCount = 6 + } + XCTAssertEqual(reporter.withProperties(\.totalFileCount), 6) } func testAddProgressReporterAsChild() { From ae2a201227d39f034218c44aefd231dc3cd09e94 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 3 Jun 2025 13:29:15 -0700 Subject: [PATCH 72/85] make progress reporter properties public --- .../ProgressManager/ProgressReporter.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift index 2d9526a43..032e16702 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift @@ -19,32 +19,32 @@ import Observation @Observable public final class ProgressReporter: Sendable { /// The total units of work. - var totalCount: Int? { + public var totalCount: Int? { manager.totalCount } /// The completed units of work. /// If `self` is indeterminate, the value will be 0. - var completedCount: Int { + public var completedCount: Int { manager.completedCount } /// The proportion of work completed. /// This takes into account the fraction completed in its children instances if children are present. /// If `self` is indeterminate, the value will be 0. - var fractionCompleted: Double { + public var fractionCompleted: Double { manager.fractionCompleted } /// The state of initialization of `totalCount`. /// If `totalCount` is `nil`, the value will be `true`. - var isIndeterminate: Bool { + public var isIndeterminate: Bool { manager.isIndeterminate } /// The state of completion of work. /// If `completedCount` >= `totalCount`, the value will be `true`. - var isFinished: Bool { + public var isFinished: Bool { manager.isFinished } From 507a8c5f1c0e455b9e0b989b0d574c43ec3da59f Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 6 Jun 2025 15:42:55 -0700 Subject: [PATCH 73/85] add values and total methods to ProgressReporter --- .../ProgressManager/ProgressManager.swift | 2 +- .../ProgressManager/ProgressReporter.swift | 14 ++++++++++++++ .../ProgressManager/ProgressManagerTests.swift | 6 ++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 2878d39d6..ec0ecf24e 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -297,11 +297,11 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } - /// Returns an array of values for specified property in subtree. /// - Parameter metatype: Type of property. /// - Returns: Array of values for property. public func values(of property: P.Type) -> [P.Value?] { + _$observationRegistrar.access(self, keyPath: \.state) return state.withLock { state in let childrenValues = getFlattenedChildrenValues(property: property, state: &state) return [state.otherProperties[AnyMetatypeWrapper(metatype: property)] as? P.Value ?? P.defaultValue] + childrenValues.map { $0 ?? P.defaultValue } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift index 032e16702..fea1b077e 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift @@ -55,6 +55,20 @@ import Observation return try manager.getAdditionalProperties(closure) } + /// Returns an array of values for specified property in subtree. + /// - Parameter metatype: Type of property. + /// - Returns: Array of values for property. + public func values(of property: P.Type) -> [P.Value?] { + manager.values(of: property) + } + + /// Returns the aggregated result of values. + /// - Parameters: + /// - property: Type of property. + public func total(of property: P.Type) -> P.Value where P.Value: AdditiveArithmetic { + manager.total(of: property) + } + internal let manager: ProgressManager internal init(manager: ProgressManager) { diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift index abb0d0206..5249cb801 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift @@ -617,6 +617,12 @@ class TestProgressReporter: XCTestCase { properties.totalFileCount = 6 } XCTAssertEqual(reporter.withProperties(\.totalFileCount), 6) + + let totalFileCount = manager.total(of: ProgressManager.Properties.TotalFileCount.self) + XCTAssertEqual(totalFileCount, 6) + + let totalFileCountValues = manager.values(of: ProgressManager.Properties.TotalFileCount.self) + XCTAssertEqual(totalFileCountValues, [6]) } func testAddProgressReporterAsChild() { From 83229a6dd4eee4a668ae2c4ac5f8c50a535ed9ba Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 10 Jun 2025 14:09:00 -0700 Subject: [PATCH 74/85] bug fix: fraction calculations for totalCount nil -> non-nil --- .../ProgressManager/ProgressManager.swift | 34 +++++++++++-- .../ProgressManagerTests.swift | 50 +++++++++++++++++++ 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index ec0ecf24e..a1fdc5d20 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -27,6 +27,7 @@ internal struct FractionState { var overallFraction: _ProgressFraction { selfFraction + childFraction } + var children: Set var interopChild: ProgressManager? // read from this if self is actually an interop ghost } @@ -153,6 +154,13 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } state.fractionState.selfFraction.total = newValue ?? 0 + // Updating my own childFraction here because previously they did not get updated because my total was 0 + if !state.fractionState.children.isEmpty { + for child in state.fractionState.children { + child.updateChildFractionSpecial(of: manager, state: &state) + } + } + // if newValue is nil, reset indeterminate to true if newValue != nil { state.fractionState.indeterminate = false @@ -227,16 +235,15 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } internal let parents: LockedState<[ProgressManager: Int]> - private let children: LockedState> private let state: LockedState internal init(total: Int?, ghostReporter: ProgressManager?, interopObservation: (any Sendable)?) { self.parents = .init(initialState: [:]) - self.children = .init(initialState: Set()) let fractionState = FractionState( indeterminate: total == nil ? true : false, selfFraction: _ProgressFraction(completed: 0, total: total ?? 0), childFraction: _ProgressFraction(completed: 0, total: 1), + children: Set(), interopChild: nil ) let state = State(fractionState: fractionState, otherProperties: [:], childrenOtherProperties: [:]) @@ -324,7 +331,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { return try state.withLock { (state) throws(E) -> T in var values = Values(manager: self, state: state) // This is done to avoid copy on write later - state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction()), otherProperties: [:], childrenOtherProperties: [:]) + state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction(), children: Set()), otherProperties: [:], childrenOtherProperties: [:]) let result = try closure(&values) state = values.state return result @@ -402,6 +409,23 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { return UpdateState(previous: previous, current: current) } + // This is used when parent has its lock acquired and wants its child to update parent's childFraction to reflect child's own changes + private func updateChildFractionSpecial(of manager: ProgressManager, state managerState: inout State) { + let portion = parents.withLock { parents in + return parents[manager] + } + + if let portionOfParent = portion { + let myFraction = state.withLock { $0.fractionState.overallFraction } + + if myFraction.isFinished { + // If I'm not finished, update my entry in parent's childFraction + managerState.fractionState.childFraction = managerState.fractionState.childFraction + _ProgressFraction(completed: portionOfParent, total: managerState.fractionState.selfFraction.total) * myFraction + + } + } + } + private func updateFractionCompleted(from: _ProgressFraction, to: _ProgressFraction) { _$observationRegistrar.withMutation(of: self, keyPath: \.fractionCompleted) { if from != to { @@ -491,8 +515,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } internal func addToChildren(childManager: ProgressManager) { - _ = children.withLock { children in - children.insert(childManager) + _ = state.withLock { state in + state.fractionState.children.insert(childManager) } } diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift index 5249cb801..e80e88fb4 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift @@ -645,6 +645,56 @@ class TestProgressReporter: XCTestCase { XCTAssertEqual(altManager2.fractionCompleted, 0.4) } + func testAssignToProgressReporterThenSetTotalCount() { + let overall = ProgressManager(totalCount: nil) + + let child1 = ProgressManager(totalCount: 10) + overall.assign(count: 10, to: child1.reporter) + child1.complete(count: 5) + + let child2 = ProgressManager(totalCount: 20) + overall.assign(count: 20, to: child2.reporter) + child2.complete(count: 20) + + overall.withProperties { properties in + properties.totalCount = 30 + } + XCTAssertEqual(overall.completedCount, 20) + XCTAssertEqual(overall.fractionCompleted, 25 / 30) + + child1.complete(count: 5) + + XCTAssertEqual(overall.completedCount, 30) + XCTAssertEqual(overall.fractionCompleted, 1.0) + } + + func testMakeSubprogressThenSetTotalCount() async { + let overall = ProgressManager(totalCount: nil) + + let reporter1 = await dummy(index: 1, subprogress: overall.subprogress(assigningCount: 10)) + + let reporter2 = await dummy(index: 2, subprogress: overall.subprogress(assigningCount: 20)) + + XCTAssertEqual(reporter1.fractionCompleted, 0.5) + + XCTAssertEqual(reporter2.fractionCompleted, 0.5) + + overall.withProperties { properties in + properties.totalCount = 30 + } + + XCTAssertEqual(overall.totalCount, 30) + XCTAssertEqual(overall.fractionCompleted, 0.5) + } + + func dummy(index: Int, subprogress: consuming Subprogress) async -> ProgressReporter { + let manager = subprogress.start(totalCount: index * 10) + + manager.complete(count: (index * 10) / 2) + + return manager.reporter + } + /// All of these test cases hit the precondition for cycle detection, but currently there's no way to check for hitting precondition in xctest. // func testProgressReporterDirectCycleDetection() { // let manager = ProgressManager(totalCount: 2) From 3b8e9fd4e6087e41106eb91544753e7ab490d11a Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 10 Jun 2025 14:20:15 -0700 Subject: [PATCH 75/85] debug fix: confition to updateChildFraction --- .../FoundationEssentials/ProgressManager/ProgressManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index a1fdc5d20..b5d125888 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -418,7 +418,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { if let portionOfParent = portion { let myFraction = state.withLock { $0.fractionState.overallFraction } - if myFraction.isFinished { + if !myFraction.isFinished { // If I'm not finished, update my entry in parent's childFraction managerState.fractionState.childFraction = managerState.fractionState.childFraction + _ProgressFraction(completed: portionOfParent, total: managerState.fractionState.selfFraction.total) * myFraction From 475a60c4bdb31efc23eab436e23186c1fb18c852 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 10 Jun 2025 17:00:32 -0700 Subject: [PATCH 76/85] fix: values should return non-optional P.Value array --- .../ProgressManager/ProgressManager.swift | 10 +++++----- .../ProgressManager/ProgressReporter.swift | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index b5d125888..cfd0dca47 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -210,12 +210,12 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = newValue // Generate an array of myself + children values of the property - let flattenedChildrenValues: [P.Value?] = { + let flattenedChildrenValues: [P.Value] = { let childrenDictionary = state.childrenOtherProperties[AnyMetatypeWrapper(metatype: P.self)] - var childrenValues: [P.Value?] = [] + var childrenValues: [P.Value] = [] if let dictionary = childrenDictionary { for (_, value) in dictionary { - if let value = value as? [P.Value?] { + if let value = value as? [P.Value] { childrenValues.append(contentsOf: value) } } @@ -224,7 +224,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { }() // Send the array of myself + children values of property to parents - let updateValueForParent: [P.Value?] = [newValue] + flattenedChildrenValues + let updateValueForParent: [P.Value] = [newValue] + flattenedChildrenValues manager.parents.withLock { [manager] parents in for (parent, _) in parents { parent.updateChildrenOtherProperties(property: P.self, child: manager, value: updateValueForParent) @@ -307,7 +307,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// Returns an array of values for specified property in subtree. /// - Parameter metatype: Type of property. /// - Returns: Array of values for property. - public func values(of property: P.Type) -> [P.Value?] { + public func values(of property: P.Type) -> [P.Value] { _$observationRegistrar.access(self, keyPath: \.state) return state.withLock { state in let childrenValues = getFlattenedChildrenValues(property: property, state: &state) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift index fea1b077e..f98d56e10 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift @@ -58,7 +58,7 @@ import Observation /// Returns an array of values for specified property in subtree. /// - Parameter metatype: Type of property. /// - Returns: Array of values for property. - public func values(of property: P.Type) -> [P.Value?] { + public func values(of property: P.Type) -> [P.Value] { manager.values(of: property) } From 3f8ba5c3e8c9663be70f35b5d7ebf64e9afd8253 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 4 Jun 2025 16:20:57 -0700 Subject: [PATCH 77/85] draft implementation of dirty bit --- .../ProgressManager/ProgressManager.swift | 233 ++++++++++++++++-- 1 file changed, 206 insertions(+), 27 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index cfd0dca47..22647b1b1 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -54,6 +54,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { var otherProperties: [AnyMetatypeWrapper: (any Sendable)] // Type: Metatype maps to dictionary of child to value var childrenOtherProperties: [AnyMetatypeWrapper: OrderedDictionary] + var dirtyCompleted: Int? = nil + var dirtyChildren: Set = Set() } // Interop states @@ -78,7 +80,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var totalCount: Int? { _$observationRegistrar.access(self, keyPath: \.totalCount) return state.withLock { state in - getTotalCount(fractionState: &state.fractionState) + getTotalCount(state: &state) } } @@ -87,7 +89,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var completedCount: Int { _$observationRegistrar.access(self, keyPath: \.completedCount) return state.withLock { state in - getCompletedCount(fractionState: &state.fractionState) + getCompletedCount(state: &state) } } @@ -97,7 +99,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var fractionCompleted: Double { _$observationRegistrar.access(self, keyPath: \.fractionCompleted) return state.withLock { state in - getFractionCompleted(fractionState: &state.fractionState) + getFractionCompleted(state: &state) } } @@ -106,7 +108,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var isIndeterminate: Bool { _$observationRegistrar.access(self, keyPath: \.isIndeterminate) return state.withLock { state in - getIsIndeterminate(fractionState: &state.fractionState) + getIsIndeterminate(state: &state) } } @@ -115,7 +117,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { public var isFinished: Bool { _$observationRegistrar.access(self, keyPath: \.isFinished) return state.withLock { state in - getIsFinished(fractionState: &state.fractionState) + getIsFinished(state: &state) } } @@ -144,7 +146,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// The total units of work. public var totalCount: Int? { mutating get { - manager.getTotalCount(fractionState: &state.fractionState) + manager.getTotalCount(state: &state) } set { @@ -182,7 +184,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// The completed units of work. public var completedCount: Int { mutating get { - manager.getCompletedCount(fractionState: &state.fractionState) + manager.getCompletedCount(state: &state) } set { @@ -292,8 +294,19 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// Increases `completedCount` by `count`. /// - Parameter count: Units of work. public func complete(count: Int) { - let updateState = updateCompletedCount(count: count) - updateFractionCompleted(from: updateState.previous, to: updateState.current) +// let updateState = updateCompletedCount(count: count) +// updateFractionCompleted(from: updateState.previous, to: updateState.current) + + // If no parents, then update self directly + let parentCount = parents.withLock { $0 }.count + if parentCount == 0 { + state.withLock { state in + state.fractionState.selfFraction.completed += count + } + } else { + // Instead of updating state directly and propagating the values up, we mark ourselves and all our parents as dirty + markDirty(increment: count) + } // Interop updates stuff ghostReporter?.notifyObservers(with: .fractionUpdated) @@ -341,24 +354,52 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { //MARK: ProgressManager Properties getters /// Returns nil if `self` was instantiated without total units; /// returns a `Int` value otherwise. - private func getTotalCount(fractionState: inout FractionState) -> Int? { - if let interopChild = fractionState.interopChild { + private func getTotalCount(state: inout State) -> Int? { + if let interopChild = state.fractionState.interopChild { return interopChild.totalCount } - if fractionState.indeterminate { + if state.fractionState.indeterminate { return nil } else { - return fractionState.selfFraction.total + return state.fractionState.selfFraction.total } } /// Returns 0 if `self` has `nil` total units; /// returns a `Int` value otherwise. - private func getCompletedCount(fractionState: inout FractionState) -> Int { - if let interopChild = fractionState.interopChild { + private func getCompletedCount(state: inout State) -> Int { + if let interopChild = state.fractionState.interopChild { return interopChild.completedCount } - return fractionState.selfFraction.completed + + // If we happen to query dirty leaf + if state.dirtyChildren.isEmpty { + if state.dirtyCompleted != nil { + let prev = state.fractionState.overallFraction + if let dirtyCompleted = state.dirtyCompleted { + state.fractionState.selfFraction.completed = dirtyCompleted + } + updateSelfInParent(from: prev, to: state.fractionState.overallFraction, exclude: nil) + } + } + + + // If we happen to query a dirty root caused by dirty descendants + // Check if update is needed + var dirtyNodes: [ProgressManager] = [] + + if !state.dirtyChildren.isEmpty || state.dirtyCompleted != nil { + // If among the dirtyChildren someone finished, then we need to update this + self.collectDirtyNodes(dirtyNodes: &dirtyNodes, state: &state) + } + + // Go to all dirty leaves and then make them make the recursive calls up + for dirtyNode in dirtyNodes { + dirtyNode.propagateValues(exclude: self) + } + + // Return the actual completedCount + return state.fractionState.selfFraction.completed } /// Returns 0.0 if `self` has `nil` total units; @@ -367,30 +408,36 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// /// The calculation of fraction completed for a ProgressManager instance that has children /// will take into account children's fraction completed as well. - private func getFractionCompleted(fractionState: inout FractionState) -> Double { - if let interopChild = fractionState.interopChild { + private func getFractionCompleted(state: inout State) -> Double { + if let interopChild = state.fractionState.interopChild { return interopChild.fractionCompleted } - if fractionState.indeterminate { + if state.fractionState.indeterminate { return 0.0 } - guard fractionState.selfFraction.total > 0 else { - return fractionState.selfFraction.fractionCompleted + + // Do an update if needed + if !state.dirtyChildren.isEmpty || state.dirtyCompleted != nil { + //TODO: Call update method } - return (fractionState.selfFraction + fractionState.childFraction).fractionCompleted + + guard state.fractionState.selfFraction.total > 0 else { + return state.fractionState.selfFraction.fractionCompleted + } + return (state.fractionState.selfFraction + state.fractionState.childFraction).fractionCompleted } /// Returns `true` if completed and total units are not `nil` and completed units is greater than or equal to total units; /// returns `false` otherwise. - private func getIsFinished(fractionState: inout FractionState) -> Bool { - return fractionState.selfFraction.isFinished + private func getIsFinished(state: inout State) -> Bool { + return state.fractionState.selfFraction.isFinished } /// Returns `true` if `self` has `nil` total units. - private func getIsIndeterminate(fractionState: inout FractionState) -> Bool { - return fractionState.indeterminate + private func getIsIndeterminate(state: inout State) -> Bool { + return state.fractionState.indeterminate } //MARK: FractionCompleted Calculation methods @@ -399,6 +446,136 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { let current: _ProgressFraction } + private func markDirty(increment: Int) { + state.withLock { state in + if let dirtyValue = state.dirtyCompleted { + // If there was a previous update + state.dirtyCompleted = dirtyValue + increment + } else { + // If this is the first update + state.dirtyCompleted = state.fractionState.selfFraction.completed + increment + } + } + + parents.withLock { parents in + for (parent, _) in parents { + parent.addDirtyChild(self) + } + } + } + + private func addDirtyChild(_ child: ProgressManager) { + _ = state.withLock { state in + state.dirtyChildren.insert(child) + } + } + + // This will only collect the bottommost dirty nodes of a subtree + private func collectDirtyNodes(dirtyNodes: inout [ProgressManager], state: inout State) { + if state.dirtyChildren.isEmpty && state.dirtyCompleted != nil { + dirtyNodes += [self] + } else { + for child in state.dirtyChildren { + child.collectDirtyNodes(dirtyNodes: &dirtyNodes) + } + } + } + + private func collectDirtyNodes(dirtyNodes: inout [ProgressManager]) { + state.withLock { state in + if state.dirtyChildren.isEmpty && state.dirtyCompleted != nil { + dirtyNodes += [self] + } else { + for child in state.dirtyChildren { + child.collectDirtyNodes(dirtyNodes: &dirtyNodes) + } + } + } + } + + private func propagateValues(exclude: ProgressManager?) { + if self === exclude { + print("I am excluded at propagate values") + return + } + // Update self's completed values + let (previous, current) = state.withLock { state in + let prev = state.fractionState.overallFraction + if let dirtyCompleted = state.dirtyCompleted { + state.fractionState.selfFraction.completed = dirtyCompleted + } + return (prev, state.fractionState.overallFraction) + } + + updateSelfInParent(from: previous, to: current, exclude: exclude) + } + + private func updateSelfInParent(from: _ProgressFraction, to: _ProgressFraction, exclude: ProgressManager?) { + if self === exclude { + print("I am excluded at update self in parent") + return + } + + _$observationRegistrar.withMutation(of: self, keyPath: \.fractionCompleted) { + if from != to { + parents.withLock { parents in + for (parent, portionOfParent) in parents { + if parent != exclude { + parent.updateChildFraction(from: from, to: to, portion: portionOfParent) + } + } + } + } + } + } + + private func updateChildFraction(from previous: _ProgressFraction, to next: _ProgressFraction, portion: Int, exclude: ProgressManager?) { + if self === exclude { + print("I am excluded at update child fraction") + return + } + + let updateState = state.withLock { state in + let previousOverallFraction = state.fractionState.overallFraction + + let multiple = _ProgressFraction(completed: portion, total: state.fractionState.selfFraction.total) + + let oldFractionOfParent = previous * multiple + + if previous.total != 0 { + state.fractionState.childFraction = state.fractionState.childFraction - oldFractionOfParent + } + + if next.total != 0 { + state.fractionState.childFraction = state.fractionState.childFraction + (next * multiple) + + if next.isFinished { + // Remove from children list +// _ = children.withLock { $0.remove(self) } + + if portion != 0 { + // Update our self completed units + state.fractionState.selfFraction.completed += portion + } + + // Subtract the (child's fraction completed * multiple) from our child fraction + state.fractionState.childFraction = state.fractionState.childFraction - (multiple * next) + } + } + return UpdateState(previous: previousOverallFraction, current: state.fractionState.overallFraction) + } + + updateSelfInParent(from: updateState.previous, to: updateState.current, exclude: exclude) + } + + + + + + + + + private func updateCompletedCount(count: Int) -> UpdateState { // Acquire and release child's lock let (previous, current) = state.withLock { state in @@ -442,7 +619,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { internal func updateChildFraction(from previous: _ProgressFraction, to next: _ProgressFraction, portion: Int) { let updateState = state.withLock { state in let previousOverallFraction = state.fractionState.overallFraction + let multiple = _ProgressFraction(completed: portion, total: state.fractionState.selfFraction.total) + let oldFractionOfParent = previous * multiple if previous.total != 0 { @@ -454,7 +633,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { if next.isFinished { // Remove from children list - // _ = children.withLock { $0.remove(self) } +// _ = children.withLock { $0.remove(self) } if portion != 0 { // Update our self completed units From f86e0062c59b02e21bf99f6452d27abeb034bd48 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 5 Jun 2025 15:41:34 -0700 Subject: [PATCH 78/85] drafting --- .../ProgressManager/ProgressManager.swift | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 22647b1b1..a9abc894b 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -304,7 +304,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { state.fractionState.selfFraction.completed += count } } else { - // Instead of updating state directly and propagating the values up, we mark ourselves and all our parents as dirty + // If there are parents, instead of updating state directly and propagating the values up, + // we mark ourselves and all our parents as dirty markDirty(increment: count) } @@ -372,8 +373,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { return interopChild.completedCount } - // If we happen to query dirty leaf - if state.dirtyChildren.isEmpty { + // If we happen to query dirty leaf, just propagate up without worrying about recursive lock issues +// if state.dirtyChildren.isEmpty { if state.dirtyCompleted != nil { let prev = state.fractionState.overallFraction if let dirtyCompleted = state.dirtyCompleted { @@ -381,11 +382,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } updateSelfInParent(from: prev, to: state.fractionState.overallFraction, exclude: nil) } - } - +// } - // If we happen to query a dirty root caused by dirty descendants - // Check if update is needed + // If we happen to query a dirty root caused by dirty descendants, we still need to make the children propagate up without trying to acquire our own lock var dirtyNodes: [ProgressManager] = [] if !state.dirtyChildren.isEmpty || state.dirtyCompleted != nil { @@ -499,14 +498,13 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { return } // Update self's completed values - let (previous, current) = state.withLock { state in + let (previous, current) = self.state.withLock { state in let prev = state.fractionState.overallFraction if let dirtyCompleted = state.dirtyCompleted { state.fractionState.selfFraction.completed = dirtyCompleted } return (prev, state.fractionState.overallFraction) } - updateSelfInParent(from: previous, to: current, exclude: exclude) } From 8d3ae934c6a3493dfd1ece2862168931941352b0 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 11 Jun 2025 14:52:56 -0700 Subject: [PATCH 79/85] move everything into one state --- .../ProgressManager/ProgressManager.swift | 148 ++++++++---------- 1 file changed, 68 insertions(+), 80 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index a9abc894b..95ea3e5e0 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -20,17 +20,6 @@ internal import OrderedCollections internal import _FoundationCollections #endif -internal struct FractionState { - var indeterminate: Bool - var selfFraction: _ProgressFraction - var childFraction: _ProgressFraction - var overallFraction: _ProgressFraction { - selfFraction + childFraction - } - var children: Set - var interopChild: ProgressManager? // read from this if self is actually an interop ghost -} - internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { let metatype: Any.Type @@ -50,7 +39,14 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Stores all the state of properties internal struct State { - var fractionState: FractionState + var indeterminate: Bool + var selfFraction: _ProgressFraction + var childFraction: _ProgressFraction + var overallFraction: _ProgressFraction { + selfFraction + childFraction + } + var children: Set + var interopChild: ProgressManager? // read from this if self is actually an interop ghost var otherProperties: [AnyMetatypeWrapper: (any Sendable)] // Type: Metatype maps to dictionary of child to value var childrenOtherProperties: [AnyMetatypeWrapper: OrderedDictionary] @@ -150,27 +146,27 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } set { - let previous = state.fractionState.overallFraction - if state.fractionState.selfFraction.total != newValue && state.fractionState.selfFraction.total > 0 { - state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: newValue ?? 1) + let previous = state.overallFraction + if state.selfFraction.total != newValue && state.selfFraction.total > 0 { + state.childFraction = state.childFraction * _ProgressFraction(completed: state.selfFraction.total, total: newValue ?? 1) } - state.fractionState.selfFraction.total = newValue ?? 0 + state.selfFraction.total = newValue ?? 0 // Updating my own childFraction here because previously they did not get updated because my total was 0 - if !state.fractionState.children.isEmpty { - for child in state.fractionState.children { + if !state.children.isEmpty { + for child in state.children { child.updateChildFractionSpecial(of: manager, state: &state) } } // if newValue is nil, reset indeterminate to true if newValue != nil { - state.fractionState.indeterminate = false + state.indeterminate = false } else { - state.fractionState.indeterminate = true + state.indeterminate = true } //TODO: rdar://149015734 Check throttling - manager.updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) + manager.updateFractionCompleted(from: previous, to: state.overallFraction) manager.ghostReporter?.notifyObservers(with: .totalCountUpdated) manager.monitorInterop.withLock { [manager] interop in if interop == true { @@ -188,9 +184,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } set { - let prev = state.fractionState.overallFraction - state.fractionState.selfFraction.completed = newValue - manager.updateFractionCompleted(from: prev, to: state.fractionState.overallFraction) + let prev = state.overallFraction + state.selfFraction.completed = newValue + manager.updateFractionCompleted(from: prev, to: state.overallFraction) manager.ghostReporter?.notifyObservers(with: .fractionUpdated) manager.monitorInterop.withLock { [manager] interop in @@ -241,14 +237,15 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { internal init(total: Int?, ghostReporter: ProgressManager?, interopObservation: (any Sendable)?) { self.parents = .init(initialState: [:]) - let fractionState = FractionState( + let state = State( indeterminate: total == nil ? true : false, selfFraction: _ProgressFraction(completed: 0, total: total ?? 0), childFraction: _ProgressFraction(completed: 0, total: 1), children: Set(), - interopChild: nil - ) - let state = State(fractionState: fractionState, otherProperties: [:], childrenOtherProperties: [:]) + interopChild: nil, + otherProperties: [:], + childrenOtherProperties: [:], + ) self.state = LockedState(initialState: state) self.interopObservation = interopObservation self.ghostReporter = ghostReporter @@ -301,7 +298,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { let parentCount = parents.withLock { $0 }.count if parentCount == 0 { state.withLock { state in - state.fractionState.selfFraction.completed += count + state.selfFraction.completed += count } } else { // If there are parents, instead of updating state directly and propagating the values up, @@ -345,7 +342,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { return try state.withLock { (state) throws(E) -> T in var values = Values(manager: self, state: state) // This is done to avoid copy on write later - state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction(), children: Set()), otherProperties: [:], childrenOtherProperties: [:]) + state = State(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction(), children: Set(), otherProperties: [:], childrenOtherProperties: [:]) let result = try closure(&values) state = values.state return result @@ -356,31 +353,31 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// Returns nil if `self` was instantiated without total units; /// returns a `Int` value otherwise. private func getTotalCount(state: inout State) -> Int? { - if let interopChild = state.fractionState.interopChild { + if let interopChild = state.interopChild { return interopChild.totalCount } - if state.fractionState.indeterminate { + if state.indeterminate { return nil } else { - return state.fractionState.selfFraction.total + return state.selfFraction.total } } /// Returns 0 if `self` has `nil` total units; /// returns a `Int` value otherwise. private func getCompletedCount(state: inout State) -> Int { - if let interopChild = state.fractionState.interopChild { + if let interopChild = state.interopChild { return interopChild.completedCount } // If we happen to query dirty leaf, just propagate up without worrying about recursive lock issues // if state.dirtyChildren.isEmpty { if state.dirtyCompleted != nil { - let prev = state.fractionState.overallFraction + let prev = state.overallFraction if let dirtyCompleted = state.dirtyCompleted { - state.fractionState.selfFraction.completed = dirtyCompleted + state.selfFraction.completed = dirtyCompleted } - updateSelfInParent(from: prev, to: state.fractionState.overallFraction, exclude: nil) + updateSelfInParent(from: prev, to: state.overallFraction, exclude: nil) } // } @@ -398,7 +395,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } // Return the actual completedCount - return state.fractionState.selfFraction.completed + return state.selfFraction.completed } /// Returns 0.0 if `self` has `nil` total units; @@ -408,10 +405,10 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// The calculation of fraction completed for a ProgressManager instance that has children /// will take into account children's fraction completed as well. private func getFractionCompleted(state: inout State) -> Double { - if let interopChild = state.fractionState.interopChild { + if let interopChild = state.interopChild { return interopChild.fractionCompleted } - if state.fractionState.indeterminate { + if state.indeterminate { return 0.0 } @@ -420,23 +417,23 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { //TODO: Call update method } - guard state.fractionState.selfFraction.total > 0 else { - return state.fractionState.selfFraction.fractionCompleted + guard state.selfFraction.total > 0 else { + return state.selfFraction.fractionCompleted } - return (state.fractionState.selfFraction + state.fractionState.childFraction).fractionCompleted + return (state.selfFraction + state.childFraction).fractionCompleted } /// Returns `true` if completed and total units are not `nil` and completed units is greater than or equal to total units; /// returns `false` otherwise. private func getIsFinished(state: inout State) -> Bool { - return state.fractionState.selfFraction.isFinished + return state.selfFraction.isFinished } /// Returns `true` if `self` has `nil` total units. private func getIsIndeterminate(state: inout State) -> Bool { - return state.fractionState.indeterminate + return state.indeterminate } //MARK: FractionCompleted Calculation methods @@ -452,7 +449,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { state.dirtyCompleted = dirtyValue + increment } else { // If this is the first update - state.dirtyCompleted = state.fractionState.selfFraction.completed + increment + state.dirtyCompleted = state.selfFraction.completed + increment } } @@ -499,11 +496,11 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } // Update self's completed values let (previous, current) = self.state.withLock { state in - let prev = state.fractionState.overallFraction + let prev = state.overallFraction if let dirtyCompleted = state.dirtyCompleted { - state.fractionState.selfFraction.completed = dirtyCompleted + state.selfFraction.completed = dirtyCompleted } - return (prev, state.fractionState.overallFraction) + return (prev, state.overallFraction) } updateSelfInParent(from: previous, to: current, exclude: exclude) } @@ -534,18 +531,18 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } let updateState = state.withLock { state in - let previousOverallFraction = state.fractionState.overallFraction + let previousOverallFraction = state.overallFraction - let multiple = _ProgressFraction(completed: portion, total: state.fractionState.selfFraction.total) + let multiple = _ProgressFraction(completed: portion, total: state.selfFraction.total) let oldFractionOfParent = previous * multiple if previous.total != 0 { - state.fractionState.childFraction = state.fractionState.childFraction - oldFractionOfParent + state.childFraction = state.childFraction - oldFractionOfParent } if next.total != 0 { - state.fractionState.childFraction = state.fractionState.childFraction + (next * multiple) + state.childFraction = state.childFraction + (next * multiple) if next.isFinished { // Remove from children list @@ -553,33 +550,25 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { if portion != 0 { // Update our self completed units - state.fractionState.selfFraction.completed += portion + state.selfFraction.completed += portion } // Subtract the (child's fraction completed * multiple) from our child fraction - state.fractionState.childFraction = state.fractionState.childFraction - (multiple * next) + state.childFraction = state.childFraction - (multiple * next) } } - return UpdateState(previous: previousOverallFraction, current: state.fractionState.overallFraction) + return UpdateState(previous: previousOverallFraction, current: state.overallFraction) } updateSelfInParent(from: updateState.previous, to: updateState.current, exclude: exclude) } - - - - - - - - private func updateCompletedCount(count: Int) -> UpdateState { // Acquire and release child's lock let (previous, current) = state.withLock { state in - let prev = state.fractionState.overallFraction - state.fractionState.selfFraction.completed += count - return (prev, state.fractionState.overallFraction) + let prev = state.overallFraction + state.selfFraction.completed += count + return (prev, state.overallFraction) } return UpdateState(previous: previous, current: current) } @@ -591,12 +580,11 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } if let portionOfParent = portion { - let myFraction = state.withLock { $0.fractionState.overallFraction } + let myFraction = state.withLock { $0.overallFraction } if !myFraction.isFinished { // If I'm not finished, update my entry in parent's childFraction - managerState.fractionState.childFraction = managerState.fractionState.childFraction + _ProgressFraction(completed: portionOfParent, total: managerState.fractionState.selfFraction.total) * myFraction - + managerState.childFraction = managerState.childFraction + _ProgressFraction(completed: portionOfParent, total: managerState.selfFraction.total) * myFraction } } } @@ -616,18 +604,18 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// A child progress has been updated, which changes our own fraction completed. internal func updateChildFraction(from previous: _ProgressFraction, to next: _ProgressFraction, portion: Int) { let updateState = state.withLock { state in - let previousOverallFraction = state.fractionState.overallFraction + let previousOverallFraction = state.overallFraction - let multiple = _ProgressFraction(completed: portion, total: state.fractionState.selfFraction.total) + let multiple = _ProgressFraction(completed: portion, total: state.selfFraction.total) let oldFractionOfParent = previous * multiple if previous.total != 0 { - state.fractionState.childFraction = state.fractionState.childFraction - oldFractionOfParent + state.childFraction = state.childFraction - oldFractionOfParent } if next.total != 0 { - state.fractionState.childFraction = state.fractionState.childFraction + (next * multiple) + state.childFraction = state.childFraction + (next * multiple) if next.isFinished { // Remove from children list @@ -635,14 +623,14 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { if portion != 0 { // Update our self completed units - state.fractionState.selfFraction.completed += portion + state.selfFraction.completed += portion } // Subtract the (child's fraction completed * multiple) from our child fraction - state.fractionState.childFraction = state.fractionState.childFraction - (multiple * next) + state.childFraction = state.childFraction - (multiple * next) } } - return UpdateState(previous: previousOverallFraction, current: state.fractionState.overallFraction) + return UpdateState(previous: previousOverallFraction, current: state.overallFraction) } updateFractionCompleted(from: updateState.previous, to: updateState.current) } @@ -687,13 +675,13 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { internal func setInteropChild(interopChild: ProgressManager) { state.withLock { state in - state.fractionState.interopChild = interopChild + state.interopChild = interopChild } } internal func addToChildren(childManager: ProgressManager) { _ = state.withLock { state in - state.fractionState.children.insert(childManager) + state.children.insert(childManager) } } @@ -711,7 +699,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } let original = _ProgressFraction(completed: 0, total: 0) - let updated = state.fractionState.overallFraction + let updated = state.overallFraction return (original, updated) } From 3c1eef0a65541d004998e08ee7a6bee75acabb1c Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 11 Jun 2025 14:55:52 -0700 Subject: [PATCH 80/85] move locked variables up --- .../ProgressManager/ProgressManager.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 95ea3e5e0..49af77cdd 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -60,6 +60,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { case totalCountUpdated } + internal let parents: LockedState<[ProgressManager: Int]> + private let state: LockedState + // Interop properties - Just kept alive internal let interopObservation: (any Sendable)? // set at init internal let interopObservationForMonitor: LockedState<(any Sendable)?> = LockedState(initialState: nil) @@ -232,9 +235,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } - internal let parents: LockedState<[ProgressManager: Int]> - private let state: LockedState - internal init(total: Int?, ghostReporter: ProgressManager?, interopObservation: (any Sendable)?) { self.parents = .init(initialState: [:]) let state = State( From 631381a2ba0c788f058248c5a27fdf0d79efd86f Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 12 Jun 2025 14:31:14 -0700 Subject: [PATCH 81/85] dirty bit implementation - minimal working version --- .../ProgressManager/ProgressManager.swift | 488 ++++++++---------- .../ProgressManager/Subprogress.swift | 4 +- 2 files changed, 214 insertions(+), 278 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 49af77cdd..7133d784b 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -39,30 +39,37 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Stores all the state of properties internal struct State { + var interopChild: ProgressManager? // read from this if self is actually an interop ghost + var isDirty: Bool + var dirtyChildren: Set = Set() var indeterminate: Bool var selfFraction: _ProgressFraction - var childFraction: _ProgressFraction var overallFraction: _ProgressFraction { - selfFraction + childFraction + var overallFraction = selfFraction + for child in children { + overallFraction = overallFraction + (_ProgressFraction(completed: child.value.portionOfSelf, total: selfFraction.total) * child.value.childFraction) + } + return overallFraction } - var children: Set - var interopChild: ProgressManager? // read from this if self is actually an interop ghost + var children: [ProgressManager: ChildState] // My children and their information in relation to me + var parents: [ProgressManager: Int] // My parents and their information in relation to me, how much of their totalCount I am a part of var otherProperties: [AnyMetatypeWrapper: (any Sendable)] - // Type: Metatype maps to dictionary of child to value - var childrenOtherProperties: [AnyMetatypeWrapper: OrderedDictionary] - var dirtyCompleted: Int? = nil - var dirtyChildren: Set = Set() + var childrenOtherProperties: [AnyMetatypeWrapper: OrderedDictionary] // Type: Metatype maps to dictionary of child to value } + internal struct ChildState { + var portionOfSelf: Int // Portion of my totalCount that this child accounts for + var childFraction: _ProgressFraction // Fraction adjusted based on portion of self; If not dirty, overallFraction should be composed of this + } + + private let state: LockedState + // Interop states internal enum ObserverState { case fractionUpdated case totalCountUpdated } - internal let parents: LockedState<[ProgressManager: Int]> - private let state: LockedState - // Interop properties - Just kept alive internal let interopObservation: (any Sendable)? // set at init internal let interopObservationForMonitor: LockedState<(any Sendable)?> = LockedState(initialState: nil) @@ -100,6 +107,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { return state.withLock { state in getFractionCompleted(state: &state) } + } /// The state of initialization of `totalCount`. @@ -149,27 +157,15 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } set { - let previous = state.overallFraction - if state.selfFraction.total != newValue && state.selfFraction.total > 0 { - state.childFraction = state.childFraction * _ProgressFraction(completed: state.selfFraction.total, total: newValue ?? 1) - } - state.selfFraction.total = newValue ?? 0 - - // Updating my own childFraction here because previously they did not get updated because my total was 0 - if !state.children.isEmpty { - for child in state.children { - child.updateChildFractionSpecial(of: manager, state: &state) - } - } - // if newValue is nil, reset indeterminate to true if newValue != nil { state.indeterminate = false } else { state.indeterminate = true } - //TODO: rdar://149015734 Check throttling - manager.updateFractionCompleted(from: previous, to: state.overallFraction) + state.selfFraction.total = newValue ?? 0 + manager.markDirty(state: &state) + manager.ghostReporter?.notifyObservers(with: .totalCountUpdated) manager.monitorInterop.withLock { [manager] interop in if interop == true { @@ -187,9 +183,10 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } set { - let prev = state.overallFraction + //TODO: Update self completedCount and notify parents that I am dirty state.selfFraction.completed = newValue - manager.updateFractionCompleted(from: prev, to: state.overallFraction) + manager.markDirty(state: &state) + manager.ghostReporter?.notifyObservers(with: .fractionUpdated) manager.monitorInterop.withLock { [manager] interop in @@ -226,26 +223,26 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Send the array of myself + children values of property to parents let updateValueForParent: [P.Value] = [newValue] + flattenedChildrenValues - manager.parents.withLock { [manager] parents in - for (parent, _) in parents { - parent.updateChildrenOtherProperties(property: P.self, child: manager, value: updateValueForParent) - } + for (parent, _) in state.parents { + parent.updateChildrenOtherProperties(property: P.self, child: manager, value: updateValueForParent) } + } } } internal init(total: Int?, ghostReporter: ProgressManager?, interopObservation: (any Sendable)?) { - self.parents = .init(initialState: [:]) let state = State( - indeterminate: total == nil ? true : false, - selfFraction: _ProgressFraction(completed: 0, total: total ?? 0), - childFraction: _ProgressFraction(completed: 0, total: 1), - children: Set(), interopChild: nil, + isDirty: false, + dirtyChildren: Set(), + indeterminate: total == nil ? true : false, + selfFraction: _ProgressFraction(completed: 0, total: total ?? 0), + children: [:], + parents: [:], otherProperties: [:], - childrenOtherProperties: [:], - ) + childrenOtherProperties: [:] + ) self.state = LockedState(initialState: state) self.interopObservation = interopObservation self.ghostReporter = ghostReporter @@ -284,26 +281,17 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { let actualManager = reporter.manager // Add reporter as child + Add self as parent - self.addToChildren(childManager: actualManager) - actualManager.addParent(parentReporter: self, portionOfParent: count) + self.addToChildren(child: actualManager, portion: count, childFraction: actualManager.getProgressFraction()) + actualManager.addParent(parent: self, portionOfParent: count) } /// Increases `completedCount` by `count`. /// - Parameter count: Units of work. public func complete(count: Int) { -// let updateState = updateCompletedCount(count: count) -// updateFractionCompleted(from: updateState.previous, to: updateState.current) - - // If no parents, then update self directly - let parentCount = parents.withLock { $0 }.count - if parentCount == 0 { - state.withLock { state in - state.selfFraction.completed += count - } - } else { - // If there are parents, instead of updating state directly and propagating the values up, - // we mark ourselves and all our parents as dirty - markDirty(increment: count) + // Update self fraction + mark dirty + state.withLock { state in + state.selfFraction.completed += count + markDirty(state: &state) } // Interop updates stuff @@ -342,7 +330,14 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { return try state.withLock { (state) throws(E) -> T in var values = Values(manager: self, state: state) // This is done to avoid copy on write later - state = State(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction(), children: Set(), otherProperties: [:], childrenOtherProperties: [:]) + state = State( + isDirty: false, + indeterminate: true, + selfFraction: _ProgressFraction(), + children: [:], + parents: [:], + otherProperties: [:], + childrenOtherProperties: [:]) let result = try closure(&values) state = values.state return result @@ -350,6 +345,12 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } //MARK: ProgressManager Properties getters + internal func getProgressFraction() -> _ProgressFraction { + return state.withLock { state in + return state.selfFraction + } + } + /// Returns nil if `self` was instantiated without total units; /// returns a `Int` value otherwise. private func getTotalCount(state: inout State) -> Int? { @@ -370,28 +371,32 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { return interopChild.completedCount } - // If we happen to query dirty leaf, just propagate up without worrying about recursive lock issues -// if state.dirtyChildren.isEmpty { - if state.dirtyCompleted != nil { - let prev = state.overallFraction - if let dirtyCompleted = state.dirtyCompleted { - state.selfFraction.completed = dirtyCompleted - } - updateSelfInParent(from: prev, to: state.overallFraction, exclude: nil) - } +// // If self is dirty, that just means I got mutated and my parents haven't received updates. +// // If my dirtyChildren list exists, that just means I have fractional updates from children, which might not have completed. +// // If at least one of my dirtyChildren actually completed, that means I would need to update my completed count actually. + +// let completedDirtyChildren = state.dirtyChildren.filter(\.isFinished) +// if !completedDirtyChildren.isEmpty { +// // update my own value based on dirty children completion +// for completedChild in completedDirtyChildren { +// // Update my completed count with the portion I assigned to this child +// state.selfFraction.completed += state.children[completedChild]?.portionOfSelf ?? 0 +// // Remove child from dirtyChildren list, so that the future updates won't be messed up +// state.dirtyChildren.remove(completedChild) +// } // } - // If we happen to query a dirty root caused by dirty descendants, we still need to make the children propagate up without trying to acquire our own lock - var dirtyNodes: [ProgressManager] = [] - - if !state.dirtyChildren.isEmpty || state.dirtyCompleted != nil { - // If among the dirtyChildren someone finished, then we need to update this - self.collectDirtyNodes(dirtyNodes: &dirtyNodes, state: &state) - } - - // Go to all dirty leaves and then make them make the recursive calls up - for dirtyNode in dirtyNodes { - dirtyNode.propagateValues(exclude: self) + // If there are dirty children, get updates first + if state.dirtyChildren.count > 0 { + + // Get dirty leaves + var dirtyLeaves: [ProgressManager] = [] + collectDirtyNodes(dirtyNodes: &dirtyLeaves, state: &state) + + // Then ask each dirty leaf to propagate values up + for leaf in dirtyLeaves { + leaf.updateState(exclude: self, lockedState: &state) + } } // Return the actual completedCount @@ -405,81 +410,55 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// The calculation of fraction completed for a ProgressManager instance that has children /// will take into account children's fraction completed as well. private func getFractionCompleted(state: inout State) -> Double { + // If my self is dirty, that means I got mutated and I have parents that haven't received updates from me. + // If my dirtyChildren list exists, that means I have fractional updates from these children, and I need these fractional updates. + // But this runs into the issue of updating only the queried branch, but not the other branch that is not queried but dirty, this would cause the leaf to be cleaned up, but the other branch which share the dirty leaf hasn't received any updates. + + // If I am clean leaf and has no dirtyChildren, directly return fractionCompleted - no need to do recalculation whenn unnecessary + // If I am dirty leaf and no dirtyChildren, directly return fractionCompleted - no need to do recalculation when unnecessary + // If I am dirty leaf and also has dirtyChildren - get updates + // If I am clean leaf and has dirtyChildren - get updates + + // Interop child if let interopChild = state.interopChild { return interopChild.fractionCompleted } + + // Indeterminate if state.indeterminate { return 0.0 } - // Do an update if needed - if !state.dirtyChildren.isEmpty || state.dirtyCompleted != nil { - //TODO: Call update method - } - - guard state.selfFraction.total > 0 else { - return state.selfFraction.fractionCompleted - } - return (state.selfFraction + state.childFraction).fractionCompleted - } - - - /// Returns `true` if completed and total units are not `nil` and completed units is greater than or equal to total units; - /// returns `false` otherwise. - private func getIsFinished(state: inout State) -> Bool { - return state.selfFraction.isFinished - } - - - /// Returns `true` if `self` has `nil` total units. - private func getIsIndeterminate(state: inout State) -> Bool { - return state.indeterminate - } - - //MARK: FractionCompleted Calculation methods - private struct UpdateState { - let previous: _ProgressFraction - let current: _ProgressFraction - } - - private func markDirty(increment: Int) { - state.withLock { state in - if let dirtyValue = state.dirtyCompleted { - // If there was a previous update - state.dirtyCompleted = dirtyValue + increment - } else { - // If this is the first update - state.dirtyCompleted = state.selfFraction.completed + increment + // If there are dirty children, get updates first + if state.dirtyChildren.count > 0 { + + // Get dirty leaves + var dirtyLeaves: [ProgressManager] = [] + collectDirtyNodes(dirtyNodes: &dirtyLeaves, state: &state) + + // Then ask each dirty leaf to propagate values up + for leaf in dirtyLeaves { + leaf.updateState(exclude: self, lockedState: &state) } } - parents.withLock { parents in - for (parent, _) in parents { - parent.addDirtyChild(self) - } - } - } - - private func addDirtyChild(_ child: ProgressManager) { - _ = state.withLock { state in - state.dirtyChildren.insert(child) - } + return state.overallFraction.fractionCompleted } - // This will only collect the bottommost dirty nodes of a subtree + /// Collect bottommost dirty nodes in a subtree private func collectDirtyNodes(dirtyNodes: inout [ProgressManager], state: inout State) { - if state.dirtyChildren.isEmpty && state.dirtyCompleted != nil { - dirtyNodes += [self] - } else { - for child in state.dirtyChildren { - child.collectDirtyNodes(dirtyNodes: &dirtyNodes) + if state.dirtyChildren.isEmpty && state.isDirty { + dirtyNodes += [self] + } else { + for child in state.dirtyChildren { + child.collectDirtyNodes(dirtyNodes: &dirtyNodes) + } } } - } - + private func collectDirtyNodes(dirtyNodes: inout [ProgressManager]) { state.withLock { state in - if state.dirtyChildren.isEmpty && state.dirtyCompleted != nil { + if state.dirtyChildren.isEmpty && state.isDirty { dirtyNodes += [self] } else { for child in state.dirtyChildren { @@ -488,153 +467,116 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } } - - private func propagateValues(exclude: ProgressManager?) { - if self === exclude { - print("I am excluded at propagate values") - return - } - // Update self's completed values - let (previous, current) = self.state.withLock { state in - let prev = state.overallFraction - if let dirtyCompleted = state.dirtyCompleted { - state.selfFraction.completed = dirtyCompleted - } - return (prev, state.overallFraction) - } - updateSelfInParent(from: previous, to: current, exclude: exclude) - } - private func updateSelfInParent(from: _ProgressFraction, to: _ProgressFraction, exclude: ProgressManager?) { - if self === exclude { - print("I am excluded at update self in parent") + private func updateState(exclude lockedRoot: ProgressManager?, lockedState: inout State, child: ProgressManager? = nil, fraction: _ProgressFraction? = nil) { + // If I am the root which was queried. + if self === lockedRoot { + print("Called updateState on self. This should never be called by a root. Only called by a leaf") return } - _$observationRegistrar.withMutation(of: self, keyPath: \.fractionCompleted) { - if from != to { - parents.withLock { parents in - for (parent, portionOfParent) in parents { - if parent != exclude { - parent.updateChildFraction(from: from, to: to, portion: portionOfParent) - } - } - } + state.withLock { state in + // Set isDirty to false + state.isDirty = false + + // Propagate these changes up to parent + for (parent, _) in state.parents { + parent.updateChildState(exclude: lockedRoot, lockedState: &lockedState, child: self, fraction: state.overallFraction) } } } - - private func updateChildFraction(from previous: _ProgressFraction, to next: _ProgressFraction, portion: Int, exclude: ProgressManager?) { - if self === exclude { - print("I am excluded at update child fraction") + + internal func updateChildState(exclude lockedRoot: ProgressManager?, lockedState: inout State, child: ProgressManager, fraction: _ProgressFraction) { + if self === lockedRoot { + print("I am locked. Lock not acquired. Use lockedState passed in.") + lockedState.children[child]?.childFraction = fraction + lockedState.dirtyChildren.remove(child) + + if fraction.isFinished { + lockedState.selfFraction.completed += lockedState.children[child]?.portionOfSelf ?? 0 + lockedState.children.removeValue(forKey: child) + } return } - let updateState = state.withLock { state in - let previousOverallFraction = state.overallFraction - - let multiple = _ProgressFraction(completed: portion, total: state.selfFraction.total) - - let oldFractionOfParent = previous * multiple + state.withLock { state in + state.children[child]?.childFraction = fraction + state.dirtyChildren.remove(child) - if previous.total != 0 { - state.childFraction = state.childFraction - oldFractionOfParent + if fraction.isFinished { + state.selfFraction.completed += state.children[child]?.portionOfSelf ?? 0 } - if next.total != 0 { - state.childFraction = state.childFraction + (next * multiple) - - if next.isFinished { - // Remove from children list -// _ = children.withLock { $0.remove(self) } - - if portion != 0 { - // Update our self completed units - state.selfFraction.completed += portion - } - - // Subtract the (child's fraction completed * multiple) from our child fraction - state.childFraction = state.childFraction - (multiple * next) - } + for (parent, _) in state.parents { + parent.updateChildState(exclude: lockedRoot, lockedState: &lockedState, child: self, fraction: state.overallFraction) } - return UpdateState(previous: previousOverallFraction, current: state.overallFraction) } - - updateSelfInParent(from: updateState.previous, to: updateState.current, exclude: exclude) + } + + /// Returns `true` if completed and total units are not `nil` and completed units is greater than or equal to total units; + /// returns `false` otherwise. + private func getIsFinished(state: inout State) -> Bool { + return state.selfFraction.isFinished } - private func updateCompletedCount(count: Int) -> UpdateState { - // Acquire and release child's lock - let (previous, current) = state.withLock { state in - let prev = state.overallFraction - state.selfFraction.completed += count - return (prev, state.overallFraction) - } - return UpdateState(previous: previous, current: current) + + /// Returns `true` if `self` has `nil` total units. + private func getIsIndeterminate(state: inout State) -> Bool { + return state.indeterminate } - // This is used when parent has its lock acquired and wants its child to update parent's childFraction to reflect child's own changes - private func updateChildFractionSpecial(of manager: ProgressManager, state managerState: inout State) { - let portion = parents.withLock { parents in - return parents[manager] + //MARK: FractionCompleted Calculation methods + private struct UpdateState { + let previous: _ProgressFraction + let current: _ProgressFraction + } + + /// If parents exist, mark self as dirty and add self to parents' dirty children list. + private func markDirty(state: inout State) { + if state.parents.count > 0 { + state.isDirty = true } - if let portionOfParent = portion { - let myFraction = state.withLock { $0.overallFraction } - - if !myFraction.isFinished { - // If I'm not finished, update my entry in parent's childFraction - managerState.childFraction = managerState.childFraction + _ProgressFraction(completed: portionOfParent, total: managerState.selfFraction.total) * myFraction - } + // Recursively add self as dirty child to parents list + for (parent, _) in state.parents { + parent.addDirtyChild(self) } - } - private func updateFractionCompleted(from: _ProgressFraction, to: _ProgressFraction) { - _$observationRegistrar.withMutation(of: self, keyPath: \.fractionCompleted) { - if from != to { - parents.withLock { parents in - for (parent, portionOfParent) in parents { - parent.updateChildFraction(from: from, to: to, portion: portionOfParent) - } - } - } - } } - /// A child progress has been updated, which changes our own fraction completed. - internal func updateChildFraction(from previous: _ProgressFraction, to next: _ProgressFraction, portion: Int) { - let updateState = state.withLock { state in - let previousOverallFraction = state.overallFraction - - let multiple = _ProgressFraction(completed: portion, total: state.selfFraction.total) - - let oldFractionOfParent = previous * multiple - - if previous.total != 0 { - state.childFraction = state.childFraction - oldFractionOfParent + /// Add a given child to self's dirty children list. + private func addDirtyChild(_ child: ProgressManager) { + state.withLock { state in + // Child already exists in dirty children + if state.dirtyChildren.contains(child) { + return } - if next.total != 0 { - state.childFraction = state.childFraction + (next * multiple) - - if next.isFinished { - // Remove from children list -// _ = children.withLock { $0.remove(self) } - - if portion != 0 { - // Update our self completed units - state.selfFraction.completed += portion - } - - // Subtract the (child's fraction completed * multiple) from our child fraction - state.childFraction = state.childFraction - (multiple * next) - } + state.dirtyChildren.insert(child) + + // Propagate dirty state up to parents + for (parent, _) in state.parents { + parent.addDirtyChild(self) } - return UpdateState(previous: previousOverallFraction, current: state.overallFraction) } - updateFractionCompleted(from: updateState.previous, to: updateState.current) } + // This is used when parent has its lock acquired and wants its child to update parent's childFraction to reflect child's own changes +// private func updateChildFractionSpecial(of manager: ProgressManager, state managerState: inout State) { +// let portion = parents.withLock { parents in +// return parents[manager] +// } +// +// if let portionOfParent = portion { +// let myFraction = state.withLock { $0.overallFraction } +// +// if !myFraction.isFinished { +// // If I'm not finished, update my entry in parent's childFraction +// managerState.childFraction = managerState.childFraction + _ProgressFraction(completed: portionOfParent, total: managerState.selfFraction.total) * myFraction +// } +// } +// } + //MARK: Interop-related internal methods /// Adds `observer` to list of `_observers` in `self`. internal func addObserver(observer: @escaping @Sendable (ObserverState) -> Void) { @@ -679,32 +621,28 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } - internal func addToChildren(childManager: ProgressManager) { - _ = state.withLock { state in - state.children.insert(childManager) + // Adds a child to the children list, with all of the info fields populated + internal func addToChildren(child: ProgressManager, portion: Int, childFraction: _ProgressFraction) { + state.withLock { state in + let childState = ChildState(portionOfSelf: portion, childFraction: childFraction) + state.children[child] = childState + + // Add child to dirtyChildren list + state.dirtyChildren.insert(child) } } - internal func addParent(parentReporter: ProgressManager, portionOfParent: Int) { - parents.withLock { parents in - parents[parentReporter] = portionOfParent - } - - let updates = state.withLock { state in + internal func addParent(parent: ProgressManager, portionOfParent: Int) { + state.withLock { state in + state.parents[parent] = portionOfParent + // Update metatype entry in parent for (metatype, value) in state.otherProperties { let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) let updatedParentEntry: [(any Sendable)?] = [value] + childrenValues - parentReporter.updateChildrenOtherPropertiesAnyValue(property: metatype, child: self, value: updatedParentEntry) + parent.updateChildrenOtherPropertiesAnyValue(property: metatype, child: self, value: updatedParentEntry) } - - let original = _ProgressFraction(completed: 0, total: 0) - let updated = state.overallFraction - return (original, updated) } - - // Update childFraction entry in parent - parentReporter.updateChildFraction(from: updates.0, to: updates.1, portion: portionOfParent) } internal func getAdditionalProperties( @@ -758,10 +696,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Ask parent to update their entry with my value + new children value let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) let updatedParentEntry: [(any Sendable)?] = [state.otherProperties[metatype]] + childrenValues - parents.withLock { parents in - for (parent, _) in parents { - parent.updateChildrenOtherPropertiesAnyValue(property: metatype, child: child, value: updatedParentEntry) - } + for (parent, _) in state.parents { + parent.updateChildrenOtherPropertiesAnyValue(property: metatype, child: child, value: updatedParentEntry) } } } @@ -780,11 +716,11 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // Ask parent to update their entry with my value + new children value let childrenValues = getFlattenedChildrenValues(property: metatype, state: &state) let updatedParentEntry: [P.Value?] = [state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.Value] + childrenValues - parents.withLock { parents in - for (parent, _) in parents { - parent.updateChildrenOtherProperties(property: metatype, child: self, value: updatedParentEntry) - } + + for (parent, _) in state.parents { + parent.updateChildrenOtherProperties(property: metatype, child: self, value: updatedParentEntry) } + } } @@ -796,8 +732,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { let updatedVisited = visited.union([self]) - return parents.withLock { parents in - for (parent, _) in parents { + return state.withLock { state in + for (parent, _) in state.parents { if !updatedVisited.contains(parent) { if parent.isCycle(reporter: reporter, visited: updatedVisited) { return true diff --git a/Sources/FoundationEssentials/ProgressManager/Subprogress.swift b/Sources/FoundationEssentials/ProgressManager/Subprogress.swift index 234b32ee4..8d850ce38 100644 --- a/Sources/FoundationEssentials/ProgressManager/Subprogress.swift +++ b/Sources/FoundationEssentials/ProgressManager/Subprogress.swift @@ -45,8 +45,8 @@ public struct Subprogress: ~Copyable, Sendable { ghostReporter?.setInteropChild(interopChild: childManager) } else { // Add child to parent's _children list & Store in child children's position in parent - parent.addToChildren(childManager: childManager) - childManager.addParent(parentReporter: parent, portionOfParent: portionOfParent) + parent.addToChildren(child: childManager, portion: portionOfParent, childFraction: childManager.getProgressFraction()) + childManager.addParent(parent: parent, portionOfParent: portionOfParent) } return childManager From 2474c4eb84682729fdc09d710ce74429a0a5f04c Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 12 Jun 2025 17:44:56 -0700 Subject: [PATCH 82/85] fix implementation --- .../ProgressManager/ProgressManager.swift | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 7133d784b..50a1e3b68 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -468,10 +468,10 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } - private func updateState(exclude lockedRoot: ProgressManager?, lockedState: inout State, child: ProgressManager? = nil, fraction: _ProgressFraction? = nil) { + private func updateState(exclude lockedRoot: ProgressManager?, lockedState: inout State) { // If I am the root which was queried. if self === lockedRoot { - print("Called updateState on self. This should never be called by a root. Only called by a leaf") + lockedState.isDirty = false return } @@ -485,6 +485,23 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } } + + private func updateStateLocked(exclude lockedRoot: ProgressManager?, lockedState: inout State, state: inout State) { + // If I am the root which was queried. + if self === lockedRoot { + lockedState.isDirty = false + return + } + + // Set isDirty to false + state.isDirty = false + + // Propagate these changes up to parent + for (parent, _) in state.parents { + parent.updateChildState(exclude: lockedRoot, lockedState: &lockedState, child: self, fraction: state.overallFraction) + } + + } internal func updateChildState(exclude lockedRoot: ProgressManager?, lockedState: inout State, child: ProgressManager, fraction: _ProgressFraction) { if self === lockedRoot { @@ -505,11 +522,13 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { if fraction.isFinished { state.selfFraction.completed += state.children[child]?.portionOfSelf ?? 0 + state.children.removeValue(forKey: child) } - for (parent, _) in state.parents { - parent.updateChildState(exclude: lockedRoot, lockedState: &lockedState, child: self, fraction: state.overallFraction) - } + updateStateLocked(exclude: lockedRoot, lockedState: &lockedState, state: &state) +// for (parent, _) in state.parents { +// parent.updateChildState(exclude: lockedRoot, lockedState: &lockedState, child: self, fraction: state.overallFraction) +// } } } From 249e987847a13032f032a0e32ef1b2d8c92c7ba0 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 12 Jun 2025 17:51:52 -0700 Subject: [PATCH 83/85] dirty bit implementation done --- .../ProgressManager/ProgressManager.swift | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 50a1e3b68..cd361f28a 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -472,6 +472,10 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // If I am the root which was queried. if self === lockedRoot { lockedState.isDirty = false + + for (parent, _) in lockedState.parents { + parent.updateChildState(exclude: lockedRoot, lockedState: &lockedState, child: self, fraction: lockedState.overallFraction) + } return } @@ -485,23 +489,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } } - - private func updateStateLocked(exclude lockedRoot: ProgressManager?, lockedState: inout State, state: inout State) { - // If I am the root which was queried. - if self === lockedRoot { - lockedState.isDirty = false - return - } - - // Set isDirty to false - state.isDirty = false - - // Propagate these changes up to parent - for (parent, _) in state.parents { - parent.updateChildState(exclude: lockedRoot, lockedState: &lockedState, child: self, fraction: state.overallFraction) - } - - } internal func updateChildState(exclude lockedRoot: ProgressManager?, lockedState: inout State, child: ProgressManager, fraction: _ProgressFraction) { if self === lockedRoot { @@ -513,6 +500,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { lockedState.selfFraction.completed += lockedState.children[child]?.portionOfSelf ?? 0 lockedState.children.removeValue(forKey: child) } + + updateState(exclude: self, lockedState: &lockedState) + return } @@ -524,12 +514,9 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { state.selfFraction.completed += state.children[child]?.portionOfSelf ?? 0 state.children.removeValue(forKey: child) } - - updateStateLocked(exclude: lockedRoot, lockedState: &lockedState, state: &state) -// for (parent, _) in state.parents { -// parent.updateChildState(exclude: lockedRoot, lockedState: &lockedState, child: self, fraction: state.overallFraction) -// } } + + updateState(exclude: lockedRoot, lockedState: &lockedState) } /// Returns `true` if completed and total units are not `nil` and completed units is greater than or equal to total units; From c7cf9ae84cbe98ea0ce6adb1c88791ec30397813 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 12 Jun 2025 17:53:47 -0700 Subject: [PATCH 84/85] clean up --- .../ProgressManager/ProgressManager.swift | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index cd361f28a..c536520f9 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -374,17 +374,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { // // If self is dirty, that just means I got mutated and my parents haven't received updates. // // If my dirtyChildren list exists, that just means I have fractional updates from children, which might not have completed. // // If at least one of my dirtyChildren actually completed, that means I would need to update my completed count actually. - -// let completedDirtyChildren = state.dirtyChildren.filter(\.isFinished) -// if !completedDirtyChildren.isEmpty { -// // update my own value based on dirty children completion -// for completedChild in completedDirtyChildren { -// // Update my completed count with the portion I assigned to this child -// state.selfFraction.completed += state.children[completedChild]?.portionOfSelf ?? 0 -// // Remove child from dirtyChildren list, so that the future updates won't be messed up -// state.dirtyChildren.remove(completedChild) -// } -// } // If there are dirty children, get updates first if state.dirtyChildren.count > 0 { @@ -476,6 +465,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { for (parent, _) in lockedState.parents { parent.updateChildState(exclude: lockedRoot, lockedState: &lockedState, child: self, fraction: lockedState.overallFraction) } + return } @@ -492,7 +482,6 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { internal func updateChildState(exclude lockedRoot: ProgressManager?, lockedState: inout State, child: ProgressManager, fraction: _ProgressFraction) { if self === lockedRoot { - print("I am locked. Lock not acquired. Use lockedState passed in.") lockedState.children[child]?.childFraction = fraction lockedState.dirtyChildren.remove(child) From fadcda7f9072310cbb3fc9d842c4d29aad634ad5 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 13 Jun 2025 17:12:59 -0700 Subject: [PATCH 85/85] fix interop implementation to sync with dirty bit implementation --- .../ProgressManager+Interop.swift | 31 ++-- .../ProgressManager/ProgressManager.swift | 153 ++++++++++-------- 2 files changed, 108 insertions(+), 76 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift index f1f01f5ef..987e1a6e9 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift @@ -89,16 +89,7 @@ extension Progress { return true } let updatedVisited = visited.union([unwrappedParent]) - return unwrappedParent.parents.withLock { parents in - for (parent, _) in parents { - if !updatedVisited.contains(parent) { - if parent.isCycle(reporter: reporter, visited: updatedVisited) { - return true - } - } - } - return false - } + return unwrappedParent.isCycleInterop(visited: updatedVisited) } return false } @@ -174,7 +165,7 @@ extension ProgressManager { public func subprogress(assigningCount count: Int, to progress: Foundation.Progress) { precondition(progress._parent() == nil, "Cannot assign a progress to more than one parent.") - let parentBridge = _NSProgressParentBridge(managerParent: self) + let parentBridge = _NSProgressParentBridge(managerParent: self, progressChild: progress, portion: count) progress._setParent(parentBridge, portion: Int64(count)) // Save ghost parent in ProgressManager so it doesn't go out of scope after assign method ends @@ -187,16 +178,30 @@ extension ProgressManager { internal final class _NSProgressParentBridge: Progress, @unchecked Sendable { internal let actualParent: ProgressManager + internal let actualChild: Progress + internal let ghostChild: ProgressManager - init(managerParent: ProgressManager) { + init(managerParent: ProgressManager, progressChild: Progress, portion: Int) { self.actualParent = managerParent + self.actualChild = progressChild + self.ghostChild = ProgressManager(totalCount: Int(progressChild.totalUnitCount)) super.init(parent: nil, userInfo: nil) + + // Make ghostChild mirror progressChild, ghostChild is added as a child to managerParent + ghostChild.withProperties { properties in + properties.completedCount = Int(progressChild.completedUnitCount) + } + + managerParent.addToChildren(child: ghostChild, portion: portion, childFraction: _ProgressFraction(completed: Int(completedUnitCount), total: Int(totalUnitCount))) + + ghostChild.addParent(parent: managerParent, portionOfParent: portion) } // Overrides the _updateChild func that Foundation.Progress calls to update parent // so that the parent that gets updated is the ProgressManager parent override func _updateChild(_ child: Foundation.Progress, fraction: _NSProgressFractionTuple, portion: Int64) { - actualParent.updateChildFraction(from: _ProgressFraction(nsProgressFraction: fraction.previous), to: _ProgressFraction(nsProgressFraction: fraction.next), portion: Int(portion)) +// actualParent.updateChildFraction(from: _ProgressFraction(nsProgressFraction: fraction.previous), to: _ProgressFraction(nsProgressFraction: fraction.next), portion: Int(portion)) + actualParent.updateChildState(child: ghostChild, fraction: _ProgressFraction(nsProgressFraction: fraction.next)) } } #endif diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index c536520f9..2e914875a 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -370,11 +370,11 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { if let interopChild = state.interopChild { return interopChild.completedCount } - -// // If self is dirty, that just means I got mutated and my parents haven't received updates. -// // If my dirtyChildren list exists, that just means I have fractional updates from children, which might not have completed. -// // If at least one of my dirtyChildren actually completed, that means I would need to update my completed count actually. - + // Implementation thoughts: + // If self is dirty, that just means I got mutated and my parents haven't received updates. + // If my dirtyChildren list exists, that just means I have fractional updates from children, which might not have completed. + // If at least one of my dirtyChildren actually completed, that means I would need to update my completed count actually. + // If there are dirty children, get updates first if state.dirtyChildren.count > 0 { @@ -399,6 +399,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { /// The calculation of fraction completed for a ProgressManager instance that has children /// will take into account children's fraction completed as well. private func getFractionCompleted(state: inout State) -> Double { + // Implementation thoughts: // If my self is dirty, that means I got mutated and I have parents that haven't received updates from me. // If my dirtyChildren list exists, that means I have fractional updates from these children, and I need these fractional updates. // But this runs into the issue of updating only the queried branch, but not the other branch that is not queried but dirty, this would cause the leaf to be cleaned up, but the other branch which share the dirty leaf hasn't received any updates. @@ -433,8 +434,56 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { return state.overallFraction.fractionCompleted } + + /// Returns `true` if completed and total units are not `nil` and completed units is greater than or equal to total units; + /// returns `false` otherwise. + private func getIsFinished(state: inout State) -> Bool { + return state.selfFraction.isFinished + } + + + /// Returns `true` if `self` has `nil` total units. + private func getIsIndeterminate(state: inout State) -> Bool { + return state.indeterminate + } + + //MARK: FractionCompleted Calculation methods + private struct UpdateState { + let previous: _ProgressFraction + let current: _ProgressFraction + } + + /// If parents exist, mark self as dirty and add self to parents' dirty children list. + private func markDirty(state: inout State) { + if state.parents.count > 0 { + state.isDirty = true + } + + // Recursively add self as dirty child to parents list + for (parent, _) in state.parents { + parent.addDirtyChild(self) + } + + } + + /// Add a given child to self's dirty children list. + private func addDirtyChild(_ child: ProgressManager) { + state.withLock { state in + // Child already exists in dirty children + if state.dirtyChildren.contains(child) { + return + } + + state.dirtyChildren.insert(child) + + // Propagate dirty state up to parents + for (parent, _) in state.parents { + parent.addDirtyChild(self) + } + } + } - /// Collect bottommost dirty nodes in a subtree + /// Collect bottommost dirty nodes in a subtree, when called from locked context. private func collectDirtyNodes(dirtyNodes: inout [ProgressManager], state: inout State) { if state.dirtyChildren.isEmpty && state.isDirty { dirtyNodes += [self] @@ -444,7 +493,8 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } } - + + /// Collect bottommost dirty nodes in a subtree, when called directly. private func collectDirtyNodes(dirtyNodes: inout [ProgressManager]) { state.withLock { state in if state.dirtyChildren.isEmpty && state.isDirty { @@ -457,6 +507,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } + /// Updates the state of current ProgressManager by setting isDirty to false. private func updateState(exclude lockedRoot: ProgressManager?, lockedState: inout State) { // If I am the root which was queried. if self === lockedRoot { @@ -480,6 +531,7 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } + /// Updates the stored state of a child, accounting for whether or not child has completed. internal func updateChildState(exclude lockedRoot: ProgressManager?, lockedState: inout State, child: ProgressManager, fraction: _ProgressFraction) { if self === lockedRoot { lockedState.children[child]?.childFraction = fraction @@ -507,71 +559,33 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { updateState(exclude: lockedRoot, lockedState: &lockedState) } - - /// Returns `true` if completed and total units are not `nil` and completed units is greater than or equal to total units; - /// returns `false` otherwise. - private func getIsFinished(state: inout State) -> Bool { - return state.selfFraction.isFinished - } - - /// Returns `true` if `self` has `nil` total units. - private func getIsIndeterminate(state: inout State) -> Bool { - return state.indeterminate - } - - //MARK: FractionCompleted Calculation methods - private struct UpdateState { - let previous: _ProgressFraction - let current: _ProgressFraction - } - - /// If parents exist, mark self as dirty and add self to parents' dirty children list. - private func markDirty(state: inout State) { - if state.parents.count > 0 { - state.isDirty = true - } - - // Recursively add self as dirty child to parents list - for (parent, _) in state.parents { - parent.addDirtyChild(self) + internal func updateState() { + state.withLock { state in + // Set isDirty to false + state.isDirty = false + + // Propagate these changes up to parent + for (parent, _) in state.parents { + parent.updateChildState(child: self, fraction: state.overallFraction) + } } - } - /// Add a given child to self's dirty children list. - private func addDirtyChild(_ child: ProgressManager) { + internal func updateChildState(child: ProgressManager, fraction: _ProgressFraction) { state.withLock { state in - // Child already exists in dirty children - if state.dirtyChildren.contains(child) { - return - } - - state.dirtyChildren.insert(child) + state.children[child]?.childFraction = fraction + state.dirtyChildren.remove(child) - // Propagate dirty state up to parents - for (parent, _) in state.parents { - parent.addDirtyChild(self) + if fraction.isFinished { + state.selfFraction.completed += state.children[child]?.portionOfSelf ?? 0 + state.children.removeValue(forKey: child) } } + + updateState() } - - // This is used when parent has its lock acquired and wants its child to update parent's childFraction to reflect child's own changes -// private func updateChildFractionSpecial(of manager: ProgressManager, state managerState: inout State) { -// let portion = parents.withLock { parents in -// return parents[manager] -// } -// -// if let portionOfParent = portion { -// let myFraction = state.withLock { $0.overallFraction } -// -// if !myFraction.isFinished { -// // If I'm not finished, update my entry in parent's childFraction -// managerState.childFraction = managerState.childFraction + _ProgressFraction(completed: portionOfParent, total: managerState.selfFraction.total) * myFraction -// } -// } -// } - + //MARK: Interop-related internal methods /// Adds `observer` to list of `_observers` in `self`. internal func addObserver(observer: @escaping @Sendable (ObserverState) -> Void) { @@ -739,6 +753,19 @@ internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { } } + func isCycleInterop(visited: Set = []) -> Bool { + return state.withLock { state in + for (parent, _) in state.parents { + if !visited.contains(parent) { + if parent.isCycle(reporter: reporter, visited: visited) { + return true + } + } + } + return false + } + } + deinit { if !isFinished { self.withProperties { properties in