Added root selector UI to Profiler

This commit is contained in:
Brian Vaughn
2019-05-22 06:40:04 -07:00
parent c6de014a9a
commit 36d51f4eb6
7 changed files with 187 additions and 212 deletions
@@ -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 <FilterModalImpl />;
}
function FilterModalImpl(_: {||}) {
const {
isCommitFilterEnabled,
minCommitDuration,
+9 -27
View File
@@ -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 (
<div className={styles.NoCommitData}>
<div className={styles.Header}>
There is no data matching the current filter criteria.
</div>
<div className={styles.FilterMessage}>
Try adjusting the commit filter <ToggleCommitFilterModalButton />
</div>
return (
<div className={styles.NoCommitData}>
<div className={styles.Header}>
There is no data matching the current filter criteria.
</div>
);
} else {
return (
<div className={styles.NoCommitData}>
<div className={styles.Header}>
There is no timing data to display for the currently selected commit.
</div>
<div>
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.
</div>
<div className={styles.FilterMessage}>
Try adjusting the commit filter <ToggleCommitFilterModalButton />
</div>
);
}
</div>
);
}
+64 -145
View File
@@ -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 (
<NonSuspendingProfiler
hasProfilingData={hasProfilingData}
isProfiling={isProfiling}
supportsProfiling={supportsProfiling}
/>
);
} else {
return (
<CommitFilterModalContextController>
<SuspendingProfiler />
</CommitFilterModalContextController>
);
}
}
// 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 = <ProfilingNotSupported />;
} else if (isProfiling) {
view = <RecortdingInProgress />;
view = <RecordingInProgress />;
} else if (isProcessingData) {
view = <ProcessingData />;
} else if (!hasProfilingData) {
view = <NoProfilingData />;
} else {
view = <NoProfilingDataForRoot />;
}
return (
<div className={styles.Profiler}>
<div className={styles.LeftColumn}>
<div className={styles.Toolbar}>
<RecordToggle disabled={!supportsProfiling} />
<ReloadAndProfileButton />
<ClearProfilingDataButton />
<ProfilingImportExportButtons />
<div className={styles.VRule} />
<TabBar
currentTab={null}
disabled
id="Profiler"
selectTab={() => {}}
size="small"
tabs={tabs}
/>
</div>
<div className={styles.Content}>
{view}
<ModalDialog />
</div>
</div>
</div>
);
}
function ContentFallback() {
return <div className={styles.ContentFallback}>Loading...</div>;
}
function SnapshotSelectorFallback() {
return <div className={styles.SnapshotSelectorFallback}>Loading...</div>;
}
// 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 = <CommitFlamegraph />;
break;
case 'ranked-chart':
view = <CommitRanked />;
break;
case 'interactions':
view = <Interactions />;
break;
default:
break;
switch (selectedTabID) {
case 'flame-chart':
view = <CommitFlamegraph />;
break;
case 'ranked-chart':
view = <CommitRanked />;
break;
case 'interactions':
view = <Interactions />;
break;
default:
break;
}
}
let sidebar = null;
@@ -155,38 +80,37 @@ function SuspendingProfiler() {
}
return (
<div className={styles.Profiler}>
<div className={styles.LeftColumn}>
<div className={styles.Toolbar}>
<RecordToggle />
<ReloadAndProfileButton />
<ClearProfilingDataButton />
<ProfilingImportExportButtons />
<div className={styles.VRule} />
<TabBar
currentTab={selectedTabID}
id="Profiler"
selectTab={selectTab}
size="small"
tabs={tabs}
/>
<div className={styles.Spacer} />
<ToggleCommitFilterModalButton />
<div className={styles.VRule} />
<Suspense fallback={<SnapshotSelectorFallback />}>
<SnapshotSelector />
</Suspense>
</div>
<div className={styles.Content}>
<Suspense fallback={<ContentFallback />}>{view}</Suspense>
{isFilterModalShowing && <CommitFilterModal />}
<ModalDialog />
<CommitFilterModalContextController>
<div className={styles.Profiler}>
<div className={styles.LeftColumn}>
<div className={styles.Toolbar}>
<RecordToggle />
<ReloadAndProfileButton />
<ClearProfilingDataButton />
<ProfilingImportExportButtons />
<div className={styles.VRule} />
<TabBar
currentTab={selectedTabID}
id="Profiler"
selectTab={selectTab}
size="small"
tabs={tabs}
/>
<RootSelector />
<div className={styles.Spacer} />
<ToggleCommitFilterModalButton />
<div className={styles.VRule} />
{hasProfilingData && <SnapshotSelector />}
</div>
<div className={styles.Content}>
{view}
<CommitFilterModal />
<ModalDialog />
</div>
</div>
<div className={styles.RightColumn}>{sidebar}</div>
</div>
<div className={styles.RightColumn}>
<Suspense fallback={<ContentFallback />}>{sidebar}</Suspense>
</div>
</div>
</CommitFilterModalContextController>
);
}
@@ -220,18 +144,6 @@ const NoProfilingData = () => (
</div>
);
const NoProfilingDataForRoot = () => (
<div className={styles.Column}>
<div className={styles.Header}>
No profiling data has been recorded for the selected root.
</div>
<div className={styles.Row}>
Select a different root in the elements panel, or click the record button{' '}
<RecordToggle /> to start recording.
</div>
</div>
);
const ProfilingNotSupported = () => (
<div className={styles.Column}>
<div className={styles.Header}>Profiling not supported.</div>
@@ -256,7 +168,14 @@ const ProfilingNotSupported = () => (
</div>
);
const RecortdingInProgress = () => (
const ProcessingData = () => (
<div className={styles.Column}>
<div className={styles.Header}>Processing data...</div>
<div className={styles.Row}>This should only take a minute.</div>
</div>
);
const RecordingInProgress = () => (
<div className={styles.Column}>
<div className={styles.Header}>Profiling is in progress...</div>
<div className={styles.Row}>
+52 -37
View File
@@ -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<StoreProfilingState, Store>(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<number | null>(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,
@@ -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) {
@@ -0,0 +1,3 @@
.Spacer {
flex: 1;
}
@@ -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(
<option key={rootID} value={rootID}>
{dataForRoot.displayName}
</option>
);
});
}
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 (
<Fragment>
<div className={styles.Spacer} />
<select value={rootID} onChange={handleChange}>
{options}
</select>
</Fragment>
);
}