Files
OpenCombine/Sources/OpenCombineDispatch/DispatchQueue+Scheduler.swift
T
Sergej Jaskiewicz 8f8ef5057d Update for Xcode 14
2023-04-23 18:11:38 +02:00

440 lines
18 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// DispatchQueue.swift
//
//
// Created by Sergej Jaskiewicz on 21.08.2019.
//
import Dispatch
import OpenCombine
extension DispatchQueue {
/// A namespace for disambiguation when both OpenCombine and Combine are imported.
///
/// Combine extends `DispatchQueue` with new methods and nested types.
/// If you import both OpenCombine and Combine (either explicitly or implicitly,
/// e. g. when importing Foundation), you will not be able
/// to write `DispatchQueue.SchedulerTimeType`,
/// because Swift is unable to understand which `SchedulerTimeType`
/// you're referring to.
///
/// So you have to write `DispatchQueue.OCombine.SchedulerTimeType`.
///
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
///
/// You can omit this whenever Combine is not available (e. g. on Linux).
public struct OCombine: Scheduler {
public let queue: DispatchQueue
public init(_ queue: DispatchQueue) {
self.queue = queue
}
/// The scheduler time type used by the dispatch queue.
public struct SchedulerTimeType: Strideable, Codable, Hashable {
/// The dispatch time represented by this type.
public var dispatchTime: DispatchTime
/// Creates a dispatch queue time type instance.
///
/// - Parameter time: The dispatch time to represent.
public init(_ time: DispatchTime) {
dispatchTime = time
}
/// Returns the distance to another dispatch queue time.
///
/// - Parameter other: Another dispatch queue time.
/// - Returns: The time interval between this time and the provided time.
public func distance(to other: SchedulerTimeType) -> Stride {
return Stride(dispatchTime.polyfillDistance(to: other.dispatchTime))
}
/// Returns a dispatch queue scheduler time calculated by advancing
/// this instances time by the given interval.
///
/// - Parameter n: A time interval to advance.
/// - Returns: A dispatch queue time advanced by the given
/// interval from this instances time.
public func advanced(by stride: Stride) -> SchedulerTimeType {
return stride.magnitude == .max
? .init(.distantFuture)
: .init(dispatchTime + stride.timeInterval)
}
public func hash(into hasher: inout Hasher) {
hasher.combine(dispatchTime.rawValue)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(dispatchTime.uptimeNanoseconds)
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
dispatchTime = try .init(uptimeNanoseconds: container.decode(UInt64.self))
}
/// A type that represents the distance between two values.
public struct Stride: SchedulerTimeIntervalConvertible,
Comparable,
SignedNumeric,
ExpressibleByFloatLiteral,
Hashable,
Codable {
/// If created via floating point literal, the value is
/// converted to nanoseconds via multiplication.
public typealias FloatLiteralType = Double
/// Nanoseconds, same as DispatchTimeInterval.
public typealias IntegerLiteralType = Int
/// A type that can represent the absolute value of any possible
/// value of the conforming type.
public typealias Magnitude = Int
private var _nanoseconds: Int64
/// The value of this time interval in nanoseconds.
public var magnitude: Int {
get {
return Int(_nanoseconds)
}
set {
_nanoseconds = Int64(newValue)
}
}
/// A `DispatchTimeInterval` created with the value of this type
/// in nanoseconds.
public var timeInterval: DispatchTimeInterval {
return .nanoseconds(magnitude)
}
private init(magnitude: Int64) {
_nanoseconds = magnitude
}
/// Creates a dispatch queue time interval from the given
/// dispatch time interval.
///
/// - Parameter timeInterval: A dispatch time interval.
public init(_ timeInterval: DispatchTimeInterval) {
switch timeInterval {
case .seconds(let seconds):
self = .seconds(seconds)
case .milliseconds(let milliseconds):
self = .milliseconds(milliseconds)
case .microseconds(let microseconds):
self = .microseconds(microseconds)
case .nanoseconds(let nanoseconds):
self = .nanoseconds(nanoseconds)
// This dance is to avoid the warning 'default will never be executed'
// on non-Darwin platforms.
// There really shouldn't be a warning.
// See https://forums.swift.org/t/unknown-default-produces-a-warning-on-linux-with-non-frozen-enum/31687
//
// Thanks to Jeremy David Giesbrecht for suggesting this workaround.
#if canImport(Darwin)
case .never:
self = .nanoseconds(.max)
@unknown default:
self.init(__guessFromUnknown: timeInterval)
#else
default:
if case .never = timeInterval {
self = .nanoseconds(.max)
} else {
self.init(__guessFromUnknown: timeInterval)
}
#endif
}
}
public // testable
init(__guessFromUnknown timeInterval: DispatchTimeInterval) {
// Let's take some reference time,
// add `timeInterval` to it, take the `rawValue` from the result
// and subtract the `rawValue` of the reference time.
//
// We won't be able to provide the exact implementation though,
// because something will definitely overflow.
//
// However, we can try to support as wide a range of values
// as possible.
//
// By trial and error I got that the `rawValue` of `UInt64.max / 13`
// gives us probably the widest range of supported values:
// from `Int.min / 6.5` to `Int.max / 2.889` nanoseconds.
// That's with Int being 64 bits. Since here only UInt64 can overflow,
// when Int is 32 bits, we don't have this issue.
// It should be more than enough.
let referenceTime = DispatchTime(uptimeNanoseconds: .max / 13)
self = SchedulerTimeType(referenceTime)
.distance(to: SchedulerTimeType(referenceTime + timeInterval))
}
/// Creates a dispatch queue time interval from a floating-point
/// seconds value.
///
/// - Parameter value: The number of seconds, as a `Double`.
public init(floatLiteral value: Double) {
self = .seconds(value)
}
/// Creates a dispatch queue time interval from an integer seconds value.
///
/// - Parameter value: The number of seconds, as an `Int`.
public init(integerLiteral value: Int) {
self = .seconds(value)
}
/// Creates a dispatch queue time interval from a binary integer type.
///
/// If `exactly` cannot convert to an `Int`, the resulting time interval
/// is `nil`.
///
/// - Parameter exactly: A binary integer representing a time interval.
public init?<Source: BinaryInteger>(exactly source: Source) {
guard let value = Int(exactly: source) else { return nil }
self = .seconds(value)
}
public static func < (lhs: Stride, rhs: Stride) -> Bool {
return lhs._nanoseconds < rhs._nanoseconds
}
public static func * (lhs: Stride, rhs: Stride) -> Stride {
return Stride(magnitude: 0)
}
public static func + (lhs: Stride, rhs: Stride) -> Stride {
return Stride(magnitude: lhs._nanoseconds + rhs._nanoseconds)
}
public static func - (lhs: Stride, rhs: Stride) -> Stride {
return Stride(magnitude: lhs._nanoseconds - rhs._nanoseconds)
}
public static func -= (lhs: inout Stride, rhs: Stride) {
lhs._nanoseconds -= rhs._nanoseconds
}
public static func *= (lhs: inout Stride, rhs: Stride) {
lhs._nanoseconds = 0
}
public static func += (lhs: inout Stride, rhs: Stride) {
lhs._nanoseconds += rhs._nanoseconds
}
public static func seconds(_ value: Double) -> Stride {
let nanoseconds = value * 1_000_000_000
if nanoseconds >= Double(Int64.max) {
return Stride(magnitude: .max)
}
if nanoseconds <= Double(Int64.min) {
return Stride(magnitude: .min)
}
return Stride(magnitude: Int64(nanoseconds))
}
public static func seconds(_ value: Int) -> Stride {
return Stride(magnitude: clampedIntProduct(Int64(value),
1_000_000_000))
}
public static func milliseconds(_ value: Int) -> Stride {
return Stride(magnitude: clampedIntProduct(Int64(value), 1_000_000))
}
public static func microseconds(_ value: Int) -> Stride {
return Stride(magnitude: clampedIntProduct(Int64(value), 1_000))
}
public static func nanoseconds(_ value: Int) -> Stride {
return Stride(magnitude: Int64(value))
}
}
}
/// Options that affect the operation of the dispatch queue scheduler.
public struct SchedulerOptions {
/// The dispatch queue quality of service.
public var qos: DispatchQoS
/// The dispatch queue work item flags.
public var flags: DispatchWorkItemFlags
/// The dispatch group, if any, that should be used for performing actions.
public var group: DispatchGroup?
public init(qos: DispatchQoS = .unspecified,
flags: DispatchWorkItemFlags = [],
group: DispatchGroup? = nil) {
self.qos = qos
self.flags = flags
self.group = group
}
}
public var minimumTolerance: SchedulerTimeType.Stride {
return .nanoseconds(0)
}
public var now: SchedulerTimeType {
return .init(.now())
}
public func schedule(options: SchedulerOptions?,
_ action: @escaping () -> Void) {
let options = options ?? .init()
queue.async(group: options.group,
qos: options.qos,
flags: options.flags,
execute: action)
}
public func schedule(after date: SchedulerTimeType,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) {
let options = options ?? .init()
queue.asyncAfter(deadline: date.dispatchTime,
qos: options.qos,
flags: options.flags,
execute: action)
}
/// Performs the action at some time after the specified date, at the specified
/// frequency, optionally taking into account tolerance if possible.
public func schedule(after date: SchedulerTimeType,
interval: SchedulerTimeType.Stride,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) -> Cancellable {
let options = options ?? .init()
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.setEventHandler(qos: options.qos,
flags: options.flags,
handler: action)
timer.schedule(deadline: date.dispatchTime,
repeating: interval.timeInterval,
leeway: tolerance.timeInterval)
timer.resume()
return AnyCancellable(timer.cancel)
}
}
/// A namespace for disambiguation when both OpenCombine and Combine are imported.
///
/// Combine extends `DispatchQueue` with new methods and nested types.
/// If you import both OpenCombine and Combine (either explicitly or implicitly,
/// e. g. when importing Foundation), you will not be able
/// to write `DispatchQueue.main.schedule { doThings() }`,
/// because Swift is unable to understand which `schedule` method
/// you're referring to.
///
/// So you have to write `DispatchQueue.main.ocombine.schedule { doThings() }`.
///
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
///
/// You can omit this whenever Combine is not available (e. g. on Linux).
public var ocombine: OCombine {
return OCombine(self)
}
}
#if !canImport(Combine)
extension DispatchQueue: OpenCombine.Scheduler {
/// Options that affect the operation of the dispatch queue scheduler.
public typealias SchedulerOptions = OCombine.SchedulerOptions
/// The scheduler time type used by the dispatch queue.
public typealias SchedulerTimeType = OCombine.SchedulerTimeType
public var minimumTolerance: OCombine.SchedulerTimeType.Stride {
return ocombine.minimumTolerance
}
public var now: OCombine.SchedulerTimeType {
return ocombine.now
}
public func schedule(options: OCombine.SchedulerOptions?,
_ action: @escaping () -> Void) {
ocombine.schedule(options: options, action)
}
public func schedule(after date: OCombine.SchedulerTimeType,
tolerance: OCombine.SchedulerTimeType.Stride,
options: OCombine.SchedulerOptions?,
_ action: @escaping () -> Void) {
ocombine.schedule(after: date, tolerance: tolerance, options: options, action)
}
public func schedule(after date: OCombine.SchedulerTimeType,
interval: OCombine.SchedulerTimeType.Stride,
tolerance: OCombine.SchedulerTimeType.Stride,
options: OCombine.SchedulerOptions?,
_ action: @escaping () -> Void) -> Cancellable {
return ocombine.schedule(after: date,
interval: interval,
tolerance: tolerance,
options: options,
action)
}
}
#endif
// This function is taken from swift-corelibs-libdispatch:
// https://github.com/apple/swift-corelibs-libdispatch/blob/c992dacf3ca114806e6ac9ffc9113b19255be9fe/src/swift/Time.swift#L134-L144
//
// Returns m1 * m2, clamped to the range [Int64.min, Int64.max].
// Because of the way this function is used, we can always assume
// that m2 > 0.
private func clampedIntProduct(_ lhs: Int64, _ rhs: Int64) -> Int64 {
assert(rhs > 0, "multiplier must be positive")
let (result, overflow) = lhs.multipliedReportingOverflow(by: rhs)
if overflow {
return lhs > 0 ? .max : .min
}
return result
}
extension DispatchTime {
fileprivate func polyfillDistance(to other: DispatchTime) -> DispatchTimeInterval {
#if canImport(Darwin) && compiler(>=5.1)
if #available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) {
return distance(to: other)
}
#endif
let start = rawValue
let end = other.rawValue
if end >= start {
let result = end &- start
if result > UInt64(Int.max) {
return .never
} else {
return .nanoseconds(Int(result))
}
} else {
let result = start &- end
if result > UInt64(Int.max) {
return .never
} else {
return .nanoseconds(-Int(result))
}
}
}
}