flamegraphZoomView.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825
  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/useFlamegraphProfiles';
  10. import {useFlamegraphSearch} from 'sentry/utils/profiling/flamegraph/useFlamegraphSearch';
  11. import {
  12. useDispatchFlamegraphState,
  13. useFlamegraphState,
  14. } from 'sentry/utils/profiling/flamegraph/useFlamegraphState';
  15. import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
  16. import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
  17. import {FlamegraphView} from 'sentry/utils/profiling/flamegraphView';
  18. import {formatColorForFrame, Rect} from 'sentry/utils/profiling/gl/utils';
  19. import {useContextMenu} from 'sentry/utils/profiling/hooks/useContextMenu';
  20. import {useInternalFlamegraphDebugMode} from 'sentry/utils/profiling/hooks/useInternalFlamegraphDebugMode';
  21. import {FlamegraphRenderer} from 'sentry/utils/profiling/renderers/flamegraphRenderer';
  22. import {GridRenderer} from 'sentry/utils/profiling/renderers/gridRenderer';
  23. import {SampleTickRenderer} from 'sentry/utils/profiling/renderers/sampleTickRenderer';
  24. import {SelectedFrameRenderer} from 'sentry/utils/profiling/renderers/selectedFrameRenderer';
  25. import {TextRenderer} from 'sentry/utils/profiling/renderers/textRenderer';
  26. import usePrevious from 'sentry/utils/usePrevious';
  27. import {FlamegraphFrame} from '../../utils/profiling/flamegraphFrame';
  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, {previousState, nextState}] = useDispatchFlamegraphState();
  67. const scheduler = useMemo(() => new CanvasScheduler(), []);
  68. const [flamegraphState, dispatchFlamegraphState] = 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. dispatchFlamegraphState({type: action});
  177. }
  178. };
  179. document.addEventListener('keydown', onKeyDown);
  180. return () => {
  181. document.removeEventListener('keydown', onKeyDown);
  182. };
  183. }, [
  184. canvasPoolManager,
  185. dispatchFlamegraphState,
  186. nextState,
  187. previousState,
  188. flamegraphView,
  189. ]);
  190. const previousInteraction = usePrevious(lastInteraction);
  191. const beforeInteractionConfigView = useRef<Rect | null>(null);
  192. useEffect(() => {
  193. if (!flamegraphView) {
  194. return;
  195. }
  196. // Check if we are starting a new interaction
  197. if (previousInteraction === null && lastInteraction) {
  198. beforeInteractionConfigView.current = flamegraphView.configView.clone();
  199. return;
  200. }
  201. if (
  202. beforeInteractionConfigView.current &&
  203. !beforeInteractionConfigView.current.equals(flamegraphView.configView)
  204. ) {
  205. dispatch({type: 'checkpoint', payload: flamegraphView.configView.clone()});
  206. }
  207. }, [dispatch, lastInteraction, previousInteraction, flamegraphView]);
  208. useEffect(() => {
  209. if (!flamegraphCanvas || !flamegraphView || !flamegraphRenderer) {
  210. return undefined;
  211. }
  212. const drawRectangles = () => {
  213. flamegraphRenderer.draw(
  214. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace),
  215. flamegraphState.search.results
  216. );
  217. };
  218. scheduler.registerBeforeFrameCallback(drawRectangles);
  219. scheduler.draw();
  220. return () => {
  221. scheduler.unregisterBeforeFrameCallback(drawRectangles);
  222. };
  223. }, [
  224. flamegraphCanvas,
  225. flamegraphRenderer,
  226. flamegraphState.search.results,
  227. scheduler,
  228. flamegraphView,
  229. ]);
  230. useEffect(() => {
  231. if (!flamegraphCanvas || !flamegraphView || !textRenderer || !gridRenderer) {
  232. return undefined;
  233. }
  234. const clearOverlayCanvas = () => {
  235. textRenderer.context.clearRect(
  236. 0,
  237. 0,
  238. textRenderer.canvas.width,
  239. textRenderer.canvas.height
  240. );
  241. };
  242. const drawText = () => {
  243. textRenderer.draw(
  244. flamegraphView.configView,
  245. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace),
  246. flamegraphSearch.results
  247. );
  248. };
  249. const drawGrid = () => {
  250. gridRenderer.draw(
  251. flamegraphView.configView,
  252. flamegraphCanvas.physicalSpace,
  253. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace),
  254. flamegraphView.toConfigView(flamegraphCanvas.logicalSpace)
  255. );
  256. };
  257. const drawInternalSampleTicks = () => {
  258. if (!sampleTickRenderer) {
  259. return;
  260. }
  261. sampleTickRenderer.draw(
  262. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace),
  263. flamegraphView.configView
  264. );
  265. };
  266. scheduler.registerBeforeFrameCallback(clearOverlayCanvas);
  267. scheduler.registerAfterFrameCallback(drawText);
  268. scheduler.registerAfterFrameCallback(drawGrid);
  269. scheduler.registerAfterFrameCallback(drawInternalSampleTicks);
  270. scheduler.draw();
  271. return () => {
  272. scheduler.unregisterBeforeFrameCallback(clearOverlayCanvas);
  273. scheduler.unregisterAfterFrameCallback(drawText);
  274. scheduler.unregisterAfterFrameCallback(drawGrid);
  275. scheduler.unregisterAfterFrameCallback(drawInternalSampleTicks);
  276. };
  277. }, [
  278. flamegraphCanvas,
  279. flamegraphView,
  280. scheduler,
  281. flamegraph,
  282. flamegraphTheme,
  283. textRenderer,
  284. gridRenderer,
  285. sampleTickRenderer,
  286. canvasPoolManager,
  287. flamegraphSearch,
  288. ]);
  289. useEffect(() => {
  290. if (!flamegraphCanvas || !flamegraphView || !selectedFrameRenderer) {
  291. return undefined;
  292. }
  293. const drawFocusedFrameBorder = () => {
  294. selectedFrameRenderer.draw(
  295. focusedRects,
  296. {
  297. BORDER_COLOR: flamegraphTheme.COLORS.FOCUSED_FRAME_BORDER_COLOR,
  298. BORDER_WIDTH: flamegraphTheme.SIZES.FOCUSED_FRAME_BORDER_WIDTH,
  299. },
  300. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace)
  301. );
  302. };
  303. scheduler.registerAfterFrameCallback(drawFocusedFrameBorder);
  304. scheduler.draw();
  305. return () => {
  306. scheduler.unregisterAfterFrameCallback(drawFocusedFrameBorder);
  307. };
  308. }, [
  309. flamegraphCanvas,
  310. flamegraphView,
  311. flamegraphTheme,
  312. focusedRects,
  313. scheduler,
  314. selectedFrameRenderer,
  315. ]);
  316. useEffect(() => {
  317. if (!flamegraphCanvas || !flamegraphView || !selectedFrameRenderer) {
  318. return undefined;
  319. }
  320. const state: {selectedNode: FlamegraphFrame | null} = {
  321. selectedNode: null,
  322. };
  323. const onNodeHighlight = (
  324. node: FlamegraphFrame | null,
  325. mode: 'hover' | 'selected'
  326. ) => {
  327. if (mode === 'selected') {
  328. state.selectedNode = node;
  329. }
  330. scheduler.draw();
  331. };
  332. const drawSelectedFrameBorder = () => {
  333. if (state.selectedNode) {
  334. selectedFrameRenderer.draw(
  335. [
  336. new Rect(
  337. state.selectedNode.start,
  338. state.selectedNode.depth,
  339. state.selectedNode.end - state.selectedNode.start,
  340. 1
  341. ),
  342. ],
  343. {
  344. BORDER_COLOR: flamegraphTheme.COLORS.SELECTED_FRAME_BORDER_COLOR,
  345. BORDER_WIDTH: flamegraphTheme.SIZES.FRAME_BORDER_WIDTH,
  346. },
  347. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace)
  348. );
  349. }
  350. };
  351. scheduler.on('highlight frame', onNodeHighlight);
  352. scheduler.registerAfterFrameCallback(drawSelectedFrameBorder);
  353. return () => {
  354. scheduler.off('highlight frame', onNodeHighlight);
  355. scheduler.unregisterAfterFrameCallback(drawSelectedFrameBorder);
  356. };
  357. }, [
  358. flamegraphView,
  359. flamegraphCanvas,
  360. scheduler,
  361. selectedFrameRenderer,
  362. flamegraphTheme,
  363. ]);
  364. useEffect(() => {
  365. if (!flamegraphCanvas || !flamegraphView || !selectedFrameRenderer) {
  366. return undefined;
  367. }
  368. const drawHoveredFrameBorder = () => {
  369. if (hoveredNode) {
  370. selectedFrameRenderer.draw(
  371. [
  372. new Rect(
  373. hoveredNode.start,
  374. hoveredNode.depth,
  375. hoveredNode.end - hoveredNode.start,
  376. 1
  377. ),
  378. ],
  379. {
  380. BORDER_COLOR: flamegraphTheme.COLORS.HOVERED_FRAME_BORDER_COLOR,
  381. BORDER_WIDTH: flamegraphTheme.SIZES.HOVERED_FRAME_BORDER_WIDTH,
  382. },
  383. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace)
  384. );
  385. }
  386. };
  387. scheduler.registerAfterFrameCallback(drawHoveredFrameBorder);
  388. scheduler.draw();
  389. return () => {
  390. scheduler.unregisterAfterFrameCallback(drawHoveredFrameBorder);
  391. };
  392. }, [
  393. flamegraphView,
  394. flamegraphCanvas,
  395. scheduler,
  396. hoveredNode,
  397. selectedFrameRenderer,
  398. flamegraphTheme,
  399. ]);
  400. useEffect(() => {
  401. if (!flamegraphCanvas || !flamegraphView) {
  402. return undefined;
  403. }
  404. const onResetZoom = () => {
  405. setConfigSpaceCursor(null);
  406. };
  407. const onZoomIntoFrame = () => {
  408. setConfigSpaceCursor(null);
  409. };
  410. scheduler.on('reset zoom', onResetZoom);
  411. scheduler.on('zoom at frame', onZoomIntoFrame);
  412. return () => {
  413. scheduler.off('reset zoom', onResetZoom);
  414. scheduler.off('zoom at frame', onZoomIntoFrame);
  415. };
  416. }, [
  417. flamegraphCanvas,
  418. canvasPoolManager,
  419. dispatchFlamegraphState,
  420. scheduler,
  421. flamegraphView,
  422. ]);
  423. useEffect(() => {
  424. canvasPoolManager.registerScheduler(scheduler);
  425. return () => canvasPoolManager.unregisterScheduler(scheduler);
  426. }, [canvasPoolManager, scheduler]);
  427. const onCanvasMouseDown = useCallback((evt: React.MouseEvent<HTMLCanvasElement>) => {
  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. setLastInteraction('click');
  438. setStartPanVector(physicalMousePos);
  439. }, []);
  440. const onCanvasMouseUp = useCallback(
  441. (evt: React.MouseEvent<HTMLCanvasElement>) => {
  442. evt.preventDefault();
  443. evt.stopPropagation();
  444. if (!configSpaceCursor) {
  445. setLastInteraction(null);
  446. setStartPanVector(null);
  447. return;
  448. }
  449. // Only dispatch the zoom action if the new clicked node is not the same as the old selected node.
  450. // This essentially tracks double click action on a rectangle
  451. if (lastInteraction === 'click') {
  452. if (
  453. hoveredNode &&
  454. flamegraphState.profiles.selectedRoot &&
  455. hoveredNode === flamegraphState.profiles.selectedRoot
  456. ) {
  457. // If double click is fired on a node, then zoom into it
  458. canvasPoolManager.dispatch('zoom at frame', [hoveredNode, 'exact']);
  459. }
  460. canvasPoolManager.dispatch('highlight frame', [hoveredNode, 'selected']);
  461. dispatchFlamegraphState({type: 'set selected root', payload: hoveredNode});
  462. }
  463. setLastInteraction(null);
  464. setStartPanVector(null);
  465. },
  466. [
  467. configSpaceCursor,
  468. flamegraphState.profiles.selectedRoot,
  469. dispatchFlamegraphState,
  470. hoveredNode,
  471. canvasPoolManager,
  472. lastInteraction,
  473. ]
  474. );
  475. const onMouseDrag = useCallback(
  476. (evt: React.MouseEvent<HTMLCanvasElement>) => {
  477. if (!flamegraphCanvas || !flamegraphView || !startPanVector) {
  478. return;
  479. }
  480. const logicalMousePos = vec2.fromValues(
  481. evt.nativeEvent.offsetX,
  482. evt.nativeEvent.offsetY
  483. );
  484. const physicalMousePos = vec2.scale(
  485. vec2.create(),
  486. logicalMousePos,
  487. window.devicePixelRatio
  488. );
  489. const physicalDelta = vec2.subtract(
  490. vec2.create(),
  491. startPanVector,
  492. physicalMousePos
  493. );
  494. if (physicalDelta[0] === 0 && physicalDelta[1] === 0) {
  495. return;
  496. }
  497. const physicalToConfig = mat3.invert(
  498. mat3.create(),
  499. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace)
  500. );
  501. const [m00, m01, m02, m10, m11, m12] = physicalToConfig;
  502. const configDelta = vec2.transformMat3(vec2.create(), physicalDelta, [
  503. m00,
  504. m01,
  505. m02,
  506. m10,
  507. m11,
  508. m12,
  509. 0,
  510. 0,
  511. 0,
  512. ]);
  513. canvasPoolManager.dispatch('transform config view', [
  514. mat3.fromTranslation(mat3.create(), configDelta),
  515. ]);
  516. setStartPanVector(physicalMousePos);
  517. },
  518. [flamegraphCanvas, flamegraphView, startPanVector, canvasPoolManager]
  519. );
  520. const onCanvasMouseMove = useCallback(
  521. (evt: React.MouseEvent<HTMLCanvasElement>) => {
  522. if (!flamegraphCanvas || !flamegraphView) {
  523. return;
  524. }
  525. const configSpaceMouse = flamegraphView.getConfigViewCursor(
  526. vec2.fromValues(evt.nativeEvent.offsetX, evt.nativeEvent.offsetY),
  527. flamegraphCanvas
  528. );
  529. setConfigSpaceCursor(configSpaceMouse);
  530. if (startPanVector) {
  531. onMouseDrag(evt);
  532. setLastInteraction('pan');
  533. } else {
  534. setLastInteraction(null);
  535. }
  536. },
  537. [flamegraphCanvas, flamegraphView, setConfigSpaceCursor, onMouseDrag, startPanVector]
  538. );
  539. const onCanvasMouseLeave = useCallback(() => {
  540. setConfigSpaceCursor(null);
  541. setStartPanVector(null);
  542. setLastInteraction(null);
  543. }, []);
  544. const zoom = useCallback(
  545. (evt: WheelEvent) => {
  546. if (!flamegraphCanvas || !flamegraphView) {
  547. return;
  548. }
  549. const identity = mat3.identity(mat3.create());
  550. const scale = 1 - evt.deltaY * 0.01 * -1; // -1 to invert scale
  551. const mouseInConfigView = flamegraphView.getConfigViewCursor(
  552. vec2.fromValues(evt.offsetX, evt.offsetY),
  553. flamegraphCanvas
  554. );
  555. const configCenter = vec2.fromValues(
  556. mouseInConfigView[0],
  557. flamegraphView.configView.y
  558. );
  559. const invertedConfigCenter = vec2.multiply(
  560. vec2.create(),
  561. vec2.fromValues(-1, -1),
  562. configCenter
  563. );
  564. const translated = mat3.translate(mat3.create(), identity, configCenter);
  565. const scaled = mat3.scale(mat3.create(), translated, vec2.fromValues(scale, 1));
  566. const translatedBack = mat3.translate(mat3.create(), scaled, invertedConfigCenter);
  567. canvasPoolManager.dispatch('transform config view', [translatedBack]);
  568. },
  569. [flamegraphCanvas, flamegraphView, canvasPoolManager]
  570. );
  571. const scroll = useCallback(
  572. (evt: WheelEvent) => {
  573. if (!flamegraphCanvas || !flamegraphView) {
  574. return;
  575. }
  576. const physicalDelta = vec2.fromValues(evt.deltaX, evt.deltaY);
  577. const physicalToConfig = mat3.invert(
  578. mat3.create(),
  579. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace)
  580. );
  581. const [m00, m01, m02, m10, m11, m12] = physicalToConfig;
  582. const configDelta = vec2.transformMat3(vec2.create(), physicalDelta, [
  583. m00,
  584. m01,
  585. m02,
  586. m10,
  587. m11,
  588. m12,
  589. 0,
  590. 0,
  591. 0,
  592. ]);
  593. const translate = mat3.fromTranslation(mat3.create(), configDelta);
  594. canvasPoolManager.dispatch('transform config view', [translate]);
  595. },
  596. [flamegraphCanvas, flamegraphView, canvasPoolManager]
  597. );
  598. useEffect(() => {
  599. if (!flamegraphCanvasRef) {
  600. return undefined;
  601. }
  602. let wheelStopTimeoutId: number | undefined;
  603. function onCanvasWheel(evt: WheelEvent) {
  604. window.clearTimeout(wheelStopTimeoutId);
  605. wheelStopTimeoutId = window.setTimeout(() => {
  606. setLastInteraction(null);
  607. }, 300);
  608. evt.preventDefault();
  609. // When we zoom, we want to clear cursor so that any tooltips
  610. // rendered on the flamegraph are removed from the flamegraphView
  611. setConfigSpaceCursor(null);
  612. // pinch to zoom is recognized as `ctrlKey + wheelEvent`
  613. if (evt.metaKey || evt.ctrlKey) {
  614. zoom(evt);
  615. setLastInteraction('zoom');
  616. } else {
  617. scroll(evt);
  618. setLastInteraction('scroll');
  619. }
  620. }
  621. flamegraphCanvasRef.addEventListener('wheel', onCanvasWheel);
  622. return () => {
  623. window.clearTimeout(wheelStopTimeoutId);
  624. flamegraphCanvasRef.removeEventListener('wheel', onCanvasWheel);
  625. };
  626. }, [flamegraphCanvasRef, zoom, scroll]);
  627. const contextMenu = useContextMenu({container: flamegraphCanvasRef});
  628. return (
  629. <CanvasContainer>
  630. <Canvas
  631. ref={canvas => setFlamegraphCanvasRef(canvas)}
  632. onMouseDown={onCanvasMouseDown}
  633. onMouseUp={onCanvasMouseUp}
  634. onMouseMove={onCanvasMouseMove}
  635. onMouseLeave={onCanvasMouseLeave}
  636. onContextMenu={contextMenu.handleContextMenu}
  637. style={{cursor: lastInteraction === 'pan' ? 'grab' : 'default'}}
  638. />
  639. <Canvas
  640. ref={canvas => setFlamegraphOverlayCanvasRef(canvas)}
  641. style={{
  642. pointerEvents: 'none',
  643. }}
  644. />
  645. <FlamegraphOptionsContextMenu contextMenu={contextMenu} />
  646. {flamegraphCanvas &&
  647. flamegraphRenderer &&
  648. flamegraphView &&
  649. configSpaceCursor &&
  650. hoveredNode?.frame?.name ? (
  651. <BoundTooltip
  652. bounds={canvasBounds}
  653. cursor={configSpaceCursor}
  654. flamegraphCanvas={flamegraphCanvas}
  655. flamegraphView={flamegraphView}
  656. >
  657. <HoveredFrameMainInfo>
  658. <FrameColorIndicator
  659. backgroundColor={formatColorForFrame(hoveredNode, flamegraphRenderer)}
  660. />
  661. {flamegraphRenderer.flamegraph.formatter(hoveredNode.node.totalWeight)}{' '}
  662. {formatWeightToProfileDuration(
  663. hoveredNode.node,
  664. flamegraphRenderer.flamegraph
  665. )}{' '}
  666. {hoveredNode.frame.name}
  667. </HoveredFrameMainInfo>
  668. <HoveredFrameTimelineInfo>
  669. {flamegraphRenderer.flamegraph.timelineFormatter(hoveredNode.start)}{' '}
  670. {' \u2014 '}
  671. {flamegraphRenderer.flamegraph.timelineFormatter(hoveredNode.end)}
  672. </HoveredFrameTimelineInfo>
  673. </BoundTooltip>
  674. ) : null}
  675. </CanvasContainer>
  676. );
  677. }
  678. const HoveredFrameTimelineInfo = styled('div')`
  679. color: ${p => p.theme.subText};
  680. `;
  681. const HoveredFrameMainInfo = styled('div')`
  682. display: flex;
  683. align-items: center;
  684. `;
  685. const FrameColorIndicator = styled('div')<{
  686. backgroundColor: React.CSSProperties['backgroundColor'];
  687. }>`
  688. width: 12px;
  689. height: 12px;
  690. min-width: 12px;
  691. min-height: 12px;
  692. border-radius: 2px;
  693. display: inline-block;
  694. background-color: ${p => p.backgroundColor};
  695. margin-right: ${space(1)};
  696. `;
  697. const CanvasContainer = styled('div')`
  698. display: flex;
  699. flex-direction: column;
  700. height: 100%;
  701. position: relative;
  702. `;
  703. const Canvas = styled('canvas')`
  704. left: 0;
  705. top: 0;
  706. width: 100%;
  707. height: 100%;
  708. user-select: none;
  709. position: absolute;
  710. `;
  711. export {FlamegraphZoomView};