flamegraphZoomView.tsx 24 KB

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