flamegraphZoomView.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785
  1. import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {mat3, vec2} from 'gl-matrix';
  4. import {CanvasPoolManager, CanvasScheduler} from 'sentry/utils/profiling/canvasScheduler';
  5. import {DifferentialFlamegraph} from 'sentry/utils/profiling/differentialFlamegraph';
  6. import {Flamegraph} from 'sentry/utils/profiling/flamegraph';
  7. import {useFlamegraphSearch} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphSearch';
  8. import {
  9. useDispatchFlamegraphState,
  10. useFlamegraphState,
  11. } from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphState';
  12. import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
  13. import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
  14. import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
  15. import {FlamegraphView} from 'sentry/utils/profiling/flamegraphView';
  16. import {Rect} from 'sentry/utils/profiling/gl/utils';
  17. import {useContextMenu} from 'sentry/utils/profiling/hooks/useContextMenu';
  18. import {useInternalFlamegraphDebugMode} from 'sentry/utils/profiling/hooks/useInternalFlamegraphDebugMode';
  19. import {FlamegraphRenderer} from 'sentry/utils/profiling/renderers/flamegraphRenderer';
  20. import {GridRenderer} from 'sentry/utils/profiling/renderers/gridRenderer';
  21. import {SampleTickRenderer} from 'sentry/utils/profiling/renderers/sampleTickRenderer';
  22. import {SelectedFrameRenderer} from 'sentry/utils/profiling/renderers/selectedFrameRenderer';
  23. import {TextRenderer} from 'sentry/utils/profiling/renderers/textRenderer';
  24. import usePrevious from 'sentry/utils/usePrevious';
  25. import {useProfileGroup} from 'sentry/views/profiling/profileGroupProvider';
  26. import {FlamegraphTooltip} from './FlamegraphTooltip/flamegraphTooltip';
  27. import {FlamegraphOptionsContextMenu} from './flamegraphOptionsContextMenu';
  28. function isHighlightingAllOccurences(
  29. node: FlamegraphFrame | null,
  30. selectedNodes: FlamegraphFrame[] | null
  31. ) {
  32. return !!(
  33. selectedNodes &&
  34. node &&
  35. selectedNodes.length > 1 &&
  36. selectedNodes.includes(node)
  37. );
  38. }
  39. interface FlamegraphZoomViewProps {
  40. canvasBounds: Rect;
  41. canvasPoolManager: CanvasPoolManager;
  42. flamegraph: Flamegraph | DifferentialFlamegraph;
  43. flamegraphCanvas: FlamegraphCanvas | null;
  44. flamegraphCanvasRef: HTMLCanvasElement | null;
  45. flamegraphOverlayCanvasRef: HTMLCanvasElement | null;
  46. flamegraphRenderer: FlamegraphRenderer | null;
  47. flamegraphView: FlamegraphView | null;
  48. setFlamegraphCanvasRef: React.Dispatch<React.SetStateAction<HTMLCanvasElement | null>>;
  49. setFlamegraphOverlayCanvasRef: React.Dispatch<
  50. React.SetStateAction<HTMLCanvasElement | null>
  51. >;
  52. }
  53. function FlamegraphZoomView({
  54. canvasPoolManager,
  55. canvasBounds,
  56. flamegraphRenderer,
  57. flamegraph,
  58. flamegraphCanvas,
  59. flamegraphCanvasRef,
  60. flamegraphOverlayCanvasRef,
  61. flamegraphView,
  62. setFlamegraphCanvasRef,
  63. setFlamegraphOverlayCanvasRef,
  64. }: FlamegraphZoomViewProps): React.ReactElement {
  65. const flamegraphTheme = useFlamegraphTheme();
  66. const [profileGroup] = useProfileGroup();
  67. const flamegraphSearch = useFlamegraphSearch();
  68. const isInternalFlamegraphDebugModeEnabled = useInternalFlamegraphDebugMode();
  69. const [lastInteraction, setLastInteraction] = useState<
  70. 'pan' | 'click' | 'zoom' | 'scroll' | null
  71. >(null);
  72. const dispatch = useDispatchFlamegraphState();
  73. const scheduler = useMemo(() => new CanvasScheduler(), []);
  74. const [flamegraphState, {previousState, nextState}] = useFlamegraphState();
  75. const [startPanVector, setStartPanVector] = useState<vec2 | null>(null);
  76. const [configSpaceCursor, setConfigSpaceCursor] = useState<vec2 | null>(null);
  77. const textRenderer: TextRenderer | null = useMemo(() => {
  78. if (!flamegraphOverlayCanvasRef) {
  79. return null;
  80. }
  81. return new TextRenderer(flamegraphOverlayCanvasRef, flamegraph, flamegraphTheme);
  82. }, [flamegraph, flamegraphOverlayCanvasRef, flamegraphTheme]);
  83. const gridRenderer: GridRenderer | null = useMemo(() => {
  84. if (!flamegraphOverlayCanvasRef) {
  85. return null;
  86. }
  87. return new GridRenderer(
  88. flamegraphOverlayCanvasRef,
  89. flamegraphTheme,
  90. flamegraph.formatter
  91. );
  92. }, [flamegraphOverlayCanvasRef, flamegraph, flamegraphTheme]);
  93. const sampleTickRenderer: SampleTickRenderer | null = useMemo(() => {
  94. if (!isInternalFlamegraphDebugModeEnabled) {
  95. return null;
  96. }
  97. if (!flamegraphOverlayCanvasRef || !flamegraphView?.configSpace) {
  98. return null;
  99. }
  100. return new SampleTickRenderer(
  101. flamegraphOverlayCanvasRef,
  102. flamegraph,
  103. flamegraphView.configSpace,
  104. flamegraphTheme
  105. );
  106. }, [
  107. isInternalFlamegraphDebugModeEnabled,
  108. flamegraphOverlayCanvasRef,
  109. flamegraph,
  110. flamegraphView?.configSpace,
  111. flamegraphTheme,
  112. ]);
  113. const selectedFrameRenderer = useMemo(() => {
  114. if (!flamegraphOverlayCanvasRef) {
  115. return null;
  116. }
  117. return new SelectedFrameRenderer(flamegraphOverlayCanvasRef);
  118. }, [flamegraphOverlayCanvasRef]);
  119. const hoveredNode: FlamegraphFrame | null = useMemo(() => {
  120. if (!configSpaceCursor || !flamegraphRenderer) {
  121. return null;
  122. }
  123. return flamegraphRenderer.getHoveredNode(configSpaceCursor);
  124. }, [configSpaceCursor, flamegraphRenderer]);
  125. useEffect(() => {
  126. const onKeyDown = (evt: KeyboardEvent) => {
  127. if (!flamegraphView) {
  128. return;
  129. }
  130. if (evt.key === 'z' && evt.metaKey) {
  131. const action = evt.shiftKey ? 'redo' : 'undo';
  132. if (action === 'undo') {
  133. const previousPosition = previousState?.position?.view;
  134. // If previous position is empty, reset the view to it's max
  135. if (previousPosition?.isEmpty()) {
  136. canvasPoolManager.dispatch('reset zoom', []);
  137. } else if (
  138. previousPosition &&
  139. !previousPosition?.equals(flamegraphView.configView)
  140. ) {
  141. // We need to always dispatch with the height of the current view,
  142. // because the height may have changed due to window resizing and
  143. // calling it with the old height may result in the flamegraph
  144. // being drawn into a very small or very large area.
  145. canvasPoolManager.dispatch('set config view', [
  146. previousPosition.withHeight(flamegraphView.configView.height),
  147. ]);
  148. }
  149. }
  150. if (action === 'redo') {
  151. const nextPosition = nextState?.position?.view;
  152. if (nextPosition && !nextPosition.equals(flamegraphView.configView)) {
  153. // We need to always dispatch with the height of the current view,
  154. // because the height may have changed due to window resizing and
  155. // calling it with the old height may result in the flamegraph
  156. // being drawn into a very small or very large area.
  157. canvasPoolManager.dispatch('set config view', [
  158. nextPosition.withHeight(flamegraphView.configView.height),
  159. ]);
  160. }
  161. }
  162. dispatch({type: action});
  163. }
  164. };
  165. document.addEventListener('keydown', onKeyDown);
  166. return () => {
  167. document.removeEventListener('keydown', onKeyDown);
  168. };
  169. }, [canvasPoolManager, dispatch, nextState, previousState, flamegraphView]);
  170. const previousInteraction = usePrevious(lastInteraction);
  171. const beforeInteractionConfigView = useRef<Rect | null>(null);
  172. useEffect(() => {
  173. if (!flamegraphView) {
  174. return;
  175. }
  176. // Check if we are starting a new interaction
  177. if (previousInteraction === null && lastInteraction) {
  178. beforeInteractionConfigView.current = flamegraphView.configView.clone();
  179. return;
  180. }
  181. if (
  182. beforeInteractionConfigView.current &&
  183. !beforeInteractionConfigView.current.equals(flamegraphView.configView)
  184. ) {
  185. dispatch({type: 'checkpoint', payload: flamegraphView.configView.clone()});
  186. }
  187. }, [dispatch, lastInteraction, previousInteraction, flamegraphView]);
  188. useEffect(() => {
  189. if (!flamegraphCanvas || !flamegraphView || !flamegraphRenderer) {
  190. return undefined;
  191. }
  192. const drawRectangles = () => {
  193. flamegraphRenderer.draw(
  194. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace),
  195. flamegraphState.search.results
  196. );
  197. };
  198. scheduler.registerBeforeFrameCallback(drawRectangles);
  199. scheduler.draw();
  200. return () => {
  201. scheduler.unregisterBeforeFrameCallback(drawRectangles);
  202. };
  203. }, [
  204. flamegraphCanvas,
  205. flamegraphRenderer,
  206. flamegraphState.search.results,
  207. scheduler,
  208. flamegraphView,
  209. ]);
  210. useEffect(() => {
  211. if (!flamegraphCanvas || !flamegraphView || !textRenderer || !gridRenderer) {
  212. return undefined;
  213. }
  214. const clearOverlayCanvas = () => {
  215. textRenderer.context.clearRect(
  216. 0,
  217. 0,
  218. textRenderer.canvas.width,
  219. textRenderer.canvas.height
  220. );
  221. };
  222. const drawText = () => {
  223. textRenderer.draw(
  224. flamegraphView.configView,
  225. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace),
  226. flamegraphSearch.results
  227. );
  228. };
  229. const drawGrid = () => {
  230. gridRenderer.draw(
  231. flamegraphView.configView,
  232. flamegraphCanvas.physicalSpace,
  233. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace),
  234. flamegraphView.toConfigView(flamegraphCanvas.logicalSpace)
  235. );
  236. };
  237. const drawInternalSampleTicks = () => {
  238. if (!sampleTickRenderer) {
  239. return;
  240. }
  241. sampleTickRenderer.draw(
  242. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace),
  243. flamegraphView.configView
  244. );
  245. };
  246. scheduler.registerBeforeFrameCallback(clearOverlayCanvas);
  247. scheduler.registerAfterFrameCallback(drawText);
  248. scheduler.registerAfterFrameCallback(drawGrid);
  249. scheduler.registerAfterFrameCallback(drawInternalSampleTicks);
  250. scheduler.draw();
  251. return () => {
  252. scheduler.unregisterBeforeFrameCallback(clearOverlayCanvas);
  253. scheduler.unregisterAfterFrameCallback(drawText);
  254. scheduler.unregisterAfterFrameCallback(drawGrid);
  255. scheduler.unregisterAfterFrameCallback(drawInternalSampleTicks);
  256. };
  257. }, [
  258. flamegraphCanvas,
  259. flamegraphView,
  260. scheduler,
  261. flamegraph,
  262. flamegraphTheme,
  263. textRenderer,
  264. gridRenderer,
  265. sampleTickRenderer,
  266. canvasPoolManager,
  267. flamegraphSearch,
  268. ]);
  269. const selectedFramesRef = useRef<FlamegraphFrame[] | null>(null);
  270. useEffect(() => {
  271. if (flamegraphState.profiles.highlightFrame) {
  272. selectedFramesRef.current = flamegraph.findAllMatchingFrames(
  273. flamegraphState.profiles.highlightFrame.name,
  274. flamegraphState.profiles.highlightFrame.package
  275. );
  276. } else {
  277. selectedFramesRef.current = null;
  278. }
  279. }, [flamegraph, flamegraphState.profiles.highlightFrame]);
  280. useEffect(() => {
  281. if (!flamegraphCanvas || !flamegraphView || !selectedFrameRenderer) {
  282. return undefined;
  283. }
  284. const onNodeHighlight = (
  285. node: FlamegraphFrame[] | null,
  286. mode: 'hover' | 'selected'
  287. ) => {
  288. if (mode === 'selected') {
  289. selectedFramesRef.current = node;
  290. }
  291. scheduler.draw();
  292. };
  293. const drawSelectedFrameBorder = () => {
  294. if (selectedFramesRef.current) {
  295. selectedFrameRenderer.draw(
  296. selectedFramesRef.current.map(frame => {
  297. return new Rect(frame.start, frame.depth, frame.end - frame.start, 1);
  298. }),
  299. {
  300. BORDER_COLOR: flamegraphTheme.COLORS.SELECTED_FRAME_BORDER_COLOR,
  301. BORDER_WIDTH: flamegraphTheme.SIZES.FRAME_BORDER_WIDTH,
  302. },
  303. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace)
  304. );
  305. }
  306. };
  307. scheduler.on('highlight frame', onNodeHighlight);
  308. scheduler.registerAfterFrameCallback(drawSelectedFrameBorder);
  309. scheduler.draw();
  310. return () => {
  311. scheduler.off('highlight frame', onNodeHighlight);
  312. scheduler.unregisterAfterFrameCallback(drawSelectedFrameBorder);
  313. };
  314. }, [
  315. flamegraphView,
  316. flamegraphCanvas,
  317. scheduler,
  318. flamegraph,
  319. flamegraphState.profiles.highlightFrame,
  320. selectedFrameRenderer,
  321. flamegraphTheme,
  322. ]);
  323. useEffect(() => {
  324. if (!flamegraphCanvas || !flamegraphView || !selectedFrameRenderer) {
  325. return undefined;
  326. }
  327. const drawHoveredFrameBorder = () => {
  328. if (hoveredNode) {
  329. selectedFrameRenderer.draw(
  330. [
  331. new Rect(
  332. hoveredNode.start,
  333. hoveredNode.depth,
  334. hoveredNode.end - hoveredNode.start,
  335. 1
  336. ),
  337. ],
  338. {
  339. BORDER_COLOR: flamegraphTheme.COLORS.HOVERED_FRAME_BORDER_COLOR,
  340. BORDER_WIDTH: flamegraphTheme.SIZES.HOVERED_FRAME_BORDER_WIDTH,
  341. },
  342. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace)
  343. );
  344. }
  345. };
  346. scheduler.registerAfterFrameCallback(drawHoveredFrameBorder);
  347. scheduler.draw();
  348. return () => {
  349. scheduler.unregisterAfterFrameCallback(drawHoveredFrameBorder);
  350. };
  351. }, [
  352. flamegraphView,
  353. flamegraphCanvas,
  354. scheduler,
  355. hoveredNode,
  356. selectedFrameRenderer,
  357. flamegraphTheme,
  358. ]);
  359. useEffect(() => {
  360. if (!flamegraphCanvas || !flamegraphView) {
  361. return undefined;
  362. }
  363. const onResetZoom = () => {
  364. setConfigSpaceCursor(null);
  365. };
  366. const onZoomIntoFrame = () => {
  367. setConfigSpaceCursor(null);
  368. };
  369. scheduler.on('reset zoom', onResetZoom);
  370. scheduler.on('zoom at frame', onZoomIntoFrame);
  371. return () => {
  372. scheduler.off('reset zoom', onResetZoom);
  373. scheduler.off('zoom at frame', onZoomIntoFrame);
  374. };
  375. }, [flamegraphCanvas, canvasPoolManager, dispatch, scheduler, flamegraphView]);
  376. useEffect(() => {
  377. canvasPoolManager.registerScheduler(scheduler);
  378. return () => canvasPoolManager.unregisterScheduler(scheduler);
  379. }, [canvasPoolManager, scheduler]);
  380. const onCanvasMouseDown = useCallback((evt: React.MouseEvent<HTMLCanvasElement>) => {
  381. const logicalMousePos = vec2.fromValues(
  382. evt.nativeEvent.offsetX,
  383. evt.nativeEvent.offsetY
  384. );
  385. const physicalMousePos = vec2.scale(
  386. vec2.create(),
  387. logicalMousePos,
  388. window.devicePixelRatio
  389. );
  390. setLastInteraction('click');
  391. setStartPanVector(physicalMousePos);
  392. }, []);
  393. const onCanvasMouseUp = useCallback(
  394. (evt: React.MouseEvent<HTMLCanvasElement>) => {
  395. evt.preventDefault();
  396. evt.stopPropagation();
  397. if (!configSpaceCursor) {
  398. setLastInteraction(null);
  399. setStartPanVector(null);
  400. return;
  401. }
  402. // Only dispatch the zoom action if the new clicked node is not the same as the old selected node.
  403. // This essentially tracks double click action on a rectangle
  404. if (lastInteraction === 'click') {
  405. if (
  406. hoveredNode &&
  407. flamegraphState.profiles.selectedRoot &&
  408. hoveredNode === flamegraphState.profiles.selectedRoot
  409. ) {
  410. // If double click is fired on a node, then zoom into it
  411. canvasPoolManager.dispatch('zoom at frame', [hoveredNode, 'exact']);
  412. }
  413. canvasPoolManager.dispatch('highlight frame', [
  414. hoveredNode ? [hoveredNode] : null,
  415. 'selected',
  416. ]);
  417. dispatch({type: 'set selected root', payload: hoveredNode});
  418. }
  419. setLastInteraction(null);
  420. setStartPanVector(null);
  421. },
  422. [
  423. configSpaceCursor,
  424. flamegraphState.profiles.selectedRoot,
  425. dispatch,
  426. hoveredNode,
  427. canvasPoolManager,
  428. lastInteraction,
  429. ]
  430. );
  431. const onMouseDrag = useCallback(
  432. (evt: React.MouseEvent<HTMLCanvasElement>) => {
  433. if (!flamegraphCanvas || !flamegraphView || !startPanVector) {
  434. return;
  435. }
  436. const logicalMousePos = vec2.fromValues(
  437. evt.nativeEvent.offsetX,
  438. evt.nativeEvent.offsetY
  439. );
  440. const physicalMousePos = vec2.scale(
  441. vec2.create(),
  442. logicalMousePos,
  443. window.devicePixelRatio
  444. );
  445. const physicalDelta = vec2.subtract(
  446. vec2.create(),
  447. startPanVector,
  448. physicalMousePos
  449. );
  450. if (physicalDelta[0] === 0 && physicalDelta[1] === 0) {
  451. return;
  452. }
  453. const physicalToConfig = mat3.invert(
  454. mat3.create(),
  455. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace)
  456. );
  457. const [m00, m01, m02, m10, m11, m12] = physicalToConfig;
  458. const configDelta = vec2.transformMat3(vec2.create(), physicalDelta, [
  459. m00,
  460. m01,
  461. m02,
  462. m10,
  463. m11,
  464. m12,
  465. 0,
  466. 0,
  467. 0,
  468. ]);
  469. canvasPoolManager.dispatch('transform config view', [
  470. mat3.fromTranslation(mat3.create(), configDelta),
  471. ]);
  472. setStartPanVector(physicalMousePos);
  473. },
  474. [flamegraphCanvas, flamegraphView, startPanVector, canvasPoolManager]
  475. );
  476. const onCanvasMouseMove = useCallback(
  477. (evt: React.MouseEvent<HTMLCanvasElement>) => {
  478. if (!flamegraphCanvas || !flamegraphView) {
  479. return;
  480. }
  481. const configSpaceMouse = flamegraphView.getConfigViewCursor(
  482. vec2.fromValues(evt.nativeEvent.offsetX, evt.nativeEvent.offsetY),
  483. flamegraphCanvas
  484. );
  485. setConfigSpaceCursor(configSpaceMouse);
  486. if (startPanVector) {
  487. onMouseDrag(evt);
  488. setLastInteraction('pan');
  489. } else {
  490. setLastInteraction(null);
  491. }
  492. },
  493. [flamegraphCanvas, flamegraphView, setConfigSpaceCursor, onMouseDrag, startPanVector]
  494. );
  495. const onCanvasMouseLeave = useCallback(() => {
  496. setConfigSpaceCursor(null);
  497. setStartPanVector(null);
  498. setLastInteraction(null);
  499. }, []);
  500. const zoom = useCallback(
  501. (evt: WheelEvent) => {
  502. if (!flamegraphCanvas || !flamegraphView) {
  503. return;
  504. }
  505. const identity = mat3.identity(mat3.create());
  506. const scale = 1 - evt.deltaY * 0.01 * -1; // -1 to invert scale
  507. const mouseInConfigView = flamegraphView.getConfigViewCursor(
  508. vec2.fromValues(evt.offsetX, evt.offsetY),
  509. flamegraphCanvas
  510. );
  511. const configCenter = vec2.fromValues(
  512. mouseInConfigView[0],
  513. flamegraphView.configView.y
  514. );
  515. const invertedConfigCenter = vec2.multiply(
  516. vec2.create(),
  517. vec2.fromValues(-1, -1),
  518. configCenter
  519. );
  520. const translated = mat3.translate(mat3.create(), identity, configCenter);
  521. const scaled = mat3.scale(mat3.create(), translated, vec2.fromValues(scale, 1));
  522. const translatedBack = mat3.translate(mat3.create(), scaled, invertedConfigCenter);
  523. canvasPoolManager.dispatch('transform config view', [translatedBack]);
  524. },
  525. [flamegraphCanvas, flamegraphView, canvasPoolManager]
  526. );
  527. const scroll = useCallback(
  528. (evt: WheelEvent) => {
  529. if (!flamegraphCanvas || !flamegraphView) {
  530. return;
  531. }
  532. const physicalDelta = vec2.fromValues(evt.deltaX, evt.deltaY);
  533. const physicalToConfig = mat3.invert(
  534. mat3.create(),
  535. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace)
  536. );
  537. const [m00, m01, m02, m10, m11, m12] = physicalToConfig;
  538. const configDelta = vec2.transformMat3(vec2.create(), physicalDelta, [
  539. m00,
  540. m01,
  541. m02,
  542. m10,
  543. m11,
  544. m12,
  545. 0,
  546. 0,
  547. 0,
  548. ]);
  549. const translate = mat3.fromTranslation(mat3.create(), configDelta);
  550. canvasPoolManager.dispatch('transform config view', [translate]);
  551. },
  552. [flamegraphCanvas, flamegraphView, canvasPoolManager]
  553. );
  554. useEffect(() => {
  555. if (!flamegraphCanvasRef) {
  556. return undefined;
  557. }
  558. let wheelStopTimeoutId: number | undefined;
  559. function onCanvasWheel(evt: WheelEvent) {
  560. window.clearTimeout(wheelStopTimeoutId);
  561. wheelStopTimeoutId = window.setTimeout(() => {
  562. setLastInteraction(null);
  563. }, 300);
  564. evt.preventDefault();
  565. // When we zoom, we want to clear cursor so that any tooltips
  566. // rendered on the flamegraph are removed from the flamegraphView
  567. setConfigSpaceCursor(null);
  568. // pinch to zoom is recognized as `ctrlKey + wheelEvent`
  569. if (evt.metaKey || evt.ctrlKey) {
  570. zoom(evt);
  571. setLastInteraction('zoom');
  572. } else {
  573. scroll(evt);
  574. setLastInteraction('scroll');
  575. }
  576. }
  577. flamegraphCanvasRef.addEventListener('wheel', onCanvasWheel);
  578. return () => {
  579. window.clearTimeout(wheelStopTimeoutId);
  580. flamegraphCanvasRef.removeEventListener('wheel', onCanvasWheel);
  581. };
  582. }, [flamegraphCanvasRef, zoom, scroll]);
  583. const hoveredNodeOnContextMenuOpen = useRef<FlamegraphFrame | null>(null);
  584. const contextMenu = useContextMenu({container: flamegraphCanvasRef});
  585. const [highlightingAllOccurences, setHighlightingAllOccurences] = useState(
  586. isHighlightingAllOccurences(hoveredNode, selectedFramesRef.current)
  587. );
  588. const handleContextMenuOpen = useCallback(
  589. (event: React.MouseEvent) => {
  590. hoveredNodeOnContextMenuOpen.current = hoveredNode;
  591. contextMenu.handleContextMenu(event);
  592. // Make sure we set the highlight state relative to the newly hovered node
  593. setHighlightingAllOccurences(
  594. isHighlightingAllOccurences(hoveredNode, selectedFramesRef.current)
  595. );
  596. },
  597. [contextMenu, hoveredNode]
  598. );
  599. const handleHighlightAllFramesClick = useCallback(() => {
  600. if (!hoveredNodeOnContextMenuOpen.current) {
  601. return;
  602. }
  603. // If all occurences are currently being highlighted, we want to unhighlight them now
  604. if (
  605. isHighlightingAllOccurences(
  606. hoveredNodeOnContextMenuOpen.current,
  607. selectedFramesRef.current
  608. )
  609. ) {
  610. setHighlightingAllOccurences(false);
  611. canvasPoolManager.dispatch('highlight frame', [null, 'selected']);
  612. scheduler.draw();
  613. return;
  614. }
  615. setHighlightingAllOccurences(true);
  616. canvasPoolManager.dispatch('highlight frame', [
  617. flamegraph.findAllMatchingFrames(hoveredNodeOnContextMenuOpen.current),
  618. 'selected',
  619. ]);
  620. }, [canvasPoolManager, flamegraph, scheduler]);
  621. return (
  622. <CanvasContainer>
  623. <Canvas
  624. ref={canvas => setFlamegraphCanvasRef(canvas)}
  625. onMouseDown={onCanvasMouseDown}
  626. onMouseUp={onCanvasMouseUp}
  627. onMouseMove={onCanvasMouseMove}
  628. onMouseLeave={onCanvasMouseLeave}
  629. onContextMenu={handleContextMenuOpen}
  630. style={{cursor: lastInteraction === 'pan' ? 'grab' : 'default'}}
  631. />
  632. <Canvas
  633. ref={canvas => setFlamegraphOverlayCanvasRef(canvas)}
  634. style={{
  635. pointerEvents: 'none',
  636. }}
  637. />
  638. <FlamegraphOptionsContextMenu
  639. contextMenu={contextMenu}
  640. hoveredNode={hoveredNodeOnContextMenuOpen.current}
  641. isHighlightingAllOccurences={highlightingAllOccurences}
  642. onHighlightAllOccurencesClick={handleHighlightAllFramesClick}
  643. />
  644. {flamegraphCanvas &&
  645. flamegraphRenderer &&
  646. flamegraphView &&
  647. configSpaceCursor &&
  648. hoveredNode ? (
  649. <FlamegraphTooltip
  650. frame={hoveredNode}
  651. configSpaceCursor={configSpaceCursor}
  652. flamegraphCanvas={flamegraphCanvas}
  653. flamegraphRenderer={flamegraphRenderer}
  654. flamegraphView={flamegraphView}
  655. canvasBounds={canvasBounds}
  656. platform={
  657. profileGroup.type === 'resolved'
  658. ? profileGroup.data.metadata.platform
  659. : undefined
  660. }
  661. />
  662. ) : null}
  663. </CanvasContainer>
  664. );
  665. }
  666. const CanvasContainer = styled('div')`
  667. display: flex;
  668. flex-direction: column;
  669. height: 100%;
  670. position: relative;
  671. `;
  672. const Canvas = styled('canvas')`
  673. left: 0;
  674. top: 0;
  675. width: 100%;
  676. height: 100%;
  677. user-select: none;
  678. position: absolute;
  679. `;
  680. export {FlamegraphZoomView};