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..05b1d6a3d3e 100644 --- a/front_end/core/root/Runtime.ts +++ b/front_end/core/root/Runtime.ts @@ -285,6 +285,14 @@ 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', +} + // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum ExperimentName { @@ -310,12 +318,14 @@ 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, } // 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..c8bab2e513f --- /dev/null +++ b/front_end/entrypoints/main/rn_experiments.ts @@ -0,0 +1,237 @@ +// 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, +}); diff --git a/front_end/entrypoints/rn_fusebox/rn_fusebox.ts b/front_end/entrypoints/rn_fusebox/rn_fusebox.ts index afb5505ce98..1b18ccb16f0 100644 --- a/front_end/entrypoints/rn_fusebox/rn_fusebox.ts +++ b/front_end/entrypoints/rn_fusebox/rn_fusebox.ts @@ -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/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; diff --git a/scripts/eslint_rules/lib/check_license_header.js b/scripts/eslint_rules/lib/check_license_header.js index 90f1e5ba59f..cd57d8e10d9 100644 --- a/scripts/eslint_rules/lib/check_license_header.js +++ b/scripts/eslint_rules/lib/check_license_header.js @@ -68,12 +68,13 @@ const EXCLUDED_FILES = [ const META_CODE_PATHS = [ 'core/host/RNPerfMetrics.ts', + 'entrypoints/main/rn_experiments.ts', + 'entrypoints/rn_fusebox', 'entrypoints/rn_inspector', 'global_typings/react_native.d.ts', 'models/react_native', 'panels/react_devtools', 'panels/rn_welcome', - 'models/react_native' ]; const OTHER_LICENSE_HEADERS = [