Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fc598d5
ProgressManager v6 implementation
chloe-yeo Jul 8, 2025
959dbec
move interopChild checks into State
chloe-yeo Aug 13, 2025
09f7262
change interop variables into an enum
chloe-yeo Aug 13, 2025
7ba9e6d
reorder interopType switch statements
chloe-yeo Aug 14, 2025
afcf85d
move interop switch-case into enum
chloe-yeo Aug 14, 2025
69ea882
change Throughput and fileURL Summary types
chloe-yeo Aug 14, 2025
5409507
remove old unused calls
chloe-yeo Aug 14, 2025
cf6ea71
introduce terminate method requirement + rewrite implementation for a…
chloe-yeo Aug 15, 2025
866947f
change additional property string summary to [String]
chloe-yeo Aug 15, 2025
c1492eb
change string type to use String? and [String?]
chloe-yeo Aug 15, 2025
545a9c2
add propertiesURL as available additional property type
chloe-yeo Aug 15, 2025
6df4e75
formatting for readability
chloe-yeo Aug 15, 2025
1380f2b
add propertiesUInt64 as available additional property type
chloe-yeo Aug 15, 2025
cd8e010
deinit test for additional properties + fix for deinit fractionComple…
chloe-yeo Aug 19, 2025
374ec32
edit test to test deinit behavior for EstimatedTimeRemaining
chloe-yeo Aug 19, 2025
f24b8b1
replace redundant deinit complete(count:) call with mark dirty + add …
chloe-yeo Aug 20, 2025
08f34f2
more deinit tests
chloe-yeo Aug 20, 2025
cb26bca
additional unit tests for deinit behavior
chloe-yeo Aug 20, 2025
0ce022c
separate string additional properties into retaining vs non-retaining…
chloe-yeo Aug 20, 2025
34ac15e
add custom URL property unit tests
chloe-yeo Aug 20, 2025
e4a64ad
add custom UInt64 property unit tests
chloe-yeo Aug 20, 2025
8a3197d
update documentation for additional properties methods
chloe-yeo Aug 21, 2025
2eaa267
updated initializer
chloe-yeo Aug 22, 2025
3baa7d6
draft additional property observation fix
chloe-yeo Aug 22, 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
5 changes: 3 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ let package = Package(
"ProcessInfo/CMakeLists.txt",
"FileManager/CMakeLists.txt",
"URL/CMakeLists.txt",
"NotificationCenter/CMakeLists.txt"
"NotificationCenter/CMakeLists.txt",
"ProgressManager/CMakeLists.txt",
],
cSettings: [
.define("_GNU_SOURCE", .when(platforms: [.linux]))
Expand Down Expand Up @@ -185,7 +186,7 @@ let package = Package(
"Locale/CMakeLists.txt",
"Calendar/CMakeLists.txt",
"CMakeLists.txt",
"Predicate/CMakeLists.txt"
"Predicate/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 @@ -45,6 +45,7 @@ add_subdirectory(Locale)
add_subdirectory(NotificationCenter)
add_subdirectory(Predicate)
add_subdirectory(ProcessInfo)
add_subdirectory(ProgressManager)
add_subdirectory(PropertyList)
add_subdirectory(String)
add_subdirectory(TimeZone)
Expand Down
23 changes: 23 additions & 0 deletions Sources/FoundationEssentials/ProgressManager/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
##===----------------------------------------------------------------------===##
##
## 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+Accessors.swift
ProgressManager+Properties+Definitions.swift
ProgressManager+Properties+Helpers.swift
ProgressManager+State.swift
ProgressReporter.swift
Subprogress.swift)
337 changes: 337 additions & 0 deletions Sources/FoundationEssentials/ProgressManager/ProgressFraction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
//===----------------------------------------------------------------------===//
//
// 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 = nil
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
} else {
(self.completed, self.total) = ProgressFraction._fromDouble(double)
}
self.overflowed = overflow
}

