[ddc] reuse the same page/script if possible on e2e tests.

Since we changed how the test driver sets up breakpoints, we now we
have the ability to reuse a page for multiple tests.

Before, the test harness would follow this sequence:
* on each group of tests, it would call `_initSource`, which sets up
  a boostraper and code to execute on the browser. This code was
  reused by all tests in the group.
* on each test, it would indirectly call `_loadScript`, which
  navigated to brand new page for each test. If there were multiple
  tests in a group, this would load the boostraper and code once per
  test.

This CL keeps the sequence of operations the same, except, that
`_loadScript` will reuse the existing page if it already loaded the
boostraper and DDC code needed by the next test.

This greatly reduces the total running time of some of the
expresison compiler shards. On my local machine one such test took
24s before both this and the parent CL, but now takes about 5s.

Change-Id: I7774df9e83142b42efc4ad523f589758a2a4660c
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/356301
Reviewed-by: Nicholas Shahan <nshahan@google.com>
Reviewed-by: Mark Zhou <markzipan@google.com>
diff --git a/pkg/dev_compiler/test/expression_compiler/expression_compiler_e2e_suite.dart b/pkg/dev_compiler/test/expression_compiler/expression_compiler_e2e_suite.dart
index 8a7d520..57ee68a 100644
--- a/pkg/dev_compiler/test/expression_compiler/expression_compiler_e2e_suite.dart
+++ b/pkg/dev_compiler/test/expression_compiler/expression_compiler_e2e_suite.dart
@@ -29,6 +29,7 @@
   late TestExpressionCompiler compiler;
   late Uri htmlBootstrapper;
   late Uri input;
+  wip.WipScript? _script;
   Uri? inputPart;
   late Uri output;
   late Uri packagesFile;
@@ -130,6 +131,7 @@
     var scriptPath = Platform.script.normalizePath().toFilePath();
     var ddcPath = p.dirname(p.dirname(p.dirname(scriptPath)));
     output = testDir.uri.resolve('test.js');
+    _script = null;
     input = testDir.uri.resolve('test.dart');
     File(input.toFilePath())
       ..createSync()
@@ -214,7 +216,7 @@
   }
 
   sdk._debugger.registerDevtoolsFormatter();
-  // Unlike the typical app boostraper, we delay calling main until all
+  // Unlike the typical app bootstraper, we delay calling main until all
   // breakpoints are setup.
   let scheduleMain = () => {
     dart_library.start('$appName', '$uuid', '$moduleName', '$mainLibraryName', false);
@@ -256,7 +258,7 @@
   });
   let dartApplication = true;
   let scheduleMainCalled = false;
-  // Unlike the typical app boostraper, we delay calling main until all
+  // Unlike the typical app bootstraper, we delay calling main until all
   // breakpoints are setup.
   // Because AMD runs the initialization asynchronously, this may be called
   // before require.js calls the initialization below.
@@ -331,10 +333,21 @@
     expect(actualScope, expectedScope);
   }
 
+  /// Ensures the current [input] script is loaded.
+  ///
+  /// The first time an input is found, this will navigate to the bootstrap page
+  /// set up by [initSource] and return the script corresponding to [input].
+  /// Any subsequent test that uses the same input will not trigger a new
+  /// navigation, but reuse the existing script on the page.
+  ///
+  /// Reusing the script is possible because the bootstrap does not run `main`,
+  /// but instead lets the test harness start main when it has prepared all
+  /// breakpoints needed for the test.
   Future<wip.WipScript> _loadScript() =>
       tracker._watch('load-script', () => _loadScriptHelper());
 
   Future<wip.WipScript> _loadScriptHelper() async {
+    if (_script != null) return Future.value(_script);
     final scriptController = StreamController<wip.ScriptParsedEvent>();
     final consoleSub = debugger.connection.runtime.onConsoleAPICalled
         .listen((e) => printOnFailure('$e'));
@@ -356,15 +369,15 @@
 
     try {
       // Navigate to the page that will load the application code.
-      // Note: the boostrapper does not invoke the application main, but exposes
-      // a function that can be called to do so.
+      // Note: the bootstrapper does not invoke the application main, but
+      // exposes a function that can be called to do so.
       await connection.page.navigate('$htmlBootstrapper').timeout(
           Duration(seconds: 5),
           onTimeout: (() => throw Exception(
               'Unable to navigate to page bootstrap script: $htmlBootstrapper')));
 
       // Poll until the script is found, or timeout after a few seconds.
-      return (await tracker._watch(
+      return _script = (await tracker._watch(
               'find-script',
               () => scriptController.stream.first.timeout(Duration(seconds: 10),
                   onTimeout: (() => throw Exception(
@@ -379,13 +392,27 @@
     }
   }
 
+  /// Uses the debugger API to trigger the execution of the app.
+  Future<wip.RemoteObject> _scheduleMain() async {
+    final context = await executionContext.id;
+    return runtime
+        .evaluate('scheduleMain()', contextId: context)
+        .catchError((Object e) {
+      printOnFailure(e is wip.ExceptionDetails
+          ? 'Exception when calling scheduleMain: ${e.json}!'
+          : 'Uncaught exception during scheduleMain: $e');
+      throw e;
+    });
+  }
+
   /// Load the script, invoke it's main method, and run [onPause] when the app
   /// pauses on [breakpointId].
   ///
-  /// Internally, this navigates to the bootstrapper page. The page only loads
-  /// code without running the DDC app main method. Once the resouces are loaded
-  /// we wait until after the breakpoint is registered before scheduling a call
-  /// to the app's main method.
+  /// Internally, this navigates to the bootstrapper page or ensures that the
+  /// bootstrapper page has already been loaded. The page only loads code
+  /// without running the DDC app main method. Once the resouces are loaded we
+  /// wait until after the breakpoint is registered before scheduling a call to
+  /// the app's main method.
   Future<T> _onBreakpoint<T>(String breakpointId,
       {required Future<T> Function(wip.DebuggerPausedEvent) onPause}) async {
     final consoleSub = debugger.connection.runtime.onConsoleAPICalled
@@ -414,15 +441,7 @@
     final atBreakpoint = breakpointCompleter.future;
     try {
       // Now that the breakpoint is set, the application can start running.
-      final context = await executionContext.id;
-      unawaited(runtime
-          .evaluate('scheduleMain()', contextId: context)
-          .catchError((Object e) {
-        printOnFailure(e is wip.ExecutionContextDescription
-            ? 'Exception when calling scheduleMain: ${e.json}!'
-            : 'Uncaught exception during scheduleMain: $e');
-        throw e;
-      }));
+      unawaited(_scheduleMain());
 
       final event = await tracker._watch(
           'pause-event-for-line',
@@ -452,9 +471,7 @@
 
     await _loadScript();
     try {
-      // Continue running, ignoring the first pause event since it corresponds
-      // to the preemptive URI breakpoint made prior to page navigation.
-      await debugger.resume();
+      await _scheduleMain();
       return await body();
     } finally {
       await consoleSub.cancel();