Scheduler.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. //
  2. // Scheduler.swift
  3. // ReactiveSwift
  4. //
  5. // Created by Justin Spahr-Summers on 2014-06-02.
  6. // Copyright (c) 2014 GitHub. All rights reserved.
  7. //
  8. import Dispatch
  9. import Foundation
  10. #if os(Linux)
  11. import let CDispatch.NSEC_PER_SEC
  12. #endif
  13. /// Represents a serial queue of work items.
  14. public protocol SchedulerProtocol {
  15. /// Enqueues an action on the scheduler.
  16. ///
  17. /// When the work is executed depends on the scheduler in use.
  18. ///
  19. /// - returns: Optional `Disposable` that can be used to cancel the work
  20. /// before it begins.
  21. @discardableResult
  22. func schedule(_ action: @escaping () -> Void) -> Disposable?
  23. }
  24. /// A particular kind of scheduler that supports enqueuing actions at future
  25. /// dates.
  26. public protocol DateSchedulerProtocol: SchedulerProtocol {
  27. /// The current date, as determined by this scheduler.
  28. ///
  29. /// This can be implemented to deterministically return a known date (e.g.,
  30. /// for testing purposes).
  31. var currentDate: Date { get }
  32. /// Schedules an action for execution at or after the given date.
  33. ///
  34. /// - parameters:
  35. /// - date: Starting time.
  36. /// - action: Closure of the action to perform.
  37. ///
  38. /// - returns: Optional `Disposable` that can be used to cancel the work
  39. /// before it begins.
  40. @discardableResult
  41. func schedule(after date: Date, action: @escaping () -> Void) -> Disposable?
  42. /// Schedules a recurring action at the given interval, beginning at the
  43. /// given date.
  44. ///
  45. /// - parameters:
  46. /// - date: Starting time.
  47. /// - repeatingEvery: Repetition interval.
  48. /// - withLeeway: Some delta for repetition.
  49. /// - action: Closure of the action to perform.
  50. ///
  51. /// - note: If you plan to specify an `interval` value greater than 200,000
  52. /// seconds, use `schedule(after:interval:leeway:action)` instead
  53. /// and specify your own `leeway` value to avoid potential overflow.
  54. ///
  55. /// - returns: Optional `Disposable` that can be used to cancel the work
  56. /// before it begins.
  57. @discardableResult
  58. func schedule(after date: Date, interval: DispatchTimeInterval, leeway: DispatchTimeInterval, action: @escaping () -> Void) -> Disposable?
  59. }
  60. /// A scheduler that performs all work synchronously.
  61. public final class ImmediateScheduler: SchedulerProtocol {
  62. public init() {}
  63. /// Immediately calls passed in `action`.
  64. ///
  65. /// - parameters:
  66. /// - action: Closure of the action to perform.
  67. ///
  68. /// - returns: `nil`.
  69. @discardableResult
  70. public func schedule(_ action: @escaping () -> Void) -> Disposable? {
  71. action()
  72. return nil
  73. }
  74. }
  75. /// A scheduler that performs all work on the main queue, as soon as possible.
  76. ///
  77. /// If the caller is already running on the main queue when an action is
  78. /// scheduled, it may be run synchronously. However, ordering between actions
  79. /// will always be preserved.
  80. public final class UIScheduler: SchedulerProtocol {
  81. private static let dispatchSpecificKey = DispatchSpecificKey<UInt8>()
  82. private static let dispatchSpecificValue = UInt8.max
  83. private static var __once: () = {
  84. DispatchQueue.main.setSpecific(key: UIScheduler.dispatchSpecificKey,
  85. value: dispatchSpecificValue)
  86. }()
  87. #if os(Linux)
  88. private var queueLength: Atomic<Int32> = Atomic(0)
  89. #else
  90. private var queueLength: Int32 = 0
  91. #endif
  92. /// Initializes `UIScheduler`
  93. public init() {
  94. /// This call is to ensure the main queue has been setup appropriately
  95. /// for `UIScheduler`. It is only called once during the application
  96. /// lifetime, since Swift has a `dispatch_once` like mechanism to
  97. /// lazily initialize global variables and static variables.
  98. _ = UIScheduler.__once
  99. }
  100. /// Queues an action to be performed on main queue. If the action is called
  101. /// on the main thread and no work is queued, no scheduling takes place and
  102. /// the action is called instantly.
  103. ///
  104. /// - parameters:
  105. /// - action: Closure of the action to perform on the main thread.
  106. ///
  107. /// - returns: `Disposable` that can be used to cancel the work before it
  108. /// begins.
  109. @discardableResult
  110. public func schedule(_ action: @escaping () -> Void) -> Disposable? {
  111. let disposable = SimpleDisposable()
  112. let actionAndDecrement = {
  113. if !disposable.isDisposed {
  114. action()
  115. }
  116. #if os(Linux)
  117. self.queueLength.modify { $0 -= 1 }
  118. #else
  119. OSAtomicDecrement32(&self.queueLength)
  120. #endif
  121. }
  122. #if os(Linux)
  123. let queued = self.queueLength.modify { value -> Int32 in
  124. value += 1
  125. return value
  126. }
  127. #else
  128. let queued = OSAtomicIncrement32(&queueLength)
  129. #endif
  130. // If we're already running on the main queue, and there isn't work
  131. // already enqueued, we can skip scheduling and just execute directly.
  132. if queued == 1 && DispatchQueue.getSpecific(key: UIScheduler.dispatchSpecificKey) == UIScheduler.dispatchSpecificValue {
  133. actionAndDecrement()
  134. } else {
  135. DispatchQueue.main.async(execute: actionAndDecrement)
  136. }
  137. return disposable
  138. }
  139. }
  140. /// A scheduler backed by a serial GCD queue.
  141. public final class QueueScheduler: DateSchedulerProtocol {
  142. /// A singleton `QueueScheduler` that always targets the main thread's GCD
  143. /// queue.
  144. ///
  145. /// - note: Unlike `UIScheduler`, this scheduler supports scheduling for a
  146. /// future date, and will always schedule asynchronously (even if
  147. /// already running on the main thread).
  148. public static let main = QueueScheduler(internalQueue: DispatchQueue.main)
  149. public var currentDate: Date {
  150. return Date()
  151. }
  152. public let queue: DispatchQueue
  153. internal init(internalQueue: DispatchQueue) {
  154. queue = internalQueue
  155. }
  156. /// Initializes a scheduler that will target the given queue with its
  157. /// work.
  158. ///
  159. /// - note: Even if the queue is concurrent, all work items enqueued with
  160. /// the `QueueScheduler` will be serial with respect to each other.
  161. ///
  162. /// - warning: Obsoleted in OS X 10.11
  163. @available(OSX, deprecated:10.10, obsoleted:10.11, message:"Use init(qos:name:targeting:) instead")
  164. @available(iOS, deprecated:8.0, obsoleted:9.0, message:"Use init(qos:name:targeting:) instead.")
  165. public convenience init(queue: DispatchQueue, name: String = "org.reactivecocoa.ReactiveSwift.QueueScheduler") {
  166. self.init(internalQueue: DispatchQueue(label: name, target: queue))
  167. }
  168. /// Initializes a scheduler that creates a new serial queue with the
  169. /// given quality of service class.
  170. ///
  171. /// - parameters:
  172. /// - qos: Dispatch queue's QoS value.
  173. /// - name: Name for the queue in the form of reverse domain.
  174. /// - targeting: (Optional) The queue on which this scheduler's work is
  175. /// targeted
  176. @available(OSX 10.10, *)
  177. public convenience init(
  178. qos: DispatchQoS = .default,
  179. name: String = "org.reactivecocoa.ReactiveSwift.QueueScheduler",
  180. targeting targetQueue: DispatchQueue? = nil
  181. ) {
  182. self.init(internalQueue: DispatchQueue(
  183. label: name,
  184. qos: qos,
  185. target: targetQueue
  186. ))
  187. }
  188. /// Schedules action for dispatch on internal queue
  189. ///
  190. /// - parameters:
  191. /// - action: Closure of the action to schedule.
  192. ///
  193. /// - returns: `Disposable` that can be used to cancel the work before it
  194. /// begins.
  195. @discardableResult
  196. public func schedule(_ action: @escaping () -> Void) -> Disposable? {
  197. let d = SimpleDisposable()
  198. queue.async {
  199. if !d.isDisposed {
  200. action()
  201. }
  202. }
  203. return d
  204. }
  205. private func wallTime(with date: Date) -> DispatchWallTime {
  206. let (seconds, frac) = modf(date.timeIntervalSince1970)
  207. let nsec: Double = frac * Double(NSEC_PER_SEC)
  208. let walltime = timespec(tv_sec: Int(seconds), tv_nsec: Int(nsec))
  209. return DispatchWallTime(timespec: walltime)
  210. }
  211. /// Schedules an action for execution at or after the given date.
  212. ///
  213. /// - parameters:
  214. /// - date: Starting time.
  215. /// - action: Closure of the action to perform.
  216. ///
  217. /// - returns: Optional `Disposable` that can be used to cancel the work
  218. /// before it begins.
  219. @discardableResult
  220. public func schedule(after date: Date, action: @escaping () -> Void) -> Disposable? {
  221. let d = SimpleDisposable()
  222. queue.asyncAfter(wallDeadline: wallTime(with: date)) {
  223. if !d.isDisposed {
  224. action()
  225. }
  226. }
  227. return d
  228. }
  229. /// Schedules a recurring action at the given interval and beginning at the
  230. /// given start time. A reasonable default timer interval leeway is
  231. /// provided.
  232. ///
  233. /// - parameters:
  234. /// - date: Date to schedule the first action for.
  235. /// - repeatingEvery: Repetition interval.
  236. /// - action: Closure of the action to repeat.
  237. ///
  238. /// - note: If you plan to specify an `interval` value greater than 200,000
  239. /// seconds, use `schedule(after:interval:leeway:action)` instead
  240. /// and specify your own `leeway` value to avoid potential overflow.
  241. ///
  242. /// - returns: Optional disposable that can be used to cancel the work
  243. /// before it begins.
  244. @discardableResult
  245. public func schedule(after date: Date, interval: DispatchTimeInterval, action: @escaping () -> Void) -> Disposable? {
  246. // Apple's "Power Efficiency Guide for Mac Apps" recommends a leeway of
  247. // at least 10% of the timer interval.
  248. return schedule(after: date, interval: interval, leeway: interval * 0.1, action: action)
  249. }
  250. /// Schedules a recurring action at the given interval with provided leeway,
  251. /// beginning at the given start time.
  252. ///
  253. /// - parameters:
  254. /// - date: Date to schedule the first action for.
  255. /// - repeatingEvery: Repetition interval.
  256. /// - leeway: Some delta for repetition interval.
  257. /// - action: Closure of the action to repeat.
  258. ///
  259. /// - returns: Optional `Disposable` that can be used to cancel the work
  260. /// before it begins.
  261. @discardableResult
  262. public func schedule(after date: Date, interval: DispatchTimeInterval, leeway: DispatchTimeInterval, action: @escaping () -> Void) -> Disposable? {
  263. precondition(interval.timeInterval >= 0)
  264. precondition(leeway.timeInterval >= 0)
  265. let timer = DispatchSource.makeTimerSource(
  266. flags: DispatchSource.TimerFlags(rawValue: UInt(0)),
  267. queue: queue
  268. )
  269. timer.scheduleRepeating(wallDeadline: wallTime(with: date),
  270. interval: interval,
  271. leeway: leeway)
  272. timer.setEventHandler(handler: action)
  273. timer.resume()
  274. return ActionDisposable {
  275. timer.cancel()
  276. }
  277. }
  278. }
  279. /// A scheduler that implements virtualized time, for use in testing.
  280. public final class TestScheduler: DateSchedulerProtocol {
  281. private final class ScheduledAction {
  282. let date: Date
  283. let action: () -> Void
  284. init(date: Date, action: @escaping () -> Void) {
  285. self.date = date
  286. self.action = action
  287. }
  288. func less(_ rhs: ScheduledAction) -> Bool {
  289. return date.compare(rhs.date) == .orderedAscending
  290. }
  291. }
  292. private let lock = NSRecursiveLock()
  293. private var _currentDate: Date
  294. /// The virtual date that the scheduler is currently at.
  295. public var currentDate: Date {
  296. let d: Date
  297. lock.lock()
  298. d = _currentDate
  299. lock.unlock()
  300. return d
  301. }
  302. private var scheduledActions: [ScheduledAction] = []
  303. /// Initializes a TestScheduler with the given start date.
  304. ///
  305. /// - parameters:
  306. /// - startDate: The start date of the scheduler.
  307. public init(startDate: Date = Date(timeIntervalSinceReferenceDate: 0)) {
  308. lock.name = "org.reactivecocoa.ReactiveSwift.TestScheduler"
  309. _currentDate = startDate
  310. }
  311. private func schedule(_ action: ScheduledAction) -> Disposable {
  312. lock.lock()
  313. scheduledActions.append(action)
  314. scheduledActions.sort { $0.less($1) }
  315. lock.unlock()
  316. return ActionDisposable {
  317. self.lock.lock()
  318. self.scheduledActions = self.scheduledActions.filter { $0 !== action }
  319. self.lock.unlock()
  320. }
  321. }
  322. /// Enqueues an action on the scheduler.
  323. ///
  324. /// - note: The work is executed on `currentDate` as it is understood by the
  325. /// scheduler.
  326. ///
  327. /// - parameters:
  328. /// - action: An action that will be performed on scheduler's
  329. /// `currentDate`.
  330. ///
  331. /// - returns: Optional `Disposable` that can be used to cancel the work
  332. /// before it begins.
  333. @discardableResult
  334. public func schedule(_ action: @escaping () -> Void) -> Disposable? {
  335. return schedule(ScheduledAction(date: currentDate, action: action))
  336. }
  337. /// Schedules an action for execution at or after the given date.
  338. ///
  339. /// - parameters:
  340. /// - date: Starting date.
  341. /// - action: Closure of the action to perform.
  342. ///
  343. /// - returns: Optional disposable that can be used to cancel the work
  344. /// before it begins.
  345. @discardableResult
  346. public func schedule(after delay: DispatchTimeInterval, action: @escaping () -> Void) -> Disposable? {
  347. return schedule(after: currentDate.addingTimeInterval(delay), action: action)
  348. }
  349. @discardableResult
  350. public func schedule(after date: Date, action: @escaping () -> Void) -> Disposable? {
  351. return schedule(ScheduledAction(date: date, action: action))
  352. }
  353. /// Schedules a recurring action at the given interval, beginning at the
  354. /// given start time
  355. ///
  356. /// - parameters:
  357. /// - date: Date to schedule the first action for.
  358. /// - repeatingEvery: Repetition interval.
  359. /// - action: Closure of the action to repeat.
  360. ///
  361. /// - note: If you plan to specify an `interval` value greater than 200,000
  362. /// seconds, use `schedule(after:interval:leeway:action)` instead
  363. /// and specify your own `leeway` value to avoid potential overflow.
  364. ///
  365. /// - returns: Optional `Disposable` that can be used to cancel the work
  366. /// before it begins.
  367. private func schedule(after date: Date, interval: DispatchTimeInterval, disposable: SerialDisposable, action: @escaping () -> Void) {
  368. precondition(interval.timeInterval >= 0)
  369. disposable.inner = schedule(after: date) { [unowned self] in
  370. action()
  371. self.schedule(after: date.addingTimeInterval(interval), interval: interval, disposable: disposable, action: action)
  372. }
  373. }
  374. /// Schedules a recurring action at the given interval, beginning at the
  375. /// given interval (counted from `currentDate`).
  376. ///
  377. /// - parameters:
  378. /// - interval: Interval to add to `currentDate`.
  379. /// - repeatingEvery: Repetition interval.
  380. /// - leeway: Some delta for repetition interval.
  381. /// - action: Closure of the action to repeat.
  382. ///
  383. /// - returns: Optional `Disposable` that can be used to cancel the work
  384. /// before it begins.
  385. @discardableResult
  386. public func schedule(after delay: DispatchTimeInterval, interval: DispatchTimeInterval, leeway: DispatchTimeInterval = .seconds(0), action: @escaping () -> Void) -> Disposable? {
  387. return schedule(after: currentDate.addingTimeInterval(delay), interval: interval, leeway: leeway, action: action)
  388. }
  389. /// Schedules a recurring action at the given interval with
  390. /// provided leeway, beginning at the given start time.
  391. ///
  392. /// - parameters:
  393. /// - date: Date to schedule the first action for.
  394. /// - repeatingEvery: Repetition interval.
  395. /// - leeway: Some delta for repetition interval.
  396. /// - action: Closure of the action to repeat.
  397. ///
  398. /// - returns: Optional `Disposable` that can be used to cancel the work
  399. /// before it begins.
  400. public func schedule(after date: Date, interval: DispatchTimeInterval, leeway: DispatchTimeInterval = .seconds(0), action: @escaping () -> Void) -> Disposable? {
  401. let disposable = SerialDisposable()
  402. schedule(after: date, interval: interval, disposable: disposable, action: action)
  403. return disposable
  404. }
  405. /// Advances the virtualized clock by an extremely tiny interval, dequeuing
  406. /// and executing any actions along the way.
  407. ///
  408. /// This is intended to be used as a way to execute actions that have been
  409. /// scheduled to run as soon as possible.
  410. public func advance() {
  411. advance(by: .nanoseconds(1))
  412. }
  413. /// Advances the virtualized clock by the given interval, dequeuing and
  414. /// executing any actions along the way.
  415. ///
  416. /// - parameters:
  417. /// - interval: Interval by which the current date will be advanced.
  418. public func advance(by interval: DispatchTimeInterval) {
  419. lock.lock()
  420. advance(to: currentDate.addingTimeInterval(interval))
  421. lock.unlock()
  422. }
  423. /// Advances the virtualized clock to the given future date, dequeuing and
  424. /// executing any actions up until that point.
  425. ///
  426. /// - parameters:
  427. /// - newDate: Future date to which the virtual clock will be advanced.
  428. public func advance(to newDate: Date) {
  429. lock.lock()
  430. assert(currentDate.compare(newDate) != .orderedDescending)
  431. while scheduledActions.count > 0 {
  432. if newDate.compare(scheduledActions[0].date) == .orderedAscending {
  433. break
  434. }
  435. _currentDate = scheduledActions[0].date
  436. let scheduledAction = scheduledActions.remove(at: 0)
  437. scheduledAction.action()
  438. }
  439. _currentDate = newDate
  440. lock.unlock()
  441. }
  442. /// Dequeues and executes all scheduled actions, leaving the scheduler's
  443. /// date at `NSDate.distantFuture()`.
  444. public func run() {
  445. advance(to: Date.distantFuture)
  446. }
  447. /// Rewinds the virtualized clock by the given interval.
  448. /// This simulates that user changes device date.
  449. ///
  450. /// - parameters:
  451. /// - interval: Interval by which the current date will be retreated.
  452. public func rewind(by interval: DispatchTimeInterval) {
  453. lock.lock()
  454. let newDate = currentDate.addingTimeInterval(-interval)
  455. assert(currentDate.compare(newDate) != .orderedAscending)
  456. _currentDate = newDate
  457. lock.unlock()
  458. }
  459. }