flamegraphZoomView.tsx 21 KB

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