flamegraphZoomView.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808
  1. import type {CSSProperties} from 'react';
  2. import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
  3. import styled from '@emotion/styled';
  4. import {vec2} from 'gl-matrix';
  5. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  6. import {FlamegraphContextMenu} from 'sentry/components/profiling/flamegraph/flamegraphContextMenu';
  7. import {FlamegraphTooltip} from 'sentry/components/profiling/flamegraph/flamegraphTooltip';
  8. import {t} from 'sentry/locale';
  9. import type {CanvasPoolManager} from 'sentry/utils/profiling/canvasScheduler';
  10. import {useCanvasScheduler} from 'sentry/utils/profiling/canvasScheduler';
  11. import type {CanvasView} from 'sentry/utils/profiling/canvasView';
  12. import type {DifferentialFlamegraph} from 'sentry/utils/profiling/differentialFlamegraph';
  13. import type {Flamegraph} from 'sentry/utils/profiling/flamegraph';
  14. import {handleFlamegraphKeyboardNavigation} from 'sentry/utils/profiling/flamegraph/flamegraphKeyboardNavigation';
  15. import {useFlamegraphSearch} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphSearch';
  16. import {
  17. useDispatchFlamegraphState,
  18. useFlamegraphState,
  19. } from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphState';
  20. import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
  21. import type {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
  22. import type {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
  23. import {
  24. computeMinZoomConfigViewForFrames,
  25. getConfigViewTranslationBetweenVectors,
  26. getPhysicalSpacePositionFromOffset,
  27. } from 'sentry/utils/profiling/gl/utils';
  28. import {useContextMenu} from 'sentry/utils/profiling/hooks/useContextMenu';
  29. import {useInternalFlamegraphDebugMode} from 'sentry/utils/profiling/hooks/useInternalFlamegraphDebugMode';
  30. import type {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile';
  31. import type {FlamegraphRenderer} from 'sentry/utils/profiling/renderers/flamegraphRenderer';
  32. import {FlamegraphTextRenderer} from 'sentry/utils/profiling/renderers/flamegraphTextRenderer';
  33. import {GridRenderer} from 'sentry/utils/profiling/renderers/gridRenderer';
  34. import {SampleTickRenderer} from 'sentry/utils/profiling/renderers/sampleTickRenderer';
  35. import {SelectedFrameRenderer} from 'sentry/utils/profiling/renderers/selectedFrameRenderer';
  36. import {Rect} from 'sentry/utils/profiling/speedscope';
  37. import {useCanvasScroll} from './interactions/useCanvasScroll';
  38. import {useCanvasZoomOrScroll} from './interactions/useCanvasZoomOrScroll';
  39. import {useDrawHoveredBorderEffect} from './interactions/useDrawHoveredBorderEffect';
  40. import {useDrawSelectedBorderEffect} from './interactions/useDrawSelectedBorderEffect';
  41. import {useInteractionViewCheckPoint} from './interactions/useInteractionViewCheckPoint';
  42. import {useWheelCenterZoom} from './interactions/useWheelCenterZoom';
  43. function isHighlightingAllOccurrences(
  44. node: FlamegraphFrame | null,
  45. selectedNodes: FlamegraphFrame[] | null
  46. ) {
  47. return !!(
  48. selectedNodes &&
  49. node &&
  50. selectedNodes.length > 1 &&
  51. selectedNodes.includes(node)
  52. );
  53. }
  54. interface FlamegraphZoomViewProps {
  55. canvasBounds: Rect;
  56. canvasPoolManager: CanvasPoolManager;
  57. flamegraph: Flamegraph | DifferentialFlamegraph;
  58. flamegraphCanvas: FlamegraphCanvas | null;
  59. flamegraphCanvasRef: HTMLCanvasElement | null;
  60. flamegraphOverlayCanvasRef: HTMLCanvasElement | null;
  61. flamegraphRenderer: FlamegraphRenderer | null;
  62. flamegraphView: CanvasView<Flamegraph> | null;
  63. profileGroup: ProfileGroup;
  64. setFlamegraphCanvasRef: React.Dispatch<React.SetStateAction<HTMLCanvasElement | null>>;
  65. setFlamegraphOverlayCanvasRef: React.Dispatch<
  66. React.SetStateAction<HTMLCanvasElement | null>
  67. >;
  68. disableCallOrderSort?: boolean;
  69. disableColorCoding?: boolean;
  70. disableGrid?: boolean;
  71. disablePanX?: boolean;
  72. disableZoom?: boolean;
  73. }
  74. function FlamegraphZoomView({
  75. canvasPoolManager,
  76. canvasBounds,
  77. flamegraphRenderer,
  78. flamegraph,
  79. flamegraphCanvas,
  80. flamegraphCanvasRef,
  81. flamegraphOverlayCanvasRef,
  82. flamegraphView,
  83. profileGroup,
  84. setFlamegraphCanvasRef,
  85. setFlamegraphOverlayCanvasRef,
  86. disablePanX = false,
  87. disableZoom = false,
  88. disableGrid = false,
  89. disableCallOrderSort = false,
  90. disableColorCoding = false,
  91. }: FlamegraphZoomViewProps): React.ReactElement {
  92. const flamegraphTheme = useFlamegraphTheme();
  93. const flamegraphSearch = useFlamegraphSearch();
  94. const isInternalFlamegraphDebugModeEnabled = useInternalFlamegraphDebugMode();
  95. const [lastInteraction, setLastInteraction] = useState<
  96. 'pan' | 'click' | 'zoom' | 'scroll' | 'select' | 'resize' | null
  97. >(null);
  98. const dispatch = useDispatchFlamegraphState();
  99. const scheduler = useCanvasScheduler(canvasPoolManager);
  100. const canvasContainerRef = useRef<HTMLDivElement | null>(null);
  101. const [flamegraphState, {previousState, nextState}] = useFlamegraphState();
  102. const [startInteractionVector, setStartInteractionVector] = useState<vec2 | null>(null);
  103. const [configSpaceCursor, setConfigSpaceCursor] = useState<vec2 | null>(null);
  104. const selectedFramesRef = useRef<FlamegraphFrame[] | null>(null);
  105. const textRenderer: FlamegraphTextRenderer | null = useMemo(() => {
  106. if (!flamegraphOverlayCanvasRef) {
  107. return null;
  108. }
  109. return new FlamegraphTextRenderer(
  110. flamegraphOverlayCanvasRef,
  111. flamegraphTheme,
  112. flamegraph
  113. );
  114. }, [flamegraph, flamegraphOverlayCanvasRef, flamegraphTheme]);
  115. const gridRenderer: GridRenderer | null = useMemo(() => {
  116. if (!flamegraphOverlayCanvasRef || disableGrid) {
  117. return null;
  118. }
  119. return new GridRenderer(
  120. flamegraphOverlayCanvasRef,
  121. flamegraphTheme,
  122. flamegraph.formatter
  123. );
  124. }, [flamegraphOverlayCanvasRef, flamegraph, flamegraphTheme, disableGrid]);
  125. const sampleTickRenderer: SampleTickRenderer | null = useMemo(() => {
  126. if (!isInternalFlamegraphDebugModeEnabled) {
  127. return null;
  128. }
  129. if (!flamegraphOverlayCanvasRef || !flamegraphView?.configSpace) {
  130. return null;
  131. }
  132. return new SampleTickRenderer(
  133. flamegraphOverlayCanvasRef,
  134. flamegraph,
  135. flamegraphView.configSpace,
  136. flamegraphTheme
  137. );
  138. }, [
  139. isInternalFlamegraphDebugModeEnabled,
  140. flamegraphOverlayCanvasRef,
  141. flamegraph,
  142. flamegraphView?.configSpace,
  143. flamegraphTheme,
  144. ]);
  145. const selectedFrameRenderer = useMemo(() => {
  146. if (!flamegraphOverlayCanvasRef) {
  147. return null;
  148. }
  149. return new SelectedFrameRenderer(flamegraphOverlayCanvasRef);
  150. }, [flamegraphOverlayCanvasRef]);
  151. const hoveredNode: FlamegraphFrame | null = useMemo(() => {
  152. if (!configSpaceCursor || !flamegraphRenderer) {
  153. return null;
  154. }
  155. return flamegraphRenderer.findHoveredNode(configSpaceCursor);
  156. }, [configSpaceCursor, flamegraphRenderer]);
  157. const hoveredNodeOnContextMenuOpen = useRef<FlamegraphFrame | null>(null);
  158. const contextMenu = useContextMenu({container: flamegraphCanvasRef});
  159. const [highlightingAllOccurrences, setHighlightingAllOccurrences] = useState(
  160. isHighlightingAllOccurrences(hoveredNode, selectedFramesRef.current)
  161. );
  162. useEffect(() => {
  163. if (!flamegraphCanvas || !flamegraphView || !textRenderer || !flamegraphRenderer) {
  164. return undefined;
  165. }
  166. const clearOverlayCanvas = () => {
  167. textRenderer.context.clearRect(
  168. 0,
  169. 0,
  170. textRenderer.canvas.width,
  171. textRenderer.canvas.height
  172. );
  173. };
  174. const drawText = () => {
  175. textRenderer.draw(
  176. flamegraphView.toOriginConfigView(flamegraphView.configView),
  177. flamegraphView.fromTransformedConfigView(flamegraphCanvas.physicalSpace),
  178. flamegraphSearch.results.frames
  179. );
  180. };
  181. const drawInternalSampleTicks = () => {
  182. if (!sampleTickRenderer) {
  183. return;
  184. }
  185. sampleTickRenderer.draw(
  186. flamegraphView.fromTransformedConfigView(flamegraphCanvas.physicalSpace),
  187. flamegraphView.toOriginConfigView(flamegraphView.configView)
  188. );
  189. };
  190. const drawGrid = gridRenderer
  191. ? () => {
  192. gridRenderer.draw(
  193. flamegraphView.configView,
  194. flamegraphCanvas.physicalSpace,
  195. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace),
  196. flamegraphView.toConfigView(flamegraphCanvas.logicalSpace),
  197. flamegraph.profile.type === 'flamechart'
  198. );
  199. }
  200. : undefined;
  201. const drawRectangles = () => {
  202. flamegraphRenderer.draw(
  203. flamegraphView.fromTransformedConfigView(flamegraphCanvas.physicalSpace)
  204. );
  205. };
  206. scheduler.registerBeforeFrameCallback(drawRectangles);
  207. scheduler.registerBeforeFrameCallback(clearOverlayCanvas);
  208. scheduler.registerAfterFrameCallback(drawText);
  209. // We want to register the grid as the last frame callback so that it is drawn on top of everything else,
  210. // including text, hovered or clicked nodes, but after the sample tick renderer as those are overlaid on top of it
  211. if (drawGrid) {
  212. scheduler.registerAfterFrameCallback(drawGrid);
  213. }
  214. scheduler.registerAfterFrameCallback(drawInternalSampleTicks);
  215. scheduler.draw();
  216. return () => {
  217. scheduler.unregisterBeforeFrameCallback(clearOverlayCanvas);
  218. scheduler.unregisterAfterFrameCallback(drawText);
  219. scheduler.unregisterAfterFrameCallback(drawInternalSampleTicks);
  220. if (drawGrid) {
  221. scheduler.unregisterAfterFrameCallback(drawGrid);
  222. }
  223. scheduler.unregisterBeforeFrameCallback(drawRectangles);
  224. };
  225. }, [
  226. flamegraphCanvas,
  227. flamegraphView,
  228. scheduler,
  229. flamegraph,
  230. flamegraphTheme,
  231. textRenderer,
  232. gridRenderer,
  233. flamegraphRenderer,
  234. sampleTickRenderer,
  235. canvasPoolManager,
  236. flamegraphSearch.results.frames,
  237. ]);
  238. useEffect(() => {
  239. if (!flamegraphRenderer) {
  240. return;
  241. }
  242. if (flamegraphState.search.highlightFrames) {
  243. let frames = flamegraph.findAllMatchingFrames(
  244. flamegraphState.search.highlightFrames.name,
  245. flamegraphState.search.highlightFrames.package
  246. );
  247. // there is a chance that the reason we did not find any frames is because
  248. // for node, we try to infer some package from the frontend code.
  249. // If that happens, we'll try and just do a search by name. This logic
  250. // is duplicated in flamegraph.tsx and should be kept in sync
  251. if (
  252. !frames.length &&
  253. !flamegraphState.search.highlightFrames.package &&
  254. flamegraphState.search.highlightFrames.name
  255. ) {
  256. frames = flamegraph.findAllMatchingFramesBy(
  257. flamegraphState.search.highlightFrames.name,
  258. ['name']
  259. );
  260. }
  261. selectedFramesRef.current = frames;
  262. }
  263. if (flamegraphState.search.query && !flamegraphState.search.highlightFrames) {
  264. flamegraphRenderer.setSearchResults(
  265. flamegraphState.search.query,
  266. flamegraphState.search.results.frames
  267. );
  268. selectedFramesRef.current = null;
  269. }
  270. if (!flamegraphState.search.query && !flamegraphState.search.highlightFrames) {
  271. flamegraphRenderer.setSearchResults('', new Map());
  272. selectedFramesRef.current = null;
  273. }
  274. }, [
  275. flamegraph,
  276. flamegraphRenderer,
  277. flamegraphState.search.results.frames,
  278. flamegraphState.search.query,
  279. flamegraphState.search.highlightFrames,
  280. ]);
  281. useInteractionViewCheckPoint({
  282. view: flamegraphView,
  283. lastInteraction,
  284. });
  285. useDrawSelectedBorderEffect({
  286. scheduler,
  287. selectedRef: selectedFramesRef,
  288. canvas: flamegraphCanvas,
  289. view: flamegraphView,
  290. eventKey: 'highlight frame',
  291. theme: flamegraphTheme,
  292. renderer: selectedFrameRenderer,
  293. });
  294. useDrawHoveredBorderEffect({
  295. scheduler,
  296. hoveredNode: hoveredNode
  297. ? hoveredNode
  298. : contextMenu.open
  299. ? hoveredNodeOnContextMenuOpen.current
  300. : null,
  301. canvas: flamegraphCanvas,
  302. view: flamegraphView,
  303. theme: flamegraphTheme,
  304. renderer: selectedFrameRenderer,
  305. });
  306. useEffect(() => {
  307. if (!flamegraphCanvas || !flamegraphView) {
  308. return undefined;
  309. }
  310. const onResetZoom = () => {
  311. setConfigSpaceCursor(null);
  312. };
  313. const onZoomIntoFrame = (frame: FlamegraphFrame, _strategy: 'min' | 'exact') => {
  314. if (frame) {
  315. selectedFramesRef.current = [frame];
  316. }
  317. setConfigSpaceCursor(null);
  318. };
  319. scheduler.on('reset zoom', onResetZoom);
  320. scheduler.on('zoom at frame', onZoomIntoFrame);
  321. return () => {
  322. scheduler.off('reset zoom', onResetZoom);
  323. scheduler.off('zoom at frame', onZoomIntoFrame);
  324. };
  325. }, [flamegraphCanvas, canvasPoolManager, dispatch, scheduler, flamegraphView]);
  326. const previousKeyPress = useRef<{at: number; key: string | null}>({
  327. key: null,
  328. at: 0,
  329. });
  330. useEffect(() => {
  331. const onKeyDown = (evt: KeyboardEvent) => {
  332. if (!flamegraphView) {
  333. return;
  334. }
  335. if (evt.key === 'Escape') {
  336. if (highlightingAllOccurrences) {
  337. setHighlightingAllOccurrences(false);
  338. dispatch({type: 'set highlight all frames', payload: null});
  339. canvasPoolManager.dispatch('highlight frame', [null, 'selected']);
  340. previousKeyPress.current = {key: null, at: 0};
  341. return;
  342. }
  343. // We'll keep 300ms as the threshold
  344. if (
  345. previousKeyPress.current.key === 'Escape' &&
  346. previousKeyPress.current.at - performance.now() < 300
  347. ) {
  348. canvasPoolManager.dispatch('reset zoom', []);
  349. previousKeyPress.current = {key: null, at: 0};
  350. } else {
  351. previousKeyPress.current = {key: evt.key, at: performance.now()};
  352. }
  353. }
  354. if (evt.key === 'z' && evt.metaKey) {
  355. const action = evt.shiftKey ? 'redo' : 'undo';
  356. if (action === 'undo') {
  357. const previousPosition = previousState?.position?.view;
  358. // If previous position is empty, reset the view to its max
  359. if (previousPosition?.isEmpty()) {
  360. canvasPoolManager.dispatch('reset zoom', []);
  361. } else if (
  362. previousPosition &&
  363. !previousPosition?.equals(flamegraphView.configView)
  364. ) {
  365. // We need to always dispatch with the height of the current view,
  366. // because the height may have changed due to window resizing and
  367. // calling it with the old height may result in the flamegraph
  368. // being drawn into a very small or very large area.
  369. canvasPoolManager.dispatch('set config view', [
  370. previousPosition,
  371. flamegraphView,
  372. ]);
  373. }
  374. }
  375. if (action === 'redo') {
  376. const nextPosition = nextState?.position?.view;
  377. if (nextPosition && !nextPosition.equals(flamegraphView.configView)) {
  378. // We need to always dispatch with the height of the current view,
  379. // because the height may have changed due to window resizing and
  380. // calling it with the old height may result in the flamegraph
  381. // being drawn into a very small or very large area.
  382. canvasPoolManager.dispatch('set config view', [nextPosition, flamegraphView]);
  383. }
  384. }
  385. dispatch({type: action});
  386. }
  387. if (evt.target === flamegraphCanvasRef) {
  388. const nextSelected = handleFlamegraphKeyboardNavigation(
  389. evt,
  390. selectedFramesRef.current?.[0],
  391. flamegraph.inverted
  392. );
  393. if (nextSelected) {
  394. canvasPoolManager.dispatch('zoom at frame', [nextSelected, 'min']);
  395. }
  396. }
  397. };
  398. document.addEventListener('keydown', onKeyDown);
  399. return () => {
  400. document.removeEventListener('keydown', onKeyDown);
  401. };
  402. }, [
  403. canvasPoolManager,
  404. setHighlightingAllOccurrences,
  405. highlightingAllOccurrences,
  406. dispatch,
  407. nextState,
  408. previousState,
  409. flamegraphView,
  410. scheduler,
  411. flamegraphCanvasRef,
  412. flamegraph.inverted,
  413. ]);
  414. const onCanvasMouseDown = useCallback((evt: React.MouseEvent<HTMLCanvasElement>) => {
  415. setLastInteraction('click');
  416. setStartInteractionVector(
  417. getPhysicalSpacePositionFromOffset(evt.nativeEvent.offsetX, evt.nativeEvent.offsetY)
  418. );
  419. }, []);
  420. const onCanvasDoubleClick = useCallback(
  421. (evt: React.MouseEvent<HTMLCanvasElement>) => {
  422. evt.preventDefault();
  423. evt.stopPropagation();
  424. if (!configSpaceCursor) {
  425. setLastInteraction(null);
  426. setStartInteractionVector(null);
  427. return;
  428. }
  429. // Only dispatch the zoom action if the new clicked node is not the same as the old selected node.
  430. // This essentially tracks double click action on a rectangle
  431. if (hoveredNode) {
  432. // If double click is fired on a node, then zoom into it
  433. canvasPoolManager.dispatch('zoom at frame', [hoveredNode, 'exact']);
  434. canvasPoolManager.dispatch('show in table view', [hoveredNode]);
  435. canvasPoolManager.dispatch('highlight frame', [[hoveredNode], 'selected']);
  436. flamegraphRenderer?.setSearchResults('', new Map());
  437. } else {
  438. canvasPoolManager.dispatch('highlight frame', [null, 'selected']);
  439. if (!flamegraphSearch.query) {
  440. flamegraphRenderer?.setSearchResults('', new Map());
  441. }
  442. }
  443. setLastInteraction('click');
  444. setStartInteractionVector(null);
  445. },
  446. [
  447. configSpaceCursor,
  448. hoveredNode,
  449. canvasPoolManager,
  450. flamegraphRenderer,
  451. flamegraphSearch.query,
  452. ]
  453. );
  454. const onMouseDrag = useCallback(
  455. (evt: React.MouseEvent<HTMLCanvasElement>) => {
  456. if (!flamegraphCanvas || !flamegraphView || !startInteractionVector) {
  457. return;
  458. }
  459. const configDelta = getConfigViewTranslationBetweenVectors(
  460. evt.nativeEvent.offsetX,
  461. evt.nativeEvent.offsetY,
  462. startInteractionVector,
  463. flamegraphView,
  464. flamegraphCanvas
  465. );
  466. if (!configDelta) {
  467. return;
  468. }
  469. canvasPoolManager.dispatch('transform config view', [configDelta, flamegraphView]);
  470. setStartInteractionVector(
  471. getPhysicalSpacePositionFromOffset(
  472. evt.nativeEvent.offsetX,
  473. evt.nativeEvent.offsetY
  474. )
  475. );
  476. },
  477. [flamegraphCanvas, flamegraphView, startInteractionVector, canvasPoolManager]
  478. );
  479. const onCanvasMouseMove = useCallback(
  480. (evt: React.MouseEvent<HTMLCanvasElement>) => {
  481. if (!flamegraphCanvas || !flamegraphView) {
  482. return;
  483. }
  484. setConfigSpaceCursor(
  485. flamegraphView.getTransformedConfigViewCursor(
  486. vec2.fromValues(evt.nativeEvent.offsetX, evt.nativeEvent.offsetY),
  487. flamegraphCanvas
  488. )
  489. );
  490. if (startInteractionVector) {
  491. onMouseDrag(evt);
  492. setLastInteraction('pan');
  493. } else {
  494. setLastInteraction(null);
  495. }
  496. },
  497. [
  498. flamegraphCanvas,
  499. flamegraphView,
  500. setConfigSpaceCursor,
  501. onMouseDrag,
  502. startInteractionVector,
  503. ]
  504. );
  505. const onCanvasMouseUp = useCallback(() => {
  506. if (hoveredNode) {
  507. // If double click is fired on a node, then zoom into it
  508. canvasPoolManager.dispatch('highlight frame', [[hoveredNode], 'selected']);
  509. }
  510. setLastInteraction(null);
  511. setStartInteractionVector(null);
  512. }, [hoveredNode, canvasPoolManager]);
  513. const onCanvasMouseLeave = useCallback(() => {
  514. setConfigSpaceCursor(null);
  515. setStartInteractionVector(null);
  516. setLastInteraction(null);
  517. }, []);
  518. const onWheelCenterZoom = useWheelCenterZoom(
  519. flamegraphCanvas,
  520. flamegraphView,
  521. canvasPoolManager,
  522. disableZoom
  523. );
  524. const onCanvasScroll = useCanvasScroll(
  525. flamegraphCanvas,
  526. flamegraphView,
  527. canvasPoolManager,
  528. disablePanX
  529. );
  530. useCanvasZoomOrScroll({
  531. setConfigSpaceCursor,
  532. setLastInteraction,
  533. handleWheel: onWheelCenterZoom,
  534. handleScroll: onCanvasScroll,
  535. canvas: flamegraphCanvasRef,
  536. });
  537. // When a user click anywhere outside the spans, clear cursor and selected node
  538. useEffect(() => {
  539. const onClickOutside = (evt: MouseEvent) => {
  540. if (
  541. !canvasContainerRef.current ||
  542. canvasContainerRef.current.contains(evt.target as Node)
  543. ) {
  544. return;
  545. }
  546. if (contextMenu.open) {
  547. evt.preventDefault();
  548. evt.stopPropagation();
  549. }
  550. setConfigSpaceCursor(null);
  551. };
  552. document.addEventListener('click', onClickOutside);
  553. return () => {
  554. document.removeEventListener('click', onClickOutside);
  555. };
  556. }, [canvasContainerRef, contextMenu, canvasPoolManager]);
  557. const handleContextMenuOpen = useCallback(
  558. (event: React.MouseEvent) => {
  559. hoveredNodeOnContextMenuOpen.current = hoveredNode;
  560. contextMenu.handleContextMenu(event);
  561. // Make sure we set the highlight state relative to the newly hovered node
  562. setHighlightingAllOccurrences(
  563. isHighlightingAllOccurrences(hoveredNode, selectedFramesRef.current)
  564. );
  565. },
  566. [contextMenu, hoveredNode]
  567. );
  568. const handleHighlightAllFramesClick = useCallback(() => {
  569. if (!hoveredNodeOnContextMenuOpen.current || !flamegraphView) {
  570. return;
  571. }
  572. // If all Occurrences are currently being highlighted, we want to unhighlight them now
  573. if (
  574. isHighlightingAllOccurrences(
  575. hoveredNodeOnContextMenuOpen.current,
  576. selectedFramesRef.current
  577. )
  578. ) {
  579. setHighlightingAllOccurrences(false);
  580. dispatch({type: 'set highlight all frames', payload: null});
  581. canvasPoolManager.dispatch('highlight frame', [null, 'selected']);
  582. return;
  583. }
  584. setHighlightingAllOccurrences(true);
  585. const frameName = hoveredNodeOnContextMenuOpen.current.frame.name;
  586. const packageName =
  587. hoveredNodeOnContextMenuOpen.current.frame.package ??
  588. hoveredNodeOnContextMenuOpen.current.frame.module ??
  589. '';
  590. dispatch({
  591. type: 'set highlight all frames',
  592. payload: {
  593. name: frameName,
  594. package: packageName,
  595. },
  596. });
  597. let frames = flamegraph.findAllMatchingFrames(frameName, packageName);
  598. if (
  599. !frames.length &&
  600. !packageName &&
  601. frameName &&
  602. profileGroup.metadata.platform === 'node'
  603. ) {
  604. // there is a chance that the reason we did not find any frames is because
  605. // for node, we try to infer some package from the frontend code.
  606. // If that happens, we'll try and just do a search by name. This logic
  607. // is duplicated in flamegraphZoomView.tsx and should be kept in sync
  608. frames = flamegraph.findAllMatchingFramesBy(frameName, ['name']);
  609. }
  610. const rectFrames = frames.map(f => new Rect(f.start, f.depth, f.end - f.start, 1));
  611. const newConfigView = computeMinZoomConfigViewForFrames(
  612. flamegraphView.configView,
  613. rectFrames
  614. ).transformRect(flamegraphView.configSpaceTransform);
  615. canvasPoolManager.dispatch('highlight frame', [frames, 'selected']);
  616. canvasPoolManager.dispatch('set config view', [newConfigView, flamegraphView]);
  617. }, [
  618. canvasPoolManager,
  619. flamegraph,
  620. flamegraphView,
  621. dispatch,
  622. profileGroup.metadata.platform,
  623. ]);
  624. const handleCopyFunctionName = useCallback(() => {
  625. if (!hoveredNodeOnContextMenuOpen.current) {
  626. return;
  627. }
  628. navigator.clipboard
  629. .writeText(hoveredNodeOnContextMenuOpen.current.frame.name)
  630. .then(() => {
  631. addSuccessMessage(t('Function name copied to clipboard'));
  632. })
  633. .catch(() => {
  634. addErrorMessage(t('Failed to copy function name to clipboard'));
  635. });
  636. }, []);
  637. const handleCopyFunctionSource = useCallback(() => {
  638. if (!hoveredNodeOnContextMenuOpen.current) {
  639. return;
  640. }
  641. const frame = hoveredNodeOnContextMenuOpen.current.frame;
  642. navigator.clipboard
  643. .writeText(frame.file ?? frame.path ?? '')
  644. .then(() => {
  645. addSuccessMessage(t('Function source copied to clipboard'));
  646. })
  647. .catch(() => {
  648. addErrorMessage(t('Failed to copy function source to clipboard'));
  649. });
  650. }, []);
  651. return (
  652. <CanvasContainer ref={canvasContainerRef}>
  653. <Canvas
  654. ref={setFlamegraphCanvasRef}
  655. onMouseDown={onCanvasMouseDown}
  656. onMouseMove={onCanvasMouseMove}
  657. onMouseLeave={onCanvasMouseLeave}
  658. onMouseUp={onCanvasMouseUp}
  659. onDoubleClick={onCanvasDoubleClick}
  660. onContextMenu={handleContextMenuOpen}
  661. cursor={lastInteraction === 'pan' ? 'grabbing' : 'default'}
  662. tabIndex={1}
  663. />
  664. <Canvas ref={setFlamegraphOverlayCanvasRef} pointerEvents="none" />
  665. <FlamegraphContextMenu
  666. contextMenu={contextMenu}
  667. profileGroup={profileGroup}
  668. hoveredNode={hoveredNodeOnContextMenuOpen.current}
  669. isHighlightingAllOccurrences={highlightingAllOccurrences}
  670. onCopyFunctionNameClick={handleCopyFunctionName}
  671. onCopyFunctionSource={handleCopyFunctionSource}
  672. onHighlightAllOccurrencesClick={handleHighlightAllFramesClick}
  673. disableCallOrderSort={disableCallOrderSort}
  674. disableColorCoding={disableColorCoding}
  675. />
  676. {flamegraphCanvas &&
  677. flamegraphRenderer &&
  678. flamegraphView &&
  679. configSpaceCursor &&
  680. hoveredNode ? (
  681. <FlamegraphTooltip
  682. flamegraph={flamegraph}
  683. frame={hoveredNode}
  684. configSpaceCursor={configSpaceCursor}
  685. flamegraphCanvas={flamegraphCanvas}
  686. flamegraphRenderer={flamegraphRenderer}
  687. flamegraphView={flamegraphView}
  688. canvasBounds={canvasBounds}
  689. platform={profileGroup.metadata.platform}
  690. />
  691. ) : null}
  692. </CanvasContainer>
  693. );
  694. }
  695. const CanvasContainer = styled('div')`
  696. display: flex;
  697. flex-direction: column;
  698. height: 100%;
  699. width: 100%;
  700. position: relative;
  701. `;
  702. const Canvas = styled('canvas')<{
  703. cursor?: CSSProperties['cursor'];
  704. pointerEvents?: CSSProperties['pointerEvents'];
  705. }>`
  706. left: 0;
  707. top: 0;
  708. width: 100%;
  709. height: 100%;
  710. user-select: none;
  711. position: absolute;
  712. pointer-events: ${p => p.pointerEvents || 'auto'};
  713. cursor: ${p => p.cursor || 'default'};
  714. &:focus {
  715. outline: none;
  716. }
  717. `;
  718. export {FlamegraphZoomView};