Add `compiler=js` escape hatch to revert to dart2js compilation  (#9406)

diff --git a/packages/devtools_app/benchmark/test_infra/common.dart b/packages/devtools_app/benchmark/test_infra/common.dart
index 2915822..32ab643 100644
--- a/packages/devtools_app/benchmark/test_infra/common.dart
+++ b/packages/devtools_app/benchmark/test_infra/common.dart
@@ -10,7 +10,7 @@
 /// found" in DevTools.
 const _benchmarkInitialPage = '';
 
-const _wasmQueryParameters = {'wasm': 'true'};
+const _wasmQueryParameters = {'compiler': 'wasm'};
 
 String benchmarkPath({required bool useWasm}) => Uri(
   path: _benchmarkInitialPage,
diff --git a/packages/devtools_app/lib/src/shared/preferences/preferences.dart b/packages/devtools_app/lib/src/shared/preferences/preferences.dart
index 637124d..cfc9230 100644
--- a/packages/devtools_app/lib/src/shared/preferences/preferences.dart
+++ b/packages/devtools_app/lib/src/shared/preferences/preferences.dart
@@ -171,6 +171,16 @@
 
   Future<void> _initWasmEnabled() async {
     wasmEnabled.value = kIsWasm;
+
+    // If the user forced the dart2js-compiled DevTools via query parameter,
+    // then set the storage value to match. This will persist across multiple
+    // sessions of DevTools.
+    if (DevToolsQueryParams.load().useJs) {
+      safeUnawaited(
+        storage.setValue(_ExperimentPreferences.wasm.storageKey, 'false'),
+      );
+    }
+
     addAutoDisposeListener(wasmEnabled, () async {
       final enabled = wasmEnabled.value;
       _log.fine('preference update (wasmEnabled = $enabled)');
@@ -188,8 +198,8 @@
           'Reloading DevTools for Wasm preference update (enabled = $enabled)',
         );
         updateQueryParameter(
-          DevToolsQueryParams.wasmKey,
-          enabled ? 'true' : null,
+          DevToolsQueryParams.compilerKey,
+          enabled ? 'wasm' : null,
           reload: true,
         );
       }
@@ -207,7 +217,7 @@
       // back to JS. We know this because the flutter_bootstrap.js logic always
       // sets the 'wasm' query parameter to 'true' when attempting to load
       // DevTools with wasm. Remove the wasm query parameter and return early.
-      updateQueryParameter(DevToolsQueryParams.wasmKey, null);
+      updateQueryParameter(DevToolsQueryParams.compilerKey, null);
       ga.impression(gac.devToolsMain, gac.jsFallback);
 
       // Do not show the JS fallback notification when embedded in VS Code
diff --git a/packages/devtools_app/lib/src/shared/primitives/query_parameters.dart b/packages/devtools_app/lib/src/shared/primitives/query_parameters.dart
index 2ca2a62..7bd99f3 100644
--- a/packages/devtools_app/lib/src/shared/primitives/query_parameters.dart
+++ b/packages/devtools_app/lib/src/shared/primitives/query_parameters.dart
@@ -64,7 +64,14 @@
 
   /// Whether DevTools should be loaded using dart2wasm + skwasm instead of
   /// dart2js + canvaskit.
-  bool get useWasm => params[wasmKey] == 'true';
+  bool get useWasm => params[compilerKey] == 'wasm';
+
+  /// Whether DevTools should be loaded using dart2js + canvaskit instead of
+  /// dart2wasm + skwasm.
+  ///
+  /// This should only ever be explicitly set by the user if their app fails to
+  /// load using wasm.
+  bool get useJs => params[compilerKey] == 'js';
 
   static const vmServiceUriKey = 'uri';
   static const hideScreensKey = 'hide';
@@ -75,10 +82,14 @@
   static const ideKey = 'ide';
   static const ideFeatureKey = 'ideFeature';
 
-  // This query parameter must match the String value in the Flutter bootstrap
-  // logic that is used to select a web renderer. See
-  // devtools/packages/devtools_app/web/flutter_bootstrap.js.
-  static const wasmKey = 'wasm';
+  /// Query parameter key to determine whether to use dart2wasm or dart2js.
+  ///
+  ///  This query parameter must match the String value in the Flutter bootstrap
+  ///  logic that is used to select a web renderer. See
+  /// devtools/packages/devtools_app/web/flutter_bootstrap.js.
+  ///
+  /// Valid values are "js" or "wasm".
+  static const compilerKey = 'compiler';
 
   // TODO(kenz): remove legacy value in May of 2025 when all IDEs are not using
   // these and 12 months have passed to allow users ample upgrade time.
diff --git a/packages/devtools_app/web/flutter_bootstrap.js b/packages/devtools_app/web/flutter_bootstrap.js
index 2836e26..59b0b9e 100644
--- a/packages/devtools_app/web/flutter_bootstrap.js
+++ b/packages/devtools_app/web/flutter_bootstrap.js
@@ -22,9 +22,15 @@
 }
 
 // This query parameter must match the String value specified by
-// `DevToolsQueryParameters.wasmKey`. See
+// `DevToolsQueryParameters.compilerKey`. See
 // devtools/packages/devtools_app/lib/src/shared/query_parameters.dart
-const wasmQueryParameterKey = 'wasm';
+const compilerQueryParameterKey = 'compiler';
+
+// Returns the value for the given search param.
+function getSearchParam(searchParamKey) {
+  const searchParams = new URLSearchParams(window.location.search);
+  return searchParams.get(searchParamKey);
+}
 
 // Calls the DevTools server API to read the user's wasm preference.
 async function getDevToolsWasmPreference() {
@@ -49,12 +55,20 @@
   }
 }
 
+// The query parameter compiler=js gives us an escape hatch we can offer users if their
+// dart2wasm app fails to load.
+const forceUseJs = () => getSearchParam(compilerQueryParameterKey) === 'js';
+
 // Returns whether DevTools should be loaded with the skwasm renderer based on the
 // value of the 'wasm' query parameter or the wasm setting from the DevTools
 // preference file.
 async function shouldUseSkwasm() {
-  const searchParams = new URLSearchParams(window.location.search);
-  const wasmEnabledFromQueryParameter = searchParams.get(wasmQueryParameterKey) === 'true';
+  // If dart2js has specifically been requested via query parameter, then do not try to
+  // use skwasm (even if the local setting is for wasm).
+  if (forceUseJs()) {
+    return false;
+  }
+  const wasmEnabledFromQueryParameter = getSearchParam(compilerQueryParameterKey) === 'wasm';
   const wasmEnabledFromDevToolsPreference = await getDevToolsWasmPreference();
   return wasmEnabledFromQueryParameter === true || wasmEnabledFromDevToolsPreference === true;
 }
@@ -64,9 +78,9 @@
 function updateWasmQueryParameter(useSkwasm) {
   const url = new URL(window.location.href);
   if (useSkwasm) {
-    url.searchParams.set(wasmQueryParameterKey, 'true');
+    url.searchParams.set(compilerQueryParameterKey, 'wasm');
   } else {
-    url.searchParams.delete(wasmQueryParameterKey);
+    url.searchParams.delete(compilerQueryParameterKey);
   }
   // Update the browser's history without reloading. This is a no-op if the wasm
   // query parameter does not actually need to be updated.
@@ -77,9 +91,11 @@
 async function bootstrapAppFor3P() {
   const useSkwasm = await shouldUseSkwasm();
 
-  // Ensure the 'wasm' query parameter in the URL is accurate for the renderer
-  // DevTools will be loaded with.
-  updateWasmQueryParameter(useSkwasm);
+  if (!forceUseJs()) {
+    // Ensure the 'wasm' query parameter in the URL is accurate for the renderer
+    // DevTools will be loaded with.
+    updateWasmQueryParameter(useSkwasm);
+  }
 
   const rendererForLog = useSkwasm ? 'skwasm' : 'canvaskit';
   console.log('Attempting to load DevTools with ' + rendererForLog + ' renderer.');