refactor[react-devtools]: flatten reload and profile config (#31132)
Stacked on https://github.com/facebook/react/pull/31131. See last commit. This is a clean-up and a pre-requisite for next changes: 1. `ReloadAndProfileConfig` is now split into boolean value and settings object. This is mainly because I will add one more setting soon, and also because settings might be persisted for a longer time than the flag which signals if the Backend was reloaded for profiling. Ideally, this settings should probably be moved to the global Hook object, same as we did for console patching. 2. Host is now responsible for reseting the cached values, Backend will execute provided `onReloadAndProfileFlagsReset` callback.
Ruslan Lesiutin committed
Oct 9, 2024 at 13:57 UTC
bfe91fbecf183f85fc1c4f909e12a6833a247319
10 files changed
+131
-99
packages/react-devtools-core/src/backend.js
+31
-10
@@ -26,8 +26,7 @@ import type {
26
import type {
27
DevToolsHook,
28
DevToolsHookSettings,
29
- ReloadAndProfileConfig,
30
- ReloadAndProfileConfigPersistence,
29
+ ProfilingSettings,
30
} from 'react-devtools-shared/src/backend/types';
31
import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
32
@@ -42,7 +41,9 @@ type ConnectOptions = {
41
websocket?: ?WebSocket,
42
onSettingsUpdated?: (settings: $ReadOnly<DevToolsHookSettings>) => void,
43
isReloadAndProfileSupported?: boolean,
45
- reloadAndProfileConfigPersistence?: ReloadAndProfileConfigPersistence,
44
+ isProfiling?: boolean,
45
+ onReloadAndProfile?: (recordChangeDescriptions: boolean) => void,
46
+ onReloadAndProfileFlagsReset?: () => void,
47
};
48
49
let savedComponentFilters: Array<ComponentFilter> =
@@ -63,9 +64,15 @@ export function initialize(
64
maybeSettingsOrSettingsPromise?:
65
| DevToolsHookSettings
66
| Promise<DevToolsHookSettings>,
66
- reloadAndProfileConfig?: ReloadAndProfileConfig,
67
+ shouldStartProfilingNow: boolean = false,
68
+ profilingSettings?: ProfilingSettings,
69
) {
68
- installHook(window, maybeSettingsOrSettingsPromise, reloadAndProfileConfig);
70
+ installHook(
71
+ window,
72
+ maybeSettingsOrSettingsPromise,
73
+ shouldStartProfilingNow,
74
+ profilingSettings,
75
+ );
76
}
77
78
export function connectToDevTools(options: ?ConnectOptions) {
@@ -86,7 +93,9 @@ export function connectToDevTools(options: ?ConnectOptions) {
93
isAppActive = () => true,
94
onSettingsUpdated,
95
isReloadAndProfileSupported = getIsReloadAndProfileSupported(),
89
- reloadAndProfileConfigPersistence,
96
+ isProfiling,
97
+ onReloadAndProfile,
98
+ onReloadAndProfileFlagsReset,
99
} = options || {};
100
101
const protocol = useHttps ? 'wss' : 'ws';
@@ -180,7 +189,11 @@ export function connectToDevTools(options: ?ConnectOptions) {
189
190
// TODO (npm-packages) Warn if "isBackendStorageAPISupported"
191
// $FlowFixMe[incompatible-call] found when upgrading Flow
183
- const agent = new Agent(bridge, reloadAndProfileConfigPersistence);
192
+ const agent = new Agent(bridge, isProfiling, onReloadAndProfile);
193
+ if (typeof onReloadAndProfileFlagsReset === 'function') {
194
+ onReloadAndProfileFlagsReset();
195
+ }
196
+
197
if (onSettingsUpdated != null) {
198
agent.addListener('updateHookSettings', onSettingsUpdated);
199
}
@@ -320,7 +333,9 @@ type ConnectWithCustomMessagingOptions = {
333
resolveRNStyle?: ResolveNativeStyle,
334
onSettingsUpdated?: (settings: $ReadOnly<DevToolsHookSettings>) => void,
335
isReloadAndProfileSupported?: boolean,
323
- reloadAndProfileConfigPersistence?: ReloadAndProfileConfigPersistence,
336
+ isProfiling?: boolean,
337
+ onReloadAndProfile?: (recordChangeDescriptions: boolean) => void,
338
+ onReloadAndProfileFlagsReset?: () => void,
339
};
340
341
export function connectWithCustomMessagingProtocol({
@@ -331,7 +346,9 @@ export function connectWithCustomMessagingProtocol({
346
resolveRNStyle,
347
onSettingsUpdated,
348
isReloadAndProfileSupported = getIsReloadAndProfileSupported(),
334
- reloadAndProfileConfigPersistence,
349
+ isProfiling,
350
+ onReloadAndProfile,
351
+ onReloadAndProfileFlagsReset,
352
}: ConnectWithCustomMessagingOptions): Function {
353
const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
354
if (hook == null) {
@@ -368,7 +385,11 @@ export function connectWithCustomMessagingProtocol({
385
bridge.send('overrideComponentFilters', savedComponentFilters);
386
}
387
371
- const agent = new Agent(bridge, reloadAndProfileConfigPersistence);
388
+ const agent = new Agent(bridge, isProfiling, onReloadAndProfile);
389
+ if (typeof onReloadAndProfileFlagsReset === 'function') {
390
+ onReloadAndProfileFlagsReset();
391
+ }
392
+
393
if (onSettingsUpdated != null) {
394
agent.addListener('updateHookSettings', onSettingsUpdated);
395
}
packages/react-devtools-extensions/src/contentScripts/backendManager.js
+14
-1
@@ -14,6 +14,11 @@ import type {
14
import {hasAssignedBackend} from 'react-devtools-shared/src/backend/utils';
15
import {COMPACT_VERSION_NAME} from 'react-devtools-extensions/src/utils';
16
import {getIsReloadAndProfileSupported} from 'react-devtools-shared/src/utils';
17
+import {
18
+ getIfReloadedAndProfiling,
19
+ onReloadAndProfile,
20
+ onReloadAndProfileFlagsReset,
21
+} from 'react-devtools-shared/src/utils';
22
23
let welcomeHasInitialized = false;
24
const requiredBackends = new Set<string>();
@@ -140,7 +145,15 @@ function activateBackend(version: string, hook: DevToolsHook) {
145
},
146
});
147
143
- const agent = new Agent(bridge);
148
+ const agent = new Agent(
149
+ bridge,
150
+ getIfReloadedAndProfiling(),
151
+ onReloadAndProfile,
152
+ );
153
+ // Agent read flags successfully, we can count it as successful launch
154
+ // Clean up flags, so that next reload won't start profiling
155
+ onReloadAndProfileFlagsReset();
156
+
157
agent.addListener('shutdown', () => {
158
// If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
159
// and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
packages/react-devtools-extensions/src/contentScripts/installHook.js
+12
-1
@@ -1,4 +1,8 @@
1
import {installHook} from 'react-devtools-shared/src/hook';
2
+import {
3
+ getIfReloadedAndProfiling,
4
+ getProfilingSettings,
5
+} from 'react-devtools-shared/src/utils';
6
7
let resolveHookSettingsInjection;
8
@@ -34,8 +38,15 @@ if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) {
38
payload: {handshake: true},
39
});
40
41
+ const shouldStartProfiling = getIfReloadedAndProfiling();
42
+ const profilingSettings = getProfilingSettings();
43
// Can't delay hook installation, inject settings lazily
38
- installHook(window, hookSettingsPromise);
44
+ installHook(
45
+ window,
46
+ hookSettingsPromise,
47
+ shouldStartProfiling,
48
+ profilingSettings,
49
+ );
50
51
// Detect React
52
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.on(
packages/react-devtools-inline/src/backend.js
+12
-2
@@ -8,7 +8,12 @@ import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyl
8
9
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
10
import type {Wall} from 'react-devtools-shared/src/frontend/types';
11
-import {getIsReloadAndProfileSupported} from 'react-devtools-shared/src/utils';
11
+import {
12
+ getIfReloadedAndProfiling,
13
+ getIsReloadAndProfileSupported,
14
+ onReloadAndProfile,
15
+ onReloadAndProfileFlagsReset,
16
+} from 'react-devtools-shared/src/utils';
17
18
function startActivation(contentWindow: any, bridge: BackendBridge) {
19
const onSavedPreferences = (data: $FlowFixMe) => {
@@ -63,7 +68,12 @@ function startActivation(contentWindow: any, bridge: BackendBridge) {
68
}
69
70
function finishActivation(contentWindow: any, bridge: BackendBridge) {
66
- const agent = new Agent(bridge);
71
+ const agent = new Agent(
72
+ bridge,
73
+ getIfReloadedAndProfiling(),
74
+ onReloadAndProfile,
75
+ );
76
+ onReloadAndProfileFlagsReset();
77
78
const hook = contentWindow.__REACT_DEVTOOLS_GLOBAL_HOOK__;
79
if (hook) {
packages/react-devtools-shared/src/attachRenderer.js
+5
-3
@@ -12,8 +12,8 @@ import type {
12
RendererInterface,
13
DevToolsHook,
14
RendererID,
15
+ ProfilingSettings,
16
} from 'react-devtools-shared/src/backend/types';
16
-import type {ReloadAndProfileConfig} from './backend/types';
17
18
import {attach as attachFlight} from 'react-devtools-shared/src/backend/flight/renderer';
19
import {attach as attachFiber} from 'react-devtools-shared/src/backend/fiber/renderer';
@@ -30,7 +30,8 @@ export default function attachRenderer(
30
id: RendererID,
31
renderer: ReactRenderer,
32
global: Object,
33
- reloadAndProfileConfig: ReloadAndProfileConfig,
33
+ shouldStartProfilingNow: boolean,
34
+ profilingSettings: ProfilingSettings,
35
): RendererInterface | void {
36
// only attach if the renderer is compatible with the current version of the backend
37
if (!isMatchingRender(renderer.reconcilerVersion || renderer.version)) {
@@ -55,7 +56,8 @@ export default function attachRenderer(
56
id,
57
renderer,
58
global,
58
- reloadAndProfileConfig,
59
+ shouldStartProfilingNow,
60
+ profilingSettings,
61
);
62
} else if (renderer.ComponentTree) {
63
// react-dom v15
packages/react-devtools-shared/src/backend/agent.js
+8
-25
@@ -26,11 +26,9 @@ import type {
26
RendererID,
27
RendererInterface,
28
DevToolsHookSettings,
29
- ReloadAndProfileConfigPersistence,
29
} from './types';
30
import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
31
import {isReactNativeEnvironment} from './utils';
33
-import {defaultReloadAndProfileConfigPersistence} from '../utils';
32
import {
33
sessionStorageGetItem,
34
sessionStorageRemoveItem,
@@ -151,33 +149,21 @@ export default class Agent extends EventEmitter<{
149
}> {
150
_bridge: BackendBridge;
151
_isProfiling: boolean = false;
154
- _recordChangeDescriptions: boolean = false;
152
_rendererInterfaces: {[key: RendererID]: RendererInterface, ...} = {};
153
_persistedSelection: PersistedSelection | null = null;
154
_persistedSelectionMatch: PathMatch | null = null;
155
_traceUpdatesEnabled: boolean = false;
159
- _reloadAndProfileConfigPersistence: ReloadAndProfileConfigPersistence;
156
+ _onReloadAndProfile: ((recordChangeDescriptions: boolean) => void) | void;
157
158
constructor(
159
bridge: BackendBridge,
163
- reloadAndProfileConfigPersistence?: ReloadAndProfileConfigPersistence = defaultReloadAndProfileConfigPersistence,
160
+ isProfiling: boolean = false,
161
+ onReloadAndProfile?: (recordChangeDescriptions: boolean) => void,
162
) {
163
super();
164
167
- this._reloadAndProfileConfigPersistence = reloadAndProfileConfigPersistence;
168
- const {getReloadAndProfileConfig, setReloadAndProfileConfig} =
169
- reloadAndProfileConfigPersistence;
170
- const reloadAndProfileConfig = getReloadAndProfileConfig();
171
- if (reloadAndProfileConfig.shouldReloadAndProfile) {
172
- this._recordChangeDescriptions =
173
- reloadAndProfileConfig.recordChangeDescriptions;
174
- this._isProfiling = true;
175
-
176
- setReloadAndProfileConfig({
177
- shouldReloadAndProfile: false,
178
- recordChangeDescriptions: false,
179
- });
180
- }
165
+ this._isProfiling = isProfiling;
166
+ this._onReloadAndProfile = onReloadAndProfile;
167
168
const persistedSelectionString = sessionStorageGetItem(
169
SESSION_STORAGE_LAST_SELECTION_KEY,
@@ -674,10 +660,9 @@ export default class Agent extends EventEmitter<{
660
661
reloadAndProfile: (recordChangeDescriptions: boolean) => void =
662
recordChangeDescriptions => {
677
- this._reloadAndProfileConfigPersistence.setReloadAndProfileConfig({
678
- shouldReloadAndProfile: true,
679
- recordChangeDescriptions,
680
- });
663
+ if (typeof this._onReloadAndProfile === 'function') {
664
+ this._onReloadAndProfile(recordChangeDescriptions);
665
+ }
666
667
// This code path should only be hit if the shell has explicitly told the Store that it supports profiling.
668
// In that case, the shell must also listen for this specific message to know when it needs to reload the app.
@@ -757,7 +742,6 @@ export default class Agent extends EventEmitter<{
742
743
startProfiling: (recordChangeDescriptions: boolean) => void =
744
recordChangeDescriptions => {
760
- this._recordChangeDescriptions = recordChangeDescriptions;
745
this._isProfiling = true;
746
for (const rendererID in this._rendererInterfaces) {
747
const renderer = ((this._rendererInterfaces[
@@ -770,7 +754,6 @@ export default class Agent extends EventEmitter<{
754
755
stopProfiling: () => void = () => {
756
this._isProfiling = false;
773
- this._recordChangeDescriptions = false;
757
for (const rendererID in this._rendererInterfaces) {
758
const renderer = ((this._rendererInterfaces[
759
(rendererID: any)
packages/react-devtools-shared/src/backend/fiber/renderer.js
+5
-6
@@ -104,7 +104,6 @@ import {
104
supportsOwnerStacks,
105
supportsConsoleTasks,
106
} from './DevToolsFiberComponentStack';
107
-import type {ReloadAndProfileConfig} from '../types';
107
108
// $FlowFixMe[method-unbinding]
109
const toString = Object.prototype.toString;
@@ -136,6 +135,7 @@ import type {
135
WorkTagMap,
136
CurrentDispatcherRef,
137
LegacyDispatcherRef,
138
+ ProfilingSettings,
139
} from '../types';
140
import type {
141
ComponentFilter,
@@ -864,7 +864,8 @@ export function attach(
864
rendererID: number,
865
renderer: ReactRenderer,
866
global: Object,
867
- reloadAndProfileConfig: ReloadAndProfileConfig,
867
+ shouldStartProfilingNow: boolean,
868
+ profilingSettings: ProfilingSettings,
869
): RendererInterface {
870
// Newer versions of the reconciler package also specific reconciler version.
871
// If that version number is present, use it.
@@ -5225,10 +5226,8 @@ export function attach(
5226
}
5227
5228
// Automatically start profiling so that we don't miss timing info from initial "mount".
5228
- if (reloadAndProfileConfig.shouldReloadAndProfile) {
5229
- const shouldRecordChangeDescriptions =
5230
- reloadAndProfileConfig.recordChangeDescriptions;
5231
- startProfiling(shouldRecordChangeDescriptions);
5229
+ if (shouldStartProfilingNow) {
5230
+ startProfiling(profilingSettings.recordChangeDescriptions);
5231
}
5232
5233
function getNearestFiber(devtoolsInstance: DevToolsInstance): null | Fiber {
packages/react-devtools-shared/src/backend/types.js
+1
-11
@@ -485,20 +485,10 @@ export type DevToolsBackend = {
485
setupNativeStyleEditor?: SetupNativeStyleEditor,
486
};
487
488
-export type ReloadAndProfileConfig = {
489
- shouldReloadAndProfile: boolean,
488
+export type ProfilingSettings = {
489
recordChangeDescriptions: boolean,
490
};
491
493
-// Linter doesn't speak Flow's `Partial` type
494
-// eslint-disable-next-line no-undef
495
-type PartialReloadAndProfileConfig = Partial<ReloadAndProfileConfig>;
496
-
497
-export type ReloadAndProfileConfigPersistence = {
498
- setReloadAndProfileConfig: (config: PartialReloadAndProfileConfig) => void,
499
- getReloadAndProfileConfig: () => ReloadAndProfileConfig,
500
-};
501
-
492
export type DevToolsHook = {
493
listeners: {[key: string]: Array<Handler>, ...},
494
rendererInterfaces: Map<RendererID, RendererInterface>,
packages/react-devtools-shared/src/hook.js
+11
-4
@@ -16,7 +16,7 @@ import type {
16
RendererInterface,
17
DevToolsBackend,
18
DevToolsHookSettings,
19
- ReloadAndProfileConfig,
19
+ ProfilingSettings,
20
} from './backend/types';
21
22
import {
@@ -27,7 +27,6 @@ import {
27
import attachRenderer from './attachRenderer';
28
import formatConsoleArguments from 'react-devtools-shared/src/backend/utils/formatConsoleArguments';
29
import formatWithStyles from 'react-devtools-shared/src/backend/utils/formatWithStyles';
30
-import {defaultReloadAndProfileConfigPersistence} from './utils';
30
31
// React's custom built component stack strings match "\s{4}in"
32
// Chrome's prefix matches "\s{4}at"
@@ -51,12 +50,17 @@ function areStackTracesEqual(a: string, b: string): boolean {
50
51
const targetConsole: Object = console;
52
53
+const defaultProfilingSettings: ProfilingSettings = {
54
+ recordChangeDescriptions: false,
55
+};
56
+
57
export function installHook(
58
target: any,
59
maybeSettingsOrSettingsPromise?:
60
| DevToolsHookSettings
61
| Promise<DevToolsHookSettings>,
59
- reloadAndProfileConfig?: ReloadAndProfileConfig = defaultReloadAndProfileConfigPersistence.getReloadAndProfileConfig(),
62
+ shouldStartProfilingNow: boolean = false,
63
+ profilingSettings: ProfilingSettings = defaultProfilingSettings,
64
): DevToolsHook | null {
65
if (target.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) {
66
return null;
@@ -195,6 +199,8 @@ export function installHook(
199
} catch (err) {}
200
}
201
202
+ // TODO: isProfiling should be stateful, and we should update it once profiling is finished
203
+ const isProfiling = shouldStartProfilingNow;
204
let uidCounter = 0;
205
function inject(renderer: ReactRenderer): number {
206
const id = ++uidCounter;
@@ -215,7 +221,8 @@ export function installHook(
221
id,
222
renderer,
223
target,
218
- reloadAndProfileConfig,
224
+ isProfiling,
225
+ profilingSettings,
226
);
227
if (rendererInterface != null) {
228
hook.rendererInterfaces.set(id, rendererInterface);
packages/react-devtools-shared/src/utils.js
+32
-36
@@ -56,8 +56,9 @@ import {
56
localStorageGetItem,
57
localStorageSetItem,
58
sessionStorageGetItem,
59
+ sessionStorageRemoveItem,
60
sessionStorageSetItem,
60
-} from './storage';
61
+} from 'react-devtools-shared/src/storage';
62
import {meta} from './hydration';
63
import isArray from './isArray';
64
@@ -67,12 +68,11 @@ import type {
68
SerializedElement as SerializedElementFrontend,
69
LRUCache,
70
} from 'react-devtools-shared/src/frontend/types';
70
-import type {SerializedElement as SerializedElementBackend} from 'react-devtools-shared/src/backend/types';
71
-import {isSynchronousXHRSupported} from './backend/utils';
71
import type {
73
- ReloadAndProfileConfig,
74
- ReloadAndProfileConfigPersistence,
75
-} from './backend/types';
72
+ ProfilingSettings,
73
+ SerializedElement as SerializedElementBackend,
74
+} from 'react-devtools-shared/src/backend/types';
75
+import {isSynchronousXHRSupported} from './backend/utils';
76
77
// $FlowFixMe[method-unbinding]
78
const hasOwnProperty = Object.prototype.hasOwnProperty;
@@ -990,34 +990,30 @@ export function getIsReloadAndProfileSupported(): boolean {
990
return isBackendStorageAPISupported && isSynchronousXHRSupported();
991
}
992
993
-export const defaultReloadAndProfileConfigPersistence: ReloadAndProfileConfigPersistence =
994
- {
995
- setReloadAndProfileConfig({
996
- shouldReloadAndProfile,
997
- recordChangeDescriptions,
998
- }): void {
999
- if (shouldReloadAndProfile != null) {
1000
- sessionStorageSetItem(
1001
- SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
1002
- shouldReloadAndProfile ? 'true' : 'false',
1003
- );
1004
- }
1005
- if (recordChangeDescriptions != null) {
1006
- sessionStorageSetItem(
1007
- SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
1008
- recordChangeDescriptions ? 'true' : 'false',
1009
- );
1010
- }
1011
- },
1012
- getReloadAndProfileConfig(): ReloadAndProfileConfig {
1013
- return {
1014
- shouldReloadAndProfile:
1015
- sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) ===
1016
- 'true',
1017
- recordChangeDescriptions:
1018
- sessionStorageGetItem(
1019
- SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
1020
- ) === 'true',
1021
- };
1022
- },
993
+// Expected to be used only by browser extension and react-devtools-inline
994
+export function getIfReloadedAndProfiling(): boolean {
995
+ return (
996
+ sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true'
997
+ );
998
+}
999
+
1000
+export function getProfilingSettings(): ProfilingSettings {
1001
+ return {
1002
+ recordChangeDescriptions:
1003
+ sessionStorageGetItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) ===
1004
+ 'true',
1005
};
1006
+}
1007
+
1008
+export function onReloadAndProfile(recordChangeDescriptions: boolean): void {
1009
+ sessionStorageSetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, 'true');
1010
+ sessionStorageSetItem(
1011
+ SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
1012
+ recordChangeDescriptions ? 'true' : 'false',
1013
+ );
1014
+}
1015
+
1016
+export function onReloadAndProfileFlagsReset(): void {
1017
+ sessionStorageRemoveItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY);
1018
+ sessionStorageRemoveItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY);
1019
+}