Golem harness for measuring startup time and frame time
diff --git a/benchmarks/lib/complex.dart b/benchmarks/lib/complex.dart
index b473f1f..496d34e 100644
--- a/benchmarks/lib/complex.dart
+++ b/benchmarks/lib/complex.dart
@@ -1,3 +1,7 @@
+// Copyright 2023 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
 // @dart=2.12
 
 import 'dart:math' as math;
@@ -6,10 +10,13 @@
 import 'package:flute/cupertino.dart';
 import 'package:flute/material.dart';
 
+import 'harness.dart';
+
 const int maxDepth = 6;
 final math.Random random = math.Random(0);
 
-void main() {
+void main(List<String> args) {
+  initializeBenchmarkHarness('Complex', args);
   ui.initializeEngine(
     screenSize: const Size(3840, 2160),  // 4k
   );
diff --git a/benchmarks/lib/counter.dart b/benchmarks/lib/counter.dart
index 8703c1b..c8d46a8 100644
--- a/benchmarks/lib/counter.dart
+++ b/benchmarks/lib/counter.dart
@@ -1,7 +1,14 @@
+// Copyright 2023 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
 import 'package:engine/ui.dart' as ui;
 import 'package:flute/material.dart';
 
-void main() {
+import 'harness.dart';
+
+void main(List<String> args) {
+  initializeBenchmarkHarness('Counter', args);
   ui.initializeEngine(
     screenSize: const Size(3840, 2160),  // 4k
   );
diff --git a/benchmarks/lib/harness.dart b/benchmarks/lib/harness.dart
new file mode 100644
index 0000000..d669570
--- /dev/null
+++ b/benchmarks/lib/harness.dart
@@ -0,0 +1,64 @@
+// Copyright 2023 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:engine/ui.dart' show PlatformDispatcher;
+
+void initializeBenchmarkHarness(String name, List<String> args) {
+  final int startOfMain = DateTime.now().microsecondsSinceEpoch;
+
+  if (args.isEmpty) {
+    // Run in default mode, rather than as a Golem benchmark.
+    return;
+  }
+
+  // First argument is time since epoch as measured just prior to launching the
+  // benchmark, in seconds, formatted as a decimal number, i.e. the output of
+  // `date '+%s.%N'`.
+  final int beforeStart = (double.parse(args[0]) * 1000000).toInt();
+  final int timeToMain = startOfMain - beforeStart;
+  print('$name.TimeToMain(StartupTime): $timeToMain us.');
+
+  // Second argument (optional) is number of frames to measure, default 1000.
+  final int framesToTime = args.length < 2 ? 1000 : int.parse(args[1]);
+
+  // Third argument (optional) is number of frames to skip before starting the
+  // measurement, default 10.
+  final int framesToSkip = args.length < 3 ? 10 : int.parse(args[2]);
+
+  double buildSum = 0;
+  double drawSum = 0;
+
+  bool averagesPrinted = false;
+
+  bool frameCallback(int frameCount, double buildTime, double drawTime) {
+    if (frameCount == 1) {
+      final int afterFirstFrame = DateTime.now().microsecondsSinceEpoch;
+      final int timeToFirstFrame = afterFirstFrame - beforeStart;
+      print('$name.TimeToFirstFrame(StartupTime): $timeToFirstFrame us.');
+    }
+
+    if (frameCount > framesToSkip + framesToTime) {
+      if (!averagesPrinted) {
+        final int averageBuild = (buildSum / framesToTime * 1000).toInt();
+        print('$name.AverageBuild(RunTimeRaw): $averageBuild us.');
+        final int averageDraw = (drawSum / framesToTime * 1000).toInt();
+        print('$name.AverageDraw(RunTimeRaw): $averageDraw us.');
+        final int averageFrame =
+            ((buildSum + drawSum) / framesToTime * 1000).toInt();
+        print('$name.AverageFrame(RunTime): $averageFrame us.');
+        averagesPrinted = true;
+      }
+      return false;
+    }
+
+    if (frameCount > framesToSkip) {
+      buildSum += buildTime;
+      drawSum += drawTime;
+    }
+
+    return true;
+  }
+
+  PlatformDispatcher.frameCallback = frameCallback;
+}
diff --git a/engine/lib/src/platform_dispatcher.dart b/engine/lib/src/platform_dispatcher.dart
index d7837b0..2355aca 100644
--- a/engine/lib/src/platform_dispatcher.dart
+++ b/engine/lib/src/platform_dispatcher.dart
@@ -546,9 +546,17 @@
 
   Timer? _frameTimer;
   Duration _frameTime = Duration.zero;
+  bool _continue = true;
 
   static int _frameCount = 1;
 
+  // Frame callback function that can be injected by the benchmark harness.
+  static bool Function(int, double, double) frameCallback =
+      (int frameCount, double buildTime, double drawTime) {
+    print('Frame #$frameCount: build $buildTime ms; draw $drawTime ms');
+    return true;
+  };
+
   /// Requests that, at the next appropriate opportunity, the [onBeginFrame] and
   /// [onDrawFrame] callbacks be invoked.
   ///
@@ -560,7 +568,9 @@
     _frameTimer ??= Timer(
       const Duration(milliseconds: 16),
       () {
-        _frameTimer = null;
+        if (_continue) {
+          _frameTimer = null;
+        }
         final int microseconds = _frameTime.inMicroseconds;
         _frameTime += const Duration(milliseconds: 16);
         Timer.run(() {
@@ -571,7 +581,10 @@
             final Stopwatch drawSw = Stopwatch()..start();
             _drawFrame();
             drawSw.stop();
-            print('Frame #$_frameCount: build ${beginSw.elapsedMicroseconds / 1000} ms; draw ${drawSw.elapsedMicroseconds / 1000} ms');
+            _continue = frameCallback(
+                _frameCount,
+                beginSw.elapsedMicroseconds / 1000,
+                drawSw.elapsedMicroseconds / 1000);
             _frameCount += 1;
           });
         });