diff --git a/packages/devtools_app/lib/src/framework/app_error_handling.dart b/packages/devtools_app/lib/src/framework/app_error_handling.dart index 5cff96016b8..86292c53cf8 100644 --- a/packages/devtools_app/lib/src/framework/app_error_handling.dart +++ b/packages/devtools_app/lib/src/framework/app_error_handling.dart @@ -53,6 +53,10 @@ void setupErrorHandling(Future Function() appStartCallback) { return appStartCallback(); }, (Object error, StackTrace stack) { + // TODO(https://github.com/flutter/devtools/issues/7856): can we detect + // severe errors here that are related to dart2wasm? Otherwise we may + // crash DevTools for the user without any way for them to force reload + // with JS. reportError(error, stack: stack, errorType: 'zoneGuarded'); throw error; }, diff --git a/packages/devtools_app/lib/src/framework/framework_core.dart b/packages/devtools_app/lib/src/framework/framework_core.dart index 1d7a4208463..788745c6a85 100644 --- a/packages/devtools_app/lib/src/framework/framework_core.dart +++ b/packages/devtools_app/lib/src/framework/framework_core.dart @@ -9,6 +9,7 @@ import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; import 'package:devtools_shared/devtools_shared.dart'; import 'package:devtools_shared/service.dart'; +import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:vm_service/vm_service.dart'; @@ -46,8 +47,10 @@ abstract class FrameworkCore { await initializePlatform(); - // Print the version number at startup. - _log.info('DevTools version $devToolsVersion.'); + // Print DevTools info at startup. + _log.info( + 'Version: $devToolsVersion, Renderer: ${kIsWasm ? 'skwasm' : 'canvaskit'}', + ); await _initDTDConnection(); diff --git a/packages/devtools_app/lib/src/framework/settings_dialog.dart b/packages/devtools_app/lib/src/framework/settings_dialog.dart index f0c874b9808..fed19127e03 100644 --- a/packages/devtools_app/lib/src/framework/settings_dialog.dart +++ b/packages/devtools_app/lib/src/framework/settings_dialog.dart @@ -12,6 +12,7 @@ import '../shared/analytics/analytics_controller.dart'; import '../shared/analytics/constants.dart' as gac; import '../shared/common_widgets.dart'; import '../shared/config_specific/copy_to_clipboard/copy_to_clipboard.dart'; +import '../shared/feature_flags.dart'; import '../shared/globals.dart'; import '../shared/log_storage.dart'; import '../shared/server/server.dart'; @@ -37,6 +38,7 @@ class SettingsDialog extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); final analyticsController = Provider.of(context); return DevToolsDialog( title: const DialogTitleText('Settings'), @@ -50,6 +52,7 @@ class SettingsDialog extends StatelessWidget { title: 'Use a dark theme', notifier: preferences.darkModeEnabled, onChanged: preferences.toggleDarkModeTheme, + gaScreen: gac.settingsDialog, gaItem: gac.darkTheme, ), ), @@ -61,6 +64,7 @@ class SettingsDialog extends StatelessWidget { onChanged: (enable) => unawaited( analyticsController.toggleAnalyticsEnabled(enable), ), + gaScreen: gac.settingsDialog, gaItem: gac.analytics, ), ), @@ -69,10 +73,29 @@ class SettingsDialog extends StatelessWidget { title: 'Enable VM developer mode', notifier: preferences.vmDeveloperModeEnabled, onChanged: preferences.toggleVmDeveloperMode, + gaScreen: gac.settingsDialog, gaItem: gac.vmDeveloperMode, ), ), - const PaddedDivider(), + if (FeatureFlags.wasmOptInSetting) ...[ + const SizedBox(height: largeSpacing), + ...dialogSubHeader(theme, 'Experimental features'), + Flexible( + child: CheckboxSetting( + title: 'Enable WebAssembly', + description: + 'This will trigger a reload of the page to load DevTools ' + 'compiled with WebAssembly. This may yield better ' + 'performance.', + notifier: preferences.wasmEnabled, + onChanged: preferences.toggleWasmEnabled, + gaScreen: gac.settingsDialog, + gaItem: gac.wasm, + ), + ), + ], + const SizedBox(height: largeSpacing), + ...dialogSubHeader(theme, 'Troubleshooting'), const _VerboseLoggingSetting(), ], ), @@ -99,6 +122,7 @@ class _VerboseLoggingSetting extends StatelessWidget { title: 'Enable verbose logging', notifier: preferences.verboseLoggingEnabled, onChanged: (enable) => preferences.toggleVerboseLogging(enable), + gaScreen: gac.settingsDialog, gaItem: gac.verboseLogging, ), ), diff --git a/packages/devtools_app/lib/src/shared/analytics/constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants.dart index b33dc8fcdd5..3b8e8bcc07b 100644 --- a/packages/devtools_app/lib/src/shared/analytics/constants.dart +++ b/packages/devtools_app/lib/src/shared/analytics/constants.dart @@ -47,6 +47,10 @@ const devToolsMain = 'main'; const appDisconnected = 'appDisconnected'; const init = 'init'; +/// Event that signals we fell back to JS when trying to load DevTools with +/// Wasm. +const jsFallback = 'jsFallback'; + // DevTools UI action selected (clicked). // Main bar UX actions: @@ -114,6 +118,7 @@ const settingsDialog = 'settings'; const darkTheme = 'darkTheme'; const analytics = 'analytics'; const vmDeveloperMode = 'vmDeveloperMode'; +const wasm = 'wasm'; const verboseLogging = 'verboseLogging'; const inspectorHoverEvalMode = 'inspectorHoverEvalMode'; const clearLogs = 'clearLogs'; diff --git a/packages/devtools_app/lib/src/shared/common_widgets.dart b/packages/devtools_app/lib/src/shared/common_widgets.dart index f46ff5761f3..ac33add94e3 100644 --- a/packages/devtools_app/lib/src/shared/common_widgets.dart +++ b/packages/devtools_app/lib/src/shared/common_widgets.dart @@ -1557,7 +1557,7 @@ class CheckboxSetting extends StatelessWidget { final gaScreen = this.gaScreen; final gaItem = this.gaItem; if (gaScreen != null && gaItem != null) { - ga.select(gaScreen, gaItem); + ga.select(gaScreen, '$gaItem-$value'); } final onChanged = this.onChanged; if (onChanged != null) { diff --git a/packages/devtools_app/lib/src/shared/feature_flags.dart b/packages/devtools_app/lib/src/shared/feature_flags.dart index 1f2785e5908..319d641c01e 100644 --- a/packages/devtools_app/lib/src/shared/feature_flags.dart +++ b/packages/devtools_app/lib/src/shared/feature_flags.dart @@ -99,17 +99,23 @@ abstract class FeatureFlags { /// https://github.com/flutter/devtools/issues/7854 static bool inspectorV2 = enableExperiments; + /// Flag to enable the DevTools setting to opt-in to WASM. + /// + /// https://github.com/flutter/devtools/issues/7856 + static bool wasmOptInSetting = true; + /// Stores a map of all the feature flags for debugging purposes. /// /// When adding a new flag, you are responsible for adding it to this map as /// well. static final _allFlags = { 'widgetRebuildStats': widgetRebuildStats, - 'memoryOffline': memoryDisconnectExperience, - 'dapDebugging': dapDebugging, - 'loggingV2': loggingV2, + 'memorySaveLoad': memorySaveLoad, 'deepLinkIosCheck': deepLinkIosCheck, + 'loggingV2': loggingV2, + 'dapDebugging': dapDebugging, 'inspectorV2': inspectorV2, + 'wasmOptInSetting': wasmOptInSetting, }; /// A helper to print the status of all the feature flags. diff --git a/packages/devtools_app/lib/src/shared/preferences/preferences.dart b/packages/devtools_app/lib/src/shared/preferences/preferences.dart index 96410a6cbc3..165e4e03e87 100644 --- a/packages/devtools_app/lib/src/shared/preferences/preferences.dart +++ b/packages/devtools_app/lib/src/shared/preferences/preferences.dart @@ -16,7 +16,9 @@ import '../analytics/constants.dart' as gac; import '../config_specific/logger/logger_helpers.dart'; import '../constants.dart'; import '../diagnostics/inspector_service.dart'; +import '../feature_flags.dart'; import '../globals.dart'; +import '../query_parameters.dart'; import '../utils.dart'; part '_extension_preferences.dart'; @@ -25,8 +27,19 @@ part '_memory_preferences.dart'; part '_logging_preferences.dart'; part '_performance_preferences.dart'; +final _log = Logger('PreferencesController'); + const _thirdPartyPathSegment = 'third_party'; +/// DevTools preferences for experimental features. +enum _ExperimentPreferences { + wasm; + + String get storageKey => '$storagePrefix.$name'; + + static const storagePrefix = 'experiment'; +} + /// DevTools preferences for UI-related settings. enum _UiPreferences { darkMode, @@ -59,6 +72,10 @@ class PreferencesController extends DisposableController final vmDeveloperModeEnabled = ValueNotifier(false); + /// Whether DevTools should loaded with the dart2wasm + skwasm instead of + /// dart2js + canvaskit + final wasmEnabled = ValueNotifier(false); + final verboseLoggingEnabled = ValueNotifier(Logger.root.level == verboseLoggingLevel); @@ -83,6 +100,9 @@ class PreferencesController extends DisposableController // Get the current values and listen for and write back changes. await _initDarkMode(); await _initVmDeveloperMode(); + if (FeatureFlags.wasmOptInSetting) { + await _initWasmEnabled(); + } await _initVerboseLogging(); await inspector.init(); @@ -123,6 +143,70 @@ class PreferencesController extends DisposableController }); } + Future _initWasmEnabled() async { + wasmEnabled.value = kIsWasm; + addAutoDisposeListener(wasmEnabled, () async { + final enabled = wasmEnabled.value; + _log.fine('preference update (wasmEnabled = $enabled)'); + + await storage.setValue( + _ExperimentPreferences.wasm.storageKey, + '$enabled', + ); + + // Update the wasm mode query parameter if it does not match the value of + // the setting. + final wasmEnabledFromQueryParams = DevToolsQueryParams.load().useWasm; + if (wasmEnabledFromQueryParams != enabled) { + _log.fine( + 'Reloading DevTools for Wasm preference update (enabled = $enabled)', + ); + updateQueryParameter( + DevToolsQueryParams.wasmKey, + enabled ? 'true' : null, + reload: true, + ); + } + }); + + final enabledFromStorage = await boolValueFromStorage( + _ExperimentPreferences.wasm.storageKey, + defaultsTo: false, + ); + final queryParams = DevToolsQueryParams.load(); + final enabledFromQueryParams = queryParams.useWasm; + + if (enabledFromQueryParams && !kIsWasm) { + // If we hit this case, we tried to load DevTools with WASM but we fell + // 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); + ga.impression(gac.devToolsMain, gac.jsFallback); + + // Do not show the JS fallback notification when embedded in VS Code + // because we do not expect the WASM build to load successfully by + // default. This is because cross-origin-isolation is disabled by VS + // Code. See https://github.com/microsoft/vscode/issues/186614. + final embeddedInVsCode = + queryParams.embedMode.embedded && queryParams.ide == 'VSCode'; + if (!embeddedInVsCode) { + notificationService.push( + 'Something went wrong when trying to load DevTools with WebAssembly. ' + 'Falling back to Javascript.', + ); + } + return; + } + + final shouldEnableWasm = enabledFromStorage || enabledFromQueryParams; + assert(kIsWasm == shouldEnableWasm); + // This should be a no-op if the flutter_bootstrap.js logic set the + // renderer properly, but we call this to be safe in case something went + // wrong. + toggleWasmEnabled(shouldEnableWasm); + } + Future _initVerboseLogging() async { final verboseLoggingEnabledValue = await boolValueFromStorage( _GeneralPreferences.verboseLogging.name, @@ -162,6 +246,13 @@ class PreferencesController extends DisposableController } } + /// Change the value of the wasm mode setting. + void toggleWasmEnabled(bool? enable) { + if (enable != null) { + wasmEnabled.value = enable; + } + } + void toggleVerboseLogging(bool? enableVerboseLogging) { if (enableVerboseLogging != null) { verboseLoggingEnabled.value = enableVerboseLogging; diff --git a/packages/devtools_app/lib/src/shared/query_parameters.dart b/packages/devtools_app/lib/src/shared/query_parameters.dart index 24f2237aced..8fa6d34b956 100644 --- a/packages/devtools_app/lib/src/shared/query_parameters.dart +++ b/packages/devtools_app/lib/src/shared/query_parameters.dart @@ -62,6 +62,10 @@ extension type DevToolsQueryParams(Map params) { /// The current [EmbedMode] of DevTools based on the query parameters. EmbedMode get embedMode => ideThemeParams.embedMode; + /// Whether DevTools should be loaded using dart2wasm + skwasm instead of + /// dart2js + canvaskit. + bool get useWasm => params[wasmKey] == 'true'; + static const vmServiceUriKey = 'uri'; static const hideScreensKey = 'hide'; static const hideExtensionsValue = 'extensions'; @@ -71,6 +75,11 @@ extension type DevToolsQueryParams(Map params) { 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'; + // 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. String? get legacyPage => params[legacyPageKey]; diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 1c8e1e370d3..fa99239b317 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -13,6 +13,11 @@ To learn more about DevTools, check out the * Fixed a bug that was causing the DevTools release notes to always show. - [#8277](https://github.com/flutter/devtools/pull/8277) +* Add a setting that allows users to opt-in to loading DevTools +with WebAssembly. - [#8270](https://github.com/flutter/devtools/pull/8270) + + ![Wasm opt-in setting](images/wasm_setting.png "DevTools setting to opt into wasm.") + ## Inspector updates TODO: Remove this section if there are not any general updates. diff --git a/packages/devtools_app/release_notes/images/wasm_setting.png b/packages/devtools_app/release_notes/images/wasm_setting.png new file mode 100644 index 00000000000..73bbc659781 Binary files /dev/null and b/packages/devtools_app/release_notes/images/wasm_setting.png differ diff --git a/packages/devtools_app/test/shared/primitives/feature_flags_test.dart b/packages/devtools_app/test/shared/primitives/feature_flags_test.dart index cf6f0bb7824..caceaab59e1 100644 --- a/packages/devtools_app/test/shared/primitives/feature_flags_test.dart +++ b/packages/devtools_app/test/shared/primitives/feature_flags_test.dart @@ -11,5 +11,11 @@ void main() { expect(enableExperiments, false); expect(enableBeta, false); expect(isExternalBuild, true); + expect(FeatureFlags.memorySaveLoad, false); + expect(FeatureFlags.deepLinkIosCheck, false); + expect(FeatureFlags.loggingV2, false); + expect(FeatureFlags.dapDebugging, false); + expect(FeatureFlags.inspectorV2, false); + expect(FeatureFlags.wasmOptInSetting, true); }); } diff --git a/packages/devtools_app/web/flutter_bootstrap.js b/packages/devtools_app/web/flutter_bootstrap.js index bcb75f5cb1a..e63cd54c178 100644 --- a/packages/devtools_app/web/flutter_bootstrap.js +++ b/packages/devtools_app/web/flutter_bootstrap.js @@ -21,14 +21,74 @@ function unregisterDevToolsServiceWorker() { } } +// This query parameter must match the String value specified by +// `DevToolsQueryParameters.wasmKey`. See +// devtools/packages/devtools_app/lib/src/shared/query_parameters.dart +const wasmQueryParameterKey = 'wasm'; + +// Calls the DevTools server API to read the user's wasm preference. +async function getDevToolsWasmPreference() { + const request = 'api/getPreferenceValue?key=experiment.wasm'; + try { + const response = await fetch(request); + if (!response.ok) { + console.warn(`[${response.status} response] ${request}`); + return false; + } + + // The response text should be an encoded boolean value ("true" or "false"). + const wasmEnabled = JSON.parse(await response.text()); + return wasmEnabled === true || wasmEnabled === 'true'; + } catch (error) { + console.error('Error fetching experiment.wasm preference value:', error); + return false; + } +} + +// 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'; + const wasmEnabledFromDevToolsPreference = await getDevToolsWasmPreference(); + return wasmEnabledFromQueryParameter === true || wasmEnabledFromDevToolsPreference === true; +} + +// Sets or removes the 'wasm' query parameter based on whether DevTools should +// be loaded with the skwasm renderer. +function updateWasmQueryParameter(useSkwasm) { + const url = new URL(window.location.href); + if (useSkwasm) { + url.searchParams.set(wasmQueryParameterKey, 'true'); + } else { + url.searchParams.delete(wasmQueryParameterKey); + } + // Update the browser's history without reloading. This is a no-op if the wasm + // query parameter does not actually need to be updated. + window.history.pushState({}, '', url); +} + // Bootstrap app for 3P environments: -function bootstrapAppFor3P() { +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); + + const rendererForLog = useSkwasm ? 'skwasm' : 'canvaskit'; + console.log('Attempting to load DevTools with ' + rendererForLog + ' renderer.'); + + const rendererConfig = useSkwasm ? {} : { renderer: 'canvaskit' }; + console.log(rendererConfig); _flutter.loader.load({ serviceWorkerSettings: { serviceWorkerVersion: {{flutter_service_worker_version}}, }, config: { - canvasKitBaseUrl: 'canvaskit/' + canvasKitBaseUrl: 'canvaskit/', + ...rendererConfig, } }); }