Skip to content

[Implementation] ProgressManager: ProgressReporting in Swift Concurrency #1270

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 76 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
e3242e3
move files to FoundationPreview
chloe-yeo Apr 18, 2025
8d6159b
change import header
chloe-yeo Apr 21, 2025
f876a46
moving files to open source
chloe-yeo Apr 21, 2025
df6a17f
resolve build issues with ProgressReporter as API"
chloe-yeo Apr 21, 2025
af5eab4
apply naming changes
chloe-yeo Apr 21, 2025
5e39edc
remove extra comma
chloe-yeo Apr 21, 2025
dd907fb
only run interop tests if foundation framework
chloe-yeo Apr 21, 2025
9089a29
rename file to subprogress
chloe-yeo Apr 21, 2025
2a66a00
add CMake files
chloe-yeo Apr 22, 2025
fdfd28a
CMake
chloe-yeo Apr 22, 2025
cc9124c
Correct CMake
chloe-yeo Apr 22, 2025
085615a
exclude CMake from Package.swift
chloe-yeo Apr 22, 2025
20386dc
add trailing comma in Package.swift
chloe-yeo Apr 22, 2025
9fa8400
formatting changes
chloe-yeo Apr 23, 2025
3b26f1a
remove unused draft
chloe-yeo Apr 23, 2025
ef3abe4
add manual codable conformance
chloe-yeo Apr 24, 2025
0bb66d0
new manual codable conformance for FileFormatStyle
chloe-yeo Apr 24, 2025
185c5f9
add default value static var to Property protocol
chloe-yeo Apr 29, 2025
08fd947
code change for defaultValue
chloe-yeo Apr 29, 2025
119fba1
fix code
chloe-yeo Apr 29, 2025
d032937
use SingleValueContainer to encode and decode FileFormatStyle
chloe-yeo Apr 29, 2025
cd5067c
update Codable implementation
chloe-yeo Apr 29, 2025
8cfdc0c
draft: reduce implementation keeping children values in array
chloe-yeo Apr 30, 2025
5ffa3e5
draft: working version of reduce with children list, to be expanded t…
chloe-yeo Apr 30, 2025
81cccf6
draft: reduce method with children list, recusive implementation done
chloe-yeo Apr 30, 2025
d37753d
cleanup: replace vars with lets where needed
chloe-yeo May 1, 2025
b0cd0e3
additional property dual node: version 3 implementation
chloe-yeo May 2, 2025
02c24b3
v3 fix to always preserve nil values in tree
chloe-yeo May 2, 2025
c0b0aec
deinit implementation for cancellation handling + getAllValues update…
chloe-yeo May 6, 2025
6d50753
change parent and portionInParent storage to be an array
chloe-yeo May 7, 2025
cfbda48
update type-safe metadata implementation for multi-parents
chloe-yeo May 7, 2025
cda0e47
remove positionInParent implementation
chloe-yeo May 7, 2025
9428d57
fix custom properties implementation for multi-parent
chloe-yeo May 7, 2025
3cbf854
fix propagation of in-progress value of children to parent in multi-p…
chloe-yeo May 7, 2025
04e2a58
fix propagation of metadata in multi-parent
chloe-yeo May 8, 2025
dc1d2f1
updated implementation to use OrderedDictionary to store metadata thr…
chloe-yeo May 8, 2025
227d119
add convenience for observing a ProgressMonitor
chloe-yeo May 8, 2025
6dea668
fix additional property propagation for more than 2 levels: typecast …
chloe-yeo May 10, 2025
b5b6acd
draft monitor interop implementation + reorganize tests
chloe-yeo May 10, 2025
c1d77e5
interop with ProgressMonitor implementation + unit tests
chloe-yeo May 12, 2025
be0de97
renaming + fix implementation
chloe-yeo May 14, 2025
d8e76b2
rename monitor
chloe-yeo May 14, 2025
ce7cd4e
renaming Subprogress and ProgressMonitor
chloe-yeo May 14, 2025
a73d183
add to ProgressOutput to have read-only properties
chloe-yeo May 15, 2025
c24dc14
rename ProgressReporter -> ProgressManager, ProgressInput -> Subprogr…
chloe-yeo May 16, 2025
57fbbba
renamed files + updated CMake files
chloe-yeo May 19, 2025
65b4f96
resolve xcconfig conflict
chloe-yeo May 19, 2025
043e6a3
add minimally working format style to progress reporter + draft forma…
chloe-yeo May 19, 2025
b382975
import observation in file
chloe-yeo May 19, 2025
16b7335
replace dynamicMemberLookup with withProperties in ProgressReporter
chloe-yeo May 19, 2025
d69325d
comment out formatting
chloe-yeo May 20, 2025
702877b
rename T to Valeu
chloe-yeo May 20, 2025
58775ef
draft formatstyle stuff
chloe-yeo May 20, 2025
95b80df
cycle detection
chloe-yeo May 27, 2025
9455f90
Update Package.swift to say ProgressManager
chloe-yeo May 28, 2025
62a16a1
enforce Progress having single parent + add test for Progress indirec…
chloe-yeo May 28, 2025
02ab998
add cycle detection to interop
chloe-yeo May 28, 2025
5bd5e6a
add cycle detection to interop
chloe-yeo May 28, 2025
61b3945
add dirty flag and update documentation
chloe-yeo May 29, 2025
8240116
totalCount + completedCount updates swap to isDirty
chloe-yeo May 29, 2025
19a20e5
debugging: using dirty bit for values update
chloe-yeo May 30, 2025
c113a98
temporary revert using dirty bit
chloe-yeo May 30, 2025
469a939
attempt at dirty flag - need to try again
chloe-yeo Jun 2, 2025
1797eb9
remove format style from PR
chloe-yeo Jun 2, 2025
12cca43
remove source from Internationalization CMake
chloe-yeo Jun 2, 2025
fa43f78
revert to recursive implementation
chloe-yeo Jun 2, 2025
3569d2d
convert to use typed throws
chloe-yeo Jun 2, 2025
f2bb6b9
remove setTotalCount method
chloe-yeo Jun 2, 2025
bb22d16
rename manager to start
chloe-yeo Jun 2, 2025
89886b9
remove benchmark stuff
chloe-yeo Jun 2, 2025
a611ae9
add code documentation
chloe-yeo Jun 3, 2025
3dbe73c
make progress reporter properties public
chloe-yeo Jun 3, 2025
85d2ce4
add values and total methods to ProgressReporter
chloe-yeo Jun 6, 2025
e6b6b15
bug fix: fraction calculations for totalCount nil -> non-nil
chloe-yeo Jun 10, 2025
a38022d
debug fix: confition to updateChildFraction
chloe-yeo Jun 10, 2025
5705b9d
fix: values should return non-optional P.Value array
chloe-yeo Jun 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ let package = Package(
"CMakeLists.txt",
"ProcessInfo/CMakeLists.txt",
"FileManager/CMakeLists.txt",
"URL/CMakeLists.txt"
"URL/CMakeLists.txt",
"ProgressManager/CMakeLists.txt",
],
cSettings: [
.define("_GNU_SOURCE", .when(platforms: [.linux]))
Expand Down Expand Up @@ -168,7 +169,8 @@ let package = Package(
"Locale/CMakeLists.txt",
"Calendar/CMakeLists.txt",
"CMakeLists.txt",
"Predicate/CMakeLists.txt"
"Predicate/CMakeLists.txt",
"ProgressManager/CMakeLists.txt",
],
cSettings: wasiLibcCSettings,
swiftSettings: [
Expand Down
1 change: 1 addition & 0 deletions Sources/FoundationEssentials/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ add_subdirectory(JSON)
add_subdirectory(Locale)
add_subdirectory(Predicate)
add_subdirectory(ProcessInfo)
add_subdirectory(ProgressManager)
add_subdirectory(PropertyList)
add_subdirectory(String)
add_subdirectory(TimeZone)
Expand Down
14 changes: 9 additions & 5 deletions Sources/FoundationEssentials/LockedState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,17 @@ package struct LockedState<State> {
return initialState
})
}

package func withLock<T>(_ body: @Sendable (inout State) throws -> T) rethrows -> T {

package func withLock<T, E: Error>(
_ body: (inout sending State) throws(E) -> sending T
) throws(E) -> sending T {
try withLockUnchecked(body)
}

package func withLockUnchecked<T>(_ body: (inout State) throws -> T) rethrows -> T {
try _buffer.withUnsafeMutablePointers { state, lock in

package func withLockUnchecked<T, E: Error>(
_ 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)
Expand Down
20 changes: 20 additions & 0 deletions Sources/FoundationEssentials/ProgressManager/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
##===----------------------------------------------------------------------===##
##
## 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
ProgressManager.swift
ProgressManager+Interop.swift
ProgressManager+Properties.swift
ProgressReporter.swift
Subprogress.swift)
282 changes: 282 additions & 0 deletions Sources/FoundationEssentials/ProgressManager/ProgressFraction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
//===----------------------------------------------------------------------===//
//
// 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 FOUNDATION_FRAMEWORK
internal import _ForSwiftFoundation
#endif

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
}

// ----

#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 {
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)
}
}
Loading
Loading