Skip to content

Commit 86e52e3

Browse files
authored
Merge pull request #42 from motiz88/refactor-rn-experiment-setup
Refactor and centralise RN experiment registration
2 parents 61d41e6 + bf919e3 commit 86e52e3

File tree

10 files changed

+282
-63
lines changed

10 files changed

+282
-63
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,7 @@ grd_files_debug_sources = [
833833
"front_end/entrypoints/main/ExecutionContextSelector.js",
834834
"front_end/entrypoints/main/MainImpl.js",
835835
"front_end/entrypoints/main/SimpleApp.js",
836+
"front_end/entrypoints/main/rn_experiments.js",
836837
"front_end/entrypoints/node_app/NodeConnectionsPanel.js",
837838
"front_end/entrypoints/node_app/NodeMain.js",
838839
"front_end/entrypoints/node_app/nodeConnectionsPanel.css.js",

front_end/core/root/Runtime.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,14 @@ export class Experiment {
285285
// This must be constructed after the query parameters have been parsed.
286286
export const experiments = new ExperimentsSupport();
287287

288+
// React Native-specific experiments, see rn_experiments.ts
289+
// eslint-disable-next-line rulesdir/const_enum
290+
export enum RNExperimentName {
291+
REACT_NATIVE_SPECIFIC_UI = 'reactNativeSpecificUI',
292+
ENABLE_REACT_DEVTOOLS_PANEL = 'enableReactDevToolsPanel',
293+
JS_HEAP_PROFILER_ENABLE = 'jsHeapProfilerEnable',
294+
}
295+
288296
// TODO(crbug.com/1167717): Make this a const enum again
289297
// eslint-disable-next-line rulesdir/const_enum
290298
export enum ExperimentName {
@@ -310,12 +318,14 @@ export enum ExperimentName {
310318
DISABLE_COLOR_FORMAT_SETTING = 'disableColorFormatSetting',
311319
TIMELINE_AS_CONSOLE_PROFILE_RESULT_PANEL = 'timelineAsConsoleProfileResultPanel',
312320
OUTERMOST_TARGET_SELECTOR = 'outermostTargetSelector',
313-
JS_HEAP_PROFILER_ENABLE = 'jsHeapProfilerEnable',
314321
JS_PROFILER_TEMP_ENABLE = 'jsProfilerTemporarilyEnable',
315322
HIGHLIGHT_ERRORS_ELEMENTS_PANEL = 'highlightErrorsElementsPanel',
316323
SET_ALL_BREAKPOINTS_EAGERLY = 'setAllBreakpointsEagerly',
317-
REACT_NATIVE_SPECIFIC_UI = 'reactNativeSpecificUI',
318-
ENABLE_REACT_DEVTOOLS_PANEL = 'enableReactDevToolsPanel',
324+
325+
// React Native-specific experiments - must mirror RNExperimentName above
326+
JS_HEAP_PROFILER_ENABLE = RNExperimentName.JS_HEAP_PROFILER_ENABLE,
327+
REACT_NATIVE_SPECIFIC_UI = RNExperimentName.REACT_NATIVE_SPECIFIC_UI,
328+
ENABLE_REACT_DEVTOOLS_PANEL = RNExperimentName.ENABLE_REACT_DEVTOOLS_PANEL,
319329
}
320330

321331
// TODO(crbug.com/1167717): Make this a const enum again

front_end/entrypoints/main/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ devtools_module("main") {
1111
"ExecutionContextSelector.ts",
1212
"MainImpl.ts",
1313
"SimpleApp.ts",
14+
"rn_experiments.ts",
1415
]
1516

1617
deps = [

front_end/entrypoints/main/MainImpl.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import * as UI from '../../ui/legacy/legacy.js';
5555
import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js';
5656

5757
import {ExecutionContextSelector} from './ExecutionContextSelector.js';
58+
import { RNExperiments } from './rn_experiments.js';
5859

5960
const UIStrings = {
6061
/**
@@ -313,6 +314,12 @@ export class MainImpl {
313314
'timelineAsConsoleProfileResultPanel', 'View console.profile() results in the Performance panel for Node.js',
314315
true);
315316

317+
// JS Profiler
318+
Root.Runtime.experiments.register(
319+
'jsProfilerTemporarilyEnable', 'Enable JavaScript Profiler temporarily', /* unstable= */ false,
320+
'https://developer.chrome.com/blog/js-profiler-deprecation/',
321+
'https://bugs.chromium.org/p/chromium/issues/detail?id=1354548');
322+
316323
// Debugging
317324
Root.Runtime.experiments.register(
318325
'wasmDWARFDebugging', 'WebAssembly Debugging: Enable DWARF support', undefined,
@@ -420,6 +427,10 @@ export class MainImpl {
420427
Root.Runtime.ExperimentName.HEADER_OVERRIDES,
421428
]);
422429

430+
// React Native experiments need to be registered for all entry points so
431+
// that they can be checked everywhere.
432+
RNExperiments.copyInto(Root.Runtime.experiments, '[React Native] ');
433+
423434
Root.Runtime.experiments.setNonConfigurableExperiments([
424435
...(!('EyeDropper' in window) ? [Root.Runtime.ExperimentName.EYEDROPPER_COLOR_PICKER] : []),
425436
]);
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
// Copyright 2024 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// Copyright (c) Meta Platforms, Inc. and affiliates.
6+
7+
// Chrome DevTools has an experiment system integrated deeply with its panel
8+
// framework and settings UI. We add some React Native-specific experiments,
9+
// some of which control new RN-specific UI and some of which add gating to
10+
// *existing* features.
11+
//
12+
// The goals are:
13+
// 1. To allow the core, non-RN entry points (like `inspector.ts`) to continue
14+
// to work, largely unmodified, for ease of testing.
15+
// 2. To allow users of each entry point to enable or disable experiments as
16+
// needed through the UI.
17+
// 3. To only show experiments in Settings if they are relevant to the current
18+
// entry point.
19+
// 4. To minimise RN-specific changes to core code, for ease of rebasing onto
20+
// new versions of Chrome DevTools.
21+
// 5. To allow RN entry points to enable/configure *core* experiments before
22+
// they are registered (in MainImpl).
23+
//
24+
// To add a new React Native-specific experiment:
25+
// - define it in the RNExperiments enum and Experiments enum (in Runtime.ts)
26+
// - register it in this file (rn_experiments.ts)
27+
// - set `enabledByDefault` and `configurable` as appropriate
28+
// - optionally, configure it further in each RN-specific entry point
29+
// (rn_inspector.ts, rn_fusebox.ts)
30+
//
31+
// React Native-specific experiments are merged into the main ExperimentsSupport
32+
// object in MainImpl and can't be configured further afterwards (except
33+
// through the UI).
34+
35+
import * as Root from '../../core/root/root.js';
36+
37+
export const RNExperimentName = Root.Runtime.RNExperimentName;
38+
export type RNExperimentName = Root.Runtime.RNExperimentName;
39+
40+
const state = {
41+
didInitializeExperiments: false,
42+
isReactNativeEntryPoint: false,
43+
};
44+
45+
/**
46+
* Set whether the current entry point is a React Native entry point.
47+
* This must be called before constructing MainImpl.
48+
*/
49+
export function setIsReactNativeEntryPoint(value: boolean) {
50+
if (state.didInitializeExperiments) {
51+
throw new Error(
52+
'setIsReactNativeEntryPoint must be called before constructing MainImpl'
53+
);
54+
}
55+
state.isReactNativeEntryPoint = value;
56+
}
57+
58+
type RNExperimentPredicate = ({
59+
isReactNativeEntryPoint,
60+
}: {
61+
isReactNativeEntryPoint: boolean;
62+
}) => boolean;
63+
type RNExperimentSpec = {
64+
name: RNExperimentName;
65+
title: string;
66+
unstable: boolean;
67+
docLink?: string;
68+
feedbackLink?: string;
69+
enabledByDefault?: boolean | RNExperimentPredicate;
70+
configurable?: boolean | RNExperimentPredicate;
71+
};
72+
73+
class RNExperiment {
74+
readonly name: RNExperimentName;
75+
readonly title: string;
76+
readonly unstable: boolean;
77+
readonly docLink?: string;
78+
readonly feedbackLink?: string;
79+
enabledByDefault: RNExperimentPredicate;
80+
configurable: RNExperimentPredicate;
81+
82+
constructor(spec: RNExperimentSpec) {
83+
this.name = spec.name;
84+
this.title = spec.title;
85+
this.unstable = spec.unstable;
86+
this.docLink = spec.docLink;
87+
this.feedbackLink = spec.feedbackLink;
88+
this.enabledByDefault = normalizePredicate(spec.enabledByDefault, false);
89+
this.configurable = normalizePredicate(spec.configurable, true);
90+
}
91+
}
92+
93+
function normalizePredicate(
94+
pred: boolean | null | undefined | RNExperimentPredicate,
95+
defaultValue: boolean
96+
): RNExperimentPredicate {
97+
if (pred == null) {
98+
return () => defaultValue;
99+
}
100+
if (typeof pred === 'boolean') {
101+
return () => pred;
102+
}
103+
return pred;
104+
}
105+
106+
class RNExperimentsSupport {
107+
#experiments: Map<Root.Runtime.RNExperimentName, RNExperiment> = new Map();
108+
#defaultEnabledCoreExperiments = new Set<Root.Runtime.ExperimentName>();
109+
#nonConfigurableCoreExperiments = new Set<Root.Runtime.ExperimentName>();
110+
111+
register(spec: RNExperimentSpec): void {
112+
if (state.didInitializeExperiments) {
113+
throw new Error(
114+
'Experiments must be registered before constructing MainImpl'
115+
);
116+
}
117+
const { name } = spec;
118+
if (this.#experiments.has(name)) {
119+
throw new Error(`React Native Experiment ${name} is already registered`);
120+
}
121+
this.#experiments.set(name, new RNExperiment(spec));
122+
}
123+
124+
/**
125+
* Enable the given (RN-specific or core) experiments by default.
126+
*/
127+
enableExperimentsByDefault(names: Root.Runtime.ExperimentName[]) {
128+
if (state.didInitializeExperiments) {
129+
throw new Error(
130+
'Experiments must be configured before constructing MainImpl'
131+
);
132+
}
133+
for (const name of names) {
134+
if (Object.prototype.hasOwnProperty.call(RNExperimentName, name)) {
135+
const experiment = this.#experiments.get(
136+
name as unknown as RNExperimentName
137+
);
138+
if (!experiment) {
139+
throw new Error(`React Native Experiment ${name} is not registered`);
140+
}
141+
experiment.enabledByDefault = () => true;
142+
} else {
143+
this.#defaultEnabledCoreExperiments.add(
144+
name as Root.Runtime.ExperimentName
145+
);
146+
}
147+
}
148+
}
149+
150+
/**
151+
* Set the given (RN-specific or core) experiments to be non-configurable.
152+
*/
153+
setNonConfigurableExperiments(names: Root.Runtime.ExperimentName[]) {
154+
if (state.didInitializeExperiments) {
155+
throw new Error(
156+
'Experiments must be configured before constructing MainImpl'
157+
);
158+
}
159+
for (const name of names) {
160+
if (Object.prototype.hasOwnProperty.call(RNExperimentName, name)) {
161+
const experiment = this.#experiments.get(
162+
name as unknown as RNExperimentName
163+
);
164+
if (!experiment) {
165+
throw new Error(`React Native Experiment ${name} is not registered`);
166+
}
167+
experiment.configurable = () => false;
168+
} else {
169+
this.#nonConfigurableCoreExperiments.add(
170+
name as Root.Runtime.ExperimentName
171+
);
172+
}
173+
}
174+
}
175+
176+
copyInto(other: Root.Runtime.ExperimentsSupport, titlePrefix: string = ''): void {
177+
for (const [name, spec] of this.#experiments) {
178+
other.register(
179+
name,
180+
titlePrefix + spec.title,
181+
spec.unstable,
182+
spec.docLink,
183+
spec.feedbackLink
184+
);
185+
if (
186+
spec.enabledByDefault({
187+
isReactNativeEntryPoint: state.isReactNativeEntryPoint,
188+
})
189+
) {
190+
other.enableExperimentsByDefault([name]);
191+
}
192+
if (
193+
!spec.configurable({
194+
isReactNativeEntryPoint: state.isReactNativeEntryPoint,
195+
})
196+
) {
197+
other.setNonConfigurableExperiments([name]);
198+
}
199+
}
200+
for (const name of this.#defaultEnabledCoreExperiments) {
201+
other.enableExperimentsByDefault([name]);
202+
}
203+
for (const name of this.#nonConfigurableCoreExperiments) {
204+
other.setNonConfigurableExperiments([name]);
205+
}
206+
state.didInitializeExperiments = true;
207+
}
208+
}
209+
210+
// Early registration for React Native-specific experiments. Only use this
211+
// *before* constructing MainImpl; afterwards read from Root.Runtime.experiments
212+
// as normal.
213+
export const RNExperiments = new RNExperimentsSupport();
214+
215+
RNExperiments.register({
216+
name: RNExperimentName.JS_HEAP_PROFILER_ENABLE,
217+
title: 'Enable Heap Profiler',
218+
unstable: false,
219+
enabledByDefault: ({ isReactNativeEntryPoint }) => !isReactNativeEntryPoint,
220+
configurable: ({ isReactNativeEntryPoint }) => isReactNativeEntryPoint,
221+
});
222+
223+
RNExperiments.register({
224+
name: RNExperimentName.ENABLE_REACT_DEVTOOLS_PANEL,
225+
title: 'Enable React DevTools panel',
226+
unstable: true,
227+
enabledByDefault: false,
228+
configurable: ({ isReactNativeEntryPoint }) => isReactNativeEntryPoint,
229+
});
230+
231+
RNExperiments.register({
232+
name: RNExperimentName.REACT_NATIVE_SPECIFIC_UI,
233+
title: 'Show React Native-specific UI',
234+
unstable: false,
235+
enabledByDefault: ({ isReactNativeEntryPoint }) => isReactNativeEntryPoint,
236+
configurable: false,
237+
});

