diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js
index 6254be06d83..a3bc84da595 100644
--- a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js
+++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js
@@ -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';
@@ -188,6 +189,9 @@ function Profiler(_: {}) {
className={styles.TimelineSearchInputContainer}
/>
)}
+ {isLegacyProfilerSelected && didRecordCommits && selectedCommitIndex !== null && (
+
+ )}
{isLegacyProfilerSelected && didRecordCommits && (
diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js
index d593558dbdd..22afe39766f 100644
--- a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js
+++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js
@@ -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';
@@ -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,
+ searchIndex: number | null,
+ setSearchText: (text: string) => void,
+ goToNextSearchResult: () => void,
+ goToPreviousSearchResult: () => void,
};
const ProfilerContext: ReactContext = createContext(
@@ -143,6 +153,9 @@ function ProfilerContextController({children}: Props): React.Node {
const [rootID, setRootID] = useState(null);
const [selectedFiberID, selectFiberID] = useState(null);
const [selectedFiberName, selectFiberName] = useState(null);
+ const [searchText, setSearchTextState] = useState('');
+ const [searchResults, setSearchResults] = useState>([]);
+ const [searchIndex, setSearchIndex] = useState(null);
const selectFiber = useCallback(
(id: number | null, name: string | null) => {
@@ -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 => {
+ if (!text || !commitTree) {
+ return [];
+ }
+
+ const regExp = createRegExp(text);
+ const results: Array = [];
+
+ // 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,
@@ -309,6 +465,13 @@ function ProfilerContextController({children}: Props): React.Node {
selectedFiberID,
selectedFiberName,
selectFiber,
+
+ searchText,
+ searchResults,
+ searchIndex,
+ setSearchText,
+ goToNextSearchResult,
+ goToPreviousSearchResult,
}),
[
selectedTabID,
@@ -340,6 +503,13 @@ function ProfilerContextController({children}: Props): React.Node {
selectedFiberID,
selectedFiberName,
selectFiber,
+
+ searchText,
+ searchResults,
+ searchIndex,
+ setSearchText,
+ goToNextSearchResult,
+ goToPreviousSearchResult,
],
);
diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerSearchInput.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerSearchInput.js
new file mode 100644
index 00000000000..278016b5af3
--- /dev/null
+++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerSearchInput.js
@@ -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 (
+
+ );
+}
+