mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Added root selector UI to Profiler
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user