Skip to content

Commit 59c63cb

Browse files
authored
Add conditional disabling of features incompatible with multiple hosts (#218)
1 parent d37d426 commit 59c63cb

File tree

12 files changed

+296
-106
lines changed

12 files changed

+296
-106
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -995,7 +995,7 @@ grd_files_debug_sources = [
995995
"front_end/entrypoints/node_app/NodeMain.js",
996996
"front_end/entrypoints/node_app/nodeConnectionsPanel.css.js",
997997
"front_end/entrypoints/rn_fusebox/FuseboxAppMetadataObserver.js",
998-
"front_end/entrypoints/rn_fusebox/FuseboxExperimentsObserver.js",
998+
"front_end/entrypoints/rn_fusebox/FuseboxFeatureObserver.js",
999999
"front_end/entrypoints/rn_fusebox/FuseboxReconnectDeviceButton.js",
10001000
"front_end/entrypoints/rn_fusebox/FuseboxWindowTitleManager.js",
10011001
"front_end/entrypoints/shell/browser_compatibility_guard.js",

front_end/core/sdk/ReactNativeApplicationModel.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ export class ReactNativeApplicationModel extends SDKModel<EventTypes> implements
5252
this.dispatchEventToListeners(Events.METADATA_UPDATED, metadata);
5353
}
5454

55+
systemStateChanged(params: Protocol.ReactNativeApplication.SystemStateChangedEvent): void {
56+
this.dispatchEventToListeners(Events.SYSTEM_STATE_CHANGED, params);
57+
}
58+
5559
traceRequested(): void {
5660
Host.rnPerfMetrics.traceRequested();
5761
this.dispatchEventToListeners(Events.TRACE_REQUESTED);
@@ -60,10 +64,12 @@ export class ReactNativeApplicationModel extends SDKModel<EventTypes> implements
6064

6165
export const enum Events {
6266
METADATA_UPDATED = 'MetadataUpdated',
67+
SYSTEM_STATE_CHANGED = 'SystemStateChanged',
6368
TRACE_REQUESTED = 'TraceRequested',
6469
}
6570

6671
export interface EventTypes {
6772
[Events.METADATA_UPDATED]: Protocol.ReactNativeApplication.MetadataUpdatedEvent;
73+
[Events.SYSTEM_STATE_CHANGED]: Protocol.ReactNativeApplication.SystemStateChangedEvent;
6874
[Events.TRACE_REQUESTED]: void;
6975
}

front_end/entrypoints/rn_fusebox/BUILD.gn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ devtools_module("rn_fusebox") {
1111
sources = [
1212
"FuseboxAppMetadataObserver.ts",
1313
"FuseboxReconnectDeviceButton.ts",
14-
"FuseboxExperimentsObserver.ts",
14+
"FuseboxFeatureObserver.ts",
1515
"FuseboxWindowTitleManager.ts",
1616
]
1717

front_end/entrypoints/rn_fusebox/FuseboxExperimentsObserver.ts

Lines changed: 0 additions & 102 deletions
This file was deleted.
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
// Copyright 2024 The Chromium Authors. All rights reserved.
3+
// Use of this source code is governed by a BSD-style license that can be
4+
// found in the LICENSE file.
5+
6+
import type * as Common from '../../core/common/common.js';
7+
import * as i18n from '../../core/i18n/i18n.js';
8+
import * as Root from '../../core/root/root.js';
9+
import * as SDK from '../../core/sdk/sdk.js';
10+
import type * as Protocol from '../../generated/protocol.js';
11+
import * as UI from '../../ui/legacy/legacy.js';
12+
import * as Lit from '../../ui/lit/lit.js';
13+
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
14+
15+
import {FuseboxWindowTitleManager} from './FuseboxWindowTitleManager.js';
16+
17+
const {html, render} = Lit;
18+
19+
const UIStrings = {
20+
/**
21+
* @description Message for the "settings changed" banner shown when a reload is required for the Network panel.
22+
*/
23+
reloadRequiredForNetworkPanelMessage: 'The Network panel is now available for dogfooding. Please reload to access it.',
24+
/**
25+
* @description Title shown when Network inspection is disabled due to multiple React Native hosts.
26+
*/
27+
networkInspectionUnavailable: 'Network inspection is unavailable',
28+
/**
29+
* @description Title shown when Performance profiling is disabled due to multiple React Native hosts.
30+
*/
31+
performanceProfilingUnavailable: 'Performance profiling is unavailable',
32+
/**
33+
* @description Title shown when a feature is unavailable due to multiple React Native hosts.
34+
*/
35+
multiHostFeatureUnavailableTitle: 'Feature is unavailable',
36+
/**
37+
* @description Detail message shown when a feature is disabled due to multiple React Native hosts.
38+
*/
39+
multiHostFeatureDisabledDetail: 'This feature is disabled as the app or framework has registered multiple React Native hosts, which is not currently supported.',
40+
} as const;
41+
42+
const str_ = i18n.i18n.registerUIStrings('entrypoints/rn_fusebox/FuseboxFeatureObserver.ts', UIStrings);
43+
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
44+
45+
/**
46+
* The set of features that are not guaranteed to behave safely with multiple
47+
* React Native hosts.
48+
*/
49+
const UNSAFE_MULTI_HOST_FEATURES = new Set([
50+
'network',
51+
'timeline',
52+
]);
53+
54+
/**
55+
* [RN] Model observer which configures available DevTools features and
56+
* experiments based on the target's capabilities.
57+
*/
58+
export class FuseboxFeatureObserver implements
59+
SDK.TargetManager.SDKModelObserver<SDK.ReactNativeApplicationModel.ReactNativeApplicationModel> {
60+
#singleHostFeaturesDisabled = false;
61+
62+
constructor(targetManager: SDK.TargetManager.TargetManager) {
63+
targetManager.observeModels(SDK.ReactNativeApplicationModel.ReactNativeApplicationModel, this);
64+
}
65+
66+
modelAdded(model: SDK.ReactNativeApplicationModel.ReactNativeApplicationModel): void {
67+
model.ensureEnabled();
68+
model.addEventListener(SDK.ReactNativeApplicationModel.Events.METADATA_UPDATED, this.#handleMetadataUpdated, this);
69+
model.addEventListener(SDK.ReactNativeApplicationModel.Events.SYSTEM_STATE_CHANGED, this.#handleSystemStateChanged, this);
70+
}
71+
72+
modelRemoved(model: SDK.ReactNativeApplicationModel.ReactNativeApplicationModel): void {
73+
model.removeEventListener(
74+
SDK.ReactNativeApplicationModel.Events.METADATA_UPDATED, this.#handleMetadataUpdated, this);
75+
model.removeEventListener(
76+
SDK.ReactNativeApplicationModel.Events.SYSTEM_STATE_CHANGED, this.#handleSystemStateChanged, this);
77+
}
78+
79+
#handleMetadataUpdated(
80+
event: Common.EventTarget.EventTargetEvent<Protocol.ReactNativeApplication.MetadataUpdatedEvent>): void {
81+
// eslint-disable-next-line @typescript-eslint/naming-convention
82+
const {unstable_isProfilingBuild, unstable_networkInspectionEnabled} = event.data;
83+
84+
if (unstable_isProfilingBuild) {
85+
FuseboxWindowTitleManager.instance().setSuffix('[PROFILING]');
86+
this.#hideUnsupportedFeaturesForProfilingBuilds();
87+
}
88+
89+
if (unstable_networkInspectionEnabled) {
90+
this.#ensureNetworkPanelEnabled();
91+
}
92+
}
93+
94+
#handleSystemStateChanged(
95+
event: Common.EventTarget.EventTargetEvent<Protocol.ReactNativeApplication.SystemStateChangedEvent>): void {
96+
const {isSingleHost} = event.data;
97+
if (!isSingleHost) {
98+
this.#disableSingleHostOnlyFeatures();
99+
}
100+
}
101+
102+
#hideUnsupportedFeaturesForProfilingBuilds(): void {
103+
UI.InspectorView.InspectorView.instance().closeDrawer();
104+
105+
const viewManager = UI.ViewManager.ViewManager.instance();
106+
const panelLocationPromise = viewManager.resolveLocation(UI.ViewManager.ViewLocationValues.PANEL);
107+
const drawerLocationPromise = viewManager.resolveLocation(UI.ViewManager.ViewLocationValues.DRAWER_VIEW);
108+
void Promise.all([panelLocationPromise, drawerLocationPromise])
109+
.then(([panelLocation, drawerLocation]) => {
110+
UI.ViewManager.getRegisteredViewExtensions().forEach(view => {
111+
if (view.location() === UI.ViewManager.ViewLocationValues.DRAWER_VIEW) {
112+
drawerLocation?.removeView(view);
113+
} else {
114+
switch (view.viewId()) {
115+
case 'console':
116+
case 'heap-profiler':
117+
case 'live-heap-profile':
118+
case 'sources':
119+
case 'network':
120+
case 'react-devtools-components':
121+
case 'react-devtools-profiler':
122+
panelLocation?.removeView(view);
123+
break;
124+
}
125+
}
126+
});
127+
});
128+
}
129+
130+
#ensureNetworkPanelEnabled(): void {
131+
if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.ENABLE_NETWORK_PANEL)) {
132+
return;
133+
}
134+
135+
Root.Runtime.experiments.setEnabled(
136+
Root.Runtime.ExperimentName.ENABLE_NETWORK_PANEL,
137+
true,
138+
);
139+
140+
UI.InspectorView?.InspectorView?.instance()?.displayReloadRequiredWarning(
141+
i18nString(UIStrings.reloadRequiredForNetworkPanelMessage),
142+
);
143+
}
144+
145+
#disableSingleHostOnlyFeatures(): void {
146+
if (this.#singleHostFeaturesDisabled) {
147+
return;
148+
}
149+
150+
// Disable relevant CDP domains
151+
const targetManager = SDK.TargetManager.TargetManager.instance();
152+
for (const target of targetManager.targets()) {
153+
void target.networkAgent().invoke_disable();
154+
}
155+
156+
// Stop network recording if active
157+
void this.#disableNetworkRecording();
158+
159+
// Show in-panel overlay when disabled panels are selected
160+
const inspectorView = UI.InspectorView.InspectorView.instance();
161+
const overlaidPanels = new Set<string>();
162+
163+
const showPanelOverlay = (panel: UI.Panel.Panel, panelId: string): void => {
164+
const titleText =
165+
panelId === 'network'
166+
? i18nString(UIStrings.networkInspectionUnavailable)
167+
: panelId === 'timeline'
168+
? i18nString(UIStrings.performanceProfilingUnavailable)
169+
: i18nString(UIStrings.multiHostFeatureUnavailableTitle);
170+
171+
// Dim the existing panel content and disable interaction
172+
for (const child of panel.element.children) {
173+
const element = child as HTMLElement;
174+
element.style.opacity = '0.5';
175+
element.style.pointerEvents = 'none';
176+
element.setAttribute('inert', '');
177+
element.setAttribute('aria-hidden', 'true');
178+
}
179+
180+
const alertBar = document.createElement('div');
181+
render(html`
182+
<style>
183+
.alert-bar {
184+
background: var(--sys-color-tonal-container);
185+
color: var(--sys-color-on-tonal-container);
186+
padding: var(--sys-size-6) var(--sys-size-8);
187+
border-bottom: 1px solid var(--sys-color-tonal-outline);
188+
}
189+
.alert-title {
190+
font: var(--sys-typescale-body2-medium);
191+
margin-bottom: var(--sys-size-3);
192+
}
193+
.alert-detail {
194+
font: var(--sys-typescale-body4-regular);
195+
}
196+
</style>
197+
<div class="alert-bar">
198+
<div class="alert-title">${titleText}</div>
199+
<div class="alert-detail">
200+
${i18nString(UIStrings.multiHostFeatureDisabledDetail)}
201+
See <x-link href="https://github.com/react-native-community/discussions-and-proposals/discussions/954" class="devtools-link" jslog=${VisualLogging.link().track({click: true, keydown:'Enter|Space'}).context('multi-host-learn-more')}>discussions/954</x-link>.
202+
</div>
203+
</div>
204+
`, alertBar, {host: this});
205+
206+
panel.element.insertBefore(alertBar, panel.element.firstChild);
207+
};
208+
209+
inspectorView.tabbedPane.addEventListener(UI.TabbedPane.Events.TabSelected, event => {
210+
const tabId = event.data.tabId;
211+
if (UNSAFE_MULTI_HOST_FEATURES.has(tabId) && !overlaidPanels.has(tabId)) {
212+
overlaidPanels.add(tabId);
213+
void inspectorView.panel(tabId).then(panel => {
214+
if (panel) {
215+
showPanelOverlay(panel, tabId);
216+
}
217+
});
218+
}
219+
});
220+
221+
// Show overlay if a disabled panel is currently selected
222+
const currentTabId = inspectorView.tabbedPane.selectedTabId;
223+
if (currentTabId && UNSAFE_MULTI_HOST_FEATURES.has(currentTabId)) {
224+
overlaidPanels.add(currentTabId);
225+
void inspectorView.panel(currentTabId).then(panel => {
226+
if (panel) {
227+
showPanelOverlay(panel, currentTabId);
228+
}
229+
});
230+
}
231+
232+
this.#singleHostFeaturesDisabled = true;
233+
}
234+
235+
async #disableNetworkRecording(): Promise<void> {
236+
const inspectorView = UI.InspectorView.InspectorView.instance();
237+
try {
238+
const networkPanel = await inspectorView.panel('network');
239+
if (networkPanel && 'toggleRecord' in networkPanel) {
240+
(networkPanel as UI.Panel.Panel & {toggleRecord: (toggled: boolean) => void}).toggleRecord(false);
241+
}
242+
} catch {
243+
}
244+
}
245+
}

front_end/entrypoints/rn_fusebox/rn_fusebox.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import * as UI from '../../ui/legacy/legacy.js';
2727
import * as Main from '../main/main.js';
2828

2929
import * as FuseboxAppMetadataObserverModule from './FuseboxAppMetadataObserver.js';
30-
import * as FuseboxFeatureObserverModule from './FuseboxExperimentsObserver.js';
30+
import * as FuseboxFeatureObserverModule from './FuseboxFeatureObserver.js';
3131
import * as FuseboxReconnectDeviceButtonModule from './FuseboxReconnectDeviceButton.js';
3232

3333
// To ensure accurate timing measurements, please make sure these perf metrics

0 commit comments

Comments
 (0)