Skip to content

Commit 9fef1b1

Browse files
committed
fix[react-devtools]: update profiling status before receiving response from backend
1 parent 1460d67 commit 9fef1b1

File tree

4 files changed

+40
-17
lines changed

4 files changed

+40
-17
lines changed

packages/react-devtools-shared/src/devtools/ProfilerStore.js

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import EventEmitter from '../events';
1111
import {prepareProfilingDataFrontendFromBackendAndStore} from './views/Profiler/utils';
1212
import ProfilingCache from './ProfilingCache';
1313
import Store from './store';
14+
import {logEvent} from 'react-devtools-shared/src/Logger';
1415

1516
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
1617
import type {ProfilingDataBackend} from 'react-devtools-shared/src/backend/types';
@@ -67,7 +68,12 @@ export default class ProfilerStore extends EventEmitter<{
6768

6869
// The backend is currently profiling.
6970
// When profiling is in progress, operations are stored so that we can later reconstruct past commit trees.
70-
_isProfiling: boolean = false;
71+
_isBackendProfiling: boolean = false;
72+
73+
// Mainly used for optimistic UI.
74+
// This could be false, but at the same time _isBackendProfiling could be true
75+
// for cases when Backend is busy serializing a chunky payload.
76+
_isProfilingBasedOnUserInput: boolean = false;
7177

7278
// Tracks whether a specific renderer logged any profiling data during the most recent session.
7379
_rendererIDsThatReportedProfilingData: Set<number> = new Set();
@@ -86,7 +92,8 @@ export default class ProfilerStore extends EventEmitter<{
8692
super();
8793

8894
this._bridge = bridge;
89-
this._isProfiling = defaultIsProfiling;
95+
this._isBackendProfiling = defaultIsProfiling;
96+
this._isProfilingBasedOnUserInput = defaultIsProfiling;
9097
this._store = store;
9198

9299
bridge.addListener('operations', this.onBridgeOperations);
@@ -139,8 +146,8 @@ export default class ProfilerStore extends EventEmitter<{
139146
return this._rendererQueue.size > 0 || this._dataBackends.length > 0;
140147
}
141148

142-
get isProfiling(): boolean {
143-
return this._isProfiling;
149+
get isProfilingBasedOnUserInput(): boolean {
150+
return this._isProfilingBasedOnUserInput;
144151
}
145152

146153
get profilingCache(): ProfilingCache {
@@ -151,7 +158,7 @@ export default class ProfilerStore extends EventEmitter<{
151158
return this._dataFrontend;
152159
}
153160
set profilingData(value: ProfilingDataFrontend | null): void {
154-
if (this._isProfiling) {
161+
if (this._isBackendProfiling) {
155162
console.warn(
156163
'Profiling data cannot be updated while profiling is in progress.',
157164
);
@@ -186,6 +193,9 @@ export default class ProfilerStore extends EventEmitter<{
186193
startProfiling(): void {
187194
this._bridge.send('startProfiling', this._store.recordChangeDescriptions);
188195

196+
this._isProfilingBasedOnUserInput = true;
197+
this.emit('isProfiling');
198+
189199
// Don't actually update the local profiling boolean yet!
190200
// Wait for onProfilingStatus() to confirm the status has changed.
191201
// This ensures the frontend and backend are in sync wrt which commits were profiled.
@@ -195,8 +205,12 @@ export default class ProfilerStore extends EventEmitter<{
195205
stopProfiling(): void {
196206
this._bridge.send('stopProfiling');
197207

198-
// Don't actually update the local profiling boolean yet!
199-
// Wait for onProfilingStatus() to confirm the status has changed.
208+
// Backend might be busy serializing the payload, so we are going to display
209+
// optimistic UI to the user that profiling is stopping.
210+
this._isProfilingBasedOnUserInput = false;
211+
this.emit('isProfiling');
212+
213+
// Wait for onProfilingStatus() to confirm the status has changed, this will update _isBackendProfiling.
200214
// This ensures the frontend and backend are in sync wrt which commits were profiled.
201215
// We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors.
202216
}
@@ -229,7 +243,7 @@ export default class ProfilerStore extends EventEmitter<{
229243
const rendererID = operations[0];
230244
const rootID = operations[1];
231245

232-
if (this._isProfiling) {
246+
if (this._isBackendProfiling) {
233247
let profilingOperations = this._inProgressOperationsByRootID.get(rootID);
234248
if (profilingOperations == null) {
235249
profilingOperations = [operations];
@@ -252,8 +266,8 @@ export default class ProfilerStore extends EventEmitter<{
252266

253267
onBridgeProfilingData: (dataBackend: ProfilingDataBackend) => void =
254268
dataBackend => {
255-
if (this._isProfiling) {
256-
// This should never happen, but if it does- ignore previous profiling data.
269+
if (this._isBackendProfiling) {
270+
// This should never happen, but if it does, then ignore previous profiling data.
257271
return;
258272
}
259273

@@ -289,7 +303,7 @@ export default class ProfilerStore extends EventEmitter<{
289303
};
290304

291305
onProfilingStatus: (isProfiling: boolean) => void = isProfiling => {
292-
if (this._isProfiling === isProfiling) {
306+
if (this._isBackendProfiling === isProfiling) {
293307
return;
294308
}
295309

@@ -319,15 +333,24 @@ export default class ProfilerStore extends EventEmitter<{
319333
});
320334
}
321335

322-
this._isProfiling = isProfiling;
336+
this._isBackendProfiling = isProfiling;
337+
// _isProfilingBasedOnUserInput should already be updated from startProfiling, stopProfiling, or constructor.
338+
if (this._isProfilingBasedOnUserInput !== isProfiling) {
339+
logEvent({
340+
event_name: 'error',
341+
error_message: `Unexpected profiling status. Expected ${this._isProfilingBasedOnUserInput}, but received ${isProfiling}.`,
342+
error_stack: new Error().stack,
343+
});
344+
345+
// If happened, fallback to displaying the value from Backend
346+
this._isProfilingBasedOnUserInput = isProfiling;
347+
}
323348

324349
// Invalidate suspense cache if profiling data is being (re-)recorded.
325350
// Note that we clear again, in case any views read from the cache while profiling.
326351
// (That would have resolved a now-stale value without any profiling data.)
327352
this._cache.invalidate();
328353

329-
this.emit('isProfiling');
330-
331354
// If we've just finished a profiling session, we need to fetch data stored in each renderer interface
332355
// and re-assemble it on the front-end into a format (ProfilingDataFrontend) that can power the Profiler UI.
333356
// During this time, DevTools UI should probably not be interactive.

packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ function ProfilerContextController({children}: Props): React.Node {
9898
getCurrentValue: () => ({
9999
didRecordCommits: profilerStore.didRecordCommits,
100100
isProcessingData: profilerStore.isProcessingData,
101-
isProfiling: profilerStore.isProfiling,
101+
isProfiling: profilerStore.isProfilingBasedOnUserInput,
102102
profilingData: profilerStore.profilingData,
103103
supportsProfiling: store.rootSupportsBasicProfiling,
104104
}),

packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export default function SettingsModal(): React.Node {
3939
// Explicitly disallow it for now.
4040
const isProfilingSubscription = useMemo(
4141
() => ({
42-
getCurrentValue: () => profilerStore.isProfiling,
42+
getCurrentValue: () => profilerStore.isProfilingBasedOnUserInput,
4343
subscribe: (callback: Function) => {
4444
profilerStore.addListener('isProfiling', callback);
4545
return () => profilerStore.removeListener('isProfiling', callback);

packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export default function SettingsModalContextToggle(): React.Node {
2929
// Explicitly disallow it for now.
3030
const isProfilingSubscription = useMemo(
3131
() => ({
32-
getCurrentValue: () => profilerStore.isProfiling,
32+
getCurrentValue: () => profilerStore.isProfilingBasedOnUserInput,
3333
subscribe: (callback: Function) => {
3434
profilerStore.addListener('isProfiling', callback);
3535
return () => profilerStore.removeListener('isProfiling', callback);

0 commit comments

Comments
 (0)