[ddc] Cancel futures after a hot restart
Outstanding async code now checks and cancels itself if it was created in a previous version of the application from before a hot restart operation. This includes outstanding `Future`s created by calling the `dart:js_util` helper `promiseToFuture`. Issue: https://github.com/flutter/flutter/issues/166004 Change-Id: I342bbd2f8eda6b58d2f0fdaf3c00f55f03561b1a Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/423961 Reviewed-by: Nate Biggs <natebiggs@google.com> Reviewed-by: Srujan Gaddam <srujzs@google.com> Commit-Queue: Nicholas Shahan <nshahan@google.com>
This commit is contained in:
committed by
Commit Queue
parent
a67b8c503e
commit
be6d2e3a00
@@ -1,5 +1,16 @@
|
||||
## 3.9.0
|
||||
|
||||
### Tools
|
||||
|
||||
#### Dart Development Compiler (dartdevc)
|
||||
|
||||
Outstanding async code now checks and cancels itself after a hot restart if
|
||||
it was started in a different generation of the application before the restart.
|
||||
This includes outstanding `Future`s created by calling `JSPromise.toDart` from
|
||||
`dart:js_interop` and the underlying the `dart:js_util` helper
|
||||
`promiseToFuture`. Dart callbacks will not be run, but callbacks on the
|
||||
JavaScript side will still be executed.
|
||||
|
||||
## 3.8.0
|
||||
|
||||
**Released on:** Unreleased
|
||||
|
||||
@@ -1641,7 +1641,7 @@ if (!self.deferred_loader) {
|
||||
|
||||
// Then we link the existing libraries. Note this may trigger initializing
|
||||
// and linking new library dependencies that were not present before and
|
||||
// requires for all library intitializers to be up to date.
|
||||
// requires for all library initializers to be up to date.
|
||||
for (let name in this.pendingHotReloadLibraryInitializers) {
|
||||
if (previouslyLoaded[name]) {
|
||||
this.libraries[name].link();
|
||||
@@ -1662,11 +1662,6 @@ if (!self.deferred_loader) {
|
||||
if (!this.savedEntryPointLibraryName) {
|
||||
throw "Error: Hot restart requested before application started.";
|
||||
}
|
||||
// TODO(nshahan): Stop calling hotRestart in the SDK when scheduled
|
||||
// futures no longer keep lazy initialized values from the previous
|
||||
// generation alive.
|
||||
let dart = this.importLibrary('dart:_runtime');
|
||||
dart.hotRestart();
|
||||
// Clear all libraries.
|
||||
this.libraries = Object.create(null);
|
||||
this.triggeredSDKLibrariesWithSideEffects = false;
|
||||
|
||||
@@ -6205,6 +6205,11 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
|
||||
|
||||
if (_isSdkInternalRuntime(enclosingLibrary)) {
|
||||
var name = target.name.text;
|
||||
if (node.arguments.positional.isEmpty) {
|
||||
if (name == 'hotRestartGeneration') {
|
||||
return _runtimeCall('hotRestartIteration');
|
||||
}
|
||||
}
|
||||
if (node.arguments.positional.length == 1) {
|
||||
var firstArg = node.arguments.positional.single;
|
||||
if (name == 'extensionSymbol' && firstArg is StringLiteral) {
|
||||
|
||||
@@ -6655,7 +6655,11 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
|
||||
|
||||
if (_isSdkInternalRuntime(enclosingLibrary)) {
|
||||
var name = target.name.text;
|
||||
if (node.arguments.positional.length == 1) {
|
||||
if (node.arguments.positional.isEmpty) {
|
||||
if (name == 'hotRestartGeneration') {
|
||||
return js.call('dartDevEmbedder.hotRestartGeneration');
|
||||
}
|
||||
} else if (node.arguments.positional.length == 1) {
|
||||
var firstArg = node.arguments.positional.single;
|
||||
if (name == 'extensionSymbol' && firstArg is StringLiteral) {
|
||||
return _getSymbol(_getExtensionSymbolInternal(firstArg.value));
|
||||
|
||||
@@ -175,15 +175,17 @@ class _AsyncRun {
|
||||
|
||||
@ReifyFunctionTypes(false)
|
||||
static void _scheduleImmediateJSOverride(void Function() callback) {
|
||||
final createdGeneration = dart.hotRestartGeneration();
|
||||
JS('void', '#.scheduleImmediate(#)', dart.global_, () {
|
||||
callback();
|
||||
if (createdGeneration == dart.hotRestartGeneration()) callback();
|
||||
});
|
||||
}
|
||||
|
||||
@ReifyFunctionTypes(false)
|
||||
static void _scheduleImmediateWithPromise(void Function() callback) {
|
||||
final createdGeneration = dart.hotRestartGeneration();
|
||||
JS('', '#.Promise.resolve(null).then(#)', dart.global_, () {
|
||||
callback();
|
||||
if (createdGeneration == dart.hotRestartGeneration()) callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -487,7 +489,6 @@ class _AsyncStarImpl<T> {
|
||||
class _AsyncAwaitCompleter<T> implements Completer<T> {
|
||||
final _future = _Future<T>();
|
||||
bool isSync;
|
||||
int hotRestartIteration = dart.hotRestartIteration;
|
||||
|
||||
_AsyncAwaitCompleter() : isSync = false;
|
||||
|
||||
|
||||
@@ -15,6 +15,18 @@ import 'dart:typed_data' show Uint8List;
|
||||
@patch
|
||||
bool typeAcceptsNull<T>() => null is T;
|
||||
|
||||
int? getHotRestartGeneration() => dart.hotRestartGeneration();
|
||||
|
||||
/// Returns `true` when the provided [generation] matches the current hot
|
||||
/// restart generation.
|
||||
///
|
||||
/// This is intended to avoid completing a Dart Future after a hot restart that
|
||||
/// originated from a converted Promise before the hot restart.
|
||||
///
|
||||
/// See uses in `promiseToFuture` from `dart:js_util`.
|
||||
bool isCurrentHotRestartGeneration(int generation) =>
|
||||
generation == dart.hotRestartGeneration();
|
||||
|
||||
@patch
|
||||
class Symbol implements core.Symbol {
|
||||
@patch
|
||||
|
||||
@@ -1242,7 +1242,7 @@ Future<void> loadLibrary(
|
||||
JS('', '#.add(#)', result, importPrefix);
|
||||
return _ddcNewLoadLibraryTiming ? Future(() {}) : Future.value();
|
||||
} else {
|
||||
int currentHotRestartIteration = hotRestartIteration;
|
||||
int hotRestartGenerationBefore = hotRestartGeneration();
|
||||
var loadId = '$libraryUri::$importPrefix';
|
||||
if (targetModule.isEmpty) {
|
||||
throw ArgumentError('Empty module passed for deferred load: $loadId.');
|
||||
@@ -1254,7 +1254,7 @@ Future<void> loadLibrary(
|
||||
|
||||
// Don't mark a load ID as loaded across hot restart boundaries.
|
||||
void internalComplete(void Function()? beforeComplete) {
|
||||
if (hotRestartIteration == currentHotRestartIteration &&
|
||||
if (hotRestartGeneration() == hotRestartGenerationBefore &&
|
||||
beforeComplete != null) {
|
||||
beforeComplete();
|
||||
}
|
||||
|
||||
@@ -245,6 +245,9 @@ final List<void Function()> resetFields = JS('', '[]');
|
||||
@notNull
|
||||
final JSArray<Object?> moduleConstCaches = JS('!', 'new Map()');
|
||||
|
||||
/// Returns the current hot restart generation number.
|
||||
external int hotRestartGeneration();
|
||||
|
||||
/// A counter to track each time [hotRestart] is invoked. This is used to ensure
|
||||
/// that pending callbacks that were created on a previous iteration (e.g. a
|
||||
/// timer callback or a DOM callback) will not execute when they get invoked.
|
||||
|
||||
@@ -39,11 +39,11 @@ class TimerImpl implements Timer {
|
||||
|
||||
TimerImpl(int milliseconds, void callback()) : _once = true {
|
||||
if (hasTimer()) {
|
||||
int currentHotRestartIteration = dart.hotRestartIteration;
|
||||
int hotRestartGenerationBefore = dart.hotRestartGeneration();
|
||||
void internalCallback() {
|
||||
_handle = null;
|
||||
_tick = 1;
|
||||
if (currentHotRestartIteration == dart.hotRestartIteration) {
|
||||
if (hotRestartGenerationBefore == dart.hotRestartGeneration()) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
@@ -64,9 +64,9 @@ class TimerImpl implements Timer {
|
||||
: _once = false {
|
||||
if (hasTimer()) {
|
||||
int start = JS<int>('!', 'Date.now()');
|
||||
int currentHotRestartIteration = dart.hotRestartIteration;
|
||||
int hotRestartGenerationBefore = dart.hotRestartGeneration();
|
||||
_handle = JS<int>('!', '#.setInterval(#, #)', global, () {
|
||||
if (currentHotRestartIteration != dart.hotRestartIteration) {
|
||||
if (hotRestartGenerationBefore != dart.hotRestartGeneration()) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,18 @@ import 'dart:typed_data' show Uint8List;
|
||||
@pragma('dart2js:tryInline')
|
||||
bool typeAcceptsNull<T>() => null is T;
|
||||
|
||||
/// No-op in dart2js.
|
||||
///
|
||||
/// Only used in DDC for hot restart correctness.
|
||||
@pragma('dart2js:tryInline')
|
||||
int? getHotRestartGeneration() => null;
|
||||
|
||||
/// No-op in dart2js.
|
||||
///
|
||||
/// Only used in DDC for hot restart correctness.
|
||||
@pragma('dart2js:tryInline')
|
||||
bool isCurrentHotRestartGeneration(int _) => true;
|
||||
|
||||
@patch
|
||||
class Symbol implements core.Symbol {
|
||||
@patch
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:_foreign_helper' show JS;
|
||||
import 'dart:_internal' show patch;
|
||||
import 'dart:_internal'
|
||||
show getHotRestartGeneration, isCurrentHotRestartGeneration, patch;
|
||||
import 'dart:_js_helper'
|
||||
show
|
||||
assertInterop,
|
||||
@@ -561,9 +562,21 @@ num unsignedRightShift(Object? leftOperand, Object? rightOperand) {
|
||||
@patch
|
||||
Future<T> promiseToFuture<T>(Object jsPromise) {
|
||||
final completer = Completer<T>();
|
||||
|
||||
final success = convertDartClosureToJS((r) => completer.complete(r), 1);
|
||||
final restartGenerationBefore = getHotRestartGeneration();
|
||||
final success = convertDartClosureToJS((r) {
|
||||
if (restartGenerationBefore != null) {
|
||||
// These nested if statements are intended for simple optimization by
|
||||
// dart2js.
|
||||
if (!isCurrentHotRestartGeneration(restartGenerationBefore)) return;
|
||||
}
|
||||
return completer.complete(r);
|
||||
}, 1);
|
||||
final error = convertDartClosureToJS((e) {
|
||||
if (restartGenerationBefore != null) {
|
||||
// These nested if statements are intended for simple optimization by
|
||||
// dart2js.
|
||||
if (!isCurrentHotRestartGeneration(restartGenerationBefore)) return;
|
||||
}
|
||||
// Note that `completeError` expects a non-nullable error regardless of
|
||||
// whether null-safety is enabled, so a `NullRejectionException` is always
|
||||
// provided if the error is `null` or `undefined`.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"exclude": ["vm"]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:js_interop';
|
||||
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
injectJS();
|
||||
createPromise().toDart.catchError((_) => throw 'Should never run.');
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
Expect.isFalse(rejectCalled);
|
||||
await hotRestart();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:js_interop';
|
||||
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
rejectPromise();
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
Expect.isTrue(rejectCalled);
|
||||
}
|
||||
|
||||
/** DIFF **/
|
||||
/*
|
||||
import 'util.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
- injectJS();
|
||||
- createPromise().toDart.catchError((_) => throw 'Should never run.');
|
||||
+ rejectPromise();
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
- Expect.isFalse(rejectCalled);
|
||||
- await hotRestart();
|
||||
+ Expect.isTrue(rejectCalled);
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'dart:js_interop';
|
||||
|
||||
@JS()
|
||||
external JSAny? eval(String script);
|
||||
|
||||
@JS()
|
||||
external JSPromise createPromise();
|
||||
|
||||
@JS()
|
||||
external void rejectPromise();
|
||||
|
||||
@JS()
|
||||
external bool rejectCalled;
|
||||
|
||||
void injectJS() {
|
||||
eval('''
|
||||
self.rejectCalled = false;
|
||||
self.rejectFunction = null;
|
||||
self.createPromise = function(s) {
|
||||
let { promise, resolve, reject } = Promise.withResolvers();
|
||||
self.rejectFunction = function() {
|
||||
self.rejectCalled = true;
|
||||
reject();
|
||||
};
|
||||
return promise;
|
||||
};
|
||||
self.rejectPromise = function() {
|
||||
self.rejectFunction();
|
||||
};
|
||||
''');
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"exclude": ["vm"]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:js_interop';
|
||||
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
injectJS();
|
||||
createPromise().toDart.then((_) => throw 'Should never run.');
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
Expect.isFalse(resolveCalled);
|
||||
await hotRestart();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:js_interop';
|
||||
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
resolvePromise();
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
Expect.isTrue(resolveCalled);
|
||||
}
|
||||
|
||||
/** DIFF **/
|
||||
/*
|
||||
import 'util.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
- injectJS();
|
||||
- createPromise().toDart.then((_) => throw 'Should never run.');
|
||||
+ resolvePromise();
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
- Expect.isFalse(resolveCalled);
|
||||
- await hotRestart();
|
||||
+ Expect.isTrue(resolveCalled);
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'dart:js_interop';
|
||||
|
||||
@JS()
|
||||
external JSAny? eval(String script);
|
||||
|
||||
@JS()
|
||||
external JSPromise createPromise();
|
||||
|
||||
@JS()
|
||||
external void resolvePromise();
|
||||
|
||||
@JS()
|
||||
external bool resolveCalled;
|
||||
|
||||
void injectJS() {
|
||||
eval('''
|
||||
self.resolveCalled = false;
|
||||
self.resolveFunction = null;
|
||||
self.createPromise = function(s) {
|
||||
let { promise, resolve, reject } = Promise.withResolvers();
|
||||
self.resolveFunction = function() {
|
||||
self.resolveCalled = true;
|
||||
resolve();
|
||||
};
|
||||
return promise;
|
||||
};
|
||||
self.resolvePromise = function() {
|
||||
self.resolveFunction();
|
||||
};
|
||||
''');
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"exclude": ["vm"]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
bool beforeRestart = true;
|
||||
bool calledBeforeRestart = false;
|
||||
bool calledAfterRestart = false;
|
||||
void callback(_) {
|
||||
if (beforeRestart) {
|
||||
calledBeforeRestart = true;
|
||||
} else {
|
||||
calledAfterRestart = true;
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
Timer.periodic(Duration(milliseconds: 10), callback);
|
||||
await new Future.delayed(Duration(milliseconds: 50));
|
||||
Expect.isTrue(beforeRestart);
|
||||
Expect.isTrue(calledBeforeRestart);
|
||||
await hotRestart();
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
bool beforeRestart = false;
|
||||
bool calledBeforeRestart = false;
|
||||
bool calledAfterRestart = false;
|
||||
void callback(_) {
|
||||
if (beforeRestart) {
|
||||
calledBeforeRestart = true;
|
||||
} else {
|
||||
calledAfterRestart = true;
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
await new Future.delayed(Duration(milliseconds: 50));
|
||||
Expect.isFalse(beforeRestart);
|
||||
Expect.isFalse(calledAfterRestart);
|
||||
}
|
||||
|
||||
/** DIFF **/
|
||||
/*
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
-bool beforeRestart = true;
|
||||
+bool beforeRestart = false;
|
||||
bool calledBeforeRestart = false;
|
||||
bool calledAfterRestart = false;
|
||||
void callback(_) {
|
||||
@@ -18,9 +18,7 @@ void callback(_) {
|
||||
}
|
||||
|
||||
void main() async {
|
||||
- Timer.periodic(Duration(milliseconds: 10), callback);
|
||||
await new Future.delayed(Duration(milliseconds: 50));
|
||||
- Expect.isTrue(beforeRestart);
|
||||
- Expect.isTrue(calledBeforeRestart);
|
||||
- await hotRestart();
|
||||
+ Expect.isFalse(beforeRestart);
|
||||
+ Expect.isFalse(calledAfterRestart);
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"exclude": ["vm"]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
bool restarted() => false;
|
||||
|
||||
void callback() {
|
||||
throw Exception('Should never run.');
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
Timer(Duration(milliseconds: 200), callback);
|
||||
await hotRestart();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
bool restarted() => true;
|
||||
|
||||
void callback() {
|
||||
throw Exception('Should never run.');
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
await new Future.delayed(Duration(milliseconds: 300));
|
||||
Expect.isTrue(restarted());
|
||||
}
|
||||
|
||||
/** DIFF **/
|
||||
/*
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
-bool restarted() => false;
|
||||
+bool restarted() => true;
|
||||
|
||||
void callback() {
|
||||
throw Exception('Should never run.');
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
- Timer(Duration(milliseconds: 200), callback);
|
||||
- await hotRestart();
|
||||
+ await new Future.delayed(Duration(milliseconds: 300));
|
||||
+ Expect.isTrue(restarted());
|
||||
}
|
||||
*/
|
||||
Reference in New Issue
Block a user