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

diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart
index b21908b..12a5530 100644
--- a/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart
@@ -305,8 +305,7 @@
 ///   }
 /// }
 /// ```
-abstract class WidgetRecorder extends Recorder
-    implements RecordingWidgetsBindingListener {
+abstract class WidgetRecorder extends Recorder implements FrameRecorder {
   WidgetRecorder({@required String name}) : super._(name);
 
   /// Creates a widget to be benchmarked.
@@ -318,9 +317,10 @@
   Widget createWidget();
 
   @override
-  Profile profile;
+  VoidCallback didStop;
 
-  final Completer<Profile> _profileCompleter = Completer<Profile>();
+  Profile profile;
+  Completer<void> _runCompleter;
 
   Stopwatch _drawFrameStopwatch;
 
@@ -340,27 +340,32 @@
     if (profile.shouldContinue()) {
       window.scheduleFrame();
     } else {
-      _profileCompleter.complete(profile);
+      didStop();
+      _runCompleter.complete();
     }
   }
 
   @override
   void _onError(dynamic error, StackTrace stackTrace) {
-    _profileCompleter.completeError(error, stackTrace);
+    _runCompleter.completeError(error, stackTrace);
   }
 
   @override
-  Future<Profile> run() {
-    profile = Profile(name: name);
+  Future<Profile> run() async {
+    _runCompleter = Completer<void>();
+    final Profile localProfile = profile = Profile(name: name);
     final _RecordingWidgetsBinding binding =
         _RecordingWidgetsBinding.ensureInitialized();
     final Widget widget = createWidget();
     binding._beginRecording(this, widget);
 
-    _profileCompleter.future.whenComplete(() {
+    try {
+      await _runCompleter.future;
+      return localProfile;
+    } finally {
+      _runCompleter = null;
       profile = null;
-    });
-    return _profileCompleter.future;
+    }
   }
 }
 
@@ -371,8 +376,7 @@
 /// another frame that clears the screen. It repeats this process, measuring the
 /// performance of frames that render the widget and ignoring the frames that
 /// clear the screen.
-abstract class WidgetBuildRecorder extends Recorder
-    implements RecordingWidgetsBindingListener {
+abstract class WidgetBuildRecorder extends Recorder implements FrameRecorder {
   WidgetBuildRecorder({@required String name}) : super._(name);
 
   /// Creates a widget to be benchmarked.
@@ -383,9 +387,10 @@
   Widget createWidget();
 
   @override
-  Profile profile;
+  VoidCallback didStop;
 
-  final Completer<Profile> _profileCompleter = Completer<Profile>();
+  Profile profile;
+  Completer<void> _runCompleter;
 
   Stopwatch _drawFrameStopwatch;
 
@@ -427,26 +432,31 @@
       showWidget = !showWidget;
       _hostState._setStateTrampoline();
     } else {
-      _profileCompleter.complete(profile);
+      didStop();
+      _runCompleter.complete();
     }
   }
 
   @override
   void _onError(dynamic error, StackTrace stackTrace) {
-    _profileCompleter.completeError(error, stackTrace);
+    _runCompleter.completeError(error, stackTrace);
   }
 
   @override
-  Future<Profile> run() {
-    profile = Profile(name: name);
+  Future<Profile> run() async {
+    _runCompleter = Completer<void>();
+    final Profile localProfile = profile = Profile(name: name);
     final _RecordingWidgetsBinding binding =
         _RecordingWidgetsBinding.ensureInitialized();
     binding._beginRecording(this, _WidgetBuildRecorderHost(this));
 
-    _profileCompleter.future.whenComplete(() {
+    try {
+      await _runCompleter.future;
+      return localProfile;
+    } finally {
+      _runCompleter = null;
       profile = null;
-    });
-    return _profileCompleter.future;
+    }
   }
 }
 
@@ -698,9 +708,10 @@
 
 /// Implemented by recorders that use [_RecordingWidgetsBinding] to receive
 /// frame life-cycle calls.
-abstract class RecordingWidgetsBindingListener {
-  /// The profile where the benchmark is collecting metrics.
-  Profile profile;
+abstract class FrameRecorder {
+  /// Called by the recorder when it stops recording and doesn't need to collect
+  /// any more data.
+  set didStop(VoidCallback cb);
 
   /// Called just before calling [SchedulerBinding.handleDrawFrame].
   void frameWillDraw();
@@ -739,19 +750,31 @@
     return WidgetsBinding.instance as _RecordingWidgetsBinding;
   }
 
-  RecordingWidgetsBindingListener _listener;
+  FrameRecorder _recorder;
   bool _hasErrored = false;
 
-  void _beginRecording(
-      RecordingWidgetsBindingListener recorder, Widget widget) {
+  /// To short-circuit all frame lifecycle methods when the benchmark has
+  /// 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;
 
+    recorder.didStop = () {
+      _benchmarkStopped = true;
+    };
+
     // Fail hard and fast on errors. Benchmarks should not have any errors.
     FlutterError.onError = (FlutterErrorDetails details) {
       _haltBenchmarkWithError(details.exception, details.stack);
       originalOnError(details);
     };
-    _listener = recorder;
+    _recorder = recorder;
     runApp(widget);
   }
 
@@ -759,22 +782,17 @@
     if (_hasErrored) {
       return;
     }
-    _listener._onError(error, stackTrace);
+    _recorder._onError(error, stackTrace);
     _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
   void handleBeginFrame(Duration rawTimeStamp) {
-    // Don't keep on truckin' if there's an error.
-    if (_hasErrored) {
+    // Don't keep on truckin' if there's an error or the benchmark has stopped.
+    if (_hasErrored || _benchmarkStopped) {
       return;
     }
     try {
-      _benchmarkStopped = !_listener.profile.shouldContinue();
       super.handleBeginFrame(rawTimeStamp);
     } catch (error, stackTrace) {
       _haltBenchmarkWithError(error, stackTrace);
@@ -784,22 +802,23 @@
 
   @override
   void scheduleFrame() {
-    // Don't keep on truckin' if there's an error.
-    if (!_benchmarkStopped && !_hasErrored) {
-      super.scheduleFrame();
+    // Don't keep on truckin' if there's an error or the benchmark has stopped.
+    if (_hasErrored || _benchmarkStopped) {
+      return;
     }
+    super.scheduleFrame();
   }
 
   @override
   void handleDrawFrame() {
-    // Don't keep on truckin' if there's an error.
-    if (_hasErrored) {
+    // Don't keep on truckin' if there's an error or the benchmark has stopped.
+    if (_hasErrored || _benchmarkStopped) {
       return;
     }
     try {
-      _listener.frameWillDraw();
+      _recorder.frameWillDraw();
       super.handleDrawFrame();
-      _listener.frameDidDraw();
+      _recorder.frameDidDraw();
     } catch (error, stackTrace) {
       _haltBenchmarkWithError(error, stackTrace);
       rethrow;