init(completed: Int, total: Int?) {
self.total = total
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() {
guard let total = self.total, total != 0 else {
return
}

(self.completed, self.total) = ProgressFraction._simplify(completed, total)
}

internal func simplified() -> ProgressFraction? {
if let total = self.total {
let simplified = ProgressFraction._simplify(completed, total)
return ProgressFraction(completed: simplified.0, total: simplified.1)
} else {
return nil
}
}

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 let lhsTotal = lhs.total, lhsTotal != 0 else {
return rhs
}
guard let rhsTotal = rhs.total, rhsTotal != 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(lhsTotal, rhsTotal) {
let result = whichOverflow(lhs.completed * (lcm / lhsTotal), rhs.completed * (lcm / rhsTotal))
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()

guard let lhsSimplified = lhsSimplified,
let rhsSimplified = rhsSimplified,
let lhsSimplifiedTotal = lhsSimplified.total,
let rhsSimplifiedTotal = rhsSimplified.total else {
// Simplification failed, fall back to double math
return ProgressFraction(double: whichOperator(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true)
}

if let lcm = _leastCommonMultiple(lhsSimplifiedTotal, rhsSimplifiedTotal) {
let result = whichOverflow(lhsSimplified.completed * (lcm / lhsSimplifiedTotal), rhsSimplified.completed * (lcm / rhsSimplifiedTotal))
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: lhs.fractionCompleted * rhs.fractionCompleted, overflow: true)
}

guard let lhsTotal = lhs.total, let rhsTotal = rhs.total else {
return nil
}

let newCompleted = lhs.completed.multipliedReportingOverflow(by: rhs.completed)
let newTotal = lhsTotal.multipliedReportingOverflow(by: rhsTotal)

if newCompleted.overflow || newTotal.overflow {
// Try simplifying, then do it again
let lhsSimplified = lhs.simplified()
let rhsSimplified = rhs.simplified()

guard let lhsSimplified = lhsSimplified,
let rhsSimplified = rhsSimplified,
let lhsSimplifiedTotal = lhsSimplified.total,
let rhsSimplifiedTotal = rhsSimplified.total else {
return nil
}

let newCompletedSimplified = lhsSimplified.completed.multipliedReportingOverflow(by: rhsSimplified.completed)
let newTotalSimplified = lhsSimplifiedTotal.multipliedReportingOverflow(by: rhsSimplifiedTotal)

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)
}

guard let lhsTotal = lhs.total else {
return nil
}

let newTotal = lhsTotal.multipliedReportingOverflow(by: rhs)

if newTotal.overflow {
let simplified = lhs.simplified()

guard let simplified = simplified,
let simplifiedTotal = simplified.total else {
return nil
}

let newTotalSimplified = simplifiedTotal.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.total == rhs.total {
// Direct comparison of numerator
return lhs.completed == rhs.completed
} else if lhs.total == nil && rhs.total != nil {
return false
} else if lhs.total != nil && rhs.total == nil {
return false
} 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
guard let lhsTotal = lhs.total, let rhsTotal = rhs.total else {
return false
}

let left = lhs.completed.multipliedReportingOverflow(by: rhsTotal)
let right = lhsTotal.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()

guard let lhsSimplified = lhsSimplified,
let rhsSimplified = rhsSimplified,
let lhsSimplifiedTotal = lhsSimplified.total,
let rhsSimplifiedTotal = rhsSimplified.total else {
// Simplification failed, fall back to doubles
return lhs.fractionCompleted == rhs.fractionCompleted
}

let leftSimplified = lhsSimplified.completed.multipliedReportingOverflow(by: rhsSimplifiedTotal)
let rightSimplified = lhsSimplifiedTotal.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 {
guard let total else {
return false
}
return completed >= total && completed > 0 && total > 0
}

internal var isIndeterminate: Bool {
return total == nil
}


internal var fractionCompleted : Double {
guard let total else {
return 0.0
}
return Double(completed) / Double(total)
}


internal var isNaN : Bool {
return total == 0
}

internal var debugDescription : String {
return "\(completed) / \(total) (\(fractionCompleted)), overflowed: \(overflowed)"
}

// ----

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?
var denominator: Int
switch Int.bitWidth {
case 32: denominator = 1048576 // 2^20 - safe for 32-bit
case 64: denominator = 1073741824 // 2^30 - high precision for 64-bit
default: denominator = 131072 // 2^17 - ultra-safe fallback
}
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