import type {ReactContext} from 'shared/ReactTypes';
import * as React from 'react';
import {
createContext,
useCallback,
useContext,
useDeferredValue,
useMemo,
useState,
useEffect,
} from 'react';
import {useLocalStorage, useSubscription} from '../hooks';
import {
TreeDispatcherContext,
TreeStateContext,
} from '../Components/TreeContext';
import {StoreContext} from '../context';
import {createRegExp} from '../utils';
import {logEvent} from 'react-devtools-shared/src/Logger';
import {useCommitFilteringAndNavigation} from './useCommitFilteringAndNavigation';
import type {
CommitDataFrontend,
CommitTree,
CommitTreeNode,
ProfilingDataFrontend,
} from './types';
export type TabID = 'flame-chart' | 'ranked-chart';
type SearchResult = {id: number, name: string | null};
function fiberMatchesQuery(node: CommitTreeNode, regExp: RegExp): boolean {
const {displayName, hocDisplayNames, key} = node;
return (
(displayName !== null && regExp.test(displayName)) ||
(hocDisplayNames !== null &&
hocDisplayNames.some(name => regExp.test(name))) ||
(key !== null && regExp.test(String(key)))
);
}
function collectSearchMatches(
commitTree: CommitTree,
text: string,
): Array<SearchResult> {
const regExp = createRegExp(text);
const matches: Array<SearchResult> = [];
const visit = (id: number) => {
const node = commitTree.nodes.get(id);
if (node == null) {
return;
}
if (fiberMatchesQuery(node, regExp)) {
matches.push({id, name: node.displayName});
}
node.children.forEach(visit);
};
visit(commitTree.rootID);
return matches;
}
export type Context = {
selectedTabID: TabID,
selectTab(id: TabID): void,
didRecordCommits: boolean,
isProcessingData: boolean,
isProfiling: boolean,
profilingData: ProfilingDataFrontend | null,
startProfiling(): void,
stopProfiling(): void,
supportsProfiling: boolean,
rootID: number | null,
setRootID: (id: number) => void,
isCommitFilterEnabled: boolean,
setIsCommitFilterEnabled: (value: boolean) => void,
minCommitDuration: number,
setMinCommitDuration: (value: number) => void,
selectedCommitIndex: number | null,
selectCommitIndex: (value: number | null) => void,
selectNextCommitIndex(): void,
selectPrevCommitIndex(): void,
filteredCommitIndices: Array<number>,
selectedFilteredCommitIndex: number | null,
selectedFiberID: number | null,
selectedFiberName: string | null,
selectFiber: (id: number | null, name: string | null) => void,
isSearchInputVisible: boolean,
showSearchInput(): void,
hideSearchInput(): void,
searchText: string,
setSearchText: (text: string) => void,
searchResults: Array<SearchResult>,
searchIndex: number,
searchIsPending: boolean,
goToNextSearchResult(): void,
goToPreviousSearchResult(): void,
goToSearchResult: (index: number) => void,
};
const ProfilerContext: ReactContext<Context> = createContext<Context>(
null as any as 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): React.Node {
const store = useContext(StoreContext);
const {inspectedElementID} = useContext(TreeStateContext);
const dispatch = useContext(TreeDispatcherContext);
const {profilerStore} = store;
const subscription = useMemo(
() => ({
getCurrentValue: () => ({
didRecordCommits: profilerStore.didRecordCommits,
isProcessingData: profilerStore.isProcessingData,
isProfiling: profilerStore.isProfilingBasedOnUserInput,
profilingData: profilerStore.profilingData,
supportsProfiling: store.rootSupportsBasicProfiling,
}),
subscribe: (callback: Function) => {
profilerStore.addListener('profilingData', callback);
profilerStore.addListener('isProcessingData', callback);
profilerStore.addListener('isProfiling', callback);
store.addListener('rootSupportsBasicProfiling', callback);
return () => {
profilerStore.removeListener('profilingData', callback);
profilerStore.removeListener('isProcessingData', callback);
profilerStore.removeListener('isProfiling', callback);
store.removeListener('rootSupportsBasicProfiling', 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 [isSearchInputVisible, setIsSearchInputVisible] =
useState<boolean>(false);
const [searchText, setSearchTextState] = useState<string>('');
const [searchIndex, setSearchIndex] = useState<number>(-1);
const selectFiber = useCallback(
(id: number | null, name: string | null) => {
selectFiberID(id);
selectFiberName(name);
if (
id !== null &&
profilingData !== null &&
profilingData.imported === false
) {
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) {
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 (inspectedElementID !== null) {
selectedElementRootID = store.getRootIDForElement(inspectedElementID);
}
if (
selectedElementRootID !== null &&
dataForRoots.has(selectedElementRootID)
) {
setRootIDAndClearFiber(selectedElementRootID);
} else {
setRootIDAndClearFiber(firstRootID);
}
}
}
}
const [persistedTabID, selectTab] = useLocalStorage<TabID>(
'React::DevTools::Profiler::defaultTab',
'flame-chart',
value => {
logEvent({
event_name: 'profiler-tab-changed',
metadata: {
tabId: value,
},
});
},
);
const selectedTabID: TabID =
persistedTabID === 'ranked-chart' ? persistedTabID : 'flame-chart';
const stopProfiling = useCallback(
() => store.profilerStore.stopProfiling(),
[store],
);
const commitData = useMemo(() => {
if (!didRecordCommits || rootID === null || profilingData === null) {
return [] as Array<CommitDataFrontend>;
}
const dataForRoot = profilingData.dataForRoots.get(rootID);
return dataForRoot
? dataForRoot.commitData
: ([] as Array<CommitDataFrontend>);
}, [didRecordCommits, rootID, profilingData]);
const {
isCommitFilterEnabled,
setIsCommitFilterEnabled,
minCommitDuration,
setMinCommitDuration,
selectedCommitIndex,
selectCommitIndex,
filteredCommitIndices,
selectedFilteredCommitIndex,
selectNextCommitIndex,
selectPrevCommitIndex,
} = useCommitFilteringAndNavigation(commitData);
const findMatches = useCallback(
(text: string): Array<SearchResult> => {
if (
text === '' ||
rootID === null ||
selectedCommitIndex === null ||
!didRecordCommits
) {
return [];
}
const commitTree = profilerStore.profilingCache.getCommitTree({
commitIndex: selectedCommitIndex,
rootID,
});
return collectSearchMatches(commitTree, text);
},
[rootID, selectedCommitIndex, didRecordCommits, profilerStore],
);
const deferredSearchText = useDeferredValue(searchText);
const searchIsPending = searchText !== deferredSearchText;
const searchResults = useMemo<Array<SearchResult>>(
() => findMatches(deferredSearchText),
[findMatches, deferredSearchText],
);
const setSearchText = useCallback((text: string) => {
setSearchTextState(text);
setSearchIndex(text === '' ? -1 : 0);
}, []);
const goToNextSearchResult = useCallback(() => {
setSearchIndex(prevIndex => {
const count = searchResults.length;
if (count === 0) {
return -1;
}
return prevIndex < 0 || prevIndex >= count ? 0 : (prevIndex + 1) % count;
});
}, [searchResults.length]);
const goToPreviousSearchResult = useCallback(() => {
setSearchIndex(prevIndex => {
const count = searchResults.length;
if (count === 0) {
return -1;
}
const current = prevIndex < 0 || prevIndex >= count ? count : prevIndex;
return current <= 0 ? count - 1 : current - 1;
});
}, [searchResults.length]);
const goToSearchResult = useCallback(
(index: number) => setSearchIndex(index),
[],
);
const [prevSearchResults, setPrevSearchResults] = useState(searchResults);
const [prevSearchIndex, setPrevSearchIndex] = useState(searchIndex);
if (prevSearchResults !== searchResults || prevSearchIndex !== searchIndex) {
setPrevSearchResults(searchResults);
setPrevSearchIndex(searchIndex);
if (searchText !== '') {
if (searchResults.length === 0) {
selectFiberID(null);
selectFiberName(null);
} else {
const index =
searchIndex < 0 || searchIndex >= searchResults.length
? 0
: searchIndex;
const match = searchResults[index];
selectFiberID(match.id);
selectFiberName(match.name);
}
}
}
const showSearchInput = useCallback(() => setIsSearchInputVisible(true), []);
const hideSearchInput = useCallback(() => {
setIsSearchInputVisible(false);
setSearchTextState('');
setSearchIndex(-1);
}, []);
const startProfiling = useCallback(() => {
logEvent({
event_name: 'profiling-start',
metadata: {current_tab: selectedTabID},
});
selectCommitIndex(null);
selectFiberID(null);
selectFiberName(null);
setIsSearchInputVisible(false);
setSearchTextState('');
setSearchIndex(-1);
store.profilerStore.startProfiling();
}, [store, selectedTabID, selectCommitIndex]);
useEffect(() => {
if (
profilingData !== null &&
selectedCommitIndex === null &&
rootID !== null
) {
const dataForRoot = profilingData.dataForRoots.get(rootID);
if (dataForRoot && dataForRoot.commitData.length > 0) {
selectCommitIndex(0);
}
}
}, [profilingData, rootID, selectCommitIndex]);
const value = useMemo(
() => ({
selectedTabID,
selectTab,
didRecordCommits,
isProcessingData,
isProfiling,
profilingData,
startProfiling,
stopProfiling,
supportsProfiling,
rootID,
setRootID: setRootIDAndClearFiber,
isCommitFilterEnabled,
setIsCommitFilterEnabled,
minCommitDuration,
setMinCommitDuration,
selectedCommitIndex,
selectCommitIndex,
selectNextCommitIndex,
selectPrevCommitIndex,
filteredCommitIndices,
selectedFilteredCommitIndex,
selectedFiberID,
selectedFiberName,
selectFiber,
isSearchInputVisible,
showSearchInput,
hideSearchInput,
searchText,
setSearchText,
searchResults,
searchIndex,
searchIsPending,
goToNextSearchResult,
goToPreviousSearchResult,
goToSearchResult,
}),
[
selectedTabID,
selectTab,
didRecordCommits,
isProcessingData,
isProfiling,
profilingData,
startProfiling,
stopProfiling,
supportsProfiling,
rootID,
setRootIDAndClearFiber,
isCommitFilterEnabled,
setIsCommitFilterEnabled,
minCommitDuration,
setMinCommitDuration,
selectedCommitIndex,
selectCommitIndex,
selectNextCommitIndex,
selectPrevCommitIndex,
filteredCommitIndices,
selectedFilteredCommitIndex,
selectedFiberID,
selectedFiberName,
selectFiber,
isSearchInputVisible,
showSearchInput,
hideSearchInput,
searchText,
setSearchText,
searchResults,
searchIndex,
searchIsPending,
goToNextSearchResult,
goToPreviousSearchResult,
goToSearchResult,
],
);
return (
<ProfilerContext.Provider value={value}>
{children}
</ProfilerContext.Provider>
);
}
export {ProfilerContext, ProfilerContextController};