Add removeListenerWhileNotifying benchmark for ChangeNotifier (#78161)

diff --git a/dev/benchmarks/microbenchmarks/lib/foundation/change_notifier_bench.dart b/dev/benchmarks/microbenchmarks/lib/foundation/change_notifier_bench.dart
index 64dfdaa..0ab7a40 100644
--- a/dev/benchmarks/microbenchmarks/lib/foundation/change_notifier_bench.dart
+++ b/dev/benchmarks/microbenchmarks/lib/foundation/change_notifier_bench.dart
@@ -6,108 +6,178 @@
 
 import '../common.dart';
 
-const int _kNumIterations = 1000;
-const double _scale = 1000.0 / _kNumIterations;
+const int _kNumIterations = 65536;
 const int _kNumWarmUp = 100;
+const int _kScale = 1000;
 
 void main() {
   assert(false, "Don't run benchmarks in checked mode! Use 'flutter run --release'.");
 
-  void listener() {}
-  void listener2() {}
-  void listener3() {}
-  void listener4() {}
-  void listener5() {}
+  // In the following benchmarks, we won't remove the listeners when we don't
+  // want to measure removeListener because we know that everything will be
+  // GC'ed in the end.
+  // Not removing listeners would cause memory leaks in a real application.
 
-  // Warm up lap
-  for (int i = 0; i < _kNumWarmUp; i += 1) {
-    _Notifier()
-      ..addListener(listener)
-      ..addListener(listener2)
-      ..addListener(listener3)
-      ..addListener(listener4)
-      ..addListener(listener5)
-      ..notify()
-      ..removeListener(listener)
-      ..removeListener(listener2)
-      ..removeListener(listener3)
-      ..removeListener(listener4)
-      ..removeListener(listener5);
-  }
-
-  final Stopwatch addListenerWatch = Stopwatch();
-  final Stopwatch removeListenerWatch = Stopwatch();
-  final Stopwatch notifyListenersWatch = Stopwatch();
   final BenchmarkResultPrinter printer = BenchmarkResultPrinter();
 
-  for (int listenersCount = 0; listenersCount <= 5; listenersCount++) {
+  void runAddListenerBenchmark(int iteration, {bool addResult = true}) {
+    const String name = 'add';
+    for (int listenerCount = 1; listenerCount <= 5; listenerCount += 1) {
+      final List<_Notifier> notifiers = List<_Notifier>.generate(
+        iteration,
+        (_) => _Notifier(),
+        growable: false,
+      );
 
-    for (int j = 0; j < _kNumIterations; j += 1) {
-      final _Notifier notifier = _Notifier();
-      addListenerWatch.start();
-
-      notifier.addListener(listener);
-      if (listenersCount > 1)
-        notifier.addListener(listener2);
-      if (listenersCount > 2)
-        notifier.addListener(listener3);
-      if (listenersCount > 3)
-        notifier.addListener(listener4);
-      if (listenersCount > 4)
-        notifier.addListener(listener5);
-
-      addListenerWatch.stop();
-      notifyListenersWatch.start();
-
-      notifier.notify();
-
-      notifyListenersWatch.stop();
-      removeListenerWatch.start();
-
-      // Remove listeners in reverse order to evaluate the worse-case scenario:
-      // the listener removed is the last listener
-      if (listenersCount > 4)
-        notifier.removeListener(listener5);
-      if (listenersCount > 3)
-        notifier.removeListener(listener4);
-      if (listenersCount > 2)
-        notifier.removeListener(listener3);
-      if (listenersCount > 1)
-        notifier.removeListener(listener2);
-      notifier.removeListener(listener);
-
-      removeListenerWatch.stop();
+      final Stopwatch watch = Stopwatch();
+      watch.start();
+      for (int i = 0; i < iteration; i += 1) {
+        for (int l = 0; l < listenerCount; l += 1) {
+          notifiers[i].addListener(() {});
+        }
+      }
+      watch.stop();
+      final int elapsed = watch.elapsedMicroseconds;
+      final double averagePerIteration = elapsed / iteration;
+      if (addResult)
+        printer.addResult(
+          description: '$name ($listenerCount listeners)',
+          value: averagePerIteration * _kScale,
+          unit: 'ns per iteration',
+          name: '$name$listenerCount',
+        );
     }
-
-    final int notifyListener = notifyListenersWatch.elapsedMicroseconds;
-    notifyListenersWatch.reset();
-    final int addListenerElapsed = addListenerWatch.elapsedMicroseconds;
-    addListenerWatch.reset();
-    final int removeListenerElapsed = removeListenerWatch.elapsedMicroseconds;
-    removeListenerWatch.reset();
-
-    printer.addResult(
-      description: 'addListener ($listenersCount listeners)',
-      value: addListenerElapsed * _scale,
-      unit: 'ns per iteration',
-      name: 'addListener${listenersCount}_iteration',
-    );
-
-    printer.addResult(
-      description: 'removeListener ($listenersCount listeners)',
-      value: removeListenerElapsed * _scale,
-      unit: 'ns per iteration',
-      name: 'removeListener${listenersCount}_iteration',
-    );
-
-    printer.addResult(
-      description: 'notifyListener ($listenersCount listeners)',
-      value: notifyListener * _scale,
-      unit: 'ns per iteration',
-      name: 'notifyListener${listenersCount}_iteration',
-    );
   }
 
+  void runNotifyListenerBenchmark(int iteration, {bool addResult = true}) {
+    const String name = 'notify';
+
+    for (int listenerCount = 0; listenerCount <= 5; listenerCount += 1) {
+      final _Notifier notifier = _Notifier();
+      for (int i = 1; i <= listenerCount; i += 1) {
+        notifier.addListener(() {});
+      }
+      final Stopwatch watch = Stopwatch();
+      watch.start();
+      for (int i = 0; i < iteration; i += 1) {
+        notifier.notify();
+      }
+      watch.stop();
+      final int elapsed = watch.elapsedMicroseconds;
+      final double averagePerIteration = elapsed / iteration;
+      if (addResult)
+        printer.addResult(
+          description: '$name ($listenerCount listeners)',
+          value: averagePerIteration * _kScale,
+          unit: 'ns per iteration',
+          name: '$name$listenerCount',
+        );
+    }
+  }
+
+  void runRemoveListenerBenchmark(int iteration, {bool addResult = true}) {
+    const String name = 'remove';
+    final List<VoidCallback> listeners = <VoidCallback>[
+      () {},
+      () {},
+      () {},
+      () {},
+      () {},
+    ];
+    for (int listenerCount = 1; listenerCount <= 5; listenerCount += 1) {
+      final List<_Notifier> notifiers = List<_Notifier>.generate(
+        iteration,
+        (_) {
+          final _Notifier notifier = _Notifier();
+          for (int l = 0; l < listenerCount; l += 1) {
+            notifier.addListener(listeners[l]);
+          }
+          return notifier;
+        },
+        growable: false,
+      );
+
+      final Stopwatch watch = Stopwatch();
+      watch.start();
+      for (int i = 0; i < iteration; i += 1) {
+        for (int l = 0; l < listenerCount; l += 1) {
+          notifiers[i].removeListener(listeners[l]);
+        }
+      }
+      watch.stop();
+      final int elapsed = watch.elapsedMicroseconds;
+      final double averagePerIteration = elapsed / iteration;
+      if (addResult)
+        printer.addResult(
+          description: '$name ($listenerCount listeners)',
+          value: averagePerIteration * _kScale,
+          unit: 'ns per iteration',
+          name: '$name$listenerCount',
+        );
+    }
+  }
+
+  void runRemoveListenerWhileNotifyingBenchmark(int iteration,
+      {bool addResult = true}) {
+    const String name = 'removeWhileNotify';
+
+    final List<VoidCallback> listeners = <VoidCallback>[
+      () {},
+      () {},
+      () {},
+      () {},
+      () {},
+    ];
+    for (int listenerCount = 1; listenerCount <= 5; listenerCount += 1) {
+      final List<_Notifier> notifiers = List<_Notifier>.generate(
+        iteration,
+        (_) {
+          final _Notifier notifier = _Notifier();
+          notifier.addListener(() {
+            // This listener will remove all other listeners. So that only this
+            // one is called and measured.
+            for (int l = 0; l < listenerCount; l += 1) {
+              notifier.removeListener(listeners[l]);
+            }
+          });
+          for (int l = 0; l < listenerCount; l += 1) {
+            notifier.addListener(listeners[l]);
+          }
+          return notifier;
+        },
+        growable: false,
+      );
+
+      final Stopwatch watch = Stopwatch();
+      watch.start();
+      for (int i = 0; i < iteration; i += 1) {
+        notifiers[i].notify();
+      }
+      watch.stop();
+      final int elapsed = watch.elapsedMicroseconds;
+      final double averagePerIteration = elapsed / iteration;
+      if (addResult)
+        printer.addResult(
+          description: '$name ($listenerCount listeners)',
+          value: averagePerIteration * _kScale,
+          unit: 'ns per iteration',
+          name: '$name$listenerCount',
+        );
+    }
+  }
+
+  runAddListenerBenchmark(_kNumWarmUp, addResult: false);
+  runAddListenerBenchmark(_kNumIterations, addResult: true);
+
+  runNotifyListenerBenchmark(_kNumWarmUp, addResult: false);
+  runNotifyListenerBenchmark(_kNumIterations, addResult: true);
+
+  runRemoveListenerBenchmark(_kNumWarmUp, addResult: false);
+  runRemoveListenerBenchmark(_kNumIterations, addResult: true);
+
+  runRemoveListenerWhileNotifyingBenchmark(_kNumWarmUp, addResult: false);
+  runRemoveListenerWhileNotifyingBenchmark(_kNumIterations, addResult: true);
+
   printer.printToStdout();
 }