Dispatch a Flutter.Navigation event each time navigation occurs. (#23126)

Dispatch a Flutter.Navigation event each time navigation occurs.
diff --git a/dev/devicelab/bin/tasks/service_extensions_test.dart b/dev/devicelab/bin/tasks/service_extensions_test.dart
index a76b37c..d946f40 100644
--- a/dev/devicelab/bin/tasks/service_extensions_test.dart
+++ b/dev/devicelab/bin/tasks/service_extensions_test.dart
@@ -59,6 +59,8 @@
       final VMIsolateRef isolate = vm.isolates.first;
       final Stream<VMExtensionEvent> frameEvents = isolate.onExtensionEvent.where(
               (VMExtensionEvent e) => e.kind == 'Flutter.Frame');
+      final Stream<VMExtensionEvent> navigationEvents = isolate.onExtensionEvent.where(
+              (VMExtensionEvent e) => e.kind == 'Flutter.Navigation');
 
       print('reassembling app...');
       final Future<VMExtensionEvent> frameFuture = frameEvents.first;
@@ -77,6 +79,13 @@
       expect(event.data['elapsed'] is int);
       expect(event.data['elapsed'] >= 0);
 
+      final Future<VMExtensionEvent> navigationFuture = navigationEvents.first;
+      // This tap triggers a navigation event.
+      device.tap(100, 100);
+      final VMExtensionEvent navigationEvent = await navigationFuture;
+      // Validate that there are not any fields.
+      expect(navigationEvent.data.isEmpty);
+
       run.stdin.write('q');
       final int result = await run.exitCode;
       if (result != 0)
diff --git a/dev/integration_tests/ui/lib/main.dart b/dev/integration_tests/ui/lib/main.dart
index 2b93a84..822d956 100644
--- a/dev/integration_tests/ui/lib/main.dart
+++ b/dev/integration_tests/ui/lib/main.dart
@@ -3,8 +3,41 @@
 // found in the LICENSE file.
 
 import 'package:flutter/widgets.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_driver/driver_extension.dart';
 
-void main() => runApp(const Center(child: Text(
-  'flutter drive lib/xxx.dart',
-  textDirection: TextDirection.ltr,
-)));
+void main() {
+  enableFlutterDriverExtension();
+
+  runApp(MaterialApp(
+    home: Material(
+      child: Builder(
+        builder: (BuildContext context) {
+          return FlatButton(
+            child: const Text(
+              'flutter drive lib/xxx.dart',
+              textDirection: TextDirection.ltr,
+            ),
+            onPressed: () {
+              Navigator.push<Object>(
+                context,
+                MaterialPageRoute<dynamic>(
+                  builder: (BuildContext context) {
+                    return const Material(
+                      child: Center(
+                        child: Text(
+                          'navigated here',
+                          textDirection: TextDirection.ltr,
+                        ),
+                      ),
+                    );
+                  },
+                ),
+              );
+            },
+          );
+        },
+      ),
+    ),
+  ));
+}
diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart
index 48aa6c9..f3c9324 100644
--- a/packages/flutter/lib/src/widgets/navigator.dart
+++ b/packages/flutter/lib/src/widgets/navigator.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:developer' as developer;
 
 import 'package:flutter/rendering.dart';
 import 'package:flutter/scheduler.dart';
@@ -1545,10 +1546,23 @@
     for (NavigatorObserver observer in widget.observers)
       observer.didPush(route, oldRoute);
     assert(() { _debugLocked = false; return true; }());
-    _cancelActivePointers();
+    _afterNavigation();
     return route.popped;
   }
 
+  void _afterNavigation() {
+    const bool isReleaseMode = bool.fromEnvironment('dart.vm.product');
+    if (!isReleaseMode) {
+      // This event is used by performance tools that show stats for the
+      // time interval since the last navigation event occurred ensuring that
+      // stats only reflect the current page.
+      // These tools do not need to know exactly what the new route is so no
+      // attempt is made to describe the current route as part of the event.
+      developer.postEvent('Flutter.Navigation', <String, dynamic>{});
+    }
+    _cancelActivePointers();
+  }
+
   /// Replace the current route of the navigator by pushing the given route and
   /// then disposing the previous route once the new route has finished
   /// animating in.
@@ -1597,7 +1611,7 @@
     for (NavigatorObserver observer in widget.observers)
       observer.didReplace(newRoute: newRoute, oldRoute: oldRoute);
     assert(() { _debugLocked = false; return true; }());
-    _cancelActivePointers();
+    _afterNavigation();
     return newRoute.popped;
   }
 
@@ -1650,7 +1664,7 @@
         observer.didRemove(removedRoute, oldRoute);
     }
     assert(() { _debugLocked = false; return true; }());
-    _cancelActivePointers();
+    _afterNavigation();
     return newRoute.popped;
   }
 
@@ -1800,7 +1814,7 @@
       assert(!debugPredictedWouldPop);
     }
     assert(() { _debugLocked = false; return true; }());
-    _cancelActivePointers();
+    _afterNavigation();
     return true;
   }
 
@@ -1841,7 +1855,7 @@
       observer.didRemove(route, previousRoute);
     route.dispose();
     assert(() { _debugLocked = false; return true; }());
-    _cancelActivePointers();
+    _afterNavigation();
   }
 
   /// Immediately remove a route from the navigator, and [Route.dispose] it. The