diff --git a/src/devtools/views/Profiler/CommitFilterModal.js b/src/devtools/views/Profiler/CommitFilterModal.js index c47c254e0a..8b359479af 100644 --- a/src/devtools/views/Profiler/CommitFilterModal.js +++ b/src/devtools/views/Profiler/CommitFilterModal.js @@ -8,6 +8,16 @@ import { useModalDismissSignal } from '../hooks'; import styles from './CommitFilterModal.css'; export default function FilterModal(_: {||}) { + const { isModalShowing } = useContext(CommitFilterModalContext); + + if (!isModalShowing) { + return null; + } + + return ; +} + +function FilterModalImpl(_: {||}) { const { isCommitFilterEnabled, minCommitDuration, diff --git a/src/devtools/views/Profiler/NoCommitData.js b/src/devtools/views/Profiler/NoCommitData.js index 3e0dab4a2d..f16cd9aa96 100644 --- a/src/devtools/views/Profiler/NoCommitData.js +++ b/src/devtools/views/Profiler/NoCommitData.js @@ -1,37 +1,19 @@ // @flow -import React, { useContext } from 'react'; -import { ProfilerContext } from './ProfilerContext'; +import React from 'react'; import ToggleCommitFilterModalButton from './ToggleCommitFilterModalButton'; import styles from './NoCommitData.css'; export default function NoCommitData(_: {||}) { - const { rootHasProfilingData } = useContext(ProfilerContext); - - if (rootHasProfilingData) { - return ( -
-
- There is no data matching the current filter criteria. -
-
- Try adjusting the commit filter -
+ return ( +
+
+ There is no data matching the current filter criteria.
- ); - } else { - return ( -
-
- There is no timing data to display for the currently selected commit. -
-
- This can indicate that a render occurred too quickly for the timing - API to measure. Try selecting another commit in the upper, right-hand - corner. -
+
+ Try adjusting the commit filter
- ); - } +
+ ); } diff --git a/src/devtools/views/Profiler/Profiler.js b/src/devtools/views/Profiler/Profiler.js index 656076c6e6..cd39b39f2f 100644 --- a/src/devtools/views/Profiler/Profiler.js +++ b/src/devtools/views/Profiler/Profiler.js @@ -1,10 +1,7 @@ // @flow -import React, { Suspense, useContext } from 'react'; -import { - CommitFilterModalContext, - CommitFilterModalContextController, -} from './CommitFilterModalContext'; +import React, { useContext } from 'react'; +import { CommitFilterModalContextController } from './CommitFilterModalContext'; import { ModalDialog } from '../ModalDialog'; import { ProfilerContext } from './ProfilerContext'; import TabBar from '../TabBar'; @@ -13,6 +10,7 @@ import CommitFlamegraph from './CommitFlamegraph'; import CommitRanked from './CommitRanked'; import CommitFilterModal from './CommitFilterModal'; import Interactions from './Interactions'; +import RootSelector from './RootSelector'; import RecordToggle from './RecordToggle'; import ReloadAndProfileButton from './ReloadAndProfileButton'; import ProfilingImportExportButtons from './ProfilingImportExportButtons'; @@ -30,111 +28,38 @@ export type Props = {| |}; function Profiler({ supportsProfiling }: Props) { - const { hasProfilingData, isProfiling, rootHasProfilingData } = useContext( - ProfilerContext - ); + const { + hasProfilingData, + isProcessingData, + isProfiling, + selectedFiberID, + selectedTabID, + selectTab, + } = useContext(ProfilerContext); - if (isProfiling || !rootHasProfilingData) { - return ( - - ); - } else { - return ( - - - - ); - } -} - -// This view is rendered when there is no profiler data (either we haven't profiled yet or we're currently profiling). -// Nothing in this view's subtree suspends. -// By not suspending while profiling is in progress, we avoid potential cache invalidation trickiness. -// NOTE that the structure of this UI should mirror SuspendingProfiler. -function NonSuspendingProfiler({ - hasProfilingData, - isProfiling, - supportsProfiling, -}: {| - hasProfilingData: boolean, - isProfiling: boolean, - supportsProfiling: boolean, -|}) { let view = null; if (!supportsProfiling) { view = ; } else if (isProfiling) { - view = ; + view = ; + } else if (isProcessingData) { + view = ; } else if (!hasProfilingData) { view = ; } else { - view = ; - } - - return ( -
-
-
- - - - -
- {}} - size="small" - tabs={tabs} - /> -
-
- {view} - -
-
-
- ); -} - -function ContentFallback() { - return
Loading...
; -} - -function SnapshotSelectorFallback() { - return
Loading...
; -} - -// This view is rendered when there is profiler data (even though there may not be any for the currently selected root). -// This view's subtree uses suspense to request profiler data from the backend. -// NOTE that the structure of this UI should mirror NonSuspendingProfiler. -function SuspendingProfiler() { - const { selectedFiberID, selectedTabID, selectTab } = useContext( - ProfilerContext - ); - - const { isModalShowing: isFilterModalShowing } = useContext( - CommitFilterModalContext - ); - - let view = null; - switch (selectedTabID) { - case 'flame-chart': - view = ; - break; - case 'ranked-chart': - view = ; - break; - case 'interactions': - view = ; - break; - default: - break; + switch (selectedTabID) { + case 'flame-chart': + view = ; + break; + case 'ranked-chart': + view = ; + break; + case 'interactions': + view = ; + break; + default: + break; + } } let sidebar = null; @@ -155,38 +80,37 @@ function SuspendingProfiler() { } return ( -
-
-
- - - - -
- -
- -
- }> - - -
-
- }>{view} - {isFilterModalShowing && } - + +
+
+
+ + + + +
+ + +
+ +
+ {hasProfilingData && } +
+
+ {view} + + +
+
{sidebar}
-
- }>{sidebar} -
-
+ ); } @@ -220,18 +144,6 @@ const NoProfilingData = () => (
); -const NoProfilingDataForRoot = () => ( -
-
- No profiling data has been recorded for the selected root. -
-
- Select a different root in the elements panel, or click the record button{' '} - to start recording. -
-
-); - const ProfilingNotSupported = () => (
Profiling not supported.
@@ -256,7 +168,14 @@ const ProfilingNotSupported = () => (
); -const RecortdingInProgress = () => ( +const ProcessingData = () => ( +
+
Processing data...
+
This should only take a minute.
+
+); + +const RecordingInProgress = () => (
Profiling is in progress...
diff --git a/src/devtools/views/Profiler/ProfilerContext.js b/src/devtools/views/Profiler/ProfilerContext.js index c829516a14..58ba6e1c52 100644 --- a/src/devtools/views/Profiler/ProfilerContext.js +++ b/src/devtools/views/Profiler/ProfilerContext.js @@ -31,16 +31,17 @@ type Context = {| // or from the backend itself (after a reload-and-profile action). // It is synced between the backend and frontend via a Store subscription. hasProfilingData: boolean, + isProcessingData: boolean, isProfiling: boolean, startProfiling(value: boolean): void, stopProfiling(value: boolean): void, - // Which renderer and root should profiling data be shown for? - // Often this will correspond to the selected renderer and root in the Elements panel. - // If nothing is selected though, this will default to the first root. - rendererID: number | null, + // Which root should profiling data be shown for? + // This value should be initialized to either: + // 1. The selected root in the Components tree (if it has any profiling data) or + // 2. The first root in the list with profiling data. rootID: number | null, - rootHasProfilingData: boolean, + setRootID: (id: number) => void, // Controls whether commits are filtered by duration. // This value is controlled by a filter toggle UI in the Profiler toolbar. @@ -72,8 +73,9 @@ ProfilerContext.displayName = 'ProfilerContext'; type StoreProfilingState = {| hasProfilingData: boolean, - profilingData: ProfilingDataFrontend | null, + isProcessingData: boolean, isProfiling: boolean, + profilingData: ProfilingDataFrontend | null, |}; type Props = {| @@ -85,44 +87,57 @@ function ProfilerContextController({ children }: Props) { const { selectedElementID } = useContext(TreeStateContext); const dispatch = useContext(TreeDispatcherContext); + const { profilerStore } = store; + const subscription = useMemo( () => ({ getCurrentValue: () => ({ - hasProfilingData: store.hasProfilingData, - profilingData: store.profilingData, - isProfiling: store.isProfiling, + hasProfilingData: profilerStore.hasProfilingData, + isProcessingData: profilerStore.isProcessingData, + isProfiling: profilerStore.isProfiling, + profilingData: profilerStore.profilingData, }), subscribe: (callback: Function) => { - store.addListener('profilingData', callback); - store.addListener('isProfiling', callback); + profilerStore.addListener('profilingData', callback); + profilerStore.addListener('isProcessingData', callback); + profilerStore.addListener('isProfiling', callback); return () => { - store.removeListener('profilingData', callback); - store.removeListener('isProfiling', callback); + profilerStore.removeListener('profilingData', callback); + profilerStore.removeListener('isProcessingData', callback); + profilerStore.removeListener('isProfiling', callback); }; }, }), - [store] + [profilerStore] ); - const { isProfiling, hasProfilingData, profilingData } = useSubscription< - StoreProfilingState, - Store - >(subscription); + const { + hasProfilingData, + isProcessingData, + isProfiling, + profilingData, + } = useSubscription(subscription); - let rendererID = null; - let rootID = null; - let rootHasProfilingData = false; - if (profilingData !== null) { - rootHasProfilingData = true; - } else if (selectedElementID !== null) { - rendererID = store.getRendererIDForElement(selectedElementID); - rootID = store.getRootIDForElement(selectedElementID); - rootHasProfilingData = - rootID === null ? false : store.profilingOperationsByRootID.has(rootID); - } else if (store.roots.length > 0) { - // If no root is selected, assume the first root; many React apps are single root anyway. - rootID = store.roots[0]; - rootHasProfilingData = store.profilingOperationsByRootID.has(rootID); - rendererID = store.getRendererIDForElement(rootID); + const [rootID, setRootID] = useState(null); + + const dataForRoots = + profilingData !== null ? profilingData.dataForRoots : null; + if (dataForRoots != null) { + const firstRootID = dataForRoots.keys().next().value || null; + + if (rootID === null || !dataForRoots.has(rootID)) { + let selectedElementRootID = null; + if (selectedElementID !== null) { + selectedElementRootID = store.getRootIDForElement(selectedElementID); + } + if ( + selectedElementRootID !== null && + dataForRoots.has(selectedElementRootID) + ) { + setRootID(selectedElementRootID); + } else { + setRootID(firstRootID); + } + } } const startProfiling = useCallback(() => store.startProfiling(), [store]); @@ -186,13 +201,13 @@ function ProfilerContextController({ children }: Props) { selectTab, hasProfilingData, + isProcessingData, isProfiling, startProfiling, stopProfiling, - rendererID, rootID, - rootHasProfilingData, + setRootID, isCommitFilterEnabled, setIsCommitFilterEnabled, @@ -214,13 +229,13 @@ function ProfilerContextController({ children }: Props) { selectTab, hasProfilingData, + isProcessingData, isProfiling, startProfiling, stopProfiling, - rendererID, rootID, - rootHasProfilingData, + setRootID, isCommitFilterEnabled, setIsCommitFilterEnabled, diff --git a/src/devtools/views/Profiler/ProfilingImportExportButtons.js b/src/devtools/views/Profiler/ProfilingImportExportButtons.js index c196e6d6b2..c29b179817 100644 --- a/src/devtools/views/Profiler/ProfilingImportExportButtons.js +++ b/src/devtools/views/Profiler/ProfilingImportExportButtons.js @@ -16,7 +16,7 @@ import styles from './ProfilingImportExportButtons.css'; import type { ProfilingDataExport } from './types'; export default function ProfilingImportExportButtons() { - const { isProfiling, rendererID, rootID } = useContext(ProfilerContext); + const { isProfiling, rootID } = useContext(ProfilerContext); const store = useContext(StoreContext); const { profilerStore } = store; @@ -25,7 +25,7 @@ export default function ProfilingImportExportButtons() { const { dispatch: modalDialogDispatch } = useContext(ModalDialogContext); const downloadData = useCallback(() => { - if (rendererID === null || rootID === null) { + if (rootID === null) { return; } @@ -37,7 +37,7 @@ export default function ProfilingImportExportButtons() { // TODO (profarc) Generate anchor "download" tag and click it console.log('profilingDataExport:', profilingDataExport); } - }, [rendererID, rootID, profilerStore.profilingData]); + }, [rootID, profilerStore.profilingData]); const uploadData = useCallback(() => { if (inputRef.current !== null) { diff --git a/src/devtools/views/Profiler/RootSelector.css b/src/devtools/views/Profiler/RootSelector.css new file mode 100644 index 0000000000..ff1a970d46 --- /dev/null +++ b/src/devtools/views/Profiler/RootSelector.css @@ -0,0 +1,3 @@ +.Spacer { + flex: 1; +} diff --git a/src/devtools/views/Profiler/RootSelector.js b/src/devtools/views/Profiler/RootSelector.js new file mode 100644 index 0000000000..0608e64feb --- /dev/null +++ b/src/devtools/views/Profiler/RootSelector.js @@ -0,0 +1,46 @@ +// @flow + +import React, { Fragment, useCallback, useContext } from 'react'; +import { ProfilerContext } from './ProfilerContext'; +import { StoreContext } from '../context'; + +import styles from './RootSelector.css'; + +export default function RootSelector(_: {||}) { + const store = useContext(StoreContext); + const { rootID, setRootID } = useContext(ProfilerContext); + + const { profilingData } = store.profilerStore; + + const options = []; + if (profilingData !== null) { + profilingData.dataForRoots.forEach((dataForRoot, rootID) => { + options.push( + + ); + }); + } + + const handleChange = useCallback( + ({ currentTarget }) => { + setRootID(parseInt(currentTarget.value, 10)); + }, + [setRootID] + ); + + if (profilingData === null || profilingData.dataForRoots.size <= 1) { + // Don't take up visual space if there's only one root. + return null; + } + + return ( + +
+ + + ); +}