Scheduler.Services.Emulation.cs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the Apache 2.0 License.
  3. // See the LICENSE file in the project root for more information.
  4. using System.Diagnostics;
  5. using System.Reactive.Disposables;
  6. using System.Reactive.PlatformServices;
  7. using System.Threading;
  8. namespace System.Reactive.Concurrency
  9. {
  10. public static partial class Scheduler
  11. {
  12. /// <summary>
  13. /// Schedules a periodic piece of work by dynamically discovering the scheduler's capabilities.
  14. /// If the scheduler supports periodic scheduling, the request will be forwarded to the periodic scheduling implementation.
  15. /// If the scheduler provides stopwatch functionality, the periodic task will be emulated using recursive scheduling with a stopwatch to correct for time slippage.
  16. /// Otherwise, the periodic task will be emulated using recursive scheduling.
  17. /// </summary>
  18. /// <typeparam name="TState">The type of the state passed to the scheduled action.</typeparam>
  19. /// <param name="scheduler">The scheduler to run periodic work on.</param>
  20. /// <param name="state">Initial state passed to the action upon the first iteration.</param>
  21. /// <param name="period">Period for running the work periodically.</param>
  22. /// <param name="action">Action to be executed, potentially updating the state.</param>
  23. /// <returns>The disposable object used to cancel the scheduled recurring action (best effort).</returns>
  24. /// <exception cref="ArgumentNullException"><paramref name="scheduler"/> or <paramref name="action"/> is <c>null</c>.</exception>
  25. /// <exception cref="ArgumentOutOfRangeException"><paramref name="period"/> is less than <see cref="TimeSpan.Zero"/>.</exception>
  26. public static IDisposable SchedulePeriodic<TState>(this IScheduler scheduler, TState state, TimeSpan period, Func<TState, TState> action)
  27. {
  28. if (scheduler == null)
  29. throw new ArgumentNullException(nameof(scheduler));
  30. if (period < TimeSpan.Zero)
  31. throw new ArgumentOutOfRangeException(nameof(period));
  32. if (action == null)
  33. throw new ArgumentNullException(nameof(action));
  34. return SchedulePeriodic_(scheduler, state, period, action);
  35. }
  36. /// <summary>
  37. /// Schedules a periodic piece of work by dynamically discovering the scheduler's capabilities.
  38. /// If the scheduler supports periodic scheduling, the request will be forwarded to the periodic scheduling implementation.
  39. /// If the scheduler provides stopwatch functionality, the periodic task will be emulated using recursive scheduling with a stopwatch to correct for time slippage.
  40. /// Otherwise, the periodic task will be emulated using recursive scheduling.
  41. /// </summary>
  42. /// <typeparam name="TState">The type of the state passed to the scheduled action.</typeparam>
  43. /// <param name="scheduler">Scheduler to execute the action on.</param>
  44. /// <param name="state">State passed to the action to be executed.</param>
  45. /// <param name="period">Period for running the work periodically.</param>
  46. /// <param name="action">Action to be executed.</param>
  47. /// <returns>The disposable object used to cancel the scheduled recurring action (best effort).</returns>
  48. /// <exception cref="ArgumentNullException"><paramref name="scheduler"/> or <paramref name="action"/> is <c>null</c>.</exception>
  49. /// <exception cref="ArgumentOutOfRangeException"><paramref name="period"/> is less than <see cref="TimeSpan.Zero"/>.</exception>
  50. public static IDisposable SchedulePeriodic<TState>(this IScheduler scheduler, TState state, TimeSpan period, Action<TState> action)
  51. {
  52. if (scheduler == null)
  53. throw new ArgumentNullException(nameof(scheduler));
  54. if (period < TimeSpan.Zero)
  55. throw new ArgumentOutOfRangeException(nameof(period));
  56. if (action == null)
  57. throw new ArgumentNullException(nameof(action));
  58. return SchedulePeriodic_(scheduler, (state, action), period, t => { t.action(t.state); return t; });
  59. }
  60. /// <summary>
  61. /// Schedules a periodic piece of work by dynamically discovering the scheduler's capabilities.
  62. /// If the scheduler supports periodic scheduling, the request will be forwarded to the periodic scheduling implementation.
  63. /// If the scheduler provides stopwatch functionality, the periodic task will be emulated using recursive scheduling with a stopwatch to correct for time slippage.
  64. /// Otherwise, the periodic task will be emulated using recursive scheduling.
  65. /// </summary>
  66. /// <param name="scheduler">Scheduler to execute the action on.</param>
  67. /// <param name="period">Period for running the work periodically.</param>
  68. /// <param name="action">Action to be executed.</param>
  69. /// <returns>The disposable object used to cancel the scheduled recurring action (best effort).</returns>
  70. /// <exception cref="ArgumentNullException"><paramref name="scheduler"/> or <paramref name="action"/> is <c>null</c>.</exception>
  71. /// <exception cref="ArgumentOutOfRangeException"><paramref name="period"/> is less than <see cref="TimeSpan.Zero"/>.</exception>
  72. public static IDisposable SchedulePeriodic(this IScheduler scheduler, TimeSpan period, Action action)
  73. {
  74. if (scheduler == null)
  75. throw new ArgumentNullException(nameof(scheduler));
  76. if (period < TimeSpan.Zero)
  77. throw new ArgumentOutOfRangeException(nameof(period));
  78. if (action == null)
  79. throw new ArgumentNullException(nameof(action));
  80. return SchedulePeriodic_(scheduler, action, period, a => { a(); return a; });
  81. }
  82. /// <summary>
  83. /// Starts a new stopwatch object by dynamically discovering the scheduler's capabilities.
  84. /// If the scheduler provides stopwatch functionality, the request will be forwarded to the stopwatch provider implementation.
  85. /// Otherwise, the stopwatch will be emulated using the scheduler's notion of absolute time.
  86. /// </summary>
  87. /// <param name="scheduler">Scheduler to obtain a stopwatch for.</param>
  88. /// <returns>New stopwatch object; started at the time of the request.</returns>
  89. /// <exception cref="ArgumentNullException"><paramref name="scheduler"/> is <c>null</c>.</exception>
  90. /// <remarks>The resulting stopwatch object can have non-monotonic behavior.</remarks>
  91. public static IStopwatch StartStopwatch(this IScheduler scheduler)
  92. {
  93. if (scheduler == null)
  94. throw new ArgumentNullException(nameof(scheduler));
  95. //
  96. // All schedulers deriving from LocalScheduler will automatically pick up this
  97. // capability based on a local stopwatch, typically using QueryPerformanceCounter
  98. // through the System.Diagnostics.Stopwatch class.
  99. //
  100. // Notice virtual time schedulers do implement this facility starting from Rx v2.0,
  101. // using subtraction of their absolute time notion to compute elapsed time values.
  102. // This is fine because those schedulers do not allow the clock to go back in time.
  103. //
  104. // For schedulers that don't have a stopwatch, we have to pick some fallback logic
  105. // here. We could either dismiss the scheduler's notion of time and go for the CAL's
  106. // stopwatch facility, or go with a stopwatch based on "scheduler.Now", which has
  107. // the drawback of potentially going back in time:
  108. //
  109. // - Using the CAL's stopwatch facility causes us to abandon the scheduler's
  110. // potentially virtualized notion of time, always going for the local system
  111. // time instead.
  112. //
  113. // - Using the scheduler's Now property for calculations can break monotonicity,
  114. // and there's no right answer on how to deal with jumps back in time.
  115. //
  116. // However, even the built-in stopwatch in the BCL can potentially fall back to
  117. // subtraction of DateTime values in case no high-resolution performance counter is
  118. // available, causing monotonicity to break down. We're not trying to solve this
  119. // problem there either (though we could check IsHighResolution and smoothen out
  120. // non-monotonic points somehow), so we pick the latter option as the lesser of
  121. // two evils (also because it should occur rarely).
  122. //
  123. // Users of the stopwatch retrieved by this method could detect nonsensical data
  124. // revealing a jump back in time, or implement custom fallback logic like the one
  125. // shown below.
  126. //
  127. var swp = scheduler.AsStopwatchProvider();
  128. if (swp != null)
  129. {
  130. return swp.StartStopwatch();
  131. }
  132. return new EmulatedStopwatch(scheduler);
  133. }
  134. private static IDisposable SchedulePeriodic_<TState>(IScheduler scheduler, TState state, TimeSpan period, Func<TState, TState> action)
  135. {
  136. //
  137. // Design rationale:
  138. //
  139. // In Rx v1.x, we employed recursive scheduling for periodic tasks. The following code
  140. // fragment shows how the Timer (and hence Interval) function used to be implemented:
  141. //
  142. // var p = Normalize(period);
  143. //
  144. // return new AnonymousObservable<long>(observer =>
  145. // {
  146. // var d = dueTime;
  147. // long count = 0;
  148. // return scheduler.Schedule(d, self =>
  149. // {
  150. // if (p > TimeSpan.Zero)
  151. // {
  152. // var now = scheduler.Now;
  153. // d = d + p;
  154. // if (d <= now)
  155. // d = now + p;
  156. // }
  157. //
  158. // observer.OnNext(count);
  159. // count = unchecked(count + 1);
  160. // self(d);
  161. // });
  162. // });
  163. //
  164. // Despite the purity of this approach, it suffered from a set of drawbacks:
  165. //
  166. // 1) Usage of IScheduler.Now to correct for time drift did have a positive effect for
  167. // a limited number of scenarios, in particular when a short period was used. The
  168. // major issues with this are:
  169. //
  170. // a) Relying on absolute time at the LINQ layer in Rx's layer map, causing issues
  171. // when the system clock changes. Various customers hit this issue, reported to
  172. // us on the MSDN forums. Basically, when the clock goes forward, the recursive
  173. // loop wants to catch up as quickly as it can; when it goes backwards, a long
  174. // silence will occur. (See 2 for a discussion of WP7 related fixes.)
  175. //
  176. // b) Even if a) would be addressed by using Rx v2.0's capabilities to monitor for
  177. // system clock changes, the solution would violate the reasonable expectation
  178. // of operators overloads using TimeSpan *not* relying on absolute time.
  179. //
  180. // c) Drift correction doesn't work for large periods when the system encounters
  181. // systematic drift. For example, in the lab we've seen cases of drift up to
  182. // tens of seconds on a 24 hour timeframe. Correcting for this drift by making
  183. // a recursive call with a due time of 24 * 3600 with 10 seconds of adjustment
  184. // won't fix systematic drift.
  185. //
  186. // 2) This implementation has been plagued with issues around application container
  187. // lifecycle models, in particular Windows Phone 7's model of tombstoning and in
  188. // particular its "dormant state". This feature was introduced in Mango to enable
  189. // fast application switching. Essentially, the phone's OS puts the application
  190. // in a suspended state when the user navigates "forward" (or takes an incoming
  191. // call for instance). When the application is woken up again, threads are resumed
  192. // and we're faced with an illusion of missed events due to the use of absolute
  193. // time, not relative to how the application observes it. This caused nightmare
  194. // scenarios of fast battery drain due to the flood of catch-up work.
  195. //
  196. // See http://msdn.microsoft.com/en-us/library/ff817008(v=vs.92).aspx for more
  197. // information on this.
  198. //
  199. // 3) Recursive scheduling imposes a non-trivial cost due to the creation of many
  200. // single-shot timers and closures. For high frequency timers, this can cause a
  201. // lot of churn in the GC, which we like to avoid (operators shouldn't have hidden
  202. // linear - or worse - allocation cost).
  203. //
  204. // Notice these drawbacks weren't limited to the use of Timer and Interval directly,
  205. // as many operators such as Sample, Buffer, and Window used such sequences for their
  206. // periodic behavior (typically by delegating to a more general overload).
  207. //
  208. // As a result, in Rx v2.0, we took the decision to improve periodic timing based on
  209. // the following design decisions:
  210. //
  211. // 1) When the scheduler has the ability to run a periodic task, it should implement
  212. // the ISchedulerPeriodic interface and expose it through the IServiceProvider
  213. // interface. Passing the intent of the user through all layers of Rx, down to the
  214. // underlying infrastructure provides delegation of responsibilities. This allows
  215. // the target scheduler to optimize execution in various ways, e.g. by employing
  216. // techniques such as timer coalescing.
  217. //
  218. // See http://www.bing.com/search?q=windows+timer+coalescing for information on
  219. // techniques like timer coalescing which may be applied more aggressively in
  220. // future OS releases in order to reduce power consumption.
  221. //
  222. // 2) Emulation of periodic scheduling is used to avoid breaking existing code that
  223. // uses schedulers without this capability. We expect those fallback paths to be
  224. // exercised rarely, though the use of DisableOptimizations can trigger them as
  225. // well. In such cases we rely on stopwatches or a carefully crafted recursive
  226. // scheme to deal with (or maximally compensate for) slippage or time. Behavior
  227. // of periodic tasks is expected to be as follows:
  228. //
  229. // timer ticks 0-------1-------2-------3-------4-------5-------6----...
  230. // | | | +====+ +==+ | |
  231. // user code +~~~| +~| +~~~~~~~~~~~|+~~~~|+~~| +~~~| +~~|
  232. //
  233. // rather than the following scheme, where time slippage is introduced by user
  234. // code running on the scheduler:
  235. //
  236. // timer ticks 0####-------1##-------2############-------3#####-----...
  237. // | | | |
  238. // user code +~~~| +~| +~~~~~~~~~~~| +~~~~|
  239. //
  240. // (Side-note: Unfortunately, we didn't reserve the name Interval for the latter
  241. // behavior, but used it as an alias for "periodic scheduling" with
  242. // the former behavior, delegating to the Timer implementation. One
  243. // can simulate this behavior using Generate, which uses tail calls.)
  244. //
  245. // This behavior is important for operations like Sample, Buffer, and Window, all
  246. // of which expect proper spacing of events, even if the user code takes a long
  247. // time to complete (considered a bad practice nonetheless, cf. ObserveOn).
  248. //
  249. // 3) To deal with the issue of suspensions induced by application lifecycle events
  250. // in Windows Phone and WinRT applications, we decided to hook available system
  251. // events through IHostLifecycleNotifications, discovered through the PEP in order
  252. // to maintain portability of the core of Rx.
  253. //
  254. var periodic = scheduler.AsPeriodic();
  255. #if WINDOWS
  256. // Workaround for WinRT not supporting <1ms resolution
  257. if(period < TimeSpan.FromMilliseconds(1))
  258. {
  259. periodic = null; // skip the periodic scheduler and use the stopwatch
  260. }
  261. #endif
  262. if (periodic != null)
  263. {
  264. return periodic.SchedulePeriodic(state, period, action);
  265. }
  266. var swp = scheduler.AsStopwatchProvider();
  267. if (swp != null)
  268. {
  269. var spr = new SchedulePeriodicStopwatch<TState>(scheduler, state, period, action, swp);
  270. return spr.Start();
  271. }
  272. else
  273. {
  274. var spr = new SchedulePeriodicRecursive<TState>(scheduler, state, period, action);
  275. return spr.Start();
  276. }
  277. }
  278. private sealed class SchedulePeriodicStopwatch<TState>
  279. {
  280. private readonly IScheduler _scheduler;
  281. private readonly TimeSpan _period;
  282. private readonly Func<TState, TState> _action;
  283. private readonly IStopwatchProvider _stopwatchProvider;
  284. public SchedulePeriodicStopwatch(IScheduler scheduler, TState state, TimeSpan period, Func<TState, TState> action, IStopwatchProvider stopwatchProvider)
  285. {
  286. _scheduler = scheduler;
  287. _period = period;
  288. _action = action;
  289. _stopwatchProvider = stopwatchProvider;
  290. _state = state;
  291. _runState = STOPPED;
  292. }
  293. private TState _state;
  294. private readonly object _gate = new object();
  295. private readonly AutoResetEvent _resumeEvent = new AutoResetEvent(false);
  296. private volatile int _runState;
  297. private IStopwatch _stopwatch;
  298. private TimeSpan _nextDue;
  299. private TimeSpan _suspendedAt;
  300. private TimeSpan _inactiveTime;
  301. //
  302. // State transition diagram:
  303. // (c)
  304. // +-----------<-----------+
  305. // / \
  306. // / (b) \
  307. // | +-->--SUSPENDED---+
  308. // (a) v / |
  309. // ^----STOPPED -->-- RUNNING -->--+ v (e)
  310. // \ |
  311. // +-->--DISPOSED----$
  312. // (d)
  313. //
  314. // (a) Start --> call to Schedule the Tick method
  315. // (b) Suspending event handler --> Tick gets blocked waiting for _resumeEvent
  316. // (c) Resuming event handler --> _resumeEvent is signaled, Tick continues
  317. // (d) Dispose returned object from Start --> scheduled work is cancelled
  318. // (e) Dispose returned object from Start --> unblocks _resumeEvent, Tick exits
  319. //
  320. private const int STOPPED = 0;
  321. private const int RUNNING = 1;
  322. private const int SUSPENDED = 2;
  323. private const int DISPOSED = 3;
  324. public IDisposable Start()
  325. {
  326. RegisterHostLifecycleEventHandlers();
  327. _stopwatch = _stopwatchProvider.StartStopwatch();
  328. _nextDue = _period;
  329. _runState = RUNNING;
  330. return StableCompositeDisposable.Create
  331. (
  332. _scheduler.Schedule(_nextDue, Tick),
  333. Disposable.Create(Cancel)
  334. );
  335. }
  336. private void Tick(Action<TimeSpan> recurse)
  337. {
  338. _nextDue += _period;
  339. _state = _action(_state);
  340. var next = default(TimeSpan);
  341. while (true)
  342. {
  343. var shouldWaitForResume = false;
  344. lock (_gate)
  345. {
  346. if (_runState == RUNNING)
  347. {
  348. //
  349. // This is the fast path. We just let the stopwatch continue to
  350. // run while we're suspended, but compensate for time that was
  351. // recorded as inactive based on cumulative deltas computed in
  352. // the suspend and resume event handlers.
  353. //
  354. next = Normalize(_nextDue - (_stopwatch.Elapsed - _inactiveTime));
  355. break;
  356. }
  357. else if (_runState == DISPOSED)
  358. {
  359. //
  360. // In case the periodic job gets disposed but we are currently
  361. // waiting to come back out of suspension, we should make sure
  362. // we don't remain blocked indefinitely. Hence, we set the event
  363. // in the Cancel method and trap this case here to bail out from
  364. // the scheduled work gracefully.
  365. //
  366. return;
  367. }
  368. else
  369. {
  370. //
  371. // This is the least common case where we got suspended and need
  372. // to block such that future reevaluations of the next due time
  373. // will pick up the cumulative inactive time delta.
  374. //
  375. Debug.Assert(_runState == SUSPENDED);
  376. shouldWaitForResume = true;
  377. }
  378. }
  379. //
  380. // Only happens in the SUSPENDED case; otherwise we will have broken from
  381. // the loop or have quit the Tick method. After returning from the wait,
  382. // we'll either be RUNNING again, quit due to a DISPOSED transition, or
  383. // be extremely unlucky to find ourselves SUSPENDED again and be blocked
  384. // once more.
  385. //
  386. if (shouldWaitForResume)
  387. {
  388. _resumeEvent.WaitOne();
  389. }
  390. }
  391. recurse(next);
  392. }
  393. private void Cancel()
  394. {
  395. UnregisterHostLifecycleEventHandlers();
  396. lock (_gate)
  397. {
  398. _runState = DISPOSED;
  399. if (!Environment.HasShutdownStarted)
  400. {
  401. _resumeEvent.Set();
  402. }
  403. }
  404. }
  405. private void Suspending(object sender, HostSuspendingEventArgs args)
  406. {
  407. //
  408. // The host is telling us we're about to be suspended. At this point, time
  409. // computations will still be in a valid range (next <= _period), but after
  410. // we're woken up again, Tick would start to go on a crusade to catch up.
  411. //
  412. // This has caused problems in the past, where the flood of events caused
  413. // batteries to drain etc (see design rationale discussion higher up).
  414. //
  415. // In order to mitigate this problem, we force Tick to suspend before its
  416. // next computation of the next due time. Notice we can't afford to block
  417. // during the Suspending event handler; the host expects us to respond to
  418. // this event quickly, such that we're not keeping the application from
  419. // suspending promptly.
  420. //
  421. lock (_gate)
  422. {
  423. if (_runState == RUNNING)
  424. {
  425. _suspendedAt = _stopwatch.Elapsed;
  426. _runState = SUSPENDED;
  427. if (!Environment.HasShutdownStarted)
  428. {
  429. _resumeEvent.Reset();
  430. }
  431. }
  432. }
  433. }
  434. private void Resuming(object sender, HostResumingEventArgs args)
  435. {
  436. //
  437. // The host is telling us we're being resumed. At this point, code will
  438. // already be running in the process, so a past timer may still expire and
  439. // cause the code in Tick to run. Two interleavings are possible now:
  440. //
  441. // 1) We enter the gate first, and will adjust the cumulative inactive
  442. // time delta used for correction. The code in Tick will have the
  443. // illusion nothing happened and find itself RUNNING when entering
  444. // the gate, resuming activities as before.
  445. //
  446. // 2) The code in Tick enters the gate first, and takes notice of the
  447. // currently SUSPENDED state. It leaves the gate, entering the wait
  448. // state for _resumeEvent. Next, we enter to adjust the cumulative
  449. // inactive time delta, switch to the RUNNING state and signal the
  450. // event for Tick to carry on and recompute its next due time based
  451. // on the new cumulative delta.
  452. //
  453. lock (_gate)
  454. {
  455. if (_runState == SUSPENDED)
  456. {
  457. _inactiveTime += _stopwatch.Elapsed - _suspendedAt;
  458. _runState = RUNNING;
  459. if (!Environment.HasShutdownStarted)
  460. {
  461. _resumeEvent.Set();
  462. }
  463. }
  464. }
  465. }
  466. private void RegisterHostLifecycleEventHandlers()
  467. {
  468. HostLifecycleService.Suspending += Suspending;
  469. HostLifecycleService.Resuming += Resuming;
  470. HostLifecycleService.AddRef();
  471. }
  472. private void UnregisterHostLifecycleEventHandlers()
  473. {
  474. HostLifecycleService.Suspending -= Suspending;
  475. HostLifecycleService.Resuming -= Resuming;
  476. HostLifecycleService.Release();
  477. }
  478. }
  479. private sealed class SchedulePeriodicRecursive<TState>
  480. {
  481. private readonly IScheduler _scheduler;
  482. private readonly TimeSpan _period;
  483. private readonly Func<TState, TState> _action;
  484. public SchedulePeriodicRecursive(IScheduler scheduler, TState state, TimeSpan period, Func<TState, TState> action)
  485. {
  486. _scheduler = scheduler;
  487. _period = period;
  488. _action = action;
  489. _state = state;
  490. }
  491. private TState _state;
  492. private int _pendingTickCount;
  493. private IDisposable _cancel;
  494. public IDisposable Start()
  495. {
  496. _pendingTickCount = 0;
  497. var d = new SingleAssignmentDisposable();
  498. _cancel = d;
  499. d.Disposable = _scheduler.Schedule(TICK, _period, Tick);
  500. return d;
  501. }
  502. //
  503. // The protocol using the three commands is explained in the Tick implementation below.
  504. //
  505. private const int TICK = 0;
  506. private const int DISPATCH_START = 1;
  507. private const int DISPATCH_END = 2;
  508. private void Tick(int command, Action<int, TimeSpan> recurse)
  509. {
  510. switch (command)
  511. {
  512. case TICK:
  513. //
  514. // Ticks keep going at the specified periodic rate. We do a head call such
  515. // that no slippage is introduced because of DISPATCH_START work involving
  516. // user code that may take arbitrarily long.
  517. //
  518. recurse(TICK, _period);
  519. //
  520. // If we're not transitioning from 0 to 1 pending tick, another processing
  521. // request is in flight which will see a non-zero pending tick count after
  522. // doing the final decrement, causing it to reschedule immediately. We can
  523. // safely bail out, delegating work to the catch-up tail calls.
  524. //
  525. if (Interlocked.Increment(ref _pendingTickCount) == 1)
  526. goto case DISPATCH_START;
  527. break;
  528. case DISPATCH_START:
  529. try
  530. {
  531. _state = _action(_state);
  532. }
  533. catch (Exception e)
  534. {
  535. _cancel.Dispose();
  536. e.Throw();
  537. }
  538. //
  539. // This is very subtle. We can't do a goto case DISPATCH_END here because it
  540. // wouldn't introduce interleaving of periodic ticks that are due. In order
  541. // to have best effort behavior for schedulers that don't have concurrency,
  542. // we yield by doing a recursive call here. Notice this doesn't heal all of
  543. // the problem, because the TICK commands that may be dispatched before the
  544. // scheduled DISPATCH_END will do a "recurse(TICK, period)", which is relative
  545. // from the point of entrance. Really all we're doing here is damage control
  546. // for the case there's no stopwatch provider which should be rare (notice
  547. // the LocalScheduler base class always imposes a stopwatch, but it can get
  548. // disabled using DisableOptimizations; legacy implementations of schedulers
  549. // from the v1.x days will not have a stopwatch).
  550. //
  551. recurse(DISPATCH_END, TimeSpan.Zero);
  552. break;
  553. case DISPATCH_END:
  554. //
  555. // If work was due while we were still running user code, the count will have
  556. // been incremented by the periodic tick handler above. In that case, we will
  557. // reschedule ourselves for dispatching work immediately.
  558. //
  559. // Notice we don't run a loop here, in order to allow interleaving of work on
  560. // the scheduler by making recursive calls. In case we would use AsyncLock to
  561. // ensure serialized execution the owner could get stuck in such a loop, thus
  562. // we make tail calls to play nice with the scheduler.
  563. //
  564. if (Interlocked.Decrement(ref _pendingTickCount) > 0)
  565. {
  566. recurse(DISPATCH_START, TimeSpan.Zero);
  567. }
  568. break;
  569. }
  570. }
  571. }
  572. private sealed class EmulatedStopwatch : IStopwatch
  573. {
  574. private readonly IScheduler _scheduler;
  575. private readonly DateTimeOffset _start;
  576. public EmulatedStopwatch(IScheduler scheduler)
  577. {
  578. _scheduler = scheduler;
  579. _start = _scheduler.Now;
  580. }
  581. public TimeSpan Elapsed => Normalize(_scheduler.Now - _start);
  582. }
  583. }
  584. }