Skip to content

Commit 4d750a8

Browse files
lrhnCommit Bot
authored and
Commit Bot
committed
Make nullFuture be per-zone.
We introduced a `nullFuture` during the null-safety migration where we changed some methods to no longer allow returning `null`, and they therefore had to return a `Future`. That affected timing, because returning `null` was processed synchronously, and that change in timing made some tests fail. Rather that fix the fragile tests, we made the function return a recognizable future, a canonical `Future<Null>.value(null)`, and then recognized it and took a synchronous path for it. That caused other issues, because the future was created in the root zone. (Well, originally, it was created in the first zone which needed one, that was worse. Now it's created in the root zone.) Some code tries to contain asynchrony inside a custom zone, and then the get a `nullFuture` and calls `then` on it, and that schedules a microtask in the root zone. (It should probably have used the listener's zone, and not store a zone in the future at all, but that's how it was first done, and now people rely on that behavior too.) This change creates a `null` future *per zone* (lazily initialized when asked for). That should be sufficient because the code recognizing a returned `null` future is generally running in the same zone, but if any other code gets the `nullFuture`, it will be in the expected zone for where it was requested. This is a reland of commit a247b15 Change-Id: Ia113756de1f6d50af4b1abfec219d6b4dcd5d59b Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/249488 Reviewed-by: Nate Bosch <[email protected]> Commit-Queue: Lasse Nielsen <[email protected]>
1 parent 391540c commit 4d750a8

13 files changed

+118
-78
lines changed

pkg/front_end/testcases/nnbd/issue42546.dart.weak.outline.expect

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,19 @@ static method main() → dynamic
2828

2929

3030
Extra constant evaluation status:
31-
Evaluated: SymbolLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:821:13 -> SymbolConstant(#catchError)
32-
Evaluated: ListLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:821:13 -> ListConstant(const <Type*>[])
33-
Evaluated: SymbolLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:821:13 -> SymbolConstant(#test)
34-
Evaluated: SymbolLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:872:13 -> SymbolConstant(#whenComplete)
35-
Evaluated: ListLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:872:13 -> ListConstant(const <Type*>[])
36-
Evaluated: MapLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:872:13 -> MapConstant(const <Symbol*, dynamic>{})
37-
Evaluated: SymbolLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:916:13 -> SymbolConstant(#timeout)
38-
Evaluated: ListLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:916:13 -> ListConstant(const <Type*>[])
39-
Evaluated: SymbolLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:916:13 -> SymbolConstant(#onTimeout)
40-
Evaluated: SymbolLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:770:13 -> SymbolConstant(#then)
41-
Evaluated: SymbolLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:770:13 -> SymbolConstant(#onError)
42-
Evaluated: SymbolLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:881:13 -> SymbolConstant(#asStream)
43-
Evaluated: ListLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:881:13 -> ListConstant(const <Type*>[])
44-
Evaluated: ListLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:881:13 -> ListConstant(const <dynamic>[])
45-
Evaluated: MapLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:881:13 -> MapConstant(const <Symbol*, dynamic>{})
31+
Evaluated: SymbolLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:814:13 -> SymbolConstant(#catchError)
32+
Evaluated: ListLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:814:13 -> ListConstant(const <Type*>[])
33+
Evaluated: SymbolLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:814:13 -> SymbolConstant(#test)
34+
Evaluated: SymbolLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:865:13 -> SymbolConstant(#whenComplete)
35+
Evaluated: ListLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:865:13 -> ListConstant(const <Type*>[])
36+
Evaluated: MapLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:865:13 -> MapConstant(const <Symbol*, dynamic>{})
37+
Evaluated: SymbolLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:909:13 -> SymbolConstant(#timeout)
38+
Evaluated: ListLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:909:13 -> ListConstant(const <Type*>[])
39+
Evaluated: SymbolLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:909:13 -> SymbolConstant(#onTimeout)
40+
Evaluated: SymbolLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:763:13 -> SymbolConstant(#then)
41+
Evaluated: SymbolLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:763:13 -> SymbolConstant(#onError)
42+
Evaluated: SymbolLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:874:13 -> SymbolConstant(#asStream)
43+
Evaluated: ListLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:874:13 -> ListConstant(const <Type*>[])
44+
Evaluated: ListLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:874:13 -> ListConstant(const <dynamic>[])
45+
Evaluated: MapLiteral @ org-dartlang-sdk:///sdk/lib/async/future.dart:874:13 -> MapConstant(const <Symbol*, dynamic>{})
4646
Extra constant evaluation: evaluated: 61, effectively constant: 15

sdk/lib/async/async.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ import "dart:_internal"
110110
checkNotNullable,
111111
EmptyIterator,
112112
IterableElementError,
113-
nullFuture,
114113
printToZone,
115114
printToConsole,
116115
Since,

sdk/lib/async/future.dart

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -223,13 +223,6 @@ abstract class FutureOr<T> {
223223
/// it's very clearly documented.
224224
@pragma("wasm:entry-point")
225225
abstract class Future<T> {
226-
/// A `Future<Null>` completed with `null`.
227-
///
228-
/// Currently shared with `dart:internal`.
229-
/// If that future can be removed, then change this back to
230-
/// `_Future<Null>.zoneValue(null, _rootZone);`
231-
static final _Future<Null> _nullFuture = nullFuture as _Future<Null>;
232-
233226
/// A `Future<bool>` completed with `false`.
234227
static final _Future<bool> _falseFuture =
235228
new _Future<bool>.zoneValue(false, _rootZone);

sdk/lib/async/stream.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ abstract class Stream<T> {
515515
controller
516516
..onCancel = () {
517517
timer.cancel();
518-
return Future._nullFuture;
518+
return Zone._current._nullFuture;
519519
}
520520
..onPause = () {
521521
watch.stop();

sdk/lib/async/stream_controller.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -588,7 +588,10 @@ abstract class _StreamController<T> implements _StreamControllerBase<T> {
588588
Future<void> get done => _ensureDoneFuture();
589589

590590
Future<void> _ensureDoneFuture() =>
591-
_doneFuture ??= _isCanceled ? Future._nullFuture : _Future<void>();
591+
_doneFuture ??
592+
(_isCanceled
593+
? Zone._current._nullFuture as Future<void>
594+
: _doneFuture = _Future<void>());
592595

593596
/// Send or enqueue a data event.
594597
void add(T value) {
@@ -919,7 +922,7 @@ class _AddStreamState<T> {
919922
var cancel = addSubscription.cancel();
920923
if (cancel == null) {
921924
addStreamFuture._asyncComplete(null);
922-
return Future._nullFuture;
925+
return Zone._current._nullFuture;
923926
}
924927
return cancel.whenComplete(() {
925928
addStreamFuture._asyncComplete(null);

sdk/lib/async/stream_impl.dart

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ class _BufferingStreamSubscription<T>
197197
if (!_isCanceled) {
198198
_cancel();
199199
}
200-
return _cancelFuture ?? Future._nullFuture;
200+
return _cancelFuture ?? Zone._current._nullFuture;
201201
}
202202

203203
Future<E> asFuture<E>([E? futureValue]) {
@@ -217,7 +217,7 @@ class _BufferingStreamSubscription<T>
217217
};
218218
_onError = (Object error, StackTrace stackTrace) {
219219
Future cancelFuture = cancel();
220-
if (!identical(cancelFuture, Future._nullFuture)) {
220+
if (!identical(Zone._current._nullFuture, cancelFuture)) {
221221
cancelFuture.whenComplete(() {
222222
result._completeError(error, stackTrace);
223223
});
@@ -297,7 +297,7 @@ class _BufferingStreamSubscription<T>
297297
// Hooks called when the input is paused, unpaused or canceled.
298298
// These must not throw. If overwritten to call user code, include suitable
299299
// try/catch wrapping and send any errors to
300-
// [_Zone.current.handleUncaughtError].
300+
// [Zone._current.handleUncaughtError].
301301
void _onPause() {
302302
assert(_isInputPaused);
303303
}
@@ -352,7 +352,6 @@ class _BufferingStreamSubscription<T>
352352
// future to finish we must not report the error.
353353
if (_isCanceled && !_waitsForCancel) return;
354354
_state |= _STATE_IN_CALLBACK;
355-
// TODO(floitsch): this dynamic should be 'void'.
356355
var onError = _onError;
357356
if (onError is void Function(Object, StackTrace)) {
358357
_zone.runBinaryGuarded<Object, StackTrace>(onError, error, stackTrace);
@@ -367,7 +366,7 @@ class _BufferingStreamSubscription<T>
367366
_cancel();
368367
var cancelFuture = _cancelFuture;
369368
if (cancelFuture != null &&
370-
!identical(cancelFuture, Future._nullFuture)) {
369+
!identical(Zone._current._nullFuture, cancelFuture)) {
371370
cancelFuture.whenComplete(sendError);
372371
} else {
373372
sendError();
@@ -396,7 +395,8 @@ class _BufferingStreamSubscription<T>
396395
_cancel();
397396
_state |= _STATE_WAIT_FOR_CANCEL;
398397
var cancelFuture = _cancelFuture;
399-
if (cancelFuture != null && !identical(cancelFuture, Future._nullFuture)) {
398+
if (cancelFuture != null &&
399+
!identical(Zone._current._nullFuture, cancelFuture)) {
400400
cancelFuture.whenComplete(sendDone);
401401
} else {
402402
sendDone();
@@ -672,7 +672,7 @@ class _DoneStreamSubscription<T> implements StreamSubscription<T> {
672672
}
673673
}
674674

675-
Future cancel() => Future._nullFuture;
675+
Future cancel() => Zone._current._nullFuture;
676676

677677
Future<E> asFuture<E>([E? futureValue]) {
678678
E resultValue;
@@ -819,7 +819,7 @@ class _BroadcastSubscriptionWrapper<T> implements StreamSubscription<T> {
819819

820820
Future cancel() {
821821
_stream._cancelSubscription();
822-
return Future._nullFuture;
822+
return Zone._current._nullFuture;
823823
}
824824

825825
bool get isPaused {
@@ -963,7 +963,7 @@ class _StreamIterator<T> implements StreamIterator<T> {
963963
}
964964
return subscription.cancel();
965965
}
966-
return Future._nullFuture;
966+
return Zone._current._nullFuture;
967967
}
968968

969969
void _onData(T data) {

sdk/lib/async/stream_pipe.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ _runUserCode<T>(T userCode(), onSuccess(T value),
2626
void _cancelAndError(StreamSubscription subscription, _Future future,
2727
Object error, StackTrace stackTrace) {
2828
var cancelFuture = subscription.cancel();
29-
if (cancelFuture != null && !identical(cancelFuture, Future._nullFuture)) {
29+
if (cancelFuture != null &&
30+
!identical(Zone._current._nullFuture, cancelFuture)) {
3031
cancelFuture.whenComplete(() => future._completeError(error, stackTrace));
3132
} else {
3233
future._completeError(error, stackTrace);
@@ -55,7 +56,8 @@ void Function(Object error, StackTrace stackTrace) _cancelAndErrorClosure(
5556
before completing with a value. */
5657
void _cancelAndValue(StreamSubscription subscription, _Future future, value) {
5758
var cancelFuture = subscription.cancel();
58-
if (cancelFuture != null && !identical(cancelFuture, Future._nullFuture)) {
59+
if (cancelFuture != null &&
60+
!identical(Zone._current._nullFuture, cancelFuture)) {
5961
cancelFuture.whenComplete(() => future._complete(value));
6062
} else {
6163
future._complete(value);

sdk/lib/async/zone.dart

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,6 +1078,28 @@ abstract class _Zone implements Zone {
10781078
implZone, e, identical(error, e) ? stackTrace : s);
10791079
}
10801080
}
1081+
1082+
/// A reusable `null`-valued future per zone used by `dart:async`.
1083+
///
1084+
/// **DO NOT USE.**
1085+
///
1086+
/// This future is used in situations where a future is expected,
1087+
/// but no asynchronous computation actually happens,
1088+
/// like cancelling a stream from a controller with no `onCancel` callback.
1089+
/// *Some code depends on recognizing this future in order to react
1090+
/// synchronously.*
1091+
/// It does so to avoid changing event interleaving during the null safety
1092+
/// migration where, for example, the [StreamSubscription.cancel] method
1093+
/// stopped being able to return `null`.
1094+
/// The code that would be broken by such a timing change is fragile,
1095+
/// but we are not able to simply change it.
1096+
/// For better or worse, code depends on the precise timing
1097+
/// that our libraries have so far exhibited.
1098+
///
1099+
/// This future will be removed again if we can ever do so.
1100+
/// Do not use it for anything other than preserving timing
1101+
/// during the null safety migration.
1102+
_Future<Null> get _nullFuture;
10811103
}
10821104

10831105
class _CustomZone extends _Zone {
@@ -1105,6 +1127,9 @@ class _CustomZone extends _Zone {
11051127
/// The parent zone.
11061128
final _Zone parent;
11071129

1130+
/// Cached value for [_nullFuture];
1131+
_Future<Null>? _nullFutureCache;
1132+
11081133
/// The zone's scoped value declaration map.
11091134
///
11101135
/// This is always a [HashMap].
@@ -1196,6 +1221,18 @@ class _CustomZone extends _Zone {
11961221
/// parent's error-zone.
11971222
Zone get errorZone => _handleUncaughtError.zone;
11981223

1224+
_Future<Null> get _nullFuture {
1225+
_Future<Null>? result = _nullFutureCache;
1226+
if (result != null) return result;
1227+
// We only care about the zone of the null future
1228+
// because of the zone it schedules microtasks in.
1229+
var microtaskZone = _scheduleMicrotask.zone;
1230+
if (!identical(microtaskZone, this)) {
1231+
return _nullFutureCache = microtaskZone._nullFuture;
1232+
}
1233+
return _nullFutureCache = _Future<Null>.value(null);
1234+
}
1235+
11991236
void runGuarded(void f()) {
12001237
try {
12011238
run(f);
@@ -1509,6 +1546,8 @@ Zone _rootFork(Zone? self, ZoneDelegate? parent, Zone zone,
15091546
}
15101547

15111548
class _RootZone extends _Zone {
1549+
static final _nullFutureCache = _Future<Null>.zoneValue(null, _rootZone);
1550+
15121551
const _RootZone();
15131552

15141553
_ZoneFunction<RunHandler> get _run =>
@@ -1547,6 +1586,8 @@ class _RootZone extends _Zone {
15471586
// The parent zone.
15481587
_Zone? get parent => null;
15491588

1589+
_Future<Null> get _nullFuture => _nullFutureCache;
1590+
15501591
/// The zone's scoped value declaration map.
15511592
///
15521593
/// This is always a [HashMap].

sdk/lib/html/dart2js/html_dart2js.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37302,13 +37302,13 @@ class _EventStreamSubscription<T extends Event> extends StreamSubscription<T> {
3730237302
}
3730337303

3730437304
Future cancel() {
37305-
if (_canceled) return nullFuture;
37306-
37307-
_unlisten();
37308-
// Clear out the target to indicate this is complete.
37309-
_target = null;
37310-
_onData = null;
37311-
return nullFuture;
37305+
if (!_canceled) {
37306+
_unlisten();
37307+
// Clear out the target to indicate this is complete.
37308+
_target = null;
37309+
_onData = null;
37310+
}
37311+
return Future<void>.value(null);
3731237312
}
3731337313

3731437314
bool get _canceled => _target == null;

sdk/lib/internal/internal.dart

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -136,27 +136,7 @@ int parseHexByte(String source, int index) {
136136
return digit1 * 16 + digit2 - (digit2 & 256);
137137
}
138138

139-
/// A reusable `null`-valued future used by `dart:async`.
140-
///
141-
/// **DO NOT USE.**
142-
///
143-
/// This future is used in situations where a future is expected,
144-
/// but no asynchronous computation actually happens,
145-
/// like cancelling a stream from a controller with no `onCancel` callback.
146-
/// *Some code depends on recognizing this future in order to react
147-
/// synchronously.*
148-
/// It does so to avoid changing event interleaving during the null safety
149-
/// migration where, for example, the [StreamSubscription.cancel] method
150-
/// stopped being able to return `null`.
151-
/// The code that would be broken by such a timing change is fragile,
152-
/// but we are not able to simply change it.
153-
/// For better or worse, code depends on the precise timing that our libraries
154-
/// have so far exhibited.
155-
///
156-
/// This future will be removed again if we can ever do so.
157-
/// Do not use it for anything other than preserving timing
158-
/// during the null safety migration.
159-
final Future<Null> nullFuture = Zone.root.run(() => Future<Null>.value(null));
139+
final Expando<Future<Null>> _nullFutures = Expando<Future<Null>>();
160140

161141
/// A default hash function used by the platform in various places.
162142
///

tests/lib/async/null_future_zone_test.dart

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,32 @@ main() {
1313
Expect.isFalse(await it.moveNext());
1414

1515
late Future nullFuture;
16-
late Future falseFuture;
1716

17+
bool nullFutureZoneUsed = false;
1818
runZoned(() {
1919
nullFuture = (new StreamController()..stream.listen(null).cancel()).done;
20-
falseFuture = it.moveNext();
2120
}, zoneSpecification: new ZoneSpecification(scheduleMicrotask:
2221
(Zone self, ZoneDelegate parent, Zone zone, void f()) {
23-
Expect.fail("Should not be called");
22+
Expect.identical(zone, self);
23+
nullFutureZoneUsed = true;
24+
parent.scheduleMicrotask(zone, f);
2425
}));
2526

2627
nullFuture.then((value) {
2728
Expect.isNull(value);
29+
Expect.isTrue(nullFutureZoneUsed);
2830
asyncEnd();
2931
});
3032

33+
late Future falseFuture;
34+
35+
runZoned(() {
36+
falseFuture = it.moveNext();
37+
}, zoneSpecification: new ZoneSpecification(scheduleMicrotask:
38+
(Zone self, ZoneDelegate parent, Zone zone, void f()) {
39+
Expect.fail("Should not be called");
40+
}));
41+
3142
falseFuture.then((value) {
3243
Expect.isFalse(value);
3344
asyncEnd();

tests/lib_2/async/null_future_zone_test.dart

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,32 @@ main() {
1515
Expect.isFalse(await it.moveNext());
1616

1717
Future nullFuture;
18-
Future falseFuture;
1918

19+
bool nullFutureZoneUsed = false;
2020
runZoned(() {
2121
nullFuture = (new StreamController()..stream.listen(null).cancel()).done;
22-
falseFuture = it.moveNext();
2322
}, zoneSpecification: new ZoneSpecification(scheduleMicrotask:
2423
(Zone self, ZoneDelegate parent, Zone zone, void f()) {
25-
Expect.fail("Should not be called");
24+
Expect.identical(zone, self);
25+
nullFutureZoneUsed = true;
26+
parent.scheduleMicrotask(zone, f);
2627
}));
2728

2829
nullFuture.then((value) {
2930
Expect.isNull(value);
31+
Expect.isTrue(nullFutureZoneUsed);
3032
asyncEnd();
3133
});
3234

35+
Future falseFuture;
36+
37+
runZoned(() {
38+
falseFuture = it.moveNext();
39+
}, zoneSpecification: new ZoneSpecification(scheduleMicrotask:
40+
(Zone self, ZoneDelegate parent, Zone zone, void f()) {
41+
Expect.fail("Should not be called");
42+
}));
43+
3344
falseFuture.then((value) {
3445
Expect.isFalse(value);
3546
asyncEnd();

0 commit comments

Comments
 (0)