front_end/entrypoints/rn_fusebox/rn_fusebox.ts

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ import * as UI from '../../ui/legacy/legacy.js';
2828
import type * as InspectorBackend from '../../core/protocol_client/InspectorBackend.js';
2929
import type * as Platform from '../../core/platform/platform.js';
3030
import type * as Sources from '../../panels/sources/sources.js';
31+
import * as RNExperiments from '../main/rn_experiments.js';
32+
33+
RNExperiments.setIsReactNativeEntryPoint(true);
34+
RNExperiments.RNExperiments.enableExperimentsByDefault([
35+
Root.Runtime.ExperimentName.REACT_NATIVE_SPECIFIC_UI,
36+
]);
3137

3238
Host.RNPerfMetrics.registerPerfMetricsGlobalPostMessageHandler();
3339

@@ -40,37 +46,6 @@ SDK.TargetManager.TargetManager.instance().addModelListener(
4046
() => Host.rnPerfMetrics.debuggerReadyToPause(),
4147
);
4248

43-
// Legacy JavaScript Profiler - disabled until we have complete support.
44-
Root.Runtime.experiments.register(
45-
Root.Runtime.ExperimentName.JS_PROFILER_TEMP_ENABLE,
46-
'Enable JavaScript Profiler (legacy)',
47-
/* unstable */ false,
48-
);
49-
50-
// Heap Profiler (Memory panel) - disabled until we have complete support.
51-
Root.Runtime.experiments.register(
52-
Root.Runtime.ExperimentName.JS_HEAP_PROFILER_ENABLE,
53-
'Enable Heap Profiler',
54-
/* unstable */ false,
55-
);
56-
57-
Root.Runtime.experiments.register(
58-
Root.Runtime.ExperimentName.REACT_NATIVE_SPECIFIC_UI,
59-
'Show React Native-specific UI',
60-
/* unstable */ false,
61-
/* docLink */ globalThis.reactNativeDocLink ?? 'https://reactnative.dev/docs/debugging',
62-
);
63-
64-
Root.Runtime.experiments.register(
65-
Root.Runtime.ExperimentName.ENABLE_REACT_DEVTOOLS_PANEL,
66-
'Enable React DevTools',
67-
/* unstable */ true,
68-
);
69-
70-
Root.Runtime.experiments.enableExperimentsByDefault([
71-
Root.Runtime.ExperimentName.REACT_NATIVE_SPECIFIC_UI,
72-
]);
73-
7449
class FuseboxClientMetadataModel extends SDK.SDKModel.SDKModel<void> {
7550
constructor(target: SDK.Target.Target) {
7651
super(target);

0 commit comments

Comments
 (0)