main
js 230 lines 7.41 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and 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 * as React from 'react';
11 import {Fragment, useContext, useEffect, useRef, useEffectEvent} from 'react';
12 import {ModalDialog} from '../ModalDialog';
13 import {ProfilerContext} from './ProfilerContext';
14 import Button from '../Button';
15 import ButtonIcon from '../ButtonIcon';
16 import TabBar from '../TabBar';
17 import ClearProfilingDataButton from './ClearProfilingDataButton';
18 import CommitFlamegraph from './CommitFlamegraph';
19 import CommitRanked from './CommitRanked';
20 import RootSelector from './RootSelector';
21 import RecordToggle from './RecordToggle';
22 import ReloadAndProfileButton from './ReloadAndProfileButton';
23 import ProfilingImportExportButtons from './ProfilingImportExportButtons';
24 import SnapshotSelector from './SnapshotSelector';
25 import ProfilerSearchInput from './ProfilerSearchInput';
26 import SidebarCommitInfo from './SidebarCommitInfo';
27 import NoProfilingData from './NoProfilingData';
28 import RecordingInProgress from './RecordingInProgress';
29 import ProcessingData from './ProcessingData';
30 import ProfilingNotSupported from './ProfilingNotSupported';
31 import SidebarSelectedFiberInfo from './SidebarSelectedFiberInfo';
32 import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
33 import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
34 import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
35 import portaledContent from '../portaledContent';
36
37 import styles from './Profiler.css';
38
39 function Profiler(_: {}) {
40 const profilerRef = useRef<HTMLDivElement | null>(null);
41 const isMac =
42 typeof navigator !== 'undefined' &&
43 navigator.platform.toUpperCase().indexOf('MAC') >= 0;
44
45 const {
46 didRecordCommits,
47 isProcessingData,
48 isProfiling,
49 selectedCommitIndex,
50 selectedFiberID,
51 selectedTabID,
52 selectTab,
53 supportsProfiling,
54 startProfiling,
55 stopProfiling,
56 selectPrevCommitIndex,
57 selectNextCommitIndex,
58 isSearchInputVisible,
59 showSearchInput,
60 hideSearchInput,
61 } = useContext(ProfilerContext);
62
63 const handleKeyDown = useEffectEvent((event: KeyboardEvent) => {
64 const correctModifier = isMac ? event.metaKey : event.ctrlKey;
65 // Cmd+E to start/stop profiler recording
66 if (correctModifier && event.key === 'e') {
67 if (isProfiling) {
68 stopProfiling();
69 } else {
70 startProfiling();
71 }
72 event.preventDefault();
73 event.stopPropagation();
74 } else if (didRecordCommits && correctModifier && event.key === 'f') {
75 // Cmd+F (Mac) or Ctrl+F (Windows/Linux) to search components in the commit
76 showSearchInput();
77 event.preventDefault();
78 event.stopPropagation();
79 } else if (isSearchInputVisible && event.key === 'Escape') {
80 // Escape closes the search input.
81 hideSearchInput();
82 event.preventDefault();
83 event.stopPropagation();
84 } else if (didRecordCommits && selectedCommitIndex !== null) {
85 // Cmd+Left/Right (Mac) or Ctrl+Left/Right (Windows/Linux) to navigate commits
86 if (
87 correctModifier &&
88 (event.key === 'ArrowLeft' || event.key === 'ArrowRight')
89 ) {
90 if (event.key === 'ArrowLeft') {
91 selectPrevCommitIndex();
92 } else {
93 selectNextCommitIndex();
94 }
95 event.preventDefault();
96 event.stopPropagation();
97 }
98 }
99 });
100
101 useEffect(() => {
102 const div = profilerRef.current;
103 if (!div) {
104 return;
105 }
106 const ownerWindow = div.ownerDocument.defaultView;
107 // Capture phase: Cmd/Ctrl+F is a reserved browser shortcut (Find), so we
108 // must intercept it before the browser to open our own search instead.
109 ownerWindow.addEventListener('keydown', handleKeyDown, true);
110 return () => {
111 ownerWindow.removeEventListener('keydown', handleKeyDown, true);
112 };
113 }, []);
114
115 let view = null;
116 if (didRecordCommits) {
117 switch (selectedTabID) {
118 case 'flame-chart':
119 view = <CommitFlamegraph />;
120 break;
121 case 'ranked-chart':
122 view = <CommitRanked />;
123 break;
124 default:
125 break;
126 }
127 } else if (isProfiling) {
128 view = <RecordingInProgress />;
129 } else if (isProcessingData) {
130 view = <ProcessingData />;
131 } else if (supportsProfiling) {
132 view = <NoProfilingData />;
133 } else {
134 view = <ProfilingNotSupported />;
135 }
136
137 let sidebar = null;
138 if (!isProfiling && !isProcessingData && didRecordCommits) {
139 switch (selectedTabID) {
140 case 'flame-chart':
141 case 'ranked-chart':
142 // TRICKY
143 // Handle edge case where no commit is selected because of a min-duration filter update.
144 // In that case, the selected commit index would be null.
145 // We could still show a sidebar for the previously selected fiber,
146 // but it would be an odd user experience.
147 // TODO (ProfilerContext) This check should not be necessary.
148 if (selectedCommitIndex !== null) {
149 if (selectedFiberID !== null) {
150 sidebar = <SidebarSelectedFiberInfo />;
151 } else {
152 sidebar = <SidebarCommitInfo />;
153 }
154 }
155 break;
156 default:
157 break;
158 }
159 }
160
161 return (
162 <SettingsModalContextController>
163 <div ref={profilerRef} className={styles.Profiler}>
164 <div className={styles.LeftColumn}>
165 <div className={styles.Toolbar}>
166 <RecordToggle disabled={!supportsProfiling} />
167 <ReloadAndProfileButton disabled={!supportsProfiling} />
168 <ClearProfilingDataButton />
169 <ProfilingImportExportButtons />
170 <div className={styles.VRule} />
171 <TabBar
172 currentTab={selectedTabID}
173 id="Profiler"
174 selectTab={selectTab}
175 tabs={tabs}
176 type="profiler"
177 />
178 <RootSelector />
179 <div className={styles.Spacer} />
180 <SettingsModalContextToggle />
181 {didRecordCommits && (
182 <Fragment>
183 <div className={styles.VRule} />
184 <Button
185 onClick={
186 isSearchInputVisible ? hideSearchInput : showSearchInput
187 }
188 title={`Search components in this commit (${
189 isMac ? '' : 'Ctrl+'
190 }F)`}
191 data-testname="ProfilerSearchButton">
192 <ButtonIcon type="find" />
193 </Button>
194 <SnapshotSelector />
195 </Fragment>
196 )}
197 </div>
198 <div className={styles.Content}>
199 {didRecordCommits && isSearchInputVisible && (
200 <div className={styles.SearchInputOverlay}>
201 <ProfilerSearchInput />
202 </div>
203 )}
204 {view}
205 <ModalDialog />
206 </div>
207 </div>
208 <div className={styles.RightColumn}>{sidebar}</div>
209 <SettingsModal />
210 </div>
211 </SettingsModalContextController>
212 );
213 }
214
215 const tabs = [
216 {
217 id: 'flame-chart',
218 icon: 'flame-chart',
219 label: 'Flamegraph',
220 title: 'Flamegraph chart',
221 },
222 {
223 id: 'ranked-chart',
224 icon: 'ranked-chart',
225 label: 'Ranked',
226 title: 'Ranked chart',
227 },
228 ];
229
230 export default portaledContent(Profiler) as component();