flamegraphZoomView.tsx 22 KB

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