-
Notifications
You must be signed in to change notification settings - Fork 213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
elapse
inside periodic timer immediately calls the same timer
#2319
Comments
Fixes #88. Currently when a periodic timer's callback gets invoked, its `_nextCall` is still the time of the current call, not the next one. If the timer callback itself calls `flushTimers` or `elapse`, this causes the same timer to immediately get called again. Fortunately the fix is easy: update `_nextCall` just before invoking `_callback`, instead of just after. --- To work through why this is a complete fix (and doesn't leave further bugs of this kind still to be fixed): After this fix, the call to the timer's callback is a tail call from `FakeTimer._fire`. Because the call site of `FakeTimer._fire` is immediately followed by `flushMicrotasks()`, this means calling other `FakeAsync` methods from the timer callback is no different from doing so in a subsequent microtask. Moreover, when running timers from `flushTimers`, if after the `flushMicrotasks` call this turns out to be the last timer to run, then `flushTimers` will return with no further updates to the state. So when the timer callback is invoked (in that case), the whole `FakeAsync` state must already be in a state that `flushTimers` would have been happy to leave it in. (And there's no special cleanup that it does only after a non-last timer.) Similarly, when running timers from `elapse` (the only other possibility), the only difference from a state that `elapse` would be happy to leave things in is that `_elapsingTo` is still set. That field affects only `elapse` and `elapseBlocking`; and those are both designed to handle being called from within `elapse`.
To be precise, the problem is not that it immediately calls the timer if it had been due again after the elapse, but that it immediately runs it at the same tick again, because the state was not updated before running user code. Dart periodic timers are under-specified. The native overs try to be isochronic, which isn't always possible and why we have the The change here makes the simulated timers act like native timers, and the (It is a little worrisome if |
) Fixes #88. Currently when a periodic timer's callback gets invoked, its `_nextCall` is still the time of the current call, not the next one. If the timer callback itself calls `flushTimers` or `elapse`, this causes the same timer to immediately get called again. Fortunately the fix is easy: update `_nextCall` just before invoking `_callback`, instead of just after. --- To work through why this is a complete fix (and doesn't leave further bugs of this kind still to be fixed): After this fix, the call to the timer's callback is a tail call from `FakeTimer._fire`. Because the call site of `FakeTimer._fire` is immediately followed by `flushMicrotasks()`, this means calling other `FakeAsync` methods from the timer callback is no different from doing so in a subsequent microtask. Moreover, when running timers from `flushTimers`, if after the `flushMicrotasks` call this turns out to be the last timer to run, then `flushTimers` will return with no further updates to the state. So when the timer callback is invoked (in that case), the whole `FakeAsync` state must already be in a state that `flushTimers` would have been happy to leave it in. (And there's no special cleanup that it does only after a non-last timer.) Similarly, when running timers from `elapse` (the only other possibility), the only difference from a state that `elapse` would be happy to leave things in is that `_elapsingTo` is still set. That field affects only `elapse` and `elapseBlocking`; and those are both designed to handle being called from within `elapse`.
…art-archive/fake_async#89) Fixes dart-lang/fake_async#88. Currently when a periodic timer's callback gets invoked, its `_nextCall` is still the time of the current call, not the next one. If the timer callback itself calls `flushTimers` or `elapse`, this causes the same timer to immediately get called again. Fortunately the fix is easy: update `_nextCall` just before invoking `_callback`, instead of just after. --- To work through why this is a complete fix (and doesn't leave further bugs of this kind still to be fixed): After this fix, the call to the timer's callback is a tail call from `FakeTimer._fire`. Because the call site of `FakeTimer._fire` is immediately followed by `flushMicrotasks()`, this means calling other `FakeAsync` methods from the timer callback is no different from doing so in a subsequent microtask. Moreover, when running timers from `flushTimers`, if after the `flushMicrotasks` call this turns out to be the last timer to run, then `flushTimers` will return with no further updates to the state. So when the timer callback is invoked (in that case), the whole `FakeAsync` state must already be in a state that `flushTimers` would have been happy to leave it in. (And there's no special cleanup that it does only after a non-last timer.) Similarly, when running timers from `elapse` (the only other possibility), the only difference from a state that `elapse` would be happy to leave things in is that `_elapsingTo` is still set. That field affects only `elapse` and `elapseBlocking`; and those are both designed to handle being called from within `elapse`.
The tick as reported by [Timer.tick] was actually already getting correctly updated — the _tick++;
if (isPeriodic) {
// ignore: avoid_dynamic_calls
_callback(this);
_nextCall += duration; and that's the field that [FakeTimer.tick] reads. I'm not sure I fully understand the next paragraph:
but it sounds like you're saying the old behavior (before dart-archive/fake_async#89 fixed this issue #2319) was within the range of behavior that the real timers can sometimes have, due to the fact that providing ideal timer behavior is impossible (at least for a general-purpose application platform). In any case I think we're all agreed that the new behavior post-#89 is preferable. At a high level the reason I'd give for that is: generally a fake implementation of something, for use in tests, should by default behave in an idealized way, with the behavior that a real implementation would have under ideal conditions (or even that the real implementation would ideally have under ideal conditions). That's good for predictability, not only in the sense of determinism (as opposed to flakiness) but also in the sense that it makes it easy to look at the test source code and reason through what will happen. Of course it's often important to test how code behaves under non-ideal conditions (for example if timers get fired late because the system was busy)… but that should be something a test chooses explicitly. After all, to really have an effective test it'll usually be necessary anyway to specify some details of just how the conditions are non-ideal. Here for example, a test can use |
That's indeed, as you say, one use of testing, which removes focus from the fake implementation by making it as predictable and controllable as possible for the test scaffolding. The other use-case you mention would be stress-testing, to have an implementation that can span the full range of valid behaviors, so that code making non-guaranteed assumptions gets caught doing so. Both have value. The |
Yeah, both kinds are important. FakeAsync as it stands (and as it stood before dart-archive/fake_async#89) doesn't really provide stress-testing of timer behavior. Just having timers fire early when If someone's writing a stress test of how some code responds to timers, then they'll want to exercise, as you say, the full range of valid timer behaviors. That means timers firing late as well as early; and non-periodic timers being affected as well as periodic timers; and applying without regard to test code happening to have a re-entrant timer callback. Probably someone doing that will want to start by forking In short, a stress-testing implementation is useful only if it actually stress-tests, systematically exercising at least a wide range of possible behavior. A fake implementation that, like |
A better behavior might be that nothing fires while elapsing, when it's done inside an existing event callback. |
If the callback of a periodic timer calls
elapse
orflushTimers
, the same periodic timer immediately gets called again — even if its next scheduled call is far in the future and there are other timers scheduled sooner.The reason is that
_nextCall
gets updated only after invoking the timer's callback, instead of before:Fortunately the fix is easy: just swap those two statements. I'll send a PR.
Discovered this when studying the code to answer @lrhn's question at dart-archive/fake_async#85 (comment) about re-entrancy.
The text was updated successfully, but these errors were encountered: