@samitouri / QOS-React-2 / commits / 36d51f4eb6

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 +}