Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DevTools: Profiler refactor incremental changes #23185

Merged
merged 9 commits into from
Jan 28, 2022
609 changes: 541 additions & 68 deletions packages/react-devtools-shared/src/backend/profilingHooks.js

Large diffs are not rendered by default.

34 changes: 33 additions & 1 deletion packages/react-devtools-shared/src/backend/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ import hasOwnProperty from 'shared/hasOwnProperty';
import {getStyleXData} from './StyleX/utils';
import {createProfilingHooks} from './profilingHooks';

import type {ToggleProfilingStatus} from './profilingHooks';
import type {GetTimelineData, ToggleProfilingStatus} from './profilingHooks';
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import type {
ChangeDescription,
Expand Down Expand Up @@ -630,6 +630,7 @@ export function attach(
};
}

let getTimelineData: null | GetTimelineData = null;
let toggleProfilingStatus: null | ToggleProfilingStatus = null;
if (typeof injectProfilingHooks === 'function') {
const response = createProfilingHooks({
Expand All @@ -643,6 +644,7 @@ export function attach(
injectProfilingHooks(response.profilingHooks);

// Hang onto this toggle so we can notify the external methods of profiling status changes.
getTimelineData = response.getTimelineData;
toggleProfilingStatus = response.toggleProfilingStatus;
}

Expand Down Expand Up @@ -3978,9 +3980,39 @@ export function attach(
},
);

let timelineData = null;
if (typeof getTimelineData === 'function') {
const currentTimelineData = getTimelineData();
if (currentTimelineData) {
const {
batchUIDToMeasuresMap,
internalModuleSourceToRanges,
laneToLabelMap,
laneToReactMeasureMap,
...rest
} = currentTimelineData;

timelineData = {
...rest,

// Most of the data is safe to parse as-is,
// but we need to convert the nested Arrays back to Maps.
// Most of the data is safe to serialize as-is,
// but we need to convert the Maps to nested Arrays.
batchUIDToMeasuresMap: Array.from(batchUIDToMeasuresMap.entries()),
internalModuleSourceToRanges: Array.from(
internalModuleSourceToRanges.entries(),
),
laneToLabelMap: Array.from(laneToLabelMap.entries()),
laneToReactMeasureMap: Array.from(laneToReactMeasureMap.entries()),
};
}
}

return {
dataForRoots,
rendererID,
timelineData,
};
}

Expand Down
3 changes: 2 additions & 1 deletion packages/react-devtools-shared/src/backend/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
Plugins,
} from 'react-devtools-shared/src/types';
import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
import type {TimelineDataExport} from 'react-devtools-timeline/src/types';

