Scheduler.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. //
  2. // Scheduler.swift
  3. // ReactiveCocoa
  4. //
  5. // Created by Justin Spahr-Summers on 2014-06-02.
  6. // Copyright (c) 2014 GitHub. All rights reserved.
  7. //
  8. import Foundation
  9. /// Represents a serial queue of work items.
  10. public protocol SchedulerType {
  11. /// Enqueues an action on the scheduler.
  12. ///
  13. /// When the work is executed depends on the scheduler in use.
  14. ///
  15. /// - returns: Optional `Disposable` that can be used to cancel the work
  16. /// before it begins.
  17. func schedule(action: () -> Void) -> Disposable?
  18. }
  19. /// A particular kind of scheduler that supports enqueuing actions at future
  20. /// dates.
  21. public protocol DateSchedulerType: SchedulerType {
  22. /// The current date, as determined by this scheduler.
  23. ///
  24. /// This can be implemented to deterministically return a known date (e.g.,
  25. /// for testing purposes).
  26. var currentDate: NSDate { get }
  27. /// Schedules an action for execution at or after the given date.
  28. ///
  29. /// - parameters:
  30. /// - date: Starting time.
  31. /// - action: Closure of the action to perform.
  32. ///
  33. /// - returns: Optional `Disposable` that can be used to cancel the work
  34. /// before it begins.
  35. func scheduleAfter(date: NSDate, action: () -> Void) -> Disposable?
  36. /// Schedules a recurring action at the given interval, beginning at the
  37. /// given date.
  38. ///
  39. /// - parameters:
  40. /// - date: Starting time.
  41. /// - repeatingEvery: Repetition interval.
  42. /// - withLeeway: Some delta for repetition.
  43. /// - action: Closure of the action to perform.
  44. ///
  45. /// - returns: Optional `Disposable` that can be used to cancel the work
  46. /// before it begins.
  47. func scheduleAfter(date: NSDate, repeatingEvery: NSTimeInterval, withLeeway: NSTimeInterval, action: () -> Void) -> Disposable?
  48. }
  49. /// A scheduler that performs all work synchronously.
  50. public final class ImmediateScheduler: SchedulerType {
  51. public init() {}
  52. /// Immediately calls passed in `action`.
  53. ///
  54. /// - parameters:
  55. /// - action: Closure of the action to perform.
  56. ///
  57. /// - returns: `nil`.
  58. public func schedule(action: () -> Void) -> Disposable? {
  59. action()
  60. return nil
  61. }
  62. }
  63. /// A scheduler that performs all work on the main queue, as soon as possible.
  64. ///
  65. /// If the caller is already running on the main queue when an action is
  66. /// scheduled, it may be run synchronously. However, ordering between actions
  67. /// will always be preserved.
  68. public final class UIScheduler: SchedulerType {
  69. private static var dispatchOnceToken: dispatch_once_t = 0
  70. private static var dispatchSpecificKey: UInt8 = 0
  71. private static var dispatchSpecificContext: UInt8 = 0
  72. private var queueLength: Int32 = 0
  73. /// Initializes `UIScheduler`
  74. public init() {
  75. dispatch_once(&UIScheduler.dispatchOnceToken) {
  76. dispatch_queue_set_specific(
  77. dispatch_get_main_queue(),
  78. &UIScheduler.dispatchSpecificKey,
  79. &UIScheduler.dispatchSpecificContext,
  80. nil
  81. )
  82. }
  83. }
  84. /// Queues an action to be performed on main queue. If the action is called
  85. /// on the main thread and no work is queued, no scheduling takes place and
  86. /// the action is called instantly.
  87. ///
  88. /// - parameters:
  89. /// - action: Closure of the action to perform on the main thread.
  90. ///
  91. /// - returns: `Disposable` that can be used to cancel the work before it
  92. /// begins.
  93. public func schedule(action: () -> Void) -> Disposable? {
  94. let disposable = SimpleDisposable()
  95. let actionAndDecrement = {
  96. if !disposable.disposed {
  97. action()
  98. }
  99. OSAtomicDecrement32(&self.queueLength)
  100. }
  101. let queued = OSAtomicIncrement32(&queueLength)
  102. // If we're already running on the main queue, and there isn't work
  103. // already enqueued, we can skip scheduling and just execute directly.
  104. if queued == 1 && dispatch_get_specific(&UIScheduler.dispatchSpecificKey) == &UIScheduler.dispatchSpecificContext {
  105. actionAndDecrement()
  106. } else {
  107. dispatch_async(dispatch_get_main_queue(), actionAndDecrement)
  108. }
  109. return disposable
  110. }
  111. }
  112. /// A scheduler backed by a serial GCD queue.
  113. public final class QueueScheduler: DateSchedulerType {
  114. internal let queue: dispatch_queue_t
  115. internal init(internalQueue: dispatch_queue_t) {
  116. queue = internalQueue
  117. }
  118. /// Initializes a scheduler that will target the given queue with its
  119. /// work.
  120. ///
  121. /// - note: Even if the queue is concurrent, all work items enqueued with
  122. /// the `QueueScheduler` will be serial with respect to each other.
  123. ///
  124. /// - warning: Obsoleted in OS X 10.11.
  125. @available(OSX, deprecated=10.10, obsoleted=10.11, message="Use init(qos:, name:) instead")
  126. public convenience init(queue: dispatch_queue_t, name: String = "org.reactivecocoa.ReactiveCocoa.QueueScheduler") {
  127. self.init(internalQueue: dispatch_queue_create(name, DISPATCH_QUEUE_SERIAL))
  128. dispatch_set_target_queue(self.queue, queue)
  129. }
  130. /// A singleton `QueueScheduler` that always targets the main thread's GCD
  131. /// queue.
  132. ///
  133. /// - note: Unlike `UIScheduler`, this scheduler supports scheduling for a
  134. /// future date, and will always schedule asynchronously (even if
  135. /// already running on the main thread).
  136. public static let mainQueueScheduler = QueueScheduler(internalQueue: dispatch_get_main_queue())
  137. public var currentDate: NSDate {
  138. return NSDate()
  139. }
  140. /// Initializes a scheduler that will target a new serial queue with the
  141. /// given quality of service class.
  142. ///
  143. /// - parameters:
  144. /// - qos: Dispatch queue's QoS value.
  145. /// - name: Name for the queue in the form of reverse domain.
  146. @available(iOS 8, watchOS 2, OSX 10.10, *)
  147. public convenience init(qos: dispatch_qos_class_t = QOS_CLASS_DEFAULT, name: String = "org.reactivecocoa.ReactiveCocoa.QueueScheduler") {
  148. self.init(internalQueue: dispatch_queue_create(name, dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, qos, 0)))
  149. }
  150. /// Schedules action for dispatch on internal queue
  151. ///
  152. /// - parameters:
  153. /// - action: Closure of the action to schedule.
  154. ///
  155. /// - returns: `Disposable` that can be used to cancel the work before it
  156. /// begins.
  157. public func schedule(action: () -> Void) -> Disposable? {
  158. let d = SimpleDisposable()
  159. dispatch_async(queue) {
  160. if !d.disposed {
  161. action()
  162. }
  163. }
  164. return d
  165. }
  166. private func wallTimeWithDate(date: NSDate) -> dispatch_time_t {
  167. let (seconds, frac) = modf(date.timeIntervalSince1970)
  168. let nsec: Double = frac * Double(NSEC_PER_SEC)
  169. var walltime = timespec(tv_sec: Int(seconds), tv_nsec: Int(nsec))
  170. return dispatch_walltime(&walltime, 0)
  171. }
  172. /// Schedules an action for execution at or after the given date.
  173. ///
  174. /// - parameters:
  175. /// - date: Starting time.
  176. /// - action: Closure of the action to perform.
  177. ///
  178. /// - returns: Optional `Disposable` that can be used to cancel the work
  179. /// before it begins.
  180. public func scheduleAfter(date: NSDate, action: () -> Void) -> Disposable? {
  181. let d = SimpleDisposable()
  182. dispatch_after(wallTimeWithDate(date), queue) {
  183. if !d.disposed {
  184. action()
  185. }
  186. }
  187. return d
  188. }
  189. /// Schedules a recurring action at the given interval and beginning at the
  190. /// given start time. A reasonable default timer interval leeway is
  191. /// provided.
  192. ///
  193. /// - parameters:
  194. /// - date: Date to schedule the first action for.
  195. /// - repeatingEvery: Repetition interval.
  196. /// - action: Closure of the action to repeat.
  197. ///
  198. /// - returns: Optional disposable that can be used to cancel the work
  199. /// before it begins.
  200. public func scheduleAfter(date: NSDate, repeatingEvery: NSTimeInterval, action: () -> Void) -> Disposable? {
  201. // Apple's "Power Efficiency Guide for Mac Apps" recommends a leeway of
  202. // at least 10% of the timer interval.
  203. return scheduleAfter(date, repeatingEvery: repeatingEvery, withLeeway: repeatingEvery * 0.1, action: action)
  204. }
  205. /// Schedules a recurring action at the given interval with provided leeway,
  206. /// beginning at the given start time.
  207. ///
  208. /// - parameters:
  209. /// - date: Date to schedule the first action for.
  210. /// - repeatingEvery: Repetition interval.
  211. /// - leeway: Some delta for repetition interval.
  212. /// - action: Closure of the action to repeat.
  213. ///
  214. /// - returns: Optional `Disposable` that can be used to cancel the work
  215. /// before it begins.
  216. public func scheduleAfter(date: NSDate, repeatingEvery: NSTimeInterval, withLeeway leeway: NSTimeInterval, action: () -> Void) -> Disposable? {
  217. precondition(repeatingEvery >= 0)
  218. precondition(leeway >= 0)
  219. let nsecInterval = repeatingEvery * Double(NSEC_PER_SEC)
  220. let nsecLeeway = leeway * Double(NSEC_PER_SEC)
  221. let timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue)
  222. dispatch_source_set_timer(timer, wallTimeWithDate(date), UInt64(nsecInterval), UInt64(nsecLeeway))
  223. dispatch_source_set_event_handler(timer, action)
  224. dispatch_resume(timer)
  225. return ActionDisposable {
  226. dispatch_source_cancel(timer)
  227. }
  228. }
  229. }
  230. /// A scheduler that implements virtualized time, for use in testing.
  231. public final class TestScheduler: DateSchedulerType {
  232. private final class ScheduledAction {
  233. let date: NSDate
  234. let action: () -> Void
  235. init(date: NSDate, action: () -> Void) {
  236. self.date = date
  237. self.action = action
  238. }
  239. func less(rhs: ScheduledAction) -> Bool {
  240. return date.compare(rhs.date) == .OrderedAscending
  241. }
  242. }
  243. private let lock = NSRecursiveLock()
  244. private var _currentDate: NSDate
  245. /// The virtual date that the scheduler is currently at.
  246. public var currentDate: NSDate {
  247. let d: NSDate
  248. lock.lock()
  249. d = _currentDate
  250. lock.unlock()
  251. return d
  252. }
  253. private var scheduledActions: [ScheduledAction] = []
  254. /// Initializes a TestScheduler with the given start date.
  255. ///
  256. /// - parameters:
  257. /// - startDate: The start date of the scheduler.
  258. public init(startDate: NSDate = NSDate(timeIntervalSinceReferenceDate: 0)) {
  259. lock.name = "org.reactivecocoa.ReactiveCocoa.TestScheduler"
  260. _currentDate = startDate
  261. }
  262. private func schedule(action: ScheduledAction) -> Disposable {
  263. lock.lock()
  264. scheduledActions.append(action)
  265. scheduledActions.sortInPlace { $0.less($1) }
  266. lock.unlock()
  267. return ActionDisposable {
  268. self.lock.lock()
  269. self.scheduledActions = self.scheduledActions.filter { $0 !== action }
  270. self.lock.unlock()
  271. }
  272. }
  273. /// Enqueues an action on the scheduler.
  274. ///
  275. /// - note: The work is executed on `currentDate` as it is understood by the
  276. /// scheduler.
  277. ///
  278. /// - parameters:
  279. /// - action: An action that will be performed on scheduler's
  280. /// `currentDate`.
  281. ///
  282. /// - returns: Optional `Disposable` that can be used to cancel the work
  283. /// before it begins.
  284. public func schedule(action: () -> Void) -> Disposable? {
  285. return schedule(ScheduledAction(date: currentDate, action: action))
  286. }
  287. /// Schedules an action for execution at or after the given date.
  288. ///
  289. /// - parameters:
  290. /// - date: Starting date.
  291. /// - action: Closure of the action to perform.
  292. ///
  293. /// - returns: Optional disposable that can be used to cancel the work
  294. /// before it begins.
  295. public func scheduleAfter(interval: NSTimeInterval, action: () -> Void) -> Disposable? {
  296. return scheduleAfter(currentDate.dateByAddingTimeInterval(interval), action: action)
  297. }
  298. public func scheduleAfter(date: NSDate, action: () -> Void) -> Disposable? {
  299. return schedule(ScheduledAction(date: date, action: action))
  300. }
  301. /// Schedules a recurring action at the given interval, beginning at the
  302. /// given start time
  303. ///
  304. /// - parameters:
  305. /// - date: Date to schedule the first action for.
  306. /// - repeatingEvery: Repetition interval.
  307. /// - action: Closure of the action to repeat.
  308. ///
  309. /// - returns: Optional `Disposable` that can be used to cancel the work
  310. /// before it begins.
  311. private func scheduleAfter(date: NSDate, repeatingEvery: NSTimeInterval, disposable: SerialDisposable, action: () -> Void) {
  312. precondition(repeatingEvery >= 0)
  313. disposable.innerDisposable = scheduleAfter(date) { [unowned self] in
  314. action()
  315. self.scheduleAfter(date.dateByAddingTimeInterval(repeatingEvery), repeatingEvery: repeatingEvery, disposable: disposable, action: action)
  316. }
  317. }
  318. /// Schedules a recurring action at the given interval, beginning at the
  319. /// given interval (counted from `currentDate`).
  320. ///
  321. /// - parameters:
  322. /// - interval: Interval to add to `currentDate`.
  323. /// - repeatingEvery: Repetition interval.
  324. /// - leeway: Some delta for repetition interval.
  325. /// - action: Closure of the action to repeat.
  326. ///
  327. /// - returns: Optional `Disposable` that can be used to cancel the work
  328. /// before it begins.
  329. public func scheduleAfter(interval: NSTimeInterval, repeatingEvery: NSTimeInterval, withLeeway leeway: NSTimeInterval = 0, action: () -> Void) -> Disposable? {
  330. return scheduleAfter(currentDate.dateByAddingTimeInterval(interval), repeatingEvery: repeatingEvery, withLeeway: leeway, action: action)
  331. }
  332. /// Schedules a recurring action at the given interval with
  333. /// provided leeway, beginning at the given start time.
  334. ///
  335. /// - parameters:
  336. /// - date: Date to schedule the first action for.
  337. /// - repeatingEvery: Repetition interval.
  338. /// - leeway: Some delta for repetition interval.
  339. /// - action: Closure of the action to repeat.
  340. ///
  341. /// - returns: Optional `Disposable` that can be used to cancel the work
  342. /// before it begins.
  343. public func scheduleAfter(date: NSDate, repeatingEvery: NSTimeInterval, withLeeway: NSTimeInterval = 0, action: () -> Void) -> Disposable? {
  344. let disposable = SerialDisposable()
  345. scheduleAfter(date, repeatingEvery: repeatingEvery, disposable: disposable, action: action)
  346. return disposable
  347. }
  348. /// Advances the virtualized clock by an extremely tiny interval, dequeuing
  349. /// and executing any actions along the way.
  350. ///
  351. /// This is intended to be used as a way to execute actions that have been
  352. /// scheduled to run as soon as possible.
  353. public func advance() {
  354. advanceByInterval(DBL_EPSILON)
  355. }
  356. /// Advances the virtualized clock by the given interval, dequeuing and
  357. /// executing any actions along the way.
  358. ///
  359. /// - parameters:
  360. /// - interval: Interval by which the current date will be advanced.
  361. public func advanceByInterval(interval: NSTimeInterval) {
  362. lock.lock()
  363. advanceToDate(currentDate.dateByAddingTimeInterval(interval))
  364. lock.unlock()
  365. }
  366. /// Advances the virtualized clock to the given future date, dequeuing and
  367. /// executing any actions up until that point.
  368. ///
  369. /// - parameters:
  370. /// - newDate: Future date to which the virtual clock will be advanced.
  371. public func advanceToDate(newDate: NSDate) {
  372. lock.lock()
  373. assert(currentDate.compare(newDate) != .OrderedDescending)
  374. while scheduledActions.count > 0 {
  375. if newDate.compare(scheduledActions[0].date) == .OrderedAscending {
  376. break
  377. }
  378. _currentDate = scheduledActions[0].date
  379. let scheduledAction = scheduledActions.removeAtIndex(0)
  380. scheduledAction.action()
  381. }
  382. _currentDate = newDate
  383. lock.unlock()
  384. }
  385. /// Dequeues and executes all scheduled actions, leaving the scheduler's
  386. /// date at `NSDate.distantFuture()`.
  387. public func run() {
  388. advanceToDate(NSDate.distantFuture())
  389. }
  390. }