improve web benchmark error reporting (#51490)

diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_build_material_checkbox.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_build_material_checkbox.dart
index 94b018b..8ced6c4 100644
--- a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_build_material_checkbox.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_build_material_checkbox.dart
@@ -18,10 +18,15 @@
 
   @override
   Widget createWidget() {
-    return Column(
-      children: List<Widget>.generate(10, (int i) {
-        return _buildRow();
-      }),
+    return Directionality(
+      textDirection: TextDirection.ltr,
+      child: Material(
+        child: Column(
+          children: List<Widget>.generate(10, (int i) {
+            return _buildRow();
+          }),
+        ),
+      ),
     );
   }
 
diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart
index d47e4cb..d93d89a 100644
--- a/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart
@@ -219,6 +219,11 @@
   }
 
   @override
+  void _onError(dynamic error, StackTrace stackTrace) {
+    _profileCompleter.completeError(error, stackTrace);
+  }
+
+  @override
   Future<Profile> run() {
     final _RecordingWidgetsBinding binding =
         _RecordingWidgetsBinding.ensureInitialized();
@@ -290,6 +295,11 @@
   }
 
   @override
+  void _onError(dynamic error, StackTrace stackTrace) {
+    _profileCompleter.completeError(error, stackTrace);
+  }
+
+  @override
   Future<Profile> run() {
     final _RecordingWidgetsBinding binding =
         _RecordingWidgetsBinding.ensureInitialized();
@@ -512,9 +522,19 @@
 /// Implemented by recorders that use [_RecordingWidgetsBinding] to receive
 /// frame life-cycle calls.
 abstract class _RecordingWidgetsBindingListener {
+  /// Whether the binding should continue pumping frames.
   bool _shouldContinue();
+
+  /// Called just before calling [SchedulerBinding.handleDrawFrame].
   void _frameWillDraw();
+
+  /// Called immediately after calling [SchedulerBinding.handleDrawFrame].
   void _frameDidDraw();
+
+  /// Reports an error.
+  ///
+  /// The implementation is expected to halt benchmark execution as soon as possible.
+  void _onError(dynamic error, StackTrace stackTrace);
 }
 
 /// A variant of [WidgetsBinding] that collaborates with a [Recorder] to decide
@@ -543,8 +563,20 @@
   }
 
   _RecordingWidgetsBindingListener _listener;
+  bool _hasErrored = false;
 
   void _beginRecording(_RecordingWidgetsBindingListener recorder, Widget widget) {
+    final FlutterExceptionHandler originalOnError = FlutterError.onError;
+
+    // Fail hard and fast on errors. Benchmarks should not have any errors.
+    FlutterError.onError = (FlutterErrorDetails details) {
+      if (_hasErrored) {
+        return;
+      }
+      _listener._onError(details.exception, details.stack);
+      _hasErrored = true;
+      originalOnError(details);
+    };
     _listener = recorder;
     runApp(widget);
   }
@@ -555,19 +587,28 @@
 
   @override
   void handleBeginFrame(Duration rawTimeStamp) {
+    // Don't keep on truckin' if there's an error.
+    if (_hasErrored) {
+      return;
+    }
     _benchmarkStopped = !_listener._shouldContinue();
     super.handleBeginFrame(rawTimeStamp);
   }
 
   @override
   void scheduleFrame() {
-    if (!_benchmarkStopped) {
+    // Don't keep on truckin' if there's an error.
+    if (!_benchmarkStopped && !_hasErrored) {
       super.scheduleFrame();
     }
   }
 
   @override
   void handleDrawFrame() {
+    // Don't keep on truckin' if there's an error.
+    if (_hasErrored) {
+      return;
+    }
     _listener._frameWillDraw();
     super.handleDrawFrame();
     _listener._frameDidDraw();
diff --git a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart
index d2a050d..b6c38c4 100644
--- a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart
@@ -63,23 +63,38 @@
   }
 
   final Recorder recorder = recorderFactory();
-  final Profile profile = await recorder.run();
 
-  if (!isInManualMode) {
-    final html.HttpRequest request = await html.HttpRequest.request(
-      '/profile-data',
+  try {
+    final Profile profile = await recorder.run();
+    if (!isInManualMode) {
+      final html.HttpRequest request = await html.HttpRequest.request(
+        '/profile-data',
+        method: 'POST',
+        mimeType: 'application/json',
+        sendData: json.encode(profile.toJson()),
+      );
+      if (request.status != 200) {
+        throw Exception(
+          'Failed to report profile data to benchmark server. '
+          'The server responded with status code ${request.status}.'
+        );
+      }
+    } else {
+      print(profile);
+    }
+  } catch (error, stackTrace) {
+    if (isInManualMode) {
+      rethrow;
+    }
+    await html.HttpRequest.request(
+      '/on-error',
       method: 'POST',
       mimeType: 'application/json',
-      sendData: json.encode(profile.toJson()),
+      sendData: json.encode(<String, dynamic>{
+        'error': '$error',
+        'stackTrace': '$stackTrace',
+      }),
     );
-    if (request.status != 200) {
-      throw Exception(
-        'Failed to report profile data to benchmark server. '
-        'The server responded with status code ${request.status}.'
-      );
-    }
-  } else {
-    print(profile);
   }
 }
 
diff --git a/dev/devicelab/lib/tasks/web_benchmarks.dart b/dev/devicelab/lib/tasks/web_benchmarks.dart
index 4c8cb43..ee1da2d 100644
--- a/dev/devicelab/lib/tasks/web_benchmarks.dart
+++ b/dev/devicelab/lib/tasks/web_benchmarks.dart
@@ -53,6 +53,12 @@
         }
         collectedProfiles.add(profile);
         return Response.ok('Profile received');
+      } else if (request.requestedUri.path.endsWith('/on-error')) {
+        final Map<String, dynamic> errorDetails = json.decode(await request.readAsString()) as Map<String, dynamic>;
+        server.close();
+        // Keep the stack trace as a string. It's thrown in the browser, not this Dart VM.
+        profileData.completeError('${errorDetails['error']}\n${errorDetails['stackTrace']}');
+        return Response.ok('');
       } else if (request.requestedUri.path.endsWith('/next-benchmark')) {
         if (benchmarks == null) {
           benchmarks = (json.decode(await request.readAsString()) as List<dynamic>).cast<String>();