Skip to content

Commit eabd18c

Browse files
authored
Scheduling Profiler: Move preprocessing to web worker and add loading indicator (#19759)
* Move preprocessData into a web worker * Add UI feedback for loading/import error states * Terminate worker when done handling profile * Add display density CSS variables
1 parent 38a512a commit eabd18c

File tree

16 files changed

+245
-93
lines changed

16 files changed

+245
-93
lines changed

packages/react-devtools-scheduling-profiler/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"url-loader": "^4.1.0",
2929
"webpack": "^4.44.1",
3030
"webpack-cli": "^3.3.12",
31-
"webpack-dev-server": "^3.11.0"
31+
"webpack-dev-server": "^3.11.0",
32+
"worker-loader": "^3.0.2"
3233
}
3334
}

packages/react-devtools-scheduling-profiler/src/App.js

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,21 @@ import '@reach/tooltip/styles.css';
1414

1515
import * as React from 'react';
1616

17-
import {ModalDialogContextController} from 'react-devtools-shared/src/devtools/views/ModalDialog';
1817
import {SchedulingProfiler} from './SchedulingProfiler';
19-
import {useBrowserTheme} from './hooks';
18+
import {useBrowserTheme, useDisplayDensity} from './hooks';
2019

2120
import styles from './App.css';
2221
import 'react-devtools-shared/src/devtools/views/root.css';
2322

2423
export default function App() {
2524
useBrowserTheme();
25+
useDisplayDensity();
2626

2727
return (
28-
<ModalDialogContextController>
29-
<div className={styles.DevTools}>
30-
<div className={styles.TabContent}>
31-
<SchedulingProfiler />
32-
</div>
28+
<div className={styles.DevTools}>
29+
<div className={styles.TabContent}>
30+
<SchedulingProfiler />
3331
</div>
34-
</ModalDialogContextController>
32+
</div>
3533
);
3634
}

packages/react-devtools-scheduling-profiler/src/ImportButton.css

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,3 @@
88
overflow: hidden;
99
clip: rect(1px, 1px, 1px, 1px);
1010
}
11-
12-
.ErrorMessage {
13-
margin: 0.5rem 0;
14-
color: var(--color-dim);
15-
font-family: var(--font-family-monospace);
16-
font-size: var(--font-size-monospace-normal);
17-
}

packages/react-devtools-scheduling-profiler/src/ImportButton.js

Lines changed: 6 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,61 +7,32 @@
77
* @flow
88
*/
99

10-
import type {TimelineEvent} from '@elg/speedscope';
11-
import type {ReactProfilerData} from './types';
12-
1310
import * as React from 'react';
14-
import {useCallback, useContext, useRef} from 'react';
11+
import {useCallback, useRef} from 'react';
1512

1613
import Button from 'react-devtools-shared/src/devtools/views/Button';
1714
import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
18-
import {ModalDialogContext} from 'react-devtools-shared/src/devtools/views/ModalDialog';
19-
20-
import preprocessData from './utils/preprocessData';
21-
import {readInputData} from './utils/readInputData';
2215

2316
import styles from './ImportButton.css';
2417

2518
type Props = {|
26-
onDataImported: (profilerData: ReactProfilerData) => void,
19+
onFileSelect: (file: File) => void,
2720
|};
2821

