Files
react/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js
Nick Reiley dd2e36df33 Profiler: Skip reading element for imported data (#18913)
* skip reading element for imported data

* rename nodes & enable store lookup for components tab

* replace names

* Added some more test coverage; reverted rename

Co-authored-by: Brian Vaughn <bvaughn@fb.com>
2020-05-15 10:38:01 -07:00

310 lines
8.9 KiB
JavaScript

/**
* 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 {createContext, useCallback, useContext, useMemo, useState} from 'react';
import {unstable_batchedUpdates as batchedUpdates} from 'react-dom';
import {useLocalStorage, useSubscription} from '../hooks';
import {
TreeDispatcherContext,
TreeStateContext,
} from '../Components/TreeContext';
import {StoreContext} from '../context';
import type {ProfilingDataFrontend} from './types';
export type TabID = 'flame-chart' | 'ranked-chart' | 'interactions';
export type Context = {|
// Which tab is selected in the Profiler UI?
selectedTabID: TabID,
selectTab(id: TabID): void,
// Store subscription based values.
// The isProfiling value may be modified by the record button in the Profiler toolbar,
// or from the backend itself (after a reload-and-profile action).
// It is synced between the backend and frontend via a Store subscription.
didRecordCommits: boolean,
isProcessingData: boolean,
isProfiling: boolean,
profilingData: ProfilingDataFrontend | null,
startProfiling(): void,
stopProfiling(): void,
supportsProfiling: boolean,
// 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,
setRootID: (id: number) => void,
// Controls whether commits are filtered by duration.
// This value is controlled by a filter toggle UI in the Profiler toolbar.
// It impacts the commit selector UI as well as the fiber commits bar chart.
isCommitFilterEnabled: boolean,
setIsCommitFilterEnabled: (value: boolean) => void,
minCommitDuration: number,
setMinCommitDuration: (value: number) => void,
// Which commit is currently selected in the commit selector UI.
// Note that this is the index of the commit in all commits (non-filtered) that were profiled.
// This value is controlled by the commit selector UI in the Profiler toolbar.
// It impacts the flame graph and ranked charts.
selectedCommitIndex: number | null,
selectCommitIndex: (value: number | null) => void,
// Which fiber is currently selected in the Ranked or Flamegraph charts?
selectedFiberID: number | null,
selectedFiberName: string | null,
selectFiber: (id: number | null, name: string | null) => void,
// Which interaction is currently selected in the Interactions graph?
selectedInteractionID: number | null,
selectInteraction: (id: number | null) => void,
|};
const ProfilerContext = createContext<Context>(((null: any): Context));
ProfilerContext.displayName = 'ProfilerContext';
type StoreProfilingState = {|
didRecordCommits: boolean,
isProcessingData: boolean,
isProfiling: boolean,
profilingData: ProfilingDataFrontend | null,
supportsProfiling: boolean,
|};
type Props = {|
children: React$Node,
|};
function ProfilerContextController({children}: Props) {
const store = useContext(StoreContext);
const {selectedElementID} = useContext(TreeStateContext);
const dispatch = useContext(TreeDispatcherContext);
const {profilerStore} = store;
const subscription = useMemo(
() => ({
getCurrentValue: () => ({
didRecordCommits: profilerStore.didRecordCommits,
isProcessingData: profilerStore.isProcessingData,
isProfiling: profilerStore.isProfiling,
profilingData: profilerStore.profilingData,
supportsProfiling: store.supportsProfiling,
}),
subscribe: (callback: Function) => {
profilerStore.addListener('profilingData', callback);
profilerStore.addListener('isProcessingData', callback);
profilerStore.addListener('isProfiling', callback);
store.addListener('supportsProfiling', callback);
return () => {
profilerStore.removeListener('profilingData', callback);
profilerStore.removeListener('isProcessingData', callback);
profilerStore.removeListener('isProfiling', callback);
store.removeListener('supportsProfiling', callback);
};
},
}),
[profilerStore, store],
);
const {
didRecordCommits,
isProcessingData,
isProfiling,
profilingData,
supportsProfiling,
} = useSubscription<StoreProfilingState>(subscription);
const [
prevProfilingData,
setPrevProfilingData,
] = useState<ProfilingDataFrontend | null>(null);
const [rootID, setRootID] = useState<number | null>(null);
const [selectedFiberID, selectFiberID] = useState<number | null>(null);
const [selectedFiberName, selectFiberName] = useState<string | null>(null);
const selectFiber = useCallback(
(id: number | null, name: string | null) => {
selectFiberID(id);
selectFiberName(name);
// Sync selection to the Components tab for convenience.
// Keep in mind that profiling data may be from a previous session.
// If data has been imported, we should skip the selection sync.
if (
id !== null &&
profilingData !== null &&
profilingData.imported === false
) {
// We should still check to see if this element is still in the store.
// It may have been removed during profiling.
if (store.containsElement(id)) {
dispatch({
type: 'SELECT_ELEMENT_BY_ID',
payload: id,
});
}
}
},
[dispatch, selectFiberID, selectFiberName, store, profilingData],
);
const setRootIDAndClearFiber = useCallback(
(id: number | null) => {
selectFiber(null, null);
setRootID(id);
},
[setRootID, selectFiber],
);
if (prevProfilingData !== profilingData) {
batchedUpdates(() => {
setPrevProfilingData(profilingData);
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)
) {
setRootIDAndClearFiber(selectedElementRootID);
} else {
setRootIDAndClearFiber(firstRootID);
}
}
}
});
}
const startProfiling = useCallback(
() => store.profilerStore.startProfiling(),
[store],
);
const stopProfiling = useCallback(() => store.profilerStore.stopProfiling(), [
store,
]);
const [
isCommitFilterEnabled,
setIsCommitFilterEnabled,
] = useLocalStorage<boolean>('React::DevTools::isCommitFilterEnabled', false);
const [minCommitDuration, setMinCommitDuration] = useLocalStorage<number>(
'minCommitDuration',
0,
);
const [selectedCommitIndex, selectCommitIndex] = useState<number | null>(
null,
);
const [selectedTabID, selectTab] = useState<TabID>('flame-chart');
const [selectedInteractionID, selectInteraction] = useState<number | null>(
null,
);
if (isProfiling) {
batchedUpdates(() => {
if (selectedCommitIndex !== null) {
selectCommitIndex(null);
}
if (selectedFiberID !== null) {
selectFiberID(null);
selectFiberName(null);
}
if (selectedInteractionID !== null) {
selectInteraction(null);
}
});
}
const value = useMemo(
() => ({
selectedTabID,
selectTab,
didRecordCommits,
isProcessingData,
isProfiling,
profilingData,
startProfiling,
stopProfiling,
supportsProfiling,
rootID,
setRootID: setRootIDAndClearFiber,
isCommitFilterEnabled,
setIsCommitFilterEnabled,
minCommitDuration,
setMinCommitDuration,
selectedCommitIndex,
selectCommitIndex,
selectedFiberID,
selectedFiberName,
selectFiber,
selectedInteractionID,
selectInteraction,
}),
[
selectedTabID,
selectTab,
didRecordCommits,
isProcessingData,
isProfiling,
profilingData,
startProfiling,
stopProfiling,
supportsProfiling,
rootID,
setRootID,
setRootIDAndClearFiber,
isCommitFilterEnabled,
setIsCommitFilterEnabled,
minCommitDuration,
setMinCommitDuration,
selectedCommitIndex,
selectCommitIndex,
selectedFiberID,
selectedFiberName,
selectFiber,
selectedInteractionID,
selectInteraction,
],
);
return (
<ProfilerContext.Provider value={value}>
{children}
</ProfilerContext.Provider>
);
}
export {ProfilerContext, ProfilerContextController};