Skip to content

Commit d2c0893

Browse files
committed
rdar://149574231 (Make ProgressReporter API) (#3296)
1 parent abfcc28 commit d2c0893

17 files changed

+7168
-2
lines changed

Package.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ let package = Package(
140140
"ProcessInfo/CMakeLists.txt",
141141
"FileManager/CMakeLists.txt",
142142
"URL/CMakeLists.txt",
143-
"NotificationCenter/CMakeLists.txt"
143+
"NotificationCenter/CMakeLists.txt",
144+
"ProgressManager/CMakeLists.txt",
144145
],
145146
cSettings: [
146147
.define("_GNU_SOURCE", .when(platforms: [.linux]))
@@ -185,7 +186,7 @@ let package = Package(
185186
"Locale/CMakeLists.txt",
186187
"Calendar/CMakeLists.txt",
187188
"CMakeLists.txt",
188-
"Predicate/CMakeLists.txt"
189+
"Predicate/CMakeLists.txt",
189190
],
190191
cSettings: wasiLibcCSettings,
191192
swiftSettings: [

Sources/FoundationEssentials/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ add_subdirectory(Locale)
4545
add_subdirectory(NotificationCenter)
4646
add_subdirectory(Predicate)
4747
add_subdirectory(ProcessInfo)
48+
add_subdirectory(ProgressManager)
4849
add_subdirectory(PropertyList)
4950
add_subdirectory(String)
5051
add_subdirectory(TimeZone)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
##===----------------------------------------------------------------------===##
2+
##
3+
## This source file is part of the Swift open source project
4+
##
5+
## Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
## Licensed under Apache License v2.0
7+
##
8+
## See LICENSE.txt for license information
9+
## See CONTRIBUTORS.md for the list of Swift project authors
10+
##
11+
## SPDX-License-Identifier: Apache-2.0
12+
##
13+
##===----------------------------------------------------------------------===##
14+
target_sources(FoundationEssentials PRIVATE
15+
ProgressFraction.swift
16+
ProgressManager.swift
17+
ProgressManager+Interop.swift
18+
ProgressManager+Properties+Accessors.swift
19+
ProgressManager+Properties+Definitions.swift
20+
ProgressManager+Properties+Helpers.swift
21+
ProgressManager+State.swift
22+
ProgressReporter.swift
23+
Subprogress.swift)
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#if FOUNDATION_FRAMEWORK
14+
internal import _ForSwiftFoundation
15+
#endif
16+
17+
internal struct ProgressFraction : Sendable, Equatable, CustomDebugStringConvertible {
18+
var completed : Int
19+
var total : Int?
20+
/// Indicates whether mathematical operations on this fraction have exceeded integer limits,
21+
/// causing the fraction to fall back to floating-point representation for accuracy.
22+
private(set) var overflowed : Bool
23+
24+
init() {
25+
completed = 0
26+
total = nil
27+
overflowed = false
28+
}
29+
30+
init(double: Double, overflow: Bool = false) {
31+
if double == 0 {
32+
self.completed = 0
33+
self.total = 1
34+
} else if double == 1 {
35+
self.completed = 1
36+
self.total = 1
37+
} else {
38+
(self.completed, self.total) = ProgressFraction._fromDouble(double)
39+
}
40+
self.overflowed = overflow
41+
}
42+
43+
init(completed: Int, total: Int?) {
44+
self.total = total
45+
self.completed = completed
46+
self.overflowed = false
47+
}
48+
49+
// ----
50+
51+
#if FOUNDATION_FRAMEWORK
52+
// Glue code for _NSProgressFraction and ProgressFraction
53+
init(nsProgressFraction: _NSProgressFraction) {
54+
self.init(completed: Int(nsProgressFraction.completed), total: Int(nsProgressFraction.total))
55+
}
56+
#endif
57+
58+
internal mutating func simplify() {
59+
guard let total = self.total, total != 0 else {
60+
return
61+
}
62+
63+
(self.completed, self.total) = ProgressFraction._simplify(completed, total)
64+
}
65+
66+
internal func simplified() -> ProgressFraction? {
67+
if let total = self.total {
68+
let simplified = ProgressFraction._simplify(completed, total)
69+
return ProgressFraction(completed: simplified.0, total: simplified.1)
70+
} else {
71+
return nil
72+
}
73+
}
74+
75+
/// A closure that performs floating-point arithmetic operations
76+
private typealias FloatingPointOperation = (_ lhs: Double, _ rhs: Double) -> Double
77+
78+
/// A closure that performs integer arithmetic operations with overflow detection
79+
private typealias OverflowReportingOperation = (_ lhs: Int, _ rhs: Int) -> (Int, overflow: Bool)
80+
81+
static private func _math(lhs: ProgressFraction, rhs: ProgressFraction, operation: FloatingPointOperation, overflowOperation: OverflowReportingOperation) -> ProgressFraction {
82+
// 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.
83+
precondition(!(lhs.total == 0 && rhs.total == 0), "Attempt to add or subtract invalid fraction")
84+
guard let lhsTotal = lhs.total, lhsTotal != 0 else {
85+
return rhs
86+
}
87+
guard let rhsTotal = rhs.total, rhsTotal != 0 else {
88+
return lhs
89+
}
90+
91+
guard !lhs.overflowed && !rhs.overflowed else {
92+
// If either has overflowed already, we preserve that
93+
return ProgressFraction(double: operation(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true)
94+
}
95+
96+
if let lcm = _leastCommonMultiple(lhsTotal, rhsTotal) {
97+
let result = overflowOperation(lhs.completed * (lcm / lhsTotal), rhs.completed * (lcm / rhsTotal))
98+
if result.overflow {
99+
return ProgressFraction(double: operation(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true)
100+
} else {
101+
return ProgressFraction(completed: result.0, total: lcm)
102+
}
103+
} else {
104+
// Overflow - simplify and then try again
105+
let lhsSimplified = lhs.simplified()
106+
let rhsSimplified = rhs.simplified()
107+
108+
guard let lhsSimplified = lhsSimplified,
109+
let rhsSimplified = rhsSimplified,
110+
let lhsSimplifiedTotal = lhsSimplified.total,
111+
let rhsSimplifiedTotal = rhsSimplified.total else {
112+
// Simplification failed, fall back to double math
113+
return ProgressFraction(double: operation(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true)
114+
}
115+
116+
if let lcm = _leastCommonMultiple(lhsSimplifiedTotal, rhsSimplifiedTotal) {
117+
let result = overflowOperation(lhsSimplified.completed * (lcm / lhsSimplifiedTotal), rhsSimplified.completed * (lcm / rhsSimplifiedTotal))
118+
if result.overflow {
119+
// Use original lhs/rhs here
120+
return ProgressFraction(double: operation(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true)
121+
} else {
122+
return ProgressFraction(completed: result.0, total: lcm)
123+
}
124+
} else {
125+
// Still overflow
126+
return ProgressFraction(double: operation(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true)
127+
}
128+
}
129+
}
130+
131+
static internal func +(lhs: ProgressFraction, rhs: ProgressFraction) -> ProgressFraction {
132+
return _math(lhs: lhs, rhs: rhs, operation: +, overflowOperation: { $0.addingReportingOverflow($1) })
133+
}
134+
135+
static internal func -(lhs: ProgressFraction, rhs: ProgressFraction) -> ProgressFraction {
136+
return _math(lhs: lhs, rhs: rhs, operation: -, overflowOperation: { $0.subtractingReportingOverflow($1) })
137+
}
138+
139+
static internal func *(lhs: ProgressFraction, rhs: ProgressFraction) -> ProgressFraction? {
140+
guard !lhs.overflowed && !rhs.overflowed else {
141+
// If either has overflowed already, we preserve that
142+
return ProgressFraction(double: lhs.fractionCompleted * rhs.fractionCompleted, overflow: true)
143+
}
144+
145+
guard let lhsTotal = lhs.total, let rhsTotal = rhs.total else {
146+
return nil
147+
}
148+
149+
let newCompleted = lhs.completed.multipliedReportingOverflow(by: rhs.completed)
150+
let newTotal = lhsTotal.multipliedReportingOverflow(by: rhsTotal)
151+
152+
if newCompleted.overflow || newTotal.overflow {
153+
// Try simplifying, then do it again
154+
let lhsSimplified = lhs.simplified()
155+
let rhsSimplified = rhs.simplified()
156+
157+
guard let lhsSimplified = lhsSimplified,
158+
let rhsSimplified = rhsSimplified,
159+
let lhsSimplifiedTotal = lhsSimplified.total,
160+
let rhsSimplifiedTotal = rhsSimplified.total else {
161+
return nil
162+
}
163+
164+
let newCompletedSimplified = lhsSimplified.completed.multipliedReportingOverflow(by: rhsSimplified.completed)
165+
let newTotalSimplified = lhsSimplifiedTotal.multipliedReportingOverflow(by: rhsSimplifiedTotal)
166+
167+
if newCompletedSimplified.overflow || newTotalSimplified.overflow {
168+
// Still overflow
169+
return ProgressFraction(double: lhs.fractionCompleted * rhs.fractionCompleted, overflow: true)
170+
} else {
171+
return ProgressFraction(completed: newCompletedSimplified.0, total: newTotalSimplified.0)
172+
}
173+
} else {
174+
return ProgressFraction(completed: newCompleted.0, total: newTotal.0)
175+
}
176+
}
177+
178+
static internal func /(lhs: ProgressFraction, rhs: Int) -> ProgressFraction? {
179+
guard !lhs.overflowed else {
180+
// If lhs has overflowed, we preserve that
181+
return ProgressFraction(double: lhs.fractionCompleted / Double(rhs), overflow: true)
182+
}
183+
184+
guard let lhsTotal = lhs.total else {
185+
return nil
186+
}
187+
188+
let newTotal = lhsTotal.multipliedReportingOverflow(by: rhs)
189+
190+
if newTotal.overflow {
191+
let simplified = lhs.simplified()
192+
193+
guard let simplified = simplified,
194+
let simplifiedTotal = simplified.total else {
195+
return nil
196+
}
197+
198+
let newTotalSimplified = simplifiedTotal.multipliedReportingOverflow(by: rhs)
199+
200+
if newTotalSimplified.overflow {
201+
// Still overflow
202+
return ProgressFraction(double: lhs.fractionCompleted / Double(rhs), overflow: true)
203+
} else {
204+
return ProgressFraction(completed: lhs.completed, total: newTotalSimplified.0)
205+
}
206+
} else {
207+
return ProgressFraction(completed: lhs.completed, total: newTotal.0)
208+
}
209+
}
210+
211+
static internal func ==(lhs: ProgressFraction, rhs: ProgressFraction) -> Bool {
212+
if lhs.isNaN || rhs.isNaN {
213+
// NaN fractions are never equal
214+
return false
215+
} else if lhs.total == rhs.total {
216+
// Direct comparison of numerator
217+
return lhs.completed == rhs.completed
218+
} else if lhs.total == nil && rhs.total != nil {
219+
return false
220+
} else if lhs.total != nil && rhs.total == nil {
221+
return false
222+
} else if lhs.completed == 0 && rhs.completed == 0 {
223+
return true
224+
} else if lhs.completed == lhs.total && rhs.completed == rhs.total {
225+
// Both finished (1)
226+
return true
227+
} else if (lhs.completed == 0 && rhs.completed != 0) || (lhs.completed != 0 && rhs.completed == 0) {
228+
// One 0, one not 0
229+
return false
230+
} else {
231+
// Cross-multiply
232+
guard let lhsTotal = lhs.total, let rhsTotal = rhs.total else {
233+
return false
234+
}
235+
236+
let left = lhs.completed.multipliedReportingOverflow(by: rhsTotal)
237+
let right = lhsTotal.multipliedReportingOverflow(by: rhs.completed)
238+
239+
if !left.overflow && !right.overflow {
240+
if left.0 == right.0 {
241+
return true
242+
}
243+
} else {
244+
// Try simplifying then cross multiply again
245+
let lhsSimplified = lhs.simplified()
246+
let rhsSimplified = rhs.simplified()
247+
248+
guard let lhsSimplified = lhsSimplified,
249+
let rhsSimplified = rhsSimplified,
250+
let lhsSimplifiedTotal = lhsSimplified.total,
251+
let rhsSimplifiedTotal = rhsSimplified.total else {
252+
// Simplification failed, fall back to doubles
253+
return lhs.fractionCompleted == rhs.fractionCompleted
254+
}
255+
256+
let leftSimplified = lhsSimplified.completed.multipliedReportingOverflow(by: rhsSimplifiedTotal)
257+
let rightSimplified = lhsSimplifiedTotal.multipliedReportingOverflow(by: rhsSimplified.completed)
258+
259+
if !leftSimplified.overflow && !rightSimplified.overflow {
260+
if leftSimplified.0 == rightSimplified.0 {
261+
return true
262+
}
263+
} else {
264+
// Ok... fallback to doubles. This doesn't use an epsilon
265+
return lhs.fractionCompleted == rhs.fractionCompleted
266+
}
267+
}
268+
}
269+
270+
return false
271+
}
272+
273+
// ----
274+
275+
internal var isFinished: Bool {
276+
guard let total else {
277+
return false
278+
}
279+
return completed >= total && completed > 0 && total > 0
280+
}
281+
282+
internal var isIndeterminate: Bool {
283+
return total == nil
284+
}
285+
286+
287+
internal var fractionCompleted : Double {
288+
guard let total else {
289+
return 0.0
290+
}
291+
return Double(completed) / Double(total)
292+
}
293+
294+
295+
internal var isNaN : Bool {
296+
return total == 0
297+
}
298+
299+
internal var debugDescription : String {
300+
return "\(completed) / \(total) (\(fractionCompleted)), overflowed: \(overflowed)"
301+
}
302+
303+
// ----
304+
305+
private static func _fromDouble(_ d : Double) -> (Int, Int) {
306+
// This simplistic algorithm could someday be replaced with something better.
307+
// Basically - how many 1/Nths is this double?
308+
var denominator: Int
309+
switch Int.bitWidth {
310+
case 32: denominator = 1048576 // 2^20 - safe for 32-bit
311+
case 64: denominator = 1073741824 // 2^30 - high precision for 64-bit
312+
default: denominator = 131072 // 2^17 - ultra-safe fallback
313+
}
314+
let numerator = Int(d / (1.0 / Double(denominator)))
315+
return (numerator, denominator)
316+
}
317+
318+
private static func _greatestCommonDivisor(_ inA : Int, _ inB : Int) -> Int {
319+
// This is Euclid's algorithm. There are faster ones, like Knuth, but this is the simplest one for now.
320+
var a = inA
321+
var b = inB
322+
repeat {
323+
let tmp = b
324+
b = a % b
325+
a = tmp
326+
} while (b != 0)
327+
return a
328+
}
329+
330+
private static func _leastCommonMultiple(_ a : Int, _ b : Int) -> Int? {
331+
// This division always results in an integer value because gcd(a,b) is a divisor of a.
332+
// lcm(a,b) == (|a|/gcd(a,b))*b == (|b|/gcd(a,b))*a
333+
let result = (a / _greatestCommonDivisor(a, b)).multipliedReportingOverflow(by: b)
334+
if result.overflow {
335+
return nil
336+
} else {
337+
return result.0
338+
}
339+
}
340+
341+
private static func _simplify(_ n : Int, _ d : Int) -> (Int, Int) {
342+
let gcd = _greatestCommonDivisor(n, d)
343+
return (n / gcd, d / gcd)
344+
}
345+
}

0 commit comments

Comments
 (0)