Scheduler.Services.Emulation.cs 32 KB

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