diff --git a/config/gni/devtools_grd_files.gni b/config/gni/devtools_grd_files.gni index 55953d67917..932b7811bc3 100644 --- a/config/gni/devtools_grd_files.gni +++ b/config/gni/devtools_grd_files.gni @@ -831,6 +831,7 @@ grd_files_debug_sources = [ "front_end/entrypoints/main/ExecutionContextSelector.js", "front_end/entrypoints/main/MainImpl.js", "front_end/entrypoints/main/SimpleApp.js", + "front_end/entrypoints/main/rn_experiments.js", "front_end/entrypoints/node_app/NodeConnectionsPanel.js", "front_end/entrypoints/node_app/NodeMain.js", "front_end/entrypoints/node_app/nodeConnectionsPanel.css.js", diff --git a/front_end/core/root/Runtime.ts b/front_end/core/root/Runtime.ts index 09651429f4c..6296cc4f231 100644 --- a/front_end/core/root/Runtime.ts +++ b/front_end/core/root/Runtime.ts @@ -285,6 +285,15 @@ export class Experiment { // This must be constructed after the query parameters have been parsed. export const experiments = new ExperimentsSupport(); +// React Native-specific experiments, see rn_experiments.ts +// eslint-disable-next-line rulesdir/const_enum +export enum RNExperimentName { + REACT_NATIVE_SPECIFIC_UI = 'reactNativeSpecificUI', + ENABLE_REACT_DEVTOOLS_PANEL = 'enableReactDevToolsPanel', + JS_HEAP_PROFILER_ENABLE = 'jsHeapProfilerEnable', + ENABLE_PERFORMANCE_PANEL = 'enablePerformancePanel', +} + // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum ExperimentName { @@ -310,12 +319,15 @@ export enum ExperimentName { DISABLE_COLOR_FORMAT_SETTING = 'disableColorFormatSetting', TIMELINE_AS_CONSOLE_PROFILE_RESULT_PANEL = 'timelineAsConsoleProfileResultPanel', OUTERMOST_TARGET_SELECTOR = 'outermostTargetSelector', - JS_HEAP_PROFILER_ENABLE = 'jsHeapProfilerEnable', JS_PROFILER_TEMP_ENABLE = 'jsProfilerTemporarilyEnable', HIGHLIGHT_ERRORS_ELEMENTS_PANEL = 'highlightErrorsElementsPanel', SET_ALL_BREAKPOINTS_EAGERLY = 'setAllBreakpointsEagerly', - REACT_NATIVE_SPECIFIC_UI = 'reactNativeSpecificUI', - ENABLE_REACT_DEVTOOLS_PANEL = 'enableReactDevToolsPanel', + + // React Native-specific experiments - must mirror RNExperimentName above + JS_HEAP_PROFILER_ENABLE = RNExperimentName.JS_HEAP_PROFILER_ENABLE, + REACT_NATIVE_SPECIFIC_UI = RNExperimentName.REACT_NATIVE_SPECIFIC_UI, + ENABLE_REACT_DEVTOOLS_PANEL = RNExperimentName.ENABLE_REACT_DEVTOOLS_PANEL, + ENABLE_PERFORMANCE_PANEL = RNExperimentName.ENABLE_PERFORMANCE_PANEL, } // TODO(crbug.com/1167717): Make this a const enum again diff --git a/front_end/entrypoints/main/BUILD.gn b/front_end/entrypoints/main/BUILD.gn index f73292b3422..ca006a78ae8 100644 --- a/front_end/entrypoints/main/BUILD.gn +++ b/front_end/entrypoints/main/BUILD.gn @@ -11,6 +11,7 @@ devtools_module("main") { "ExecutionContextSelector.ts", "MainImpl.ts", "SimpleApp.ts", + "rn_experiments.ts", ] deps = [ diff --git a/front_end/entrypoints/main/MainImpl.ts b/front_end/entrypoints/main/MainImpl.ts index 99f440486be..2c55f94e2f7 100644 --- a/front_end/entrypoints/main/MainImpl.ts +++ b/front_end/entrypoints/main/MainImpl.ts @@ -55,6 +55,7 @@ import * as UI from '../../ui/legacy/legacy.js'; import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js'; import {ExecutionContextSelector} from './ExecutionContextSelector.js'; +import { RNExperiments } from './rn_experiments.js'; const UIStrings = { /** @@ -313,6 +314,12 @@ export class MainImpl { 'timelineAsConsoleProfileResultPanel', 'View console.profile() results in the Performance panel for Node.js', true); + // JS Profiler + Root.Runtime.experiments.register( + 'jsProfilerTemporarilyEnable', 'Enable JavaScript Profiler temporarily', /* unstable= */ false, + 'https://developer.chrome.com/blog/js-profiler-deprecation/', + 'https://bugs.chromium.org/p/chromium/issues/detail?id=1354548'); + // Debugging Root.Runtime.experiments.register( 'wasmDWARFDebugging', 'WebAssembly Debugging: Enable DWARF support', undefined, @@ -420,6 +427,10 @@ export class MainImpl { Root.Runtime.ExperimentName.HEADER_OVERRIDES, ]); + // React Native experiments need to be registered for all entry points so + // that they can be checked everywhere. + RNExperiments.copyInto(Root.Runtime.experiments, '[React Native] '); + Root.Runtime.experiments.setNonConfigurableExperiments([ ...(!('EyeDropper' in window) ? [Root.Runtime.ExperimentName.EYEDROPPER_COLOR_PICKER] : []), ]); diff --git a/front_end/entrypoints/main/rn_experiments.ts b/front_end/entrypoints/main/rn_experiments.ts new file mode 100644 index 00000000000..e9df5b70e44 --- /dev/null +++ b/front_end/entrypoints/main/rn_experiments.ts @@ -0,0 +1,245 @@ +// Copyright 2024 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Copyright (c) Meta Platforms, Inc. and affiliates. + +// Chrome DevTools has an experiment system integrated deeply with its panel +// framework and settings UI. We add some React Native-specific experiments, +// some of which control new RN-specific UI and some of which add gating to +// *existing* features. +// +// The goals are: +// 1. To allow the core, non-RN entry points (like `inspector.ts`) to continue +// to work, largely unmodified, for ease of testing. +// 2. To allow users of each entry point to enable or disable experiments as +// needed through the UI. +// 3. To only show experiments in Settings if they are relevant to the current +// entry point. +// 4. To minimise RN-specific changes to core code, for ease of rebasing onto +// new versions of Chrome DevTools. +// 5. To allow RN entry points to enable/configure *core* experiments before +// they are registered (in MainImpl). +// +// To add a new React Native-specific experiment: +// - define it in the RNExperiments enum and Experiments enum (in Runtime.ts) +// - register it in this file (rn_experiments.ts) +// - set `enabledByDefault` and `configurable` as appropriate +// - optionally, configure it further in each RN-specific entry point +// (rn_inspector.ts, rn_fusebox.ts) +// +// React Native-specific experiments are merged into the main ExperimentsSupport +// object in MainImpl and can't be configured further afterwards (except +// through the UI). + +import * as Root from '../../core/root/root.js'; + +export const RNExperimentName = Root.Runtime.RNExperimentName; +export type RNExperimentName = Root.Runtime.RNExperimentName; + +const state = { + didInitializeExperiments: false, + isReactNativeEntryPoint: false, +}; + +/** + * Set whether the current entry point is a React Native entry point. + * This must be called before constructing MainImpl. + */ +export function setIsReactNativeEntryPoint(value: boolean) { + if (state.didInitializeExperiments) { + throw new Error( + 'setIsReactNativeEntryPoint must be called before constructing MainImpl' + ); + } + state.isReactNativeEntryPoint = value; +} + +type RNExperimentPredicate = ({ + isReactNativeEntryPoint, +}: { + isReactNativeEntryPoint: boolean; +}) => boolean; +type RNExperimentSpec = { + name: RNExperimentName; + title: string; + unstable: boolean; + docLink?: string; + feedbackLink?: string; + enabledByDefault?: boolean | RNExperimentPredicate; + configurable?: boolean | RNExperimentPredicate; +}; + +class RNExperiment { + readonly name: RNExperimentName; + readonly title: string; + readonly unstable: boolean; + readonly docLink?: string; + readonly feedbackLink?: string; + enabledByDefault: RNExperimentPredicate; + configurable: RNExperimentPredicate; + + constructor(spec: RNExperimentSpec) { + this.name = spec.name; + this.title = spec.title; + this.unstable = spec.unstable; + this.docLink = spec.docLink; + this.feedbackLink = spec.feedbackLink; + this.enabledByDefault = normalizePredicate(spec.enabledByDefault, false); + this.configurable = normalizePredicate(spec.configurable, true); + } +} + +function normalizePredicate( + pred: boolean | null | undefined | RNExperimentPredicate, + defaultValue: boolean +): RNExperimentPredicate { + if (pred == null) { + return () => defaultValue; + } + if (typeof pred === 'boolean') { + return () => pred; + } + return pred; +} + +class RNExperimentsSupport { + #experiments: Map = new Map(); + #defaultEnabledCoreExperiments = new Set(); + #nonConfigurableCoreExperiments = new Set(); + + register(spec: RNExperimentSpec): void { + if (state.didInitializeExperiments) { + throw new Error( + 'Experiments must be registered before constructing MainImpl' + ); + } + const { name } = spec; + if (this.#experiments.has(name)) { + throw new Error(`React Native Experiment ${name} is already registered`); + } + this.#experiments.set(name, new RNExperiment(spec)); + } + + /** + * Enable the given (RN-specific or core) experiments by default. + */ + enableExperimentsByDefault(names: Root.Runtime.ExperimentName[]) { + if (state.didInitializeExperiments) { + throw new Error( + 'Experiments must be configured before constructing MainImpl' + ); + } + for (const name of names) { + if (Object.prototype.hasOwnProperty.call(RNExperimentName, name)) { + const experiment = this.#experiments.get( + name as unknown as RNExperimentName + ); + if (!experiment) { + throw new Error(`React Native Experiment ${name} is not registered`); + } + experiment.enabledByDefault = () => true; + } else { + this.#defaultEnabledCoreExperiments.add( + name as Root.Runtime.ExperimentName + ); + } + } + } + + /** + * Set the given (RN-specific or core) experiments to be non-configurable. + */ + setNonConfigurableExperiments(names: Root.Runtime.ExperimentName[]) { + if (state.didInitializeExperiments) { + throw new Error( + 'Experiments must be configured before constructing MainImpl' + ); + } + for (const name of names) { + if (Object.prototype.hasOwnProperty.call(RNExperimentName, name)) { + const experiment = this.#experiments.get( + name as unknown as RNExperimentName + ); + if (!experiment) { + throw new Error(`React Native Experiment ${name} is not registered`); + } + experiment.configurable = () => false; + } else { + this.#nonConfigurableCoreExperiments.add( + name as Root.Runtime.ExperimentName + ); + } + } + } + + copyInto(other: Root.Runtime.ExperimentsSupport, titlePrefix: string = ''): void { + for (const [name, spec] of this.#experiments) { + other.register( + name, + titlePrefix + spec.title, + spec.unstable, + spec.docLink, + spec.feedbackLink + ); + if ( + spec.enabledByDefault({ + isReactNativeEntryPoint: state.isReactNativeEntryPoint, + }) + ) { + other.enableExperimentsByDefault([name]); + } + if ( + !spec.configurable({ + isReactNativeEntryPoint: state.isReactNativeEntryPoint, + }) + ) { + other.setNonConfigurableExperiments([name]); + } + } + for (const name of this.#defaultEnabledCoreExperiments) { + other.enableExperimentsByDefault([name]); + } + for (const name of this.#nonConfigurableCoreExperiments) { + other.setNonConfigurableExperiments([name]); + } + state.didInitializeExperiments = true; + } +} + +// Early registration for React Native-specific experiments. Only use this +// *before* constructing MainImpl; afterwards read from Root.Runtime.experiments +// as normal. +export const RNExperiments = new RNExperimentsSupport(); + +RNExperiments.register({ + name: RNExperimentName.JS_HEAP_PROFILER_ENABLE, + title: 'Enable Heap Profiler', + unstable: false, + enabledByDefault: ({ isReactNativeEntryPoint }) => !isReactNativeEntryPoint, + configurable: ({ isReactNativeEntryPoint }) => isReactNativeEntryPoint, +}); + +RNExperiments.register({ + name: RNExperimentName.ENABLE_REACT_DEVTOOLS_PANEL, + title: 'Enable React DevTools panel', + unstable: true, + enabledByDefault: false, + configurable: ({ isReactNativeEntryPoint }) => isReactNativeEntryPoint, +}); + +RNExperiments.register({ + name: RNExperimentName.REACT_NATIVE_SPECIFIC_UI, + title: 'Show React Native-specific UI', + unstable: false, + enabledByDefault: ({ isReactNativeEntryPoint }) => isReactNativeEntryPoint, + configurable: false, +}); + +RNExperiments.register({ + name: RNExperimentName.ENABLE_PERFORMANCE_PANEL, + title: 'Enable Performance panel', + unstable: true, + enabledByDefault: ({ isReactNativeEntryPoint }) => !isReactNativeEntryPoint, + configurable: ({ isReactNativeEntryPoint }) => isReactNativeEntryPoint, +}); diff --git a/front_end/entrypoints/rn_fusebox/BUILD.gn b/front_end/entrypoints/rn_fusebox/BUILD.gn index 1e8b948d15c..66dbd2494c9 100644 --- a/front_end/entrypoints/rn_fusebox/BUILD.gn +++ b/front_end/entrypoints/rn_fusebox/BUILD.gn @@ -32,6 +32,7 @@ devtools_entrypoint("entrypoint") { "../../panels/rn_welcome:meta", "../../panels/security:meta", "../../panels/sensors:meta", + "../../panels/timeline:meta", "../../panels/web_audio:meta", "../../panels/webauthn:meta", "../main:bundle", diff --git a/front_end/entrypoints/rn_fusebox/rn_fusebox.ts b/front_end/entrypoints/rn_fusebox/rn_fusebox.ts index afb5505ce98..3f5b9021800 100644 --- a/front_end/entrypoints/rn_fusebox/rn_fusebox.ts +++ b/front_end/entrypoints/rn_fusebox/rn_fusebox.ts @@ -15,9 +15,9 @@ import '../inspector_main/inspector_main-meta.js'; import '../../panels/issues/issues-meta.js'; import '../../panels/mobile_throttling/mobile_throttling-meta.js'; import '../../panels/network/network-meta.js'; -import '../../panels/js_profiler/js_profiler-meta.js'; import '../../panels/react_devtools/react_devtools-meta.js'; import '../../panels/rn_welcome/rn_welcome-meta.js'; +import '../../panels/timeline/timeline-meta.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Host from '../../core/host/host.js'; @@ -28,6 +28,12 @@ import * as UI from '../../ui/legacy/legacy.js'; import type * as InspectorBackend from '../../core/protocol_client/InspectorBackend.js'; import type * as Platform from '../../core/platform/platform.js'; import type * as Sources from '../../panels/sources/sources.js'; +import * as RNExperiments from '../main/rn_experiments.js'; + +RNExperiments.setIsReactNativeEntryPoint(true); +RNExperiments.RNExperiments.enableExperimentsByDefault([ + Root.Runtime.ExperimentName.REACT_NATIVE_SPECIFIC_UI, +]); Host.RNPerfMetrics.registerPerfMetricsGlobalPostMessageHandler(); @@ -40,37 +46,6 @@ SDK.TargetManager.TargetManager.instance().addModelListener( () => Host.rnPerfMetrics.debuggerReadyToPause(), ); -// Legacy JavaScript Profiler - disabled until we have complete support. -Root.Runtime.experiments.register( - Root.Runtime.ExperimentName.JS_PROFILER_TEMP_ENABLE, - 'Enable JavaScript Profiler (legacy)', - /* unstable */ false, -); - -// Heap Profiler (Memory panel) - disabled until we have complete support. -Root.Runtime.experiments.register( - Root.Runtime.ExperimentName.JS_HEAP_PROFILER_ENABLE, - 'Enable Heap Profiler', - /* unstable */ false, -); - -Root.Runtime.experiments.register( - Root.Runtime.ExperimentName.REACT_NATIVE_SPECIFIC_UI, - 'Show React Native-specific UI', - /* unstable */ false, - /* docLink */ globalThis.reactNativeDocLink ?? 'https://reactnative.dev/docs/debugging', -); - -Root.Runtime.experiments.register( - Root.Runtime.ExperimentName.ENABLE_REACT_DEVTOOLS_PANEL, - 'Enable React DevTools', - /* unstable */ true, -); - -Root.Runtime.experiments.enableExperimentsByDefault([ - Root.Runtime.ExperimentName.REACT_NATIVE_SPECIFIC_UI, -]); - class FuseboxClientMetadataModel extends SDK.SDKModel.SDKModel { constructor(target: SDK.Target.Target) { super(target); diff --git a/front_end/entrypoints/rn_inspector/rn_inspector.ts b/front_end/entrypoints/rn_inspector/rn_inspector.ts index 2eb8c1fb587..1be6ef4a36c 100644 --- a/front_end/entrypoints/rn_inspector/rn_inspector.ts +++ b/front_end/entrypoints/rn_inspector/rn_inspector.ts @@ -19,35 +19,20 @@ import * as Root from '../../core/root/root.js'; import * as Main from '../main/main.js'; import * as UI from '../../ui/legacy/legacy.js'; import type * as Sources from '../../panels/sources/sources.js'; +import * as RNExperiments from '../main/rn_experiments.js'; -// Legacy JavaScript Profiler - we support this until Hermes can support the -// modern Performance panel. -Root.Runtime.experiments.register( - Root.Runtime.ExperimentName.JS_PROFILER_TEMP_ENABLE, - 'Enable JavaScript Profiler (legacy)', - /* unstable */ false, -); - -// Heap Profiler (Memory panel) - supported, but disabled in rn_fusebox. -Root.Runtime.experiments.register( - Root.Runtime.ExperimentName.JS_HEAP_PROFILER_ENABLE, - 'Enable Heap Profiler', - /* unstable */ false, -); - -Root.Runtime.experiments.register( - Root.Runtime.ExperimentName.REACT_NATIVE_SPECIFIC_UI, - 'Show React Native-specific UI', - /* unstable */ false, - /* docLink */ globalThis.reactNativeDocLink ?? 'https://reactnative.dev/docs/debugging', - /* feedbackLink */ globalThis.FB_ONLY__reactNativeFeedbackLink, -); - -Root.Runtime.experiments.enableExperimentsByDefault([ +RNExperiments.setIsReactNativeEntryPoint(true); +RNExperiments.RNExperiments.enableExperimentsByDefault([ Root.Runtime.ExperimentName.JS_HEAP_PROFILER_ENABLE, Root.Runtime.ExperimentName.JS_PROFILER_TEMP_ENABLE, Root.Runtime.ExperimentName.REACT_NATIVE_SPECIFIC_UI, ]); +RNExperiments.RNExperiments.setNonConfigurableExperiments( + [ + // RDT support is Fusebox-only + Root.Runtime.ExperimentName.ENABLE_REACT_DEVTOOLS_PANEL, + ], +); const UIStrings = { /** @@ -88,4 +73,3 @@ UI.ViewManager.registerViewExtension({ // @ts-ignore Exposed for legacy layout tests self.runtime = Root.Runtime.Runtime.instance({forceNew: true}); new Main.MainImpl.MainImpl(); - diff --git a/front_end/global_typings/react_native.d.ts b/front_end/global_typings/react_native.d.ts index 2e3047d83cc..3efcf36c66e 100644 --- a/front_end/global_typings/react_native.d.ts +++ b/front_end/global_typings/react_native.d.ts @@ -9,7 +9,6 @@ declare global { namespace globalThis { var enableReactNativePerfMetrics: boolean|undefined; var enableReactNativePerfMetricsGlobalPostMessage: boolean|undefined; - var reactNativeDocLink: string|undefined; var FB_ONLY__reactNativeFeedbackLink: string|undefined; } } diff --git a/front_end/panels/react_devtools/react_devtools-meta.ts b/front_end/panels/react_devtools/react_devtools-meta.ts index ba72f21d988..f806c42303a 100644 --- a/front_end/panels/react_devtools/react_devtools-meta.ts +++ b/front_end/panels/react_devtools/react_devtools-meta.ts @@ -38,6 +38,7 @@ UI.ViewManager.registerViewExtension({ title: i18nLazyString(UIStrings.title), commandPrompt: i18nLazyString(UIStrings.command), persistence: UI.ViewManager.ViewPersistence.PERMANENT, + order: 1000, async loadView() { const Module = await loadModule(); diff --git a/front_end/panels/timeline/timeline-meta.ts b/front_end/panels/timeline/timeline-meta.ts index cdc41d5541d..7468f25f5b9 100644 --- a/front_end/panels/timeline/timeline-meta.ts +++ b/front_end/panels/timeline/timeline-meta.ts @@ -125,6 +125,7 @@ UI.ViewManager.registerViewExtension({ title: i18nLazyString(UIStrings.performance), commandPrompt: i18nLazyString(UIStrings.showPerformance), order: 50, + experiment: Root.Runtime.ExperimentName.ENABLE_PERFORMANCE_PANEL, async loadView() { const Timeline = await loadTimelineModule(); return Timeline.TimelinePanel.TimelinePanel.instance(); @@ -136,7 +137,7 @@ UI.ViewManager.registerViewExtension({ id: 'js_profiler', title: i18nLazyString(UIStrings.javascriptProfiler), commandPrompt: i18nLazyString(UIStrings.showJavascriptProfiler), - persistence: UI.ViewManager.ViewPersistence.CLOSEABLE, + persistence: UI.ViewManager.ViewPersistence.PERMANENT, order: 65, experiment: Root.Runtime.ExperimentName.JS_PROFILER_TEMP_ENABLE, async loadView() { diff --git a/front_end/ui/components/text_editor/javascript.ts b/front_end/ui/components/text_editor/javascript.ts index 411aca026b9..96fb4631e92 100644 --- a/front_end/ui/components/text_editor/javascript.ts +++ b/front_end/ui/components/text_editor/javascript.ts @@ -450,11 +450,11 @@ async function completeExpressionGlobal(): Promise { } const baseCompletionsForTarget = Root.Runtime.experiments.isEnabled( - Root.Runtime.ExperimentName.REACT_NATIVE_SPECIFIC_UI + Root.Runtime.ExperimentName.REACT_NATIVE_SPECIFIC_UI, ) ? reactNativeBaseCompletions : baseCompletions; - + const context = getExecutionContext(); if (!context) { return baseCompletionsForTarget;