14
createContext,
15
useCallback,
16
useContext,
17
+ useDeferredValue,
18
useMemo,
19
useState,
20
useEffect,
25
TreeStateContext,
26
} from '../Components/TreeContext';
27
import {StoreContext} from '../context';
28
+import {createRegExp} from '../utils';
29
import {logEvent} from 'react-devtools-shared/src/Logger';
30
import {useCommitFilteringAndNavigation} from './useCommitFilteringAndNavigation';
31
30
-import type {CommitDataFrontend, ProfilingDataFrontend} from './types';
32
+import type {
33
+ CommitDataFrontend,
34
+ CommitTree,
35
+ CommitTreeNode,
36
+ ProfilingDataFrontend,
37
+} from './types';
38
39
export type TabID = 'flame-chart' | 'ranked-chart';
40
41
+type SearchResult = {id: number, name: string | null};
42
+
43
+function fiberMatchesQuery(node: CommitTreeNode, regExp: RegExp): boolean {
44
+ const {displayName, hocDisplayNames, key} = node;
45
+ return (
46
+ (displayName !== null && regExp.test(displayName)) ||
47
+ (hocDisplayNames !== null &&
48
+ hocDisplayNames.some(name => regExp.test(name))) ||
49
+ (key !== null && regExp.test(String(key)))
50
+ );
51
+}
52
+
53
+// Collect the fibers in a commit tree that match `text`, in tree (pre-order)
54
+// order. Kept module-level and pure so it isn't recreated on every render.
55
+function collectSearchMatches(
56
+ commitTree: CommitTree,
57
+ text: string,
58
+): Array<SearchResult> {
59
+ const regExp = createRegExp(text);
60
+ const matches: Array<SearchResult> = [];
61
+ const visit = (id: number) => {
62
+ const node = commitTree.nodes.get(id);
63
+ if (node == null) {
64
+ return;
65
+ }
66
+ if (fiberMatchesQuery(node, regExp)) {
67
+ matches.push({id, name: node.displayName});
68
+ }
69
+ node.children.forEach(visit);
70
+ };
71
+ visit(commitTree.rootID);
72
+ return matches;
73
+}
74
+
75
export type Context = {
76
// Which tab is selected in the Profiler UI?
77
selectedTabID: TabID,
121
selectedFiberID: number | null,
122
selectedFiberName: string | null,
123
selectFiber: (id: number | null, name: string | null) => void,
124
+
125
+ // Component search within the currently selected commit.
126
+ // Toggled by Cmd/Ctrl+F in the flame graph and ranked charts.
127
+ // Unlike the Components tab, results are scoped to the selected commit only.
128
+ isSearchInputVisible: boolean,
129
+ showSearchInput(): void,
130
+ hideSearchInput(): void,
131
+ searchText: string,
132
+ setSearchText: (text: string) => void,
133
+ searchResults: Array<SearchResult>,
134
+ searchIndex: number,
135
+ searchIsPending: boolean,
136
+ goToNextSearchResult(): void,
137
+ goToPreviousSearchResult(): void,
138
+ goToSearchResult: (index: number) => void,
139
};
140
141
const ProfilerContext: ReactContext<Context> = createContext<Context>(
200
const [selectedFiberID, selectFiberID] = useState<number | null>(null);
201
const [selectedFiberName, selectFiberName] = useState<string | null>(null);
202
203
+ // Component search (scoped to the currently selected commit).
204
+ const [isSearchInputVisible, setIsSearchInputVisible] =
205
+ useState<boolean>(false);
206
+ const [searchText, setSearchTextState] = useState<string>('');
207
+ const [searchIndex, setSearchIndex] = useState<number>(-1);
208
+
209
const selectFiber = useCallback(
210
(id: number | null, name: string | null) => {
211
selectFiberID(id);
317
selectPrevCommitIndex,
318
} = useCommitFilteringAndNavigation(commitData);
319
320
+ // Fibers in the selected commit matching `text`, scoped to the current
321
+ // commit only (never the whole trace).
322
+ const findMatches = useCallback(
323
+ (text: string): Array<SearchResult> => {
324
+ if (
325
+ text === '' ||
326
+ rootID === null ||
327
+ selectedCommitIndex === null ||
328
+ !didRecordCommits
329
+ ) {
330
+ return [];
331
+ }
332
+ const commitTree = profilerStore.profilingCache.getCommitTree({
333
+ commitIndex: selectedCommitIndex,
334
+ rootID,
335
+ });
336
+ return collectSearchMatches(commitTree, text);
337
+ },
338
+ [rootID, selectedCommitIndex, didRecordCommits, profilerStore],
339
+ );
340
+
341
+ // Keep the controlled input update synchronous (see setSearchText), but
342
+ // derive matches from a *deferred* value so the tree walk runs at transition
343
+ // priority and never blocks typing. Deriving via a memo also keeps results
344
+ // scoped to the current commit for free (findMatches tracks selectedCommitIndex).
345
+ const deferredSearchText = useDeferredValue(searchText);
346
+ const searchIsPending = searchText !== deferredSearchText;
347
+ const searchResults = useMemo<Array<SearchResult>>(
348
+ () => findMatches(deferredSearchText),
349
+ [findMatches, deferredSearchText],
350
+ );
351
+
352
+ const setSearchText = useCallback((text: string) => {
353
+ // Synchronous so the input stays responsive; searchResults recomputes off
354
+ // the deferred value at transition priority.
355
+ setSearchTextState(text);
356
+ setSearchIndex(text === '' ? -1 : 0);
357
+ }, []);
358
+
359
+ const goToNextSearchResult = useCallback(() => {
360
+ setSearchIndex(prevIndex => {
361
+ const count = searchResults.length;
362
+ if (count === 0) {
363
+ return -1;
364
+ }
365
+ return prevIndex < 0 || prevIndex >= count ? 0 : (prevIndex + 1) % count;
366
+ });
367
+ }, [searchResults.length]);
368
+
369
+ const goToPreviousSearchResult = useCallback(() => {
370
+ setSearchIndex(prevIndex => {
371
+ const count = searchResults.length;
372
+ if (count === 0) {
373
+ return -1;
374
+ }
375
+ const current = prevIndex < 0 || prevIndex >= count ? count : prevIndex;
376
+ return current <= 0 ? count - 1 : current - 1;
377
+ });
378
+ }, [searchResults.length]);
379
+
380
+ const goToSearchResult = useCallback(
381
+ (index: number) => setSearchIndex(index),
382
+ [],
383
+ );
384
+
385
+ // Keep the selected fiber in sync with the current search match *during
386
+ // render* rather than in an effect, so results and selection commit together
387
+ // (no post-paint frame showing a stale/empty selection). This mirrors the
388
+ // existing prevProfilingData pattern above and follows
389
+ // https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
390
+ // Note: only the profiler's own selection state is updated here (a render is
391
+ // not allowed to dispatch into the Components tree), so search navigation
392
+ // intentionally does not sync selection to the Components tab.
393
+ const [prevSearchResults, setPrevSearchResults] = useState(searchResults);
394
+ const [prevSearchIndex, setPrevSearchIndex] = useState(searchIndex);
395
+ if (prevSearchResults !== searchResults || prevSearchIndex !== searchIndex) {
396
+ setPrevSearchResults(searchResults);
397
+ setPrevSearchIndex(searchIndex);
398
+ if (searchText !== '') {
399
+ if (searchResults.length === 0) {
400
+ selectFiberID(null);
401
+ selectFiberName(null);
402
+ } else {
403
+ const index =
404
+ searchIndex < 0 || searchIndex >= searchResults.length
405
+ ? 0
406
+ : searchIndex;
407
+ const match = searchResults[index];
408
+ selectFiberID(match.id);
409
+ selectFiberName(match.name);
410
+ }
411
+ }
412
+ }
413
+
414
+ const showSearchInput = useCallback(() => setIsSearchInputVisible(true), []);
415
+
416
+ const hideSearchInput = useCallback(() => {
417
+ setIsSearchInputVisible(false);
418
+ setSearchTextState('');
419
+ setSearchIndex(-1);
420
+ }, []);
421
+
422
const startProfiling = useCallback(() => {
423
logEvent({
424
event_name: 'profiling-start',
430
selectFiberID(null);
431
selectFiberName(null);
432
433
+ // Clear any active search from the previous session.
434
+ setIsSearchInputVisible(false);
435
+ setSearchTextState('');
436
+ setSearchIndex(-1);
437
+
438
store.profilerStore.startProfiling();
439
}, [store, selectedTabID, selectCommitIndex]);
440
483
selectedFiberID,
484
selectedFiberName,
485
selectFiber,
486
+
487
+ isSearchInputVisible,
488
+ showSearchInput,
489
+ hideSearchInput,
490
+ searchText,
491
+ setSearchText,
492
+ searchResults,
493
+ searchIndex,
494
+ searchIsPending,
495
+ goToNextSearchResult,
496
+ goToPreviousSearchResult,
497
+ goToSearchResult,
498
}),
499
[
500
selectedTabID,
526
selectedFiberID,
527
selectedFiberName,
528
selectFiber,
529
+
530
+ isSearchInputVisible,
531
+ showSearchInput,
532
+ hideSearchInput,
533
+ searchText,
534
+ setSearchText,
535
+ searchResults,
536
+ searchIndex,
537
+ searchIsPending,
538
+ goToNextSearchResult,
539
+ goToPreviousSearchResult,
540
+ goToSearchResult,
541
],
542
);
543