Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import RecordingInProgress from './RecordingInProgress';
import ProcessingData from './ProcessingData';
import ProfilingNotSupported from './ProfilingNotSupported';
import SidebarSelectedFiberInfo from './SidebarSelectedFiberInfo';
import ProfilerSearchInput from './ProfilerSearchInput';
import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
Expand Down Expand Up @@ -188,6 +189,9 @@ function Profiler(_: {}) {
className={styles.TimelineSearchInputContainer}
/>
)}
{isLegacyProfilerSelected && didRecordCommits && selectedCommitIndex !== null && (
<ProfilerSearchInput />
)}
<SettingsModalContextToggle />
{isLegacyProfilerSelected && didRecordCommits && (
<Fragment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ import {
import {StoreContext} from '../context';
import {logEvent} from 'react-devtools-shared/src/Logger';
import {useCommitFilteringAndNavigation} from './useCommitFilteringAndNavigation';
import {createRegExp} from '../utils';

import type {CommitDataFrontend, ProfilingDataFrontend} from './types';
import type {CommitTree} from './types';

export type TabID = 'flame-chart' | 'ranked-chart' | 'timeline';

Expand Down Expand Up @@ -80,6 +82,14 @@ export type Context = {
selectedFiberID: number | null,
selectedFiberName: string | null,
selectFiber: (id: number | null, name: string | null) => void,

// Search functionality for components in the selected commit
searchText: string,
searchResults: Array<number>,
searchIndex: number | null,
setSearchText: (text: string) => void,
goToNextSearchResult: () => void,
goToPreviousSearchResult: () => void,
};

const ProfilerContext: ReactContext<Context> = createContext<Context>(
Expand Down Expand Up @@ -143,6 +153,9 @@ function ProfilerContextController({children}: Props): React.Node {
const [rootID, setRootID] = useState<number | null>(null);
const [selectedFiberID, selectFiberID] = useState<number | null>(null);
const [selectedFiberName, selectFiberName] = useState<string | null>(null);
const [searchText, setSearchTextState] = useState<string>('');
const [searchResults, setSearchResults] = useState<Array<number>>([]);
const [searchIndex, setSearchIndex] = useState<number | null>(null);

const selectFiber = useCallback(
(id: number | null, name: string | null) => {
Expand Down Expand Up @@ -278,6 +291,149 @@ function ProfilerContextController({children}: Props): React.Node {
}
}, [profilingData, rootID, selectCommitIndex]);

// Search functionality: filter components in the selected commit by name
const performSearch = useCallback(
(text: string, commitTree: CommitTree | null): Array<number> => {
if (!text || !commitTree) {
return [];
}

const regExp = createRegExp(text);
const results: Array<number> = [];

// Recursively search through the commit tree
const searchTree = (nodeID: number) => {
const node = commitTree.nodes.get(nodeID);
if (!node) {
return;
}

const displayName = node.displayName || 'Anonymous';
if (regExp.test(displayName)) {
results.push(nodeID);
}

// Search children
node.children.forEach(childID => {
searchTree(childID);
});
};

if (commitTree.rootID !== null) {
searchTree(commitTree.rootID);
}

return results;
},
[],
);

// Update search results when search text, commit, or root changes
useEffect(() => {
if (
searchText === '' ||
selectedCommitIndex === null ||
rootID === null ||
!profilingData
) {
setSearchResults([]);
setSearchIndex(null);
return;
}

const commitTree = profilerStore.profilingCache.getCommitTree({
commitIndex: selectedCommitIndex,
rootID,
});

const results = performSearch(searchText, commitTree);
setSearchResults(results);

// Set initial search index
if (results.length > 0) {
// If current selected fiber matches, find its index
if (selectedFiberID !== null && results.includes(selectedFiberID)) {
setSearchIndex(results.indexOf(selectedFiberID));
} else {
setSearchIndex(0);
// Auto-select first result
const firstResultID = results[0];
const node = commitTree.nodes.get(firstResultID);
if (node) {
selectFiber(firstResultID, node.displayName || 'Anonymous');
}
}
} else {
setSearchIndex(null);
}
}, [
searchText,
selectedCommitIndex,
rootID,
profilingData,
profilerStore,
performSearch,
selectedFiberID,
selectFiber,
]);

// Clear search when commit changes
useEffect(() => {
setSearchTextState('');
setSearchResults([]);
setSearchIndex(null);
}, [selectedCommitIndex]);

const setSearchText = useCallback((text: string) => {
setSearchTextState(text);
}, []);

const goToNextSearchResult = useCallback(() => {
if (searchResults.length === 0) {
return;
}

const currentIndex = searchIndex !== null ? searchIndex : -1;
const nextIndex =
currentIndex + 1 < searchResults.length ? currentIndex + 1 : 0;
setSearchIndex(nextIndex);

const nextResultID = searchResults[nextIndex];
if (nextResultID !== null && selectedCommitIndex !== null && rootID !== null) {
const commitTree = profilerStore.profilingCache.getCommitTree({
commitIndex: selectedCommitIndex,
rootID,
});
const node = commitTree.nodes.get(nextResultID);
if (node) {
selectFiber(nextResultID, node.displayName || 'Anonymous');
}
}
}, [searchResults, searchIndex, selectedCommitIndex, rootID, profilerStore, selectFiber]);

const goToPreviousSearchResult = useCallback(() => {
if (searchResults.length === 0) {
return;
}

const currentIndex = searchIndex !== null ? searchIndex : searchResults.length;
const prevIndex =
currentIndex > 0 ? currentIndex - 1 : searchResults.length - 1;
setSearchIndex(prevIndex);

const prevResultID = searchResults[prevIndex];
if (prevResultID !== null && selectedCommitIndex !== null && rootID !== null) {
const commitTree = profilerStore.profilingCache.getCommitTree({
commitIndex: selectedCommitIndex,
rootID,
});
const node = commitTree.nodes.get(prevResultID);
if (node) {
selectFiber(prevResultID, node.displayName || 'Anonymous');
}
}
}, [searchResults, searchIndex, selectedCommitIndex, rootID, profilerStore, selectFiber]);

const value = useMemo(
() => ({
selectedTabID,
Expand Down Expand Up @@ -309,6 +465,13 @@ function ProfilerContextController({children}: Props): React.Node {
selectedFiberID,
selectedFiberName,
selectFiber,

searchText,
searchResults,
searchIndex,
setSearchText,
goToNextSearchResult,
goToPreviousSearchResult,
}),
[
selectedTabID,
Expand Down Expand Up @@ -340,6 +503,13 @@ function ProfilerContextController({children}: Props): React.Node {
selectedFiberID,
selectedFiberName,
selectFiber,

searchText,
searchResults,
searchIndex,
setSearchText,
goToNextSearchResult,
goToPreviousSearchResult,
],
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Copyright (c) Meta Platforms, Inc. and 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 {useState, useContext, useCallback, useEffect} from 'react';

import SearchInput from 'react-devtools-shared/src/devtools/views/SearchInput';
import {ProfilerContext} from './ProfilerContext';

export default function ProfilerSearchInput(): React.Node {
const [localSearchQuery, setLocalSearchQuery] = useState('');
const {
searchIndex,
searchResults,
searchText,
setSearchText,
goToNextSearchResult,
goToPreviousSearchResult,
} = useContext(ProfilerContext);

// Sync local state with context when search is cleared externally (e.g., commit change)
useEffect(() => {
if (searchText === '' && localSearchQuery !== '') {
setLocalSearchQuery('');
}
}, [searchText, localSearchQuery]);

const search = useCallback(
(text: string) => {
setLocalSearchQuery(text);
setSearchText(text);
},
[setLocalSearchQuery, setSearchText],
);
const goToNextResult = useCallback(
() => goToNextSearchResult(),
[goToNextSearchResult],
);
const goToPreviousResult = useCallback(
() => goToPreviousSearchResult(),
[goToPreviousSearchResult],
);

return (
<SearchInput
goToNextResult={goToNextResult}
goToPreviousResult={goToPreviousResult}
placeholder="Search components (text or /regex/)"
search={search}
searchIndex={searchIndex !== null ? searchIndex : 0}
searchResultsCount={searchResults.length}
searchText={localSearchQuery}
testName="ProfilerSearchInput"
/>
);
}