flamegraphZoomView.tsx 25 KB

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