mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Fix assertion thrown when hot restarting during animation (#175856)
Fixes https://github.com/flutter/flutter/issues/140684 Fixes https://github.com/flutter/flutter/issues/175260
This commit is contained in:
parent
35b5342f9c
commit
6c5df591ab
@ -10,6 +10,7 @@ import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import 'dom.dart';
|
||||
import 'frame_timing_recorder.dart';
|
||||
import 'initialization.dart';
|
||||
import 'platform_dispatcher.dart';
|
||||
|
||||
/// Provides frame scheduling functionality and frame lifecycle information to
|
||||
@ -18,6 +19,10 @@ import 'platform_dispatcher.dart';
|
||||
/// If new frame-related functionality needs to be added to the web engine,
|
||||
/// prefer to add it here instead of implementing it ad hoc.
|
||||
class FrameService {
|
||||
FrameService() {
|
||||
registerHotRestartListener(_dispose);
|
||||
}
|
||||
|
||||
/// The singleton instance of the [FrameService] used to schedule frames.
|
||||
///
|
||||
/// This may be overridden in tests, for example, to pump fake frames, using
|
||||
@ -25,6 +30,8 @@ class FrameService {
|
||||
static FrameService get instance => _instance ??= FrameService();
|
||||
static FrameService? _instance;
|
||||
|
||||
bool _isDisposed = false;
|
||||
|
||||
/// Overrides the value returned by [instance].
|
||||
///
|
||||
/// If [mock] is null, resets the value of [instance] back to real
|
||||
@ -98,6 +105,18 @@ class FrameService {
|
||||
// functionality that may throw exceptions, or produce wasm traps.
|
||||
_isFrameScheduled = false;
|
||||
|
||||
if (_isDisposed) {
|
||||
// Skip this animation frame because the instance has been disposed, meaning there was a
|
||||
// hot restart performed. During a hot restart, Dart automatically cancels timers and
|
||||
// microtasks, but animation frames are requested directly from the browser which isn't
|
||||
// aware of hot restarts, and that leads to problems.
|
||||
//
|
||||
// See:
|
||||
// - https://github.com/flutter/flutter/issues/175260
|
||||
// - https://github.com/flutter/flutter/issues/140684#issuecomment-3251179364
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
_isRenderingFrame = true;
|
||||
_frameData = ui.FrameData(frameNumber: _frameData.frameNumber + 1);
|
||||
@ -196,4 +215,11 @@ class FrameService {
|
||||
EnginePlatformDispatcher.instance.invokeOnDrawFrame();
|
||||
}
|
||||
}
|
||||
|
||||
void _dispose() {
|
||||
if (identical(this, _instance)) {
|
||||
_instance = null;
|
||||
}
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,12 +14,18 @@ void main() {
|
||||
|
||||
void testMain() {
|
||||
group('FrameService', () {
|
||||
setUp(() {
|
||||
void resetFrameService() {
|
||||
// Emulate a hot restart to clear listeners from previous tests.
|
||||
debugEmulateHotRestart();
|
||||
|
||||
FrameService.debugOverrideFrameService(null);
|
||||
expect(FrameService.instance.runtimeType, FrameService);
|
||||
EnginePlatformDispatcher.instance.onBeginFrame = null;
|
||||
EnginePlatformDispatcher.instance.onDrawFrame = null;
|
||||
});
|
||||
}
|
||||
|
||||
setUp(resetFrameService);
|
||||
tearDownAll(resetFrameService);
|
||||
|
||||
test('instance is valid and can be overridden', () {
|
||||
final defaultInstance = FrameService.instance;
|
||||
@ -151,6 +157,40 @@ void testMain() {
|
||||
expect(isRenderingInOnDrawFrame, isTrue);
|
||||
expect(valueInOnFinishedRenderingFrame, isFalse);
|
||||
});
|
||||
|
||||
test('Frame is cancelled after a hot restart', () async {
|
||||
final instance = FrameService.instance;
|
||||
|
||||
final frameCompleter = Completer<void>();
|
||||
instance.onFinishedRenderingFrame = () {
|
||||
frameCompleter.complete();
|
||||
};
|
||||
|
||||
expect(instance.isFrameScheduled, isFalse);
|
||||
instance.scheduleFrame();
|
||||
expect(instance.isFrameScheduled, isTrue);
|
||||
// Perform a hot restart immediately after scheduling the frame.
|
||||
debugEmulateHotRestart();
|
||||
|
||||
// Wait for 1 second for the frame to be rendered.
|
||||
bool timedOut = false;
|
||||
await frameCompleter.future.timeout(
|
||||
const Duration(seconds: 1),
|
||||
onTimeout: () {
|
||||
timedOut = true;
|
||||
},
|
||||
);
|
||||
// No frame should've been rendered because the animation frame callback should've been
|
||||
// cancelled on hot restart.
|
||||
expect(timedOut, isTrue);
|
||||
expect(instance.frameData.frameNumber, isZero);
|
||||
expect(frameCompleter.isCompleted, isFalse);
|
||||
// ... and no frame is scheduled.
|
||||
expect(instance.isFrameScheduled, isFalse);
|
||||
|
||||
// To avoid leaving an uncompleted completer, let's complete it.
|
||||
frameCompleter.complete();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user