flamegraphZoomView.tsx 25 KB

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