[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:
Nicholas Shahan
2025-04-28 23:25:56 -07:00
committed by Commit Queue
parent a67b8c503e
commit be6d2e3a00
25 changed files with 381 additions and 19 deletions
+11
View File
@@ -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());
}
*/