From 7e5478419bd8c5a2ab869297bcbfe706fed2f15e Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Mon, 2 Dec 2024 11:59:16 +0000 Subject: [PATCH] Add initial feature/UI customisations for profiling builds (#138) --- config/gni/devtools_grd_files.gni | 1 + front_end/entrypoints/rn_fusebox/BUILD.gn | 17 ++++ .../FuseboxProfilingBuildObserver.ts | 89 +++++++++++++++++++ .../entrypoints/rn_fusebox/rn_fusebox.ts | 2 + front_end/generated/protocol.ts | 4 + front_end/panels/timeline/TimelinePanel.ts | 16 ++-- .../devtools_protocol/browser_protocol.json | 8 +- .../react_native_domains.pdl | 2 + 8 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 front_end/entrypoints/rn_fusebox/FuseboxProfilingBuildObserver.ts diff --git a/config/gni/devtools_grd_files.gni b/config/gni/devtools_grd_files.gni index 8d9faab8e7a..1a02f0e72a7 100644 --- a/config/gni/devtools_grd_files.gni +++ b/config/gni/devtools_grd_files.gni @@ -842,6 +842,7 @@ grd_files_debug_sources = [ "front_end/entrypoints/node_app/NodeConnectionsPanel.js", "front_end/entrypoints/node_app/NodeMain.js", "front_end/entrypoints/node_app/nodeConnectionsPanel.css.js", + "front_end/entrypoints/rn_fusebox/FuseboxProfilingBuildObserver.js", "front_end/entrypoints/shell/browser_compatibility_guard.js", "front_end/entrypoints/wasmparser_worker/WasmParserWorker.js", "front_end/entrypoints/worker_app/WorkerMain.js", diff --git a/front_end/entrypoints/rn_fusebox/BUILD.gn b/front_end/entrypoints/rn_fusebox/BUILD.gn index bd59ad615d8..5034fe7ae30 100644 --- a/front_end/entrypoints/rn_fusebox/BUILD.gn +++ b/front_end/entrypoints/rn_fusebox/BUILD.gn @@ -2,9 +2,25 @@ # Copyright 2021 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. + import("../../../scripts/build/ninja/devtools_entrypoint.gni") +import("../../../scripts/build/ninja/devtools_module.gni") import("../visibility.gni") +devtools_module("rn_fusebox") { + sources = [ + "FuseboxProfilingBuildObserver.ts", + ] + + deps = [ + "../../core/common:bundle", + "../../core/i18n:bundle", + "../../core/root:bundle", + "../../core/sdk:bundle", + "../../ui/legacy:bundle", + ] +} + devtools_entrypoint("entrypoint") { entrypoint = "rn_fusebox.ts" @@ -39,6 +55,7 @@ devtools_entrypoint("entrypoint") { "../../panels/webauthn:meta", "../main:bundle", "../shell", + ":rn_fusebox", ] visibility = [ "../../:*" ] diff --git a/front_end/entrypoints/rn_fusebox/FuseboxProfilingBuildObserver.ts b/front_end/entrypoints/rn_fusebox/FuseboxProfilingBuildObserver.ts new file mode 100644 index 00000000000..670b1a8c888 --- /dev/null +++ b/front_end/entrypoints/rn_fusebox/FuseboxProfilingBuildObserver.ts @@ -0,0 +1,89 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// 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. + +import * as Common from '../../core/common/common.js'; +import * as Protocol from '../../generated/protocol.js'; +import * as Root from '../../core/root/root.js'; +import * as SDK from '../../core/sdk/sdk.js'; +import * as UI from '../../ui/legacy/legacy.js'; +import * as i18n from '../../core/i18n/i18n.js'; + +const UIStrings = { + /** + * @description Message for the "settings changed" banner shown when a reload is required. + */ + reloadRequiredMessage: '[Profiling build first run] One or more settings have changed. Please reload to access the Performance panel.', +}; + +const str_ = i18n.i18n.registerUIStrings('entrypoints/rn_fusebox/FuseboxProfilingBuildModeObserver.ts', UIStrings); +const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); + +/** + * [Experimental] Model observer which configures available DevTools features + * when a profiling build is identified. + */ +export default class FuseboxProfilingBuildObserver implements + SDK.TargetManager.SDKModelObserver { + constructor(targetManager: SDK.TargetManager.TargetManager) { + targetManager.observeModels(SDK.ReactNativeApplicationModel.ReactNativeApplicationModel, this); + } + + modelAdded(model: SDK.ReactNativeApplicationModel.ReactNativeApplicationModel): void { + model.ensureEnabled(); + model.addEventListener(SDK.ReactNativeApplicationModel.Events.MetadataUpdated, this.#handleMetadataUpdated, this); + } + + modelRemoved(model: SDK.ReactNativeApplicationModel.ReactNativeApplicationModel): void { + model.removeEventListener( + SDK.ReactNativeApplicationModel.Events.MetadataUpdated, this.#handleMetadataUpdated, this); + } + + #handleMetadataUpdated( + event: Common.EventTarget.EventTargetEvent): void { + const {unstable_isProfilingBuild} = event.data; + + if (unstable_isProfilingBuild) { + this.#hideUnsupportedFeatures(); + this.#ensurePerformancePanelEnabled(); + } + } + + #hideUnsupportedFeatures(): void { + UI.ViewManager.ViewManager.instance() + .resolveLocation(UI.ViewManager.ViewLocationValues.PANEL) + .then(location => { + UI.ViewManager.getRegisteredViewExtensions().forEach(view => { + switch (view.viewId()) { + case 'sources': + case 'network': + case 'react-devtools-components': + case 'react-devtools-profiler': + location?.removeView(view); + break; + } + }); + }); + } + + #ensurePerformancePanelEnabled(): void { + if ( + !Root.Runtime.experiments.isEnabled( + Root.Runtime.ExperimentName.ENABLE_PERFORMANCE_PANEL, + ) + ) { + Root.Runtime.experiments.setEnabled( + Root.Runtime.ExperimentName.ENABLE_PERFORMANCE_PANEL, + true, + ); + + const inspectorView = UI.InspectorView?.InspectorView?.instance(); + if (inspectorView) { + inspectorView.displayReloadRequiredWarning( + i18nString(UIStrings.reloadRequiredMessage), + ); + } + } + } +} diff --git a/front_end/entrypoints/rn_fusebox/rn_fusebox.ts b/front_end/entrypoints/rn_fusebox/rn_fusebox.ts index 68729990f53..1227bffc679 100644 --- a/front_end/entrypoints/rn_fusebox/rn_fusebox.ts +++ b/front_end/entrypoints/rn_fusebox/rn_fusebox.ts @@ -28,6 +28,7 @@ import * as Protocol from '../../generated/protocol.js'; import type * as Platform from '../../core/platform/platform.js'; import type * as Sources from '../../panels/sources/sources.js'; import * as RNExperiments from '../../core/rn_experiments/rn_experiments.js'; +import FuseboxProfilingBuildObserver from './FuseboxProfilingBuildObserver.js'; /* * To ensure accurate timing measurements, @@ -275,5 +276,6 @@ class FuseboxReactNativeApplicationObserver implements } new FuseboxReactNativeApplicationObserver(SDK.TargetManager.TargetManager.instance()); +new FuseboxProfilingBuildObserver(SDK.TargetManager.TargetManager.instance()); Host.rnPerfMetrics.entryPointLoadingFinished('rn_fusebox'); diff --git a/front_end/generated/protocol.ts b/front_end/generated/protocol.ts index 1ae5157608b..5579af87fc4 100644 --- a/front_end/generated/protocol.ts +++ b/front_end/generated/protocol.ts @@ -60,6 +60,10 @@ export namespace ReactNativeApplication { * The React Native version. */ reactNativeVersion?: string; + /** + * Whether the app is a profiling build. + */ + unstable_isProfilingBuild?: boolean; } } diff --git a/front_end/panels/timeline/TimelinePanel.ts b/front_end/panels/timeline/TimelinePanel.ts index 98e5bf1e6db..6bb95396e6b 100644 --- a/front_end/panels/timeline/TimelinePanel.ts +++ b/front_end/panels/timeline/TimelinePanel.ts @@ -261,6 +261,7 @@ const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); let timelinePanelInstance: TimelinePanel; let isNode: boolean; +let isReactNative: boolean; export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineModeViewDelegate { private readonly dropTarget: UI.DropTarget.DropTarget; @@ -364,7 +365,7 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod this.captureLayersAndPicturesSetting.setTitle(i18nString(UIStrings.enableAdvancedPaint)); this.showScreenshotsSetting = - Common.Settings.Settings.instance().createSetting('timeline-show-screenshots', isNode ? false : true); + Common.Settings.Settings.instance().createSetting('timeline-show-screenshots', isNode || isReactNative ? false : true); this.showScreenshotsSetting.setTitle(i18nString(UIStrings.screenshots)); this.showScreenshotsSetting.addChangeListener(this.updateOverviewControls, this); @@ -377,7 +378,7 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod this.panelToolbar = new UI.Toolbar.Toolbar('timeline-main-toolbar', timelineToolbarContainer); this.panelToolbar.makeWrappable(true); this.panelRightToolbar = new UI.Toolbar.Toolbar('', timelineToolbarContainer); - if (!isNode) { + if (!isNode && !isReactNative) { this.createSettingsPane(); this.updateShowSettingsToolbarButton(); } @@ -454,6 +455,11 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod const {forceNew, isNode: isNodeMode} = opts; isNode = isNodeMode; + // [RN] Used to scope down available features for React Native targets + isReactNative = Root.Runtime.experiments.isEnabled( + Root.Runtime.ExperimentName.REACT_NATIVE_SPECIFIC_UI, + ); + if (!timelinePanelInstance || forceNew) { timelinePanelInstance = new TimelinePanel(); } @@ -560,7 +566,7 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod // View this.panelToolbar.appendSeparator(); - if (!isNode) { + if (!isNode && !isReactNative) { this.showScreenshotsToolbarCheckbox = this.createSettingCheckbox(this.showScreenshotsSetting, i18nString(UIStrings.captureScreenshots)); this.panelToolbar.appendToolbarItem(this.showScreenshotsToolbarCheckbox); @@ -581,7 +587,7 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod } // Settings - if (!isNode) { + if (!isNode && !isReactNative) { this.panelRightToolbar.appendSeparator(); this.panelRightToolbar.appendToolbarItem(this.showSettingsPaneButton); } @@ -801,7 +807,7 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod } private updateSettingsPaneVisibility(): void { - if (isNode) { + if (isNode || isReactNative) { return; } if (this.showSettingsPaneSetting.get()) { diff --git a/third_party/blink/public/devtools_protocol/browser_protocol.json b/third_party/blink/public/devtools_protocol/browser_protocol.json index 12944b13af7..95975d435f5 100644 --- a/third_party/blink/public/devtools_protocol/browser_protocol.json +++ b/third_party/blink/public/devtools_protocol/browser_protocol.json @@ -68,6 +68,12 @@ "description": "The React Native version.", "optional": true, "type": "string" + }, + { + "name": "unstable_isProfilingBuild", + "description": "Whether the app is a profiling build.", + "optional": true, + "type": "boolean" } ] } @@ -29262,4 +29268,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/third_party/blink/public/devtools_protocol/react_native_domains.pdl b/third_party/blink/public/devtools_protocol/react_native_domains.pdl index 60ba84b1a90..d9d10291c9d 100644 --- a/third_party/blink/public/devtools_protocol/react_native_domains.pdl +++ b/third_party/blink/public/devtools_protocol/react_native_domains.pdl @@ -37,3 +37,5 @@ experimental domain ReactNativeApplication optional string platform # The React Native version. optional string reactNativeVersion + # Whether the app is a profiling build. + optional boolean unstable_isProfilingBuild