Skip to content

Commit 92eed6c

Browse files
authored
Make zone handlers call zone.run to run callbacks. (#2451)
The platform library `Zone` implementation will change to not use `bindCallbackGuarded` in the `Timer` constructors and top-level `scheduleMicrotask` function. Instead it only calls `registerCallback`, and then the `Zone.create{,Periodic}Timer` and `Zone.scheduleMicrotask` are in charge of using `Zone.runGuarded` on the callback when timer/microtask event happens. This ensures that a surrounding zone's `registerCallback` can't make the callback throw in a way that is outside of the `runGuarded` call. Also makes periodic timers that are delayed past multiple tick points, update their `tick` count by more than one instead of running multiple times, like timers should. Adds testing of using the `FakeAsync`'s `Zone` functions directly, and embedding it in another zone to check that it does run using `Zone.run`. Adds testing of periodic timers incrementing `tick` by more than one.
2 parents ad04f17 + 1f292db commit 92eed6c

File tree

3 files changed

+300
-41
lines changed

3 files changed

+300
-41
lines changed

pkgs/fake_async/CHANGELOG.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
## 1.3.3-wip
22

3+
* Make the zone `create*Timer` and `scheduleMicrotask`
4+
be responsible for running callbacks in the zone they're
5+
scheduled in, matching (new) standard zone behavior.
6+
(The `Timer` constructors and top-level `scheduleMicrotask`
7+
used to bind their callback, but now only registers it,
8+
leaving the zone to run in the correct zone and handle errors.)
9+
* Make periodic timers increment their `tick` by more than one
10+
if `elapseBlocking` advanced time past multiple ticks.
11+
312
## 1.3.2
413

514
* Require Dart 3.3
615
* Fix bug where a `flushTimers` or `elapse` call from within
716
the callback of a periodic timer would immediately invoke
817
the same timer.
918
* Move to `dart-lang/test` monorepo.
10-
* Require Dart 3.5.
1119

1220
## 1.3.1
1321

pkgs/fake_async/lib/fake_async.dart

+71-36
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ class FakeAsync {
152152
/// Throws an [ArgumentError] if [duration] is negative.
153153
void elapseBlocking(Duration duration) {
154154
if (duration.inMicroseconds < 0) {
155-
throw ArgumentError('Cannot call elapse with negative duration');
155+
throw ArgumentError.value(duration, 'duration', 'Must not be negative');
156156
}
157157

158158
_elapsed += duration;
@@ -178,15 +178,18 @@ class FakeAsync {
178178
///
179179
/// Note: it's usually more convenient to use [fakeAsync] rather than creating
180180
/// a [FakeAsync] object and calling [run] manually.
181-
T run<T>(T Function(FakeAsync self) callback) =>
182-
runZoned(() => withClock(_clock, () => callback(this)),
183-
zoneSpecification: ZoneSpecification(
184-
createTimer: (_, __, ___, duration, callback) =>
185-
_createTimer(duration, callback, false),
186-
createPeriodicTimer: (_, __, ___, duration, callback) =>
187-
_createTimer(duration, callback, true),
188-
scheduleMicrotask: (_, __, ___, microtask) =>
189-
_microtasks.add(microtask)));
181+
T run<T>(T Function(FakeAsync self) callback) => runZoned(
182+
() => withClock(_clock, () => callback(this)),
183+
zoneSpecification: ZoneSpecification(
184+
createTimer: (_, __, zone, duration, callback) =>
185+
_createTimer(duration, zone, callback, false),
186+
createPeriodicTimer: (_, __, zone, duration, callback) =>
187+
_createTimer(duration, zone, callback, true),
188+
scheduleMicrotask: (_, __, zone, microtask) => _microtasks.add(() {
189+
zone.runGuarded(microtask);
190+
}),
191+
),
192+
);
190193

191194
/// Runs all pending microtasks scheduled within a call to [run] or
192195
/// [fakeAsync] until there are no more microtasks scheduled.
@@ -207,23 +210,25 @@ class FakeAsync {
207210
/// The [timeout] controls how much fake time may elapse before a [StateError]
208211
/// is thrown. This ensures that a periodic timer doesn't cause this method to
209212
/// deadlock. It defaults to one hour.
210-
void flushTimers(
211-
{Duration timeout = const Duration(hours: 1),
212-
bool flushPeriodicTimers = true}) {
213+
void flushTimers({
214+
Duration timeout = const Duration(hours: 1),
215+
bool flushPeriodicTimers = true,
216+
}) {
213217
final absoluteTimeout = _elapsed + timeout;
214218
_fireTimersWhile((timer) {
215219
if (timer._nextCall > absoluteTimeout) {
216220
// TODO(nweiz): Make this a [TimeoutException].
217221
throw StateError('Exceeded timeout $timeout while flushing timers');
218222
}
219223

220-
if (flushPeriodicTimers) return _timers.isNotEmpty;
224+
// Always run timer if it's due.
225+
if (timer._nextCall <= elapsed) return true;
221226

222-
// Continue firing timers until the only ones left are periodic *and*
223-
// every periodic timer has had a change to run against the final
224-
// value of [_elapsed].
225-
return _timers
226-
.any((timer) => !timer.isPeriodic || timer._nextCall <= _elapsed);
227+
// If no timers are due, continue running timers
228+
// (and advancing time to their next due time)
229+
// if flushing periodic timers,
230+
// or if there is any non-periodic timer left.
231+
return flushPeriodicTimers || _timers.any((timer) => !timer.isPeriodic);
227232
});
228233
}
229234

@@ -234,9 +239,7 @@ class FakeAsync {
234239
/// timer fires, [_elapsed] is updated to the appropriate duration.
235240
void _fireTimersWhile(bool Function(FakeTimer timer) predicate) {
236241
flushMicrotasks();
237-
for (;;) {
238-
if (_timers.isEmpty) break;
239-
242+
while (_timers.isNotEmpty) {
240243
final timer = minBy(_timers, (FakeTimer timer) => timer._nextCall)!;
241244
if (!predicate(timer)) break;
242245

@@ -248,9 +251,20 @@ class FakeAsync {
248251

249252
/// Creates a new timer controlled by `this` that fires [callback] after
250253
/// [duration] (or every [duration] if [periodic] is `true`).
251-
Timer _createTimer(Duration duration, Function callback, bool periodic) {
252-
final timer = FakeTimer._(duration, callback, periodic, this,
253-
includeStackTrace: includeTimerStackTrace);
254+
Timer _createTimer(
255+
Duration duration,
256+
Zone zone,
257+
Function callback,
258+
bool periodic,
259+
) {
260+
final timer = FakeTimer._(
261+
duration,
262+
zone,
263+
callback,
264+
periodic,
265+
this,
266+
includeStackTrace: includeTimerStackTrace,
267+
);
254268
_timers.add(timer);
255269
return timer;
256270
}
@@ -262,7 +276,20 @@ class FakeAsync {
262276
}
263277

264278
/// An implementation of [Timer] that's controlled by a [FakeAsync].
279+
///
280+
/// Periodic timers attempt to be isochronous. They trigger as soon as possible
281+
/// after a multiple of the [duration] has passed since they started,
282+
/// independently of when prior callbacks actually ran.
283+
/// This behavior matches VM timers.
284+
///
285+
/// Repeating web timers instead reschedule themselves a [duration] after
286+
/// their last callback ended, which shifts the timing both if a callback
287+
/// is delayed or if it runs for a long time. In return it guarantees
288+
/// that there is always at least [duration] between two callbacks.
265289
class FakeTimer implements Timer {
290+
/// The zone to run the callback in.
291+
final Zone _zone;
292+
266293
/// If this is periodic, the time that should elapse between firings of this
267294
/// timer.
268295
///
@@ -283,7 +310,7 @@ class FakeTimer implements Timer {
283310

284311
/// The value of [FakeAsync._elapsed] at (or after) which this timer should be
285312
/// fired.
286-
late Duration _nextCall;
313+
Duration _nextCall;
287314

288315
/// The current stack trace when this timer was created.
289316
///
@@ -302,12 +329,17 @@ class FakeTimer implements Timer {
302329
String get debugString => 'Timer (duration: $duration, periodic: $isPeriodic)'
303330
'${_creationStackTrace != null ? ', created:\n$creationStackTrace' : ''}';
304331

305-
FakeTimer._(Duration duration, this._callback, this.isPeriodic, this._async,
306-
{bool includeStackTrace = true})
307-
: duration = duration < Duration.zero ? Duration.zero : duration,
308-
_creationStackTrace = includeStackTrace ? StackTrace.current : null {
309-
_nextCall = _async._elapsed + this.duration;
310-
}
332+
FakeTimer._(
333+
Duration duration,
334+
this._zone,
335+
this._callback,
336+
this.isPeriodic,
337+
this._async, {
338+
bool includeStackTrace = true,
339+
}) : duration =
340+
duration < Duration.zero ? (duration = Duration.zero) : duration,
341+
_nextCall = _async._elapsed + duration,
342+
_creationStackTrace = includeStackTrace ? StackTrace.current : null;
311343

312344
@override
313345
bool get isActive => _async._timers.contains(this);
@@ -318,15 +350,18 @@ class FakeTimer implements Timer {
318350
/// Fires this timer's callback and updates its state as necessary.
319351
void _fire() {
320352
assert(isActive);
353+
assert(_nextCall <= _async._elapsed);
321354
_tick++;
322355
if (isPeriodic) {
323356
_nextCall += duration;
324-
// ignore: avoid_dynamic_calls
325-
_callback(this);
357+
while (_nextCall < _async._elapsed) {
358+
_tick++;
359+
_nextCall += duration;
360+
}
361+
_zone.runUnaryGuarded(_callback as void Function(Timer), this);
326362
} else {
327363
cancel();
328-
// ignore: avoid_dynamic_calls
329-
_callback();
364+
_zone.runGuarded(_callback as void Function());
330365
}
331366
}
332367
}

0 commit comments

Comments
 (0)