29-
export default function ImportButton({onDataImported}: Props) {
22+
export default function ImportButton({onFileSelect}: Props) {
3023
const inputRef = useRef<HTMLInputElement | null>(null);
31-
const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext);
3224

33-
const handleFiles = useCallback(async () => {
25+
const handleFiles = useCallback(() => {
3426
const input = inputRef.current;
3527
if (input === null) {
3628
return;
3729
}
38-
3930
if (input.files.length > 0) {
40-
try {
41-
const readFile = await readInputData(input.files[0]);
42-
const events: TimelineEvent[] = JSON.parse(readFile);
43-
if (events.length > 0) {
44-
onDataImported(preprocessData(events));
45-
}
46-
} catch (error) {
47-
modalDialogDispatch({
48-
type: 'SHOW',
49-
title: 'Import failed',
50-
content: (
51-
<>
52-
<div>The profiling data you selected cannot be imported.</div>
53-
{error !== null && (
54-
<div className={styles.ErrorMessage}>{error.message}</div>
55-
)}
56-
</>
57-
),
58-
});
59-
}
31+
onFileSelect(input.files[0]);
6032
}
61-
6233
// Reset input element to allow the same file to be re-imported
6334
input.value = '';
64-
}, [onDataImported, modalDialogDispatch]);
35+
}, [onFileSelect]);
6536

6637
const uploadData = useCallback(() => {
6738
if (inputRef.current !== null) {

packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
text-align: center;
2929
}
3030

31+
.ErrorMessage {
32+
margin: 0.5rem 0;
33+
color: var(--color-dim);
34+
font-family: var(--font-family-monospace);
35+
font-size: var(--font-size-monospace-normal);
36+
}
37+
3138
.Row {
3239
display: flex;
3340
flex-direction: row;

packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,53 +7,87 @@
77
* @flow
88
*/
99

10+
import type {Resource} from 'react-devtools-shared/src/devtools/cache';
1011
import type {ReactProfilerData} from './types';
12+
import type {ImportWorkerOutputData} from './import-worker/import.worker';
1113

1214
import * as React from 'react';
13-
import {useState} from 'react';
14-
15-
import ImportButton from './ImportButton';
16-
import {ModalDialog} from 'react-devtools-shared/src/devtools/views/ModalDialog';
15+
import {Suspense, useCallback, useState} from 'react';
16+
import {createResource} from 'react-devtools-shared/src/devtools/cache';
1717
import ReactLogo from 'react-devtools-shared/src/devtools/views/ReactLogo';
1818

19+
import ImportButton from './ImportButton';
1920
import CanvasPage from './CanvasPage';
21+
import ImportWorker from './import-worker/import.worker';
2022

2123
import profilerBrowser from './assets/profilerBrowser.png';
2224
import styles from './SchedulingProfiler.css';
2325

24-
export function SchedulingProfiler(_: {||}) {
25-
const [profilerData, setProfilerData] = useState<ReactProfilerData | null>(
26-
null,
27-
);
26+
type DataResource = Resource<void, File, ReactProfilerData | Error>;
27+
28+
function createDataResourceFromImportedFile(file: File): DataResource {
29+
return createResource(
30+
() => {
31+
return new Promise<ReactProfilerData | Error>((resolve, reject) => {
32+
const worker: Worker = new (ImportWorker: any)();
2833

29-
const view = profilerData ? (
30-
<CanvasPage profilerData={profilerData} />
31-
) : (
32-
<Welcome onDataImported={setProfilerData} />
34+
worker.onmessage = function(event) {
35+
const data = ((event.data: any): ImportWorkerOutputData);
36+
switch (data.status) {
37+
case 'SUCCESS':
38+
resolve(data.processedData);
39+
break;
40+
case 'INVALID_PROFILE_ERROR':
41+
resolve(data.error);
42+
break;
43+
case 'UNEXPECTED_ERROR':
44+
reject(data.error);
45+
break;
46+
}
47+
worker.terminate();
48+
};
49+
50+
worker.postMessage({file});
51+
});
52+
},
53+
() => file,
54+
{useWeakMap: true},
3355
);
56+
}
57+
58+
export function SchedulingProfiler(_: {||}) {
59+
const [dataResource, setDataResource] = useState<DataResource | null>(null);
60+
61+
const handleFileSelect = useCallback((file: File) => {
62+
setDataResource(createDataResourceFromImportedFile(file));
63+
}, []);
3464

3565
return (
3666
<div className={styles.SchedulingProfiler}>
3767
<div className={styles.Toolbar}>
3868
<ReactLogo />
3969
<span className={styles.AppName}>Concurrent Mode Profiler</span>
4070
<div className={styles.VRule} />
41-
<ImportButton onDataImported={setProfilerData} />
71+
<ImportButton onFileSelect={handleFileSelect} />
4272
<div className={styles.Spacer} />
4373
</div>
4474
<div className={styles.Content}>
45-
{view}
46-
<ModalDialog />
75+
{dataResource ? (
76+
<Suspense fallback={<ProcessingData />}>
77+
<DataResourceComponent
78+
dataResource={dataResource}
79+
onFileSelect={handleFileSelect}
80+
/>
81+
</Suspense>
82+
) : (
83+
<Welcome onFileSelect={handleFileSelect} />
84+
)}
4785
</div>
4886
</div>
4987
);
5088
}
5189

52-
type WelcomeProps = {|
53-
onDataImported: (profilerData: ReactProfilerData) => void,
54-
|};
55-
56-
const Welcome = ({onDataImported}: WelcomeProps) => (
90+
const Welcome = ({onFileSelect}: {|onFileSelect: (file: File) => void|}) => (
5791
<div className={styles.EmptyStateContainer}>
5892
<div className={styles.ScreenshotWrapper}>
5993
<img
@@ -65,8 +99,47 @@ const Welcome = ({onDataImported}: WelcomeProps) => (
6599
<div className={styles.Header}>Welcome!</div>
66100
<div className={styles.Row}>
67101
Click the import button
68-
<ImportButton onDataImported={onDataImported} /> to import a Chrome
102+
<ImportButton onFileSelect={onFileSelect} /> to import a Chrome
69103
performance profile.
70104
</div>
71105
</div>
72106
);
107+
108+
const ProcessingData = () => (
109+
<div className={styles.EmptyStateContainer}>
110+
<div className={styles.Header}>Processing data...</div>
111+
<div className={styles.Row}>This should only take a minute.</div>
112+
</div>
113+
);
114+
115+
const CouldNotLoadProfile = ({error, onFileSelect}) => (
116+
<div className={styles.EmptyStateContainer}>
117+
<div className={styles.Header}>Could not load profile</div>
118+
{error.message && (
119+
<div className={styles.Row}>
120+
<div className={styles.ErrorMessage}>{error.message}</div>
121+
</div>
122+
)}
123+
<div className={styles.Row}>
124+
Try importing
125+
<ImportButton onFileSelect={onFileSelect} />
126+
another Chrome performance profile.
127+
</div>
128+
</div>
129+
);
130+
131+
const DataResourceComponent = ({
132+
dataResource,
133+
onFileSelect,
134+
}: {|
135+
dataResource: DataResource,
136+
onFileSelect: (file: File) => void,
137+
|}) => {
138+
const dataOrError = dataResource.read();
139+
if (dataOrError instanceof Error) {
140+
return (
141+
<CouldNotLoadProfile error={dataOrError} onFileSelect={onFileSelect} />
142+
);
143+
}
144+
return <CanvasPage profilerData={dataOrError} />;
145+
};

packages/react-devtools-scheduling-profiler/src/hooks.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import {
1313
useLayoutEffect,
1414
} from 'react';
1515

16-
import {updateThemeVariables} from 'react-devtools-shared/src/devtools/views/Settings/SettingsContext';
16+
import {
17+
updateDisplayDensity,
18+
updateThemeVariables,
19+
} from 'react-devtools-shared/src/devtools/views/Settings/SettingsContext';
1720
import {enableDarkMode} from './SchedulingProfilerFeatureFlags';
1821

1922
export type BrowserTheme = 'dark' | 'light';
@@ -57,3 +60,10 @@ export function useBrowserTheme(): void {
5760
}
5861
}, [theme]);
5962
}
63+
64+
export function useDisplayDensity(): void {
65+
useLayoutEffect(() => {
66+
const documentElements = [((document.documentElement: any): HTMLElement)];
67+
updateDisplayDensity('comfortable', documentElements);
68+
}, []);
69+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
/**
11+
* An error thrown when an invalid profile could not be processed.
12+
*/
13+
export default class InvalidProfileError extends Error {}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import 'regenerator-runtime/runtime';
11+
12+
import type {TimelineEvent} from '@elg/speedscope';
13+
import type {ReactProfilerData} from '../types';
14+
15+
import preprocessData from './preprocessData';
16+
import {readInputData} from './readInputData';
17+
import InvalidProfileError from './InvalidProfileError';
18+
19+
declare var self: DedicatedWorkerGlobalScope;
20+
21+
type ImportWorkerInputData = {|
22+
file: File,
23+
|};
24+
25+
export type ImportWorkerOutputData =
26+
| {|status: 'SUCCESS', processedData: ReactProfilerData|}
27+
| {|status: 'INVALID_PROFILE_ERROR', error: Error|}
28+
| {|status: 'UNEXPECTED_ERROR', error: Error|};
29+
30+
self.onmessage = async function(event: MessageEvent) {
31+
const {file} = ((event.data: any): ImportWorkerInputData);
32+
33+
try {
34+
const readFile = await readInputData(file);
35+
const events: TimelineEvent[] = JSON.parse(readFile);
36+
if (events.length === 0) {
37+
throw new InvalidProfileError('No profiling data found in file.');
38+
}
39+
40+
self.postMessage({
41+
status: 'SUCCESS',
42+
processedData: preprocessData(events),
43+
});
44+
} catch (error) {
45+
if (error instanceof InvalidProfileError) {
46+
self.postMessage({
47+
status: 'INVALID_PROFILE_ERROR',
48+
error,
49+
});
50+
} else {
51+
self.postMessage({
52+
status: 'UNEXPECTED_ERROR',
53+
error,
54+
});
55+
}
56+
}
57+
};

0 commit comments

Comments
 (0)