flamegraphSearch.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import {useCallback, useEffect, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import Fuse from 'fuse.js';
  5. import SearchBar from 'sentry/components/searchBar';
  6. import {t} from 'sentry/locale';
  7. import {CanvasPoolManager} from 'sentry/utils/profiling/canvasScheduler';
  8. import {Flamegraph} from 'sentry/utils/profiling/flamegraph';
  9. import {FlamegraphSearchResult} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphSearch';
  10. import {useFlamegraphSearch} from 'sentry/utils/profiling/flamegraph/useFlamegraphSearch';
  11. import {
  12. FlamegraphFrame,
  13. getFlamegraphFrameSearchId,
  14. } from 'sentry/utils/profiling/flamegraphFrame';
  15. import {memoizeByReference} from 'sentry/utils/profiling/profile/utils';
  16. import {isRegExpString, parseRegExp} from 'sentry/utils/profiling/validators/regExp';
  17. type FrameSearchResults = Record<string, FlamegraphSearchResult>;
  18. function sortFrameResults(frames: FrameSearchResults | null): Array<FlamegraphFrame> {
  19. // If frames have the same start times, move frames with lower stack depth first.
  20. // This results in top down and left to right iteration
  21. return Object.values(frames ?? {})
  22. .map(f => f.frame)
  23. .sort((a, b) =>
  24. a.start === b.start
  25. ? numericSort(a.depth, b.depth, 'asc')
  26. : numericSort(a.start, b.start, 'asc')
  27. );
  28. }
  29. const memoizedSortFrameResults = memoizeByReference(sortFrameResults);
  30. function frameSearch(
  31. query: string,
  32. frames: ReadonlyArray<FlamegraphFrame>,
  33. index: Fuse<FlamegraphFrame>
  34. ): FrameSearchResults | null {
  35. const results: FrameSearchResults = {};
  36. if (isRegExpString(query)) {
  37. const [_, lookup, flags] = parseRegExp(query) ?? [];
  38. let matches = 0;
  39. try {
  40. if (!lookup) {
  41. throw new Error('Invalid RegExp');
  42. }
  43. for (let i = 0; i < frames.length; i++) {
  44. const frame = frames[i];
  45. const re = new RegExp(lookup, flags ?? 'g');
  46. const reMatches = Array.from(frame.frame.name.trim().matchAll(re));
  47. if (reMatches.length > 0) {
  48. const frameId = getFlamegraphFrameSearchId(frame);
  49. results[frameId] = {
  50. frame,
  51. matchIndices: reMatches.reduce((acc, match) => {
  52. if (typeof match.index === 'undefined') {
  53. return acc;
  54. }
  55. acc.push([match.index, match.index + match[0].length]);
  56. return acc;
  57. }, [] as Fuse.RangeTuple[]),
  58. };
  59. matches += 1;
  60. }
  61. }
  62. } catch (e) {
  63. Sentry.captureMessage(e.message);
  64. }
  65. if (matches <= 0) {
  66. return null;
  67. }
  68. return results;
  69. }
  70. const fuseResults = index.search(query);
  71. if (fuseResults.length <= 0) {
  72. return null;
  73. }
  74. for (let i = 0; i < fuseResults.length; i++) {
  75. const fuseFrameResult = fuseResults[i];
  76. const frame = fuseFrameResult.item;
  77. const frameId = getFlamegraphFrameSearchId(frame);
  78. results[frameId] = {
  79. frame,
  80. // matches will be defined when using 'includeMatches' in FuseOptions
  81. matchIndices: fuseFrameResult.matches!.reduce((acc, val) => {
  82. acc.push(...val.indices);
  83. return acc;
  84. }, [] as Fuse.RangeTuple[]),
  85. };
  86. }
  87. return results;
  88. }
  89. const numericSort = (
  90. a: null | undefined | number,
  91. b: null | undefined | number,
  92. direction: 'asc' | 'desc'
  93. ): number => {
  94. if (a === b) {
  95. return 0;
  96. }
  97. if (a === null || a === undefined) {
  98. return 1;
  99. }
  100. if (b === null || b === undefined) {
  101. return -1;
  102. }
  103. return direction === 'asc' ? a - b : b - a;
  104. };
  105. interface FlamegraphSearchProps {
  106. canvasPoolManager: CanvasPoolManager;
  107. flamegraphs: Flamegraph | Flamegraph[];
  108. }
  109. function FlamegraphSearch({
  110. flamegraphs,
  111. canvasPoolManager,
  112. }: FlamegraphSearchProps): React.ReactElement | null {
  113. const [search, dispatchSearch] = useFlamegraphSearch();
  114. const [didInitialSearch, setDidInitialSearch] = useState(!search.query);
  115. const allFrames = useMemo(() => {
  116. if (Array.isArray(flamegraphs)) {
  117. return flamegraphs.reduce(
  118. (acc: FlamegraphFrame[], graph) => acc.concat(graph.frames),
  119. []
  120. );
  121. }
  122. return flamegraphs.frames;
  123. }, [flamegraphs]);
  124. const searchIndex = useMemo(() => {
  125. return new Fuse(allFrames, {
  126. keys: ['frame.name'],
  127. threshold: 0.3,
  128. includeMatches: true,
  129. });
  130. }, [allFrames]);
  131. const onZoomIntoFrame = useCallback(
  132. (frame: FlamegraphFrame) => {
  133. canvasPoolManager.dispatch('zoomIntoFrame', [frame]);
  134. },
  135. [canvasPoolManager]
  136. );
  137. useEffect(() => {
  138. if (typeof search.index !== 'number') {
  139. return;
  140. }
  141. const frames = memoizedSortFrameResults(search.results);
  142. if (frames[search.index]) {
  143. onZoomIntoFrame(frames[search.index]);
  144. }
  145. }, [search.results, search.index, onZoomIntoFrame]);
  146. const handleChange: (value: string) => void = useCallback(
  147. value => {
  148. if (!value) {
  149. dispatchSearch({type: 'clear search'});
  150. return;
  151. }
  152. dispatchSearch({
  153. type: 'set results',
  154. payload: {
  155. results: frameSearch(value, allFrames, searchIndex),
  156. query: value,
  157. },
  158. });
  159. },
  160. [dispatchSearch, allFrames, searchIndex]
  161. );
  162. useEffect(() => {
  163. if (didInitialSearch || allFrames.length === 0) {
  164. return;
  165. }
  166. handleChange(search.query);
  167. setDidInitialSearch(true);
  168. }, [didInitialSearch, handleChange, allFrames, search.query]);
  169. const onNextSearchClick = useCallback(() => {
  170. const frames = memoizedSortFrameResults(search.results);
  171. if (!frames.length) {
  172. return;
  173. }
  174. if (search.index === null || search.index === frames.length - 1) {
  175. dispatchSearch({type: 'set search index position', payload: 0});
  176. return;
  177. }
  178. dispatchSearch({
  179. type: 'set search index position',
  180. payload: search.index + 1,
  181. });
  182. }, [search.results, search.index, dispatchSearch]);
  183. const onPreviousSearchClick = useCallback(() => {
  184. const frames = memoizedSortFrameResults(search.results);
  185. if (!frames.length) {
  186. return;
  187. }
  188. if (search.index === null || search.index === 0) {
  189. dispatchSearch({
  190. type: 'set search index position',
  191. payload: frames.length - 1,
  192. });
  193. return;
  194. }
  195. dispatchSearch({
  196. type: 'set search index position',
  197. payload: search.index - 1,
  198. });
  199. }, [search.results, search.index, dispatchSearch]);
  200. const handleKeyDown = useCallback(
  201. (evt: React.KeyboardEvent<HTMLInputElement>) => {
  202. if (evt.key === 'ArrowDown') {
  203. evt.preventDefault();
  204. onNextSearchClick();
  205. } else if (evt.key === 'ArrowUp') {
  206. evt.preventDefault();
  207. onPreviousSearchClick();
  208. }
  209. },
  210. [onNextSearchClick, onPreviousSearchClick]
  211. );
  212. return (
  213. <StyledSearchBar
  214. placeholder={t('Find Frames')}
  215. query={search.query}
  216. onChange={handleChange}
  217. onKeyDown={handleKeyDown}
  218. />
  219. );
  220. }
  221. const StyledSearchBar = styled(SearchBar)`
  222. .search-input {
  223. height: 28px;
  224. }
  225. flex: 1 1 100%;
  226. `;
  227. export {FlamegraphSearch};