Skip to content

[Implementation] ProgressManager Implementation #1362

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 85 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
ae0d724
move files to FoundationPreview
chloe-yeo Apr 18, 2025
469aea8
change import header
chloe-yeo Apr 21, 2025
23d9f29
moving files to open source
chloe-yeo Apr 21, 2025
db15363
resolve build issues with ProgressReporter as API"
chloe-yeo Apr 21, 2025
49c433a
apply naming changes
chloe-yeo Apr 21, 2025
4738c66
remove extra comma
chloe-yeo Apr 21, 2025
70f28ab
only run interop tests if foundation framework
chloe-yeo Apr 21, 2025
13cb72a
rename file to subprogress
chloe-yeo Apr 21, 2025
07510ee
add CMake files
chloe-yeo Apr 22, 2025
3defc03
CMake
chloe-yeo Apr 22, 2025
23f39f8
Correct CMake
chloe-yeo Apr 22, 2025
351b77a
exclude CMake from Package.swift
chloe-yeo Apr 22, 2025
12bed94
add trailing comma in Package.swift
chloe-yeo Apr 22, 2025
97b15f7
formatting changes
chloe-yeo Apr 23, 2025
41130b6
remove unused draft
chloe-yeo Apr 23, 2025
3e3ccb0
add manual codable conformance
chloe-yeo Apr 24, 2025
f5e8522
new manual codable conformance for FileFormatStyle
chloe-yeo Apr 24, 2025
d840b1c
add default value static var to Property protocol
chloe-yeo Apr 29, 2025
1afa651
code change for defaultValue
chloe-yeo Apr 29, 2025
c4b94e4
fix code
chloe-yeo Apr 29, 2025
5d4ee8e
use SingleValueContainer to encode and decode FileFormatStyle
chloe-yeo Apr 29, 2025
2609895
update Codable implementation
chloe-yeo Apr 29, 2025
8d1bad9
draft: reduce implementation keeping children values in array
chloe-yeo Apr 30, 2025
93c6741
draft: working version of reduce with children list, to be expanded t…
chloe-yeo Apr 30, 2025
f186548
draft: reduce method with children list, recusive implementation done
chloe-yeo Apr 30, 2025
bf20bb4
cleanup: replace vars with lets where needed
chloe-yeo May 1, 2025
55c1daf
additional property dual node: version 3 implementation
chloe-yeo May 2, 2025
1783e04
v3 fix to always preserve nil values in tree
chloe-yeo May 2, 2025
28d5718
deinit implementation for cancellation handling + getAllValues update…
chloe-yeo May 6, 2025
d95a1a9
change parent and portionInParent storage to be an array
chloe-yeo May 7, 2025
62f982e
update type-safe metadata implementation for multi-parents
chloe-yeo May 7, 2025
ca7698a
remove positionInParent implementation
chloe-yeo May 7, 2025
16c43d5
fix custom properties implementation for multi-parent
chloe-yeo May 7, 2025
9e5693e
fix propagation of in-progress value of children to parent in multi-p…
chloe-yeo May 7, 2025
5fc0c0a
fix propagation of metadata in multi-parent
chloe-yeo May 8, 2025
5f0c4a9
updated implementation to use OrderedDictionary to store metadata thr…
chloe-yeo May 8, 2025
ecd71ea
add convenience for observing a ProgressMonitor
chloe-yeo May 8, 2025
29ad39a
fix additional property propagation for more than 2 levels: typecast …
chloe-yeo May 10, 2025
fa37634
draft monitor interop implementation + reorganize tests
chloe-yeo May 10, 2025
3a1500e
interop with ProgressMonitor implementation + unit tests
chloe-yeo May 12, 2025
8a67e85
renaming + fix implementation
chloe-yeo May 14, 2025
c2673c9
rename monitor
chloe-yeo May 14, 2025
8b3769e
renaming Subprogress and ProgressMonitor
chloe-yeo May 14, 2025
ba499c1
add to ProgressOutput to have read-only properties
chloe-yeo May 15, 2025
d658037
rename ProgressReporter -> ProgressManager, ProgressInput -> Subprogr…
chloe-yeo May 16, 2025
8f3d6c0
renamed files + updated CMake files
chloe-yeo May 19, 2025
01f8dd2
resolve xcconfig conflict
chloe-yeo May 19, 2025
8dc20aa
add minimally working format style to progress reporter + draft forma…
chloe-yeo May 19, 2025
0be8f6c
import observation in file
chloe-yeo May 19, 2025
c4088f6
replace dynamicMemberLookup with withProperties in ProgressReporter
chloe-yeo May 19, 2025
08d4b0e
comment out formatting
chloe-yeo May 20, 2025
6308579
rename T to Valeu
chloe-yeo May 20, 2025
7f1fa05
draft formatstyle stuff
chloe-yeo May 20, 2025
5002418
cycle detection
chloe-yeo May 27, 2025
bb6a5b1
Update Package.swift to say ProgressManager
chloe-yeo May 28, 2025
59cea41
enforce Progress having single parent + add test for Progress indirec…
chloe-yeo May 28, 2025
3305aef
add cycle detection to interop
chloe-yeo May 28, 2025
c175d00
add cycle detection to interop
chloe-yeo May 28, 2025
322a7df
add dirty flag and update documentation
chloe-yeo May 29, 2025
c81e334
totalCount + completedCount updates swap to isDirty
chloe-yeo May 29, 2025
834ead5
debugging: using dirty bit for values update
chloe-yeo May 30, 2025
803103a
temporary revert using dirty bit
chloe-yeo May 30, 2025
a60bdfb
attempt at dirty flag - need to try again
chloe-yeo Jun 2, 2025
e40c904
remove format style from PR
chloe-yeo Jun 2, 2025
d5665bb
remove source from Internationalization CMake
chloe-yeo Jun 2, 2025
a2540a7
revert to recursive implementation
chloe-yeo Jun 2, 2025
a28d0b7
convert to use typed throws
chloe-yeo Jun 2, 2025
e65e5d7
remove setTotalCount method
chloe-yeo Jun 2, 2025
f11ba3e
rename manager to start
chloe-yeo Jun 2, 2025
97b723f
remove benchmark stuff
chloe-yeo Jun 2, 2025
126e650
add code documentation
chloe-yeo Jun 3, 2025
ae2a201
make progress reporter properties public
chloe-yeo Jun 3, 2025
507a8c5
add values and total methods to ProgressReporter
chloe-yeo Jun 6, 2025
83229a6
bug fix: fraction calculations for totalCount nil -> non-nil
chloe-yeo Jun 10, 2025
3b8e9fd
debug fix: confition to updateChildFraction
chloe-yeo Jun 10, 2025
475a60c
fix: values should return non-optional P.Value array
chloe-yeo Jun 11, 2025
3f8ba5c
draft implementation of dirty bit
chloe-yeo Jun 4, 2025
f86e006
drafting
chloe-yeo Jun 5, 2025
8d3ae93
move everything into one state
chloe-yeo Jun 11, 2025
3c1eef0
move locked variables up
chloe-yeo Jun 11, 2025
631381a
dirty bit implementation - minimal working version
chloe-yeo Jun 12, 2025
2474c4e
fix implementation
chloe-yeo Jun 13, 2025
249e987
dirty bit implementation done
chloe-yeo Jun 13, 2025
c7cf9ae
clean up
chloe-yeo Jun 13, 2025
fadcda7
fix interop implementation to sync with dirty bit implementation
chloe-yeo Jun 14, 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 @@ -133,7 +133,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 @@ -195,7 +196,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