Unverified Commit e3e18921 authored by Mouad Debbar's avatar Mouad Debbar Committed by GitHub

[web] Fix race condition in widget benchmarks (#53952)

parent de27d713
...@@ -305,8 +305,7 @@ abstract class SceneBuilderRecorder extends Recorder { ...@@ -305,8 +305,7 @@ abstract class SceneBuilderRecorder extends Recorder {
/// } /// }
/// } /// }
/// ``` /// ```
abstract class WidgetRecorder extends Recorder abstract class WidgetRecorder extends Recorder implements FrameRecorder {
implements RecordingWidgetsBindingListener {
WidgetRecorder({@required String name}) : super._(name); WidgetRecorder({@required String name}) : super._(name);
/// Creates a widget to be benchmarked. /// Creates a widget to be benchmarked.
...@@ -318,9 +317,10 @@ abstract class WidgetRecorder extends Recorder ...@@ -318,9 +317,10 @@ abstract class WidgetRecorder extends Recorder
Widget createWidget(); Widget createWidget();
@override @override
Profile profile; VoidCallback didStop;
final Completer<Profile> _profileCompleter = Completer<Profile>(); Profile profile;
Completer<void> _runCompleter;
Stopwatch _drawFrameStopwatch; Stopwatch _drawFrameStopwatch;
...@@ -340,27 +340,32 @@ abstract class WidgetRecorder extends Recorder ...@@ -340,27 +340,32 @@ abstract class WidgetRecorder extends Recorder
if (profile.shouldContinue()) { if (profile.shouldContinue()) {
window.scheduleFrame(); window.scheduleFrame();
} else { } else {
_profileCompleter.complete(profile); didStop();
_runCompleter.complete();
} }
} }
@override @override
void _onError(dynamic error, StackTrace stackTrace) { void _onError(dynamic error, StackTrace stackTrace) {
_profileCompleter.completeError(error, stackTrace); _runCompleter.completeError(error, stackTrace);
} }
@override @override
Future<Profile> run() { Future<Profile> run() async {
profile = Profile(name: name); _runCompleter = Completer<void>();
final Profile localProfile = profile = Profile(name: name);
final _RecordingWidgetsBinding binding = final _RecordingWidgetsBinding binding =
_RecordingWidgetsBinding.ensureInitialized(); _RecordingWidgetsBinding.ensureInitialized();
final Widget widget = createWidget(); final Widget widget = createWidget();
binding._beginRecording(this, widget); binding._beginRecording(this, widget);
_profileCompleter.future.whenComplete(() { try {
await _runCompleter.future;
return localProfile;
} finally {
_runCompleter = null;
profile = null; profile = null;
}); }
return _profileCompleter.future;
} }
} }
...@@ -371,8 +376,7 @@ abstract class WidgetRecorder extends Recorder ...@@ -371,8 +376,7 @@ abstract class WidgetRecorder extends Recorder
/// another frame that clears the screen. It repeats this process, measuring the /// another frame that clears the screen. It repeats this process, measuring the
/// performance of frames that render the widget and ignoring the frames that /// performance of frames that render the widget and ignoring the frames that
/// clear the screen. /// clear the screen.
abstract class WidgetBuildRecorder extends Recorder abstract class WidgetBuildRecorder extends Recorder implements FrameRecorder {
implements RecordingWidgetsBindingListener {
WidgetBuildRecorder({@required String name}) : super._(name); WidgetBuildRecorder({@required String name}) : super._(name);
/// Creates a widget to be benchmarked. /// Creates a widget to be benchmarked.
...@@ -383,9 +387,10 @@ abstract class WidgetBuildRecorder extends Recorder ...@@ -383,9 +387,10 @@ abstract class WidgetBuildRecorder extends Recorder
Widget createWidget(); Widget createWidget();
@override @override
Profile profile; VoidCallback didStop;
final Completer<Profile> _profileCompleter = Completer<Profile>(); Profile profile;
Completer<void> _runCompleter;
Stopwatch _drawFrameStopwatch; Stopwatch _drawFrameStopwatch;
...@@ -427,26 +432,31 @@ abstract class WidgetBuildRecorder extends Recorder ...@@ -427,26 +432,31 @@ abstract class WidgetBuildRecorder extends Recorder
showWidget = !showWidget; showWidget = !showWidget;
_hostState._setStateTrampoline(); _hostState._setStateTrampoline();
} else { } else {
_profileCompleter.complete(profile); didStop();
_runCompleter.complete();
} }
} }
@override @override
void _onError(dynamic error, StackTrace stackTrace) { void _onError(dynamic error, StackTrace stackTrace) {
_profileCompleter.completeError(error, stackTrace); _runCompleter.completeError(error, stackTrace);
} }
@override @override
Future<Profile> run() { Future<Profile> run() async {
profile = Profile(name: name); _runCompleter = Completer<void>();
final Profile localProfile = profile = Profile(name: name);
final _RecordingWidgetsBinding binding = final _RecordingWidgetsBinding binding =
_RecordingWidgetsBinding.ensureInitialized(); _RecordingWidgetsBinding.ensureInitialized();
binding._beginRecording(this, _WidgetBuildRecorderHost(this)); binding._beginRecording(this, _WidgetBuildRecorderHost(this));
_profileCompleter.future.whenComplete(() { try {
await _runCompleter.future;
return localProfile;
} finally {
_runCompleter = null;
profile = null; profile = null;
}); }
return _profileCompleter.future;
} }
} }
...@@ -698,9 +708,10 @@ String _ratioToPercent(double value) { ...@@ -698,9 +708,10 @@ String _ratioToPercent(double value) {
/// Implemented by recorders that use [_RecordingWidgetsBinding] to receive /// Implemented by recorders that use [_RecordingWidgetsBinding] to receive
/// frame life-cycle calls. /// frame life-cycle calls.
abstract class RecordingWidgetsBindingListener { abstract class FrameRecorder {
/// The profile where the benchmark is collecting metrics. /// Called by the recorder when it stops recording and doesn't need to collect
Profile profile; /// any more data.
set didStop(VoidCallback cb);
/// Called just before calling [SchedulerBinding.handleDrawFrame]. /// Called just before calling [SchedulerBinding.handleDrawFrame].
void frameWillDraw(); void frameWillDraw();
...@@ -739,19 +750,31 @@ class _RecordingWidgetsBinding extends BindingBase ...@@ -739,19 +750,31 @@ class _RecordingWidgetsBinding extends BindingBase
return WidgetsBinding.instance as _RecordingWidgetsBinding; return WidgetsBinding.instance as _RecordingWidgetsBinding;
} }
RecordingWidgetsBindingListener _listener; FrameRecorder _recorder;
bool _hasErrored = false; bool _hasErrored = false;
void _beginRecording( /// To short-circuit all frame lifecycle methods when the benchmark has
RecordingWidgetsBindingListener recorder, Widget widget) { /// stopped collecting data.
bool _benchmarkStopped = false;
void _beginRecording(FrameRecorder recorder, Widget widget) {
if (_recorder != null) {
throw Exception(
'Cannot call _RecordingWidgetsBinding._beginRecording more than once',
);
}
final FlutterExceptionHandler originalOnError = FlutterError.onError; final FlutterExceptionHandler originalOnError = FlutterError.onError;
recorder.didStop = () {
_benchmarkStopped = true;
};
// Fail hard and fast on errors. Benchmarks should not have any errors. // Fail hard and fast on errors. Benchmarks should not have any errors.
FlutterError.onError = (FlutterErrorDetails details) { FlutterError.onError = (FlutterErrorDetails details) {
_haltBenchmarkWithError(details.exception, details.stack); _haltBenchmarkWithError(details.exception, details.stack);
originalOnError(details); originalOnError(details);
}; };
_listener = recorder; _recorder = recorder;
runApp(widget); runApp(widget);
} }
...@@ -759,22 +782,17 @@ class _RecordingWidgetsBinding extends BindingBase ...@@ -759,22 +782,17 @@ class _RecordingWidgetsBinding extends BindingBase
if (_hasErrored) { if (_hasErrored) {
return; return;
} }
_listener._onError(error, stackTrace); _recorder._onError(error, stackTrace);
_hasErrored = true; _hasErrored = true;
} }
/// To avoid calling [Profile.shouldContinue] every time [scheduleFrame] is
/// called, we cache this value at the beginning of the frame.
bool _benchmarkStopped = false;
@override @override
void handleBeginFrame(Duration rawTimeStamp) { void handleBeginFrame(Duration rawTimeStamp) {
// Don't keep on truckin' if there's an error. // Don't keep on truckin' if there's an error or the benchmark has stopped.
if (_hasErrored) { if (_hasErrored || _benchmarkStopped) {
return; return;
} }
try { try {
_benchmarkStopped = !_listener.profile.shouldContinue();
super.handleBeginFrame(rawTimeStamp); super.handleBeginFrame(rawTimeStamp);
} catch (error, stackTrace) { } catch (error, stackTrace) {
_haltBenchmarkWithError(error, stackTrace); _haltBenchmarkWithError(error, stackTrace);
...@@ -784,22 +802,23 @@ class _RecordingWidgetsBinding extends BindingBase ...@@ -784,22 +802,23 @@ class _RecordingWidgetsBinding extends BindingBase
@override @override
void scheduleFrame() { void scheduleFrame() {
// Don't keep on truckin' if there's an error. // Don't keep on truckin' if there's an error or the benchmark has stopped.
if (!_benchmarkStopped && !_hasErrored) { if (_hasErrored || _benchmarkStopped) {
super.scheduleFrame(); return;
} }
super.scheduleFrame();
} }
@override @override
void handleDrawFrame() { void handleDrawFrame() {
// Don't keep on truckin' if there's an error. // Don't keep on truckin' if there's an error or the benchmark has stopped.
if (_hasErrored) { if (_hasErrored || _benchmarkStopped) {
return; return;
} }
try { try {
_listener.frameWillDraw(); _recorder.frameWillDraw();
super.handleDrawFrame(); super.handleDrawFrame();
_listener.frameDidDraw(); _recorder.frameDidDraw();
} catch (error, stackTrace) { } catch (error, stackTrace) {
_haltBenchmarkWithError(error, stackTrace); _haltBenchmarkWithError(error, stackTrace);
rethrow; rethrow;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment