Added root selector UI to Profiler
Brian Vaughn committed
May 22, 2019 at 06:40 UTC
36d51f4eb69b89a22746aacc1550edde5f385410
7 files changed
+188
-213
src/devtools/views/Profiler/CommitFilterModal.js
+10
@@ -8,6 +8,16 @@ import { useModalDismissSignal } from '../hooks';
8
import styles from './CommitFilterModal.css';
9
10
export default function FilterModal(_: {||}) {
11
+ const { isModalShowing } = useContext(CommitFilterModalContext);
12
+
13
+ if (!isModalShowing) {
14
+ return null;
15
+ }
16
+
17
+ return <FilterModalImpl />;
18
+}
19
+
20
+function FilterModalImpl(_: {||}) {
21
const {
22
isCommitFilterEnabled,
23
minCommitDuration,
src/devtools/views/Profiler/NoCommitData.js
+9
-27
@@ -1,37 +1,19 @@
1
// @flow
2
3
-import React, { useContext } from 'react';
4
-import { ProfilerContext } from './ProfilerContext';
3
+import React from 'react';
4
import ToggleCommitFilterModalButton from './ToggleCommitFilterModalButton';
5
6
import styles from './NoCommitData.css';
7
8
export default function NoCommitData(_: {||}) {
10
- const { rootHasProfilingData } = useContext(ProfilerContext);
11
-
12
- if (rootHasProfilingData) {
13
- return (
14
- <div className={styles.NoCommitData}>
15
- <div className={styles.Header}>
16
- There is no data matching the current filter criteria.
17
- </div>
18
- <div className={styles.FilterMessage}>
19
- Try adjusting the commit filter <ToggleCommitFilterModalButton />
20
- </div>
9
+ return (
10
+ <div className={styles.NoCommitData}>
11
+ <div className={styles.Header}>
12
+ There is no data matching the current filter criteria.
13
</div>
22
- );
23
- } else {
24
- return (
25
- <div className={styles.NoCommitData}>
26
- <div className={styles.Header}>
27
- There is no timing data to display for the currently selected commit.
28
- </div>
29
- <div>
30
- This can indicate that a render occurred too quickly for the timing
31
- API to measure. Try selecting another commit in the upper, right-hand
32
- corner.
33
- </div>
14
+ <div className={styles.FilterMessage}>
15
+ Try adjusting the commit filter <ToggleCommitFilterModalButton />
16
</div>
35
- );
36
- }
17
+ </div>
18
+ );
19
}
src/devtools/views/Profiler/Profiler.js
+64
-145
@@ -1,10 +1,7 @@
1
// @flow
2
3
-import React, { Suspense, useContext } from 'react';
4
-import {
5
- CommitFilterModalContext,
6
- CommitFilterModalContextController,
7
-} from './CommitFilterModalContext';
3
+import React, { useContext } from 'react';
4
+import { CommitFilterModalContextController } from './CommitFilterModalContext';
5
import { ModalDialog } from '../ModalDialog';
6
import { ProfilerContext } from './ProfilerContext';
7
import TabBar from '../TabBar';
@@ -13,6 +10,7 @@ import CommitFlamegraph from './CommitFlamegraph';
10
import CommitRanked from './CommitRanked';
11
import CommitFilterModal from './CommitFilterModal';
12
import Interactions from './Interactions';
13
+import RootSelector from './RootSelector';
14
import RecordToggle from './RecordToggle';
15
import ReloadAndProfileButton from './ReloadAndProfileButton';
16
import ProfilingImportExportButtons from './ProfilingImportExportButtons';
@@ -30,111 +28,38 @@ export type Props = {|
28
|};
29
30
function Profiler({ supportsProfiling }: Props) {
33
- const { hasProfilingData, isProfiling, rootHasProfilingData } = useContext(
34
- ProfilerContext
35
- );
36
-
37
- if (isProfiling || !rootHasProfilingData) {
38
- return (
39
- <NonSuspendingProfiler
40
- hasProfilingData={hasProfilingData}
41
- isProfiling={isProfiling}
42
- supportsProfiling={supportsProfiling}
43
- />
44
- );
45
- } else {
46
- return (
47
- <CommitFilterModalContextController>
48
- <SuspendingProfiler />
49
- </CommitFilterModalContextController>
50
- );
51
- }
52
-}
31
+ const {
32
+ hasProfilingData,
33
+ isProcessingData,
34
+ isProfiling,
35
+ selectedFiberID,
36
+ selectedTabID,
37
+ selectTab,
38
+ } = useContext(ProfilerContext);
39
54
-// This view is rendered when there is no profiler data (either we haven't profiled yet or we're currently profiling).
55
-// Nothing in this view's subtree suspends.
56
-// By not suspending while profiling is in progress, we avoid potential cache invalidation trickiness.
57
-// NOTE that the structure of this UI should mirror SuspendingProfiler.
58
-function NonSuspendingProfiler({
59
- hasProfilingData,
60
- isProfiling,
61
- supportsProfiling,
62
-}: {|
63
- hasProfilingData: boolean,
64
- isProfiling: boolean,
65
- supportsProfiling: boolean,
66
-|}) {
40
let view = null;
41
if (!supportsProfiling) {
42
view = <ProfilingNotSupported />;
43
} else if (isProfiling) {
71
- view = <RecortdingInProgress />;
44
+ view = <RecordingInProgress />;
45
+ } else if (isProcessingData) {
46
+ view = <ProcessingData />;
47
} else if (!hasProfilingData) {
48
view = <NoProfilingData />;
49
} else {
75
- view = <NoProfilingDataForRoot />;
76
- }
77
-
78
- return (
79
- <div className={styles.Profiler}>
80
- <div className={styles.LeftColumn}>
81
- <div className={styles.Toolbar}>
82
- <RecordToggle disabled={!supportsProfiling} />
83
- <ReloadAndProfileButton />
84
- <ClearProfilingDataButton />
85
- <ProfilingImportExportButtons />
86
- <div className={styles.VRule} />
87
- <TabBar
88
- currentTab={null}
89
- disabled
90
- id="Profiler"
91
- selectTab={() => {}}
92
- size="small"
93
- tabs={tabs}
94
- />
95
- </div>
96
- <div className={styles.Content}>
97
- {view}
98
- <ModalDialog />
99
- </div>
100
- </div>
101
- </div>
102
- );
103
-}
104
-
105
-function ContentFallback() {
106
- return <div className={styles.ContentFallback}>Loading...</div>;
107
-}
108
-
109
-function SnapshotSelectorFallback() {
110
- return <div className={styles.SnapshotSelectorFallback}>Loading...</div>;
111
-}
112
-
113
-// This view is rendered when there is profiler data (even though there may not be any for the currently selected root).
114
-// This view's subtree uses suspense to request profiler data from the backend.
115
-// NOTE that the structure of this UI should mirror NonSuspendingProfiler.
116
-function SuspendingProfiler() {
117
- const { selectedFiberID, selectedTabID, selectTab } = useContext(
118
- ProfilerContext
119
- );
120
-
121
- const { isModalShowing: isFilterModalShowing } = useContext(
122
- CommitFilterModalContext
123
- );
124
-
125
- let view = null;
126
- switch (selectedTabID) {
127
- case 'flame-chart':
128
- view = <CommitFlamegraph />;
129
- break;
130
- case 'ranked-chart':
131
- view = <CommitRanked />;
132
- break;
133
- case 'interactions':
134
- view = <Interactions />;
135
- break;
136
- default:
137
- break;
50
+ switch (selectedTabID) {
51
+ case 'flame-chart':
52
+ view = <CommitFlamegraph />;
53
+ break;
54
+ case 'ranked-chart':
55
+ view = <CommitRanked />;
56
+ break;
57
+ case 'interactions':
58
+ view = <Interactions />;
59
+ break;
60
+ default:
61
+ break;
62
+ }
63
}
64
65
let sidebar = null;
@@ -155,38 +80,37 @@ function SuspendingProfiler() {
80
}
81
82
return (
158
- <div className={styles.Profiler}>
159
- <div className={styles.LeftColumn}>
160
- <div className={styles.Toolbar}>
161
- <RecordToggle />
162
- <ReloadAndProfileButton />
163
- <ClearProfilingDataButton />
164
- <ProfilingImportExportButtons />
165
- <div className={styles.VRule} />
166
- <TabBar
167
- currentTab={selectedTabID}
168
- id="Profiler"
169
- selectTab={selectTab}
170
- size="small"
171
- tabs={tabs}
172
- />
173
- <div className={styles.Spacer} />
174
- <ToggleCommitFilterModalButton />
175
- <div className={styles.VRule} />
176
- <Suspense fallback={<SnapshotSelectorFallback />}>
177
- <SnapshotSelector />
178
- </Suspense>
179
- </div>
180
- <div className={styles.Content}>
181
- <Suspense fallback={<ContentFallback />}>{view}</Suspense>
182
- {isFilterModalShowing && <CommitFilterModal />}
183
- <ModalDialog />
83
+ <CommitFilterModalContextController>
84
+ <div className={styles.Profiler}>
85
+ <div className={styles.LeftColumn}>
86
+ <div className={styles.Toolbar}>
87
+ <RecordToggle />
88
+ <ReloadAndProfileButton />
89
+ <ClearProfilingDataButton />
90
+ <ProfilingImportExportButtons />
91
+ <div className={styles.VRule} />
92
+ <TabBar
93
+ currentTab={selectedTabID}
94
+ id="Profiler"
95
+ selectTab={selectTab}
96
+ size="small"
97
+ tabs={tabs}
98
+ />
99
+ <RootSelector />
100
+ <div className={styles.Spacer} />
101
+ <ToggleCommitFilterModalButton />
102
+ <div className={styles.VRule} />
103
+ {hasProfilingData && <SnapshotSelector />}
104
+ </div>
105
+ <div className={styles.Content}>
106
+ {view}
107
+ <CommitFilterModal />
108
+ <ModalDialog />
109
+ </div>
110
</div>
111
+ <div className={styles.RightColumn}>{sidebar}</div>
112
</div>
186
- <div className={styles.RightColumn}>
187
- <Suspense fallback={<ContentFallback />}>{sidebar}</Suspense>
188
- </div>
189
- </div>
113
+ </CommitFilterModalContextController>
114
);
115
}
116
@@ -220,18 +144,6 @@ const NoProfilingData = () => (
144
</div>
145
);
146
223
-const NoProfilingDataForRoot = () => (
224
- <div className={styles.Column}>
225
- <div className={styles.Header}>
226
- No profiling data has been recorded for the selected root.
227
- </div>
228
- <div className={styles.Row}>
229
- Select a different root in the elements panel, or click the record button{' '}
230
- <RecordToggle /> to start recording.
231
- </div>
232
- </div>
233
-);
234
-
147
const ProfilingNotSupported = () => (
148
<div className={styles.Column}>
149
<div className={styles.Header}>Profiling not supported.</div>
@@ -256,7 +168,14 @@ const ProfilingNotSupported = () => (
168
</div>
169
);
170
259
-const RecortdingInProgress = () => (
171
+const ProcessingData = () => (
172
+ <div className={styles.Column}>
173
+ <div className={styles.Header}>Processing data...</div>
174
+ <div className={styles.Row}>This should only take a minute.</div>
175
+ </div>
176
+);
177
+
178
+const RecordingInProgress = () => (
179
<div className={styles.Column}>
180
<div className={styles.Header}>Profiling is in progress...</div>
181
<div className={styles.Row}>
src/devtools/views/Profiler/ProfilerContext.js
+53
-38
@@ -31,16 +31,17 @@ type Context = {|
31
// or from the backend itself (after a reload-and-profile action).
32
// It is synced between the backend and frontend via a Store subscription.
33
hasProfilingData: boolean,
34
+ isProcessingData: boolean,
35
isProfiling: boolean,
36
startProfiling(value: boolean): void,
37
stopProfiling(value: boolean): void,
38
38
- // Which renderer and root should profiling data be shown for?
39
- // Often this will correspond to the selected renderer and root in the Elements panel.
40
- // If nothing is selected though, this will default to the first root.
41
- rendererID: number | null,
39
+ // Which root should profiling data be shown for?
40
+ // This value should be initialized to either:
41
+ // 1. The selected root in the Components tree (if it has any profiling data) or
42
+ // 2. The first root in the list with profiling data.
43
rootID: number | null,
43
- rootHasProfilingData: boolean,
44
+ setRootID: (id: number) => void,
45
46
// Controls whether commits are filtered by duration.
47
// This value is controlled by a filter toggle UI in the Profiler toolbar.
@@ -72,8 +73,9 @@ ProfilerContext.displayName = 'ProfilerContext';
73
74
type StoreProfilingState = {|
75
hasProfilingData: boolean,
75
- profilingData: ProfilingDataFrontend | null,
76
+ isProcessingData: boolean,
77
isProfiling: boolean,
78
+ profilingData: ProfilingDataFrontend | null,
79
|};
80
81
type Props = {|
@@ -85,44 +87,57 @@ function ProfilerContextController({ children }: Props) {
87
const { selectedElementID } = useContext(TreeStateContext);
88
const dispatch = useContext(TreeDispatcherContext);
89
90
+ const { profilerStore } = store;
91
+
92
const subscription = useMemo(
93
() => ({
94
getCurrentValue: () => ({
91
- hasProfilingData: store.hasProfilingData,
92
- profilingData: store.profilingData,
93
- isProfiling: store.isProfiling,
95
+ hasProfilingData: profilerStore.hasProfilingData,
96
+ isProcessingData: profilerStore.isProcessingData,
97
+ isProfiling: profilerStore.isProfiling,
98
+ profilingData: profilerStore.profilingData,
99
}),
100
subscribe: (callback: Function) => {
96
- store.addListener('profilingData', callback);
97
- store.addListener('isProfiling', callback);
101
+ profilerStore.addListener('profilingData', callback);
102
+ profilerStore.addListener('isProcessingData', callback);
103
+ profilerStore.addListener('isProfiling', callback);
104
return () => {
99
- store.removeListener('profilingData', callback);
100
- store.removeListener('isProfiling', callback);
105
+ profilerStore.removeListener('profilingData', callback);
106
+ profilerStore.removeListener('isProcessingData', callback);
107
+ profilerStore.removeListener('isProfiling', callback);
108
};
109
},
110
}),
104
- [store]
111
+ [profilerStore]
112
);
106
- const { isProfiling, hasProfilingData, profilingData } = useSubscription<
107
- StoreProfilingState,
108
- Store
109
- >(subscription);
110
-
111
- let rendererID = null;
112
- let rootID = null;
113
- let rootHasProfilingData = false;
114
- if (profilingData !== null) {
115
- rootHasProfilingData = true;
116
- } else if (selectedElementID !== null) {
117
- rendererID = store.getRendererIDForElement(selectedElementID);
118
- rootID = store.getRootIDForElement(selectedElementID);
119
- rootHasProfilingData =
120
- rootID === null ? false : store.profilingOperationsByRootID.has(rootID);
121
- } else if (store.roots.length > 0) {
122
- // If no root is selected, assume the first root; many React apps are single root anyway.
123
- rootID = store.roots[0];
124
- rootHasProfilingData = store.profilingOperationsByRootID.has(rootID);
125
- rendererID = store.getRendererIDForElement(rootID);
113
+ const {
114
+ hasProfilingData,
115
+ isProcessingData,
116
+ isProfiling,
117
+ profilingData,
118
+ } = useSubscription<StoreProfilingState, Store>(subscription);
119
+
120
+ const [rootID, setRootID] = useState<number | null>(null);
121
+
122
+ const dataForRoots =
123
+ profilingData !== null ? profilingData.dataForRoots : null;
124
+ if (dataForRoots != null) {
125
+ const firstRootID = dataForRoots.keys().next().value || null;
126
+
127
+ if (rootID === null || !dataForRoots.has(rootID)) {
128
+ let selectedElementRootID = null;
129
+ if (selectedElementID !== null) {
130
+ selectedElementRootID = store.getRootIDForElement(selectedElementID);
131
+ }
132
+ if (
133
+ selectedElementRootID !== null &&
134
+ dataForRoots.has(selectedElementRootID)
135
+ ) {
136
+ setRootID(selectedElementRootID);
137
+ } else {
138
+ setRootID(firstRootID);
139
+ }
140
+ }
141
}
142
143
const startProfiling = useCallback(() => store.startProfiling(), [store]);
@@ -186,13 +201,13 @@ function ProfilerContextController({ children }: Props) {
201
selectTab,
202
203
hasProfilingData,
204
+ isProcessingData,
205
isProfiling,
206
startProfiling,
207
stopProfiling,
208
193
- rendererID,
209
rootID,
195
- rootHasProfilingData,
210
+ setRootID,
211
212
isCommitFilterEnabled,
213
setIsCommitFilterEnabled,
@@ -214,13 +229,13 @@ function ProfilerContextController({ children }: Props) {
229
selectTab,
230
231
hasProfilingData,
232
+ isProcessingData,
233
isProfiling,
234
startProfiling,
235
stopProfiling,
236
221
- rendererID,
237
rootID,
223
- rootHasProfilingData,
238
+ setRootID,
239
240
isCommitFilterEnabled,
241
setIsCommitFilterEnabled,
src/devtools/views/Profiler/ProfilingImportExportButtons.js
+3
-3
@@ -16,7 +16,7 @@ import styles from './ProfilingImportExportButtons.css';
16
import type { ProfilingDataExport } from './types';
17
18
export default function ProfilingImportExportButtons() {
19
- const { isProfiling, rendererID, rootID } = useContext(ProfilerContext);
19
+ const { isProfiling, rootID } = useContext(ProfilerContext);
20
const store = useContext(StoreContext);
21
const { profilerStore } = store;
22
@@ -25,7 +25,7 @@ export default function ProfilingImportExportButtons() {
25
const { dispatch: modalDialogDispatch } = useContext(ModalDialogContext);
26
27
const downloadData = useCallback(() => {
28
- if (rendererID === null || rootID === null) {
28
+ if (rootID === null) {
29
return;
30
}
31
@@ -37,7 +37,7 @@ export default function ProfilingImportExportButtons() {
37
// TODO (profarc) Generate anchor "download" tag and click it
38
console.log('profilingDataExport:', profilingDataExport);
39
}
40
- }, [rendererID, rootID, profilerStore.profilingData]);
40
+ }, [rootID, profilerStore.profilingData]);
41
42
const uploadData = useCallback(() => {
43
if (inputRef.current !== null) {
src/devtools/views/Profiler/RootSelector.css
new
+3
@@ -0,0 +1,3 @@
1
+.Spacer {
2
+ flex: 1;
3
+}
src/devtools/views/Profiler/RootSelector.js
new
+46
@@ -0,0 +1,46 @@
1
+// @flow
2
+
3
+import React, { Fragment, useCallback, useContext } from 'react';
4
+import { ProfilerContext } from './ProfilerContext';
5
+import { StoreContext } from '../context';
6
+
7
+import styles from './RootSelector.css';
8
+
9
+export default function RootSelector(_: {||}) {
10
+ const store = useContext(StoreContext);
11
+ const { rootID, setRootID } = useContext(ProfilerContext);
12
+
13
+ const { profilingData } = store.profilerStore;
14
+
15
+ const options = [];
16
+ if (profilingData !== null) {
17
+ profilingData.dataForRoots.forEach((dataForRoot, rootID) => {
18
+ options.push(
19
+ <option key={rootID} value={rootID}>
20
+ {dataForRoot.displayName}
21
+ </option>
22
+ );
23
+ });
24
+ }
25
+
26
+ const handleChange = useCallback(
27
+ ({ currentTarget }) => {
28
+ setRootID(parseInt(currentTarget.value, 10));
29
+ },
30
+ [setRootID]
31
+ );
32
+
33
+ if (profilingData === null || profilingData.dataForRoots.size <= 1) {
34
+ // Don't take up visual space if there's only one root.
35
+ return null;
36
+ }
37
+
38
+ return (
39
+ <Fragment>
40
+ <div className={styles.Spacer} />
41
+ <select value={rootID} onChange={handleChange}>
42
+ {options}
43
+ </select>
44
+ </Fragment>
45
+ );
46
+}