Fix harness to not call timer repeatedly in the measured loop. (#38)

Co-authored-by: Ömer Sinan Ağacan <omersa@google.com>
Co-authored-by: Vyacheslav Egorov <vegorov@google.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c6d581f..7c808cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 2.2.0
+
+- Change measuring algorithm in `BenchmarkBase` to avoid calling stopwatch
+methods repeatedly in the measuring loop. This makes measurement work better
+for `run` methods which are small themselves.
+
 ## 2.1.0
 
 - Add AsyncBenchmarkBase.
diff --git a/lib/benchmark_harness.dart b/lib/benchmark_harness.dart
index 2809777..6170115 100644
--- a/lib/benchmark_harness.dart
+++ b/lib/benchmark_harness.dart
@@ -5,6 +5,7 @@
 library benchmark_harness;
 
 import 'dart:async';
+import 'dart:math' as math;
 
 part 'src/benchmark_base.dart';
 part 'src/async_benchmark_base.dart';
diff --git a/lib/src/benchmark_base.dart b/lib/src/benchmark_base.dart
index b8a08a7..4a6acfa 100644
--- a/lib/src/benchmark_base.dart
+++ b/lib/src/benchmark_base.dart
@@ -4,63 +4,97 @@
 
 part of benchmark_harness;
 
+const int _minimumMeasureDurationMillis = 2000;
+
 class BenchmarkBase {
   final String name;
   final ScoreEmitter emitter;
 
-  // Empty constructor.
   const BenchmarkBase(this.name, {this.emitter = const PrintEmitter()});
 
-  // The benchmark code.
-  // This function is not used, if both [warmup] and [exercise] are overwritten.
+  /// The benchmark code.
+  ///
+  /// This function is not used, if both [warmup] and [exercise] are overwritten.
   void run() {}
 
-  // Runs a short version of the benchmark. By default invokes [run] once.
+  /// Runs a short version of the benchmark. By default invokes [run] once.
   void warmup() {
     run();
   }
 
-  // Exercises the benchmark. By default invokes [run] 10 times.
+  /// Exercises the benchmark. By default invokes [run] 10 times.
   void exercise() {
     for (var i = 0; i < 10; i++) {
       run();
     }
   }
 
-  // Not measured setup code executed prior to the benchmark runs.
+  /// Not measured setup code executed prior to the benchmark runs.
   void setup() {}
 
-  // Not measures teardown code executed after the benchmark runs.
+  /// Not measured teardown code executed after the benchmark runs.
   void teardown() {}
 
-  // Measures the score for this benchmark by executing it repeatedly until
-  // time minimum has been reached.
-  static double measureFor(void Function() f, int minimumMillis) {
-    var minimumMicros = minimumMillis * 1000;
-    var iter = 0;
-    var watch = Stopwatch();
-    watch.start();
-    var elapsed = 0;
-    while (elapsed < minimumMicros) {
-      f();
-      elapsed = watch.elapsedMicroseconds;
-      iter++;
+  /// Measures the score for this benchmark by executing it enough times
+  /// to reach [minimumMillis].
+  static _Measurement _measureForImpl(void Function() f, int minimumMillis) {
+    final minimumMicros = minimumMillis * 1000;
+    var iter = 2;
+    final watch = Stopwatch()..start();
+    while (true) {
+      watch.reset();
+      for (var i = 0; i < iter; i++) {
+        f();
+      }
+      final elapsed = watch.elapsedMicroseconds;
+      final measurement = _Measurement(elapsed, iter);
+      if (measurement.elapsedMicros >= minimumMicros) {
+        return measurement;
+      }
+
+      iter = measurement.estimateIterationsNeededToReach(
+          minimumMicros: minimumMicros);
     }
-    return elapsed / iter;
   }
 
-  // Measures the score for the benchmark and returns it.
+  /// Measures the score for this benchmark by executing it repeatedly until
+  /// time minimum has been reached.
+  static double measureFor(void Function() f, int minimumMillis) =>
+      _measureForImpl(f, minimumMillis).score;
+
+  /// Measures the score for the benchmark and returns it.
   double measure() {
     setup();
     // Warmup for at least 100ms. Discard result.
-    measureFor(warmup, 100);
+    _measureForImpl(warmup, 100);
     // Run the benchmark for at least 2000ms.
-    var result = measureFor(exercise, 2000);
+    var result = _measureForImpl(exercise, _minimumMeasureDurationMillis);
     teardown();
-    return result;
+    return result.score;
   }
 
   void report() {
     emitter.emit(name, measure());
   }
 }
+
+class _Measurement {
+  final int elapsedMicros;
+  final int iterations;
+
+  _Measurement(this.elapsedMicros, this.iterations);
+
+  double get score => elapsedMicros / iterations;
+
+  int estimateIterationsNeededToReach({required int minimumMicros}) {
+    final elapsed = roundDownToMillisecond(elapsedMicros);
+    return elapsed == 0
+        ? iterations * 1000
+        : (iterations * math.max(minimumMicros / elapsed, 1.5)).ceil();
+  }
+
+  static int roundDownToMillisecond(int micros) => (micros ~/ 1000) * 1000;
+
+  @override
+  String toString() => '$elapsedMicros in $iterations iterations';
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 4c436c6..62675b9 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: benchmark_harness
-version: 2.1.0
+version: 2.2.0
 description: The official Dart project benchmark harness.
 repository: https://github.com/dart-lang/benchmark_harness