Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/gni/devtools_grd_files.gni
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 15 additions & 3 deletions front_end/core/root/Runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions front_end/entrypoints/main/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ devtools_module("main") {
"ExecutionContextSelector.ts",
"MainImpl.ts",
"SimpleApp.ts",
"rn_experiments.ts",
]

deps = [
Expand Down
11 changes: 11 additions & 0 deletions front_end/entrypoints/main/MainImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
/**
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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] : []),
]);
Expand Down
245 changes: 245 additions & 0 deletions front_end/entrypoints/main/rn_experiments.ts
Original file line number Diff line number Diff line change
@@ -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<Root.Runtime.RNExperimentName, RNExperiment> = new Map();
#defaultEnabledCoreExperiments = new Set<Root.Runtime.ExperimentName>();
#nonConfigurableCoreExperiments = new Set<Root.Runtime.ExperimentName>();

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,
});
1 change: 1 addition & 0 deletions front_end/entrypoints/rn_fusebox/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
39 changes: 7 additions & 32 deletions front_end/entrypoints/rn_fusebox/rn_fusebox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();

Expand All @@ -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<void> {
constructor(target: SDK.Target.Target) {
super(target);
Expand Down
Loading