type BundleType =
| 0 // PROD
Expand Down Expand Up @@ -195,7 +196,7 @@ export type ProfilingDataForRootBackend = {|
export type ProfilingDataBackend = {|
dataForRoots: Array<ProfilingDataForRootBackend>,
rendererID: number,
// TODO (timeline) Add (optional) Timeline data.
timelineData: TimelineDataExport | null,
|};

export type PathFrame = {|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,23 @@ export default function ClearProfilingDataButton() {
const {file, setFile} = useContext(TimelineContext);
const {profilerStore} = store;

const doesHaveLegacyData = didRecordCommits;
const doesHaveTimelineData = file !== null;
const doesHaveInMemoryData = didRecordCommits;
const doesHaveUserTimingData = file !== null;

const clear = () => {
if (doesHaveLegacyData) {
if (doesHaveInMemoryData) {
profilerStore.clear();
}
if (doesHaveTimelineData) {
if (doesHaveUserTimingData) {
setFile(null);
}
};

return (
<Button
disabled={isProfiling || !(doesHaveLegacyData || doesHaveTimelineData)}
disabled={
isProfiling || !(doesHaveInMemoryData || doesHaveUserTimingData)
}
onClick={clear}
title="Clear profiling data">
<ButtonIcon type="clear" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import * as React from 'react';

import styles from './Profiler.css';

export default function ProcessingData() {
return (
<div className={styles.Column}>
<div className={styles.Header}>Processing data...</div>
<div className={styles.Row}>This should only take a minute.</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import ProfilingImportExportButtons from './ProfilingImportExportButtons';
import SnapshotSelector from './SnapshotSelector';
import SidebarCommitInfo from './SidebarCommitInfo';
import NoProfilingData from './NoProfilingData';
import RecordingInProgress from './RecordingInProgress';
import ProcessingData from './ProcessingData';
import ProfilingNotSupported from './ProfilingNotSupported';
import SidebarSelectedFiberInfo from './SidebarSelectedFiberInfo';
import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
Expand All @@ -46,7 +48,9 @@ function Profiler(_: {||}) {
supportsProfiling,
} = useContext(ProfilerContext);

const {searchInputContainerRef} = useContext(TimelineContext);
const {file: timelineTraceEventData, searchInputContainerRef} = useContext(
TimelineContext,
);

const {supportsTimeline} = useContext(StoreContext);

Expand All @@ -71,6 +75,8 @@ function Profiler(_: {||}) {
view = <RecordingInProgress />;
} else if (isProcessingData) {
view = <ProcessingData />;
} else if (timelineTraceEventData) {
view = <OnlyTimelineData />;
} else if (supportsProfiling) {
view = <NoProfilingData />;
} else {
Expand Down Expand Up @@ -150,6 +156,15 @@ function Profiler(_: {||}) {
);
}

const OnlyTimelineData = () => (
<div className={styles.Column}>
<div className={styles.Header}>Timeline only</div>
<div className={styles.Row}>
The current profile contains only Timeline data.
</div>
</div>
);

const tabs = [
{
id: 'flame-chart',
Expand All @@ -175,20 +190,5 @@ const tabsWithTimeline = [
title: 'Timeline',
},
];
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}>
Click the record button <RecordToggle /> to stop recording.
</div>
</div>
);

export default portaledContent(Profiler);
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {StoreContext} from '../context';

import type {ProfilingDataFrontend} from './types';

// TODO (timeline) Should this be its own context?
export type TabID = 'flame-chart' | 'ranked-chart' | 'timeline';

export type Context = {|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ import {
} from './utils';
import {downloadFile} from '../utils';
import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext';
import isArray from 'shared/isArray';
import hasOwnProperty from 'shared/hasOwnProperty';

import styles from './ProfilingImportExportButtons.css';

import type {ProfilingDataExport} from './types';

export default function ProfilingImportExportButtons() {
const {isProfiling, profilingData, rootID, selectedTabID} = useContext(
ProfilerContext,
);
const {isProfiling, profilingData, rootID} = useContext(ProfilerContext);
const {setFile} = useContext(TimelineContext);
const store = useContext(StoreContext);
const {profilerStore} = store;
Expand All @@ -38,6 +38,8 @@ export default function ProfilingImportExportButtons() {

const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext);

const doesHaveInMemoryData = profilerStore.didRecordCommits;

const downloadData = useCallback(() => {
if (rootID === null) {
return;
Expand Down Expand Up @@ -74,45 +76,54 @@ export default function ProfilingImportExportButtons() {
}
}, []);

const importProfilerData = useCallback(() => {
// TODO (profiling) We should probably use a transition for this and suspend while loading the file.
// Local files load so fast it's probably not very noticeable though.
const handleChange = () => {
const input = inputRef.current;
if (input !== null && input.files.length > 0) {
const file = input.files[0];

// TODO (profiling) Handle fileReader errors.
const fileReader = new FileReader();
fileReader.addEventListener('load', () => {
try {
const raw = ((fileReader.result: any): string);
const profilingDataExport = ((JSON.parse(
raw,
): any): ProfilingDataExport);
profilerStore.profilingData = prepareProfilingDataFrontendFromExport(
profilingDataExport,
);
} catch (error) {
modalDialogDispatch({
id: 'ProfilingImportExportButtons',
type: 'SHOW',
title: 'Import failed',
content: (
<Fragment>
<div>The profiling data you selected cannot be imported.</div>
{error !== null && (
<div className={styles.ErrorMessage}>{error.message}</div>
)}
</Fragment>
),
});
const raw = ((fileReader.result: any): string);
const json = JSON.parse(raw);

if (!isArray(json) && hasOwnProperty.call(json, 'version')) {
// This looks like React profiling data.
// But first, clear any User Timing marks; we should only have one type open at a time.
setFile(null);

try {
const profilingDataExport = ((json: any): ProfilingDataExport);
profilerStore.profilingData = prepareProfilingDataFrontendFromExport(
profilingDataExport,
);
} catch (error) {
modalDialogDispatch({
id: 'ProfilingImportExportButtons',
type: 'SHOW',
title: 'Import failed',
content: (
<Fragment>
<div>The profiling data you selected cannot be imported.</div>
{error !== null && (
<div className={styles.ErrorMessage}>{error.message}</div>
)}
</Fragment>
),
});
}
} else {
// Otherwise let's assume this is Trace Event data and pass it to the Timeline preprocessor.
// But first, clear React profiling data; we should only have one type open at a time.
profilerStore.clear();

// TODO (timeline) We shouldn't need to re-open the File but we'll need to refactor to avoid this.
setFile(file);
}
});
// TODO (profiling) Handle fileReader errors.
fileReader.readAsText(input.files[0]);
}
}, [modalDialogDispatch, profilerStore]);

const importTimelineDataWrapper = event => {
const input = inputRef.current;
if (input !== null && input.files.length > 0) {
const file = input.files[0];
setFile(file);
fileReader.readAsText(file);
}
};

Expand All @@ -124,11 +135,7 @@ export default function ProfilingImportExportButtons() {
className={styles.Input}
type="file"
accept=".json"
onChange={
selectedTabID === 'timeline'
? importTimelineDataWrapper
: importProfilerData
}
onChange={handleChange}
tabIndex={-1}
/>
<a ref={downloadRef} className={styles.Input} />
Expand All @@ -139,11 +146,7 @@ export default function ProfilingImportExportButtons() {
<ButtonIcon type="import" />
</Button>
<Button
disabled={
isProfiling ||
!profilerStore.didRecordCommits ||
selectedTabID === 'timeline'
}
disabled={isProfiling || !doesHaveInMemoryData}
onClick={downloadData}
title="Save profile...">
<ButtonIcon type="export" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import * as React from 'react';
import RecordToggle from './RecordToggle';

import styles from './Profiler.css';

export default function RecordingInProgress() {
return (
<div className={styles.Column}>
<div className={styles.Header}>Profiling is in progress...</div>
<div className={styles.Row}>
Click the record button <RecordToggle /> to stop recording.
</div>
</div>
);
}
Loading