Scheduler.Services.Emulation.cs 32 KB

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