flamegraphZoomViewMinimap.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  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 {CanvasPoolManager, CanvasScheduler} from 'sentry/utils/profiling/canvasScheduler';
  5. import {Flamegraph} from 'sentry/utils/profiling/flamegraph';
  6. import {useDispatchFlamegraphState} from 'sentry/utils/profiling/flamegraph/useFlamegraphState';
  7. import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
  8. import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
  9. import {FlamegraphView} from 'sentry/utils/profiling/flamegraphView';
  10. import {Rect} from 'sentry/utils/profiling/gl/utils';
  11. import {FlamegraphRenderer} from 'sentry/utils/profiling/renderers/flamegraphRenderer';
  12. import {PositionIndicatorRenderer} from 'sentry/utils/profiling/renderers/positionIndicatorRenderer';
  13. import usePrevious from 'sentry/utils/usePrevious';
  14. interface FlamegraphZoomViewMinimapProps {
  15. canvasPoolManager: CanvasPoolManager;
  16. flamegraph: Flamegraph;
  17. flamegraphMiniMapCanvas: FlamegraphCanvas | null;
  18. flamegraphMiniMapCanvasRef: HTMLCanvasElement | null;
  19. flamegraphMiniMapOverlayCanvasRef: HTMLCanvasElement | null;
  20. flamegraphMiniMapView: FlamegraphView | null;
  21. setFlamegraphMiniMapCanvasRef: React.Dispatch<
  22. React.SetStateAction<HTMLCanvasElement | null>
  23. >;
  24. setFlamegraphMiniMapOverlayCanvasRef: React.Dispatch<
  25. React.SetStateAction<HTMLCanvasElement | null>
  26. >;
  27. }
  28. function FlamegraphZoomViewMinimap({
  29. canvasPoolManager,
  30. flamegraph,
  31. flamegraphMiniMapCanvas,
  32. flamegraphMiniMapCanvasRef,
  33. flamegraphMiniMapOverlayCanvasRef,
  34. flamegraphMiniMapView,
  35. setFlamegraphMiniMapCanvasRef,
  36. setFlamegraphMiniMapOverlayCanvasRef,
  37. }: FlamegraphZoomViewMinimapProps): React.ReactElement {
  38. const flamegraphTheme = useFlamegraphTheme();
  39. const [lastInteraction, setLastInteraction] = useState<
  40. 'pan' | 'click' | 'zoom' | 'scroll' | 'select' | null
  41. >(null);
  42. const [dispatch] = useDispatchFlamegraphState();
  43. const [configSpaceCursor, setConfigSpaceCursor] = useState<vec2 | null>(null);
  44. const scheduler = useMemo(() => new CanvasScheduler(), []);
  45. const flamegraphMiniMapRenderer = useMemo(() => {
  46. if (!flamegraphMiniMapCanvasRef) {
  47. return null;
  48. }
  49. const BAR_HEIGHT =
  50. flamegraphTheme.SIZES.MINIMAP_HEIGHT /
  51. (flamegraph.depth + flamegraphTheme.SIZES.FLAMEGRAPH_DEPTH_OFFSET);
  52. return new FlamegraphRenderer(flamegraphMiniMapCanvasRef, flamegraph, {
  53. ...flamegraphTheme,
  54. SIZES: {
  55. ...flamegraphTheme.SIZES,
  56. BAR_HEIGHT,
  57. },
  58. });
  59. }, [flamegraph, flamegraphMiniMapCanvasRef, flamegraphTheme]);
  60. const positionIndicatorRenderer: PositionIndicatorRenderer | null = useMemo(() => {
  61. if (!flamegraphMiniMapOverlayCanvasRef) {
  62. return null;
  63. }
  64. return new PositionIndicatorRenderer(
  65. flamegraphMiniMapOverlayCanvasRef,
  66. flamegraphTheme
  67. );
  68. }, [flamegraphMiniMapOverlayCanvasRef, flamegraphTheme]);
  69. useEffect(() => {
  70. if (
  71. !flamegraphMiniMapCanvas ||
  72. !flamegraphMiniMapView ||
  73. !flamegraphMiniMapRenderer
  74. ) {
  75. return undefined;
  76. }
  77. const drawRectangles = () => {
  78. flamegraphMiniMapRenderer.draw(
  79. flamegraphMiniMapView.fromConfigSpace(flamegraphMiniMapCanvas.physicalSpace)
  80. );
  81. };
  82. scheduler.registerBeforeFrameCallback(drawRectangles);
  83. return () => {
  84. scheduler.unregisterBeforeFrameCallback(drawRectangles);
  85. };
  86. }, [
  87. flamegraphMiniMapCanvas,
  88. flamegraphMiniMapRenderer,
  89. scheduler,
  90. flamegraphMiniMapView,
  91. ]);
  92. useEffect(() => {
  93. if (
  94. !flamegraphMiniMapCanvas ||
  95. !flamegraphMiniMapView ||
  96. !positionIndicatorRenderer
  97. ) {
  98. return undefined;
  99. }
  100. const clearOverlayCanvas = () => {
  101. positionIndicatorRenderer.context.clearRect(
  102. 0,
  103. 0,
  104. positionIndicatorRenderer.canvas.width,
  105. positionIndicatorRenderer.canvas.height
  106. );
  107. };
  108. const drawPosition = () => {
  109. positionIndicatorRenderer.draw(
  110. flamegraphMiniMapView.configView,
  111. flamegraphMiniMapView.configSpace,
  112. flamegraphMiniMapView.fromConfigSpace(flamegraphMiniMapCanvas.physicalSpace)
  113. );
  114. };
  115. scheduler.registerBeforeFrameCallback(clearOverlayCanvas);
  116. scheduler.registerAfterFrameCallback(drawPosition);
  117. scheduler.draw();
  118. return () => {
  119. scheduler.unregisterBeforeFrameCallback(clearOverlayCanvas);
  120. scheduler.unregisterAfterFrameCallback(drawPosition);
  121. };
  122. }, [
  123. flamegraphMiniMapCanvas,
  124. flamegraphMiniMapView,
  125. scheduler,
  126. positionIndicatorRenderer,
  127. ]);
  128. const previousInteraction = usePrevious(lastInteraction);
  129. const beforeInteractionConfigView = useRef<Rect | null>(null);
  130. useEffect(() => {
  131. if (!flamegraphMiniMapView) {
  132. return;
  133. }
  134. // Check if we are starting a new interaction
  135. if (previousInteraction === null && lastInteraction) {
  136. beforeInteractionConfigView.current = flamegraphMiniMapView.configView.clone();
  137. return;
  138. }
  139. if (
  140. beforeInteractionConfigView.current &&
  141. !beforeInteractionConfigView.current.equals(flamegraphMiniMapView.configView)
  142. ) {
  143. dispatch({
  144. type: 'checkpoint',
  145. payload: flamegraphMiniMapView.configView.clone(),
  146. });
  147. }
  148. }, [lastInteraction, flamegraphMiniMapView, dispatch, previousInteraction]);
  149. const [startDragVector, setStartDragConfigSpaceCursor] = useState<vec2 | null>(null);
  150. const [lastDragVector, setLastDragVector] = useState<vec2 | null>(null);
  151. useEffect(() => {
  152. canvasPoolManager.registerScheduler(scheduler);
  153. return () => canvasPoolManager.unregisterScheduler(scheduler);
  154. }, [scheduler, canvasPoolManager]);
  155. const onMouseDrag = useCallback(
  156. (evt: React.MouseEvent<HTMLCanvasElement>) => {
  157. if (!lastDragVector || !flamegraphMiniMapCanvas || !flamegraphMiniMapView) {
  158. return;
  159. }
  160. const logicalMousePos = vec2.fromValues(
  161. evt.nativeEvent.offsetX,
  162. evt.nativeEvent.offsetY
  163. );
  164. const physicalMousePos = vec2.scale(
  165. vec2.create(),
  166. logicalMousePos,
  167. window.devicePixelRatio
  168. );
  169. const physicalDelta = vec2.subtract(
  170. vec2.create(),
  171. physicalMousePos,
  172. lastDragVector
  173. );
  174. if (physicalDelta[0] === 0 && physicalDelta[1] === 0) {
  175. return;
  176. }
  177. const physicalToConfig = mat3.invert(
  178. mat3.create(),
  179. flamegraphMiniMapView.fromConfigSpace(flamegraphMiniMapCanvas.physicalSpace)
  180. );
  181. const configDelta = vec2.transformMat3(
  182. vec2.create(),
  183. physicalDelta,
  184. physicalToConfig
  185. );
  186. canvasPoolManager.dispatch('transformConfigView', [
  187. mat3.fromTranslation(mat3.create(), configDelta),
  188. ]);
  189. setLastDragVector(physicalMousePos);
  190. },
  191. [flamegraphMiniMapCanvas, flamegraphMiniMapView, lastDragVector, canvasPoolManager]
  192. );
  193. const onMinimapCanvasMouseMove = useCallback(
  194. (evt: React.MouseEvent<HTMLCanvasElement>) => {
  195. if (!flamegraphMiniMapCanvas || !flamegraphMiniMapView) {
  196. return;
  197. }
  198. const configSpaceMouse = flamegraphMiniMapView.getConfigSpaceCursor(
  199. vec2.fromValues(evt.nativeEvent.offsetX, evt.nativeEvent.offsetY),
  200. flamegraphMiniMapCanvas
  201. );
  202. setConfigSpaceCursor(configSpaceMouse);
  203. if (lastDragVector) {
  204. onMouseDrag(evt);
  205. setLastInteraction('pan');
  206. return;
  207. }
  208. if (startDragVector) {
  209. const start = vec2.min(vec2.create(), startDragVector, configSpaceMouse);
  210. const end = vec2.max(vec2.create(), startDragVector, configSpaceMouse);
  211. const rect = new Rect(
  212. start[0],
  213. configSpaceMouse[1] - flamegraphMiniMapView.configView.height / 2,
  214. end[0] - start[0],
  215. flamegraphMiniMapView.configView.height
  216. );
  217. canvasPoolManager.dispatch('setConfigView', [rect]);
  218. setLastInteraction('select');
  219. return;
  220. }
  221. setLastInteraction(null);
  222. },
  223. [
  224. flamegraphMiniMapCanvas,
  225. flamegraphMiniMapView,
  226. canvasPoolManager,
  227. lastDragVector,
  228. onMouseDrag,
  229. startDragVector,
  230. ]
  231. );
  232. const onMinimapScroll = useCallback(
  233. (evt: WheelEvent) => {
  234. if (!flamegraphMiniMapCanvas || !flamegraphMiniMapView) {
  235. return;
  236. }
  237. {
  238. const physicalDelta = vec2.fromValues(evt.deltaX * 0.8, evt.deltaY);
  239. const physicalToConfig = mat3.invert(
  240. mat3.create(),
  241. flamegraphMiniMapView.fromConfigView(flamegraphMiniMapCanvas.physicalSpace)
  242. );
  243. const [m00, m01, m02, m10, m11, m12] = physicalToConfig;
  244. const configDelta = vec2.transformMat3(vec2.create(), physicalDelta, [
  245. m00,
  246. m01,
  247. m02,
  248. m10,
  249. m11,
  250. m12,
  251. 0,
  252. 0,
  253. 0,
  254. ]);
  255. const translate = mat3.fromTranslation(mat3.create(), configDelta);
  256. canvasPoolManager.dispatch('transformConfigView', [translate]);
  257. }
  258. },
  259. [flamegraphMiniMapCanvas, flamegraphMiniMapView, canvasPoolManager]
  260. );
  261. const onMinimapZoom = useCallback(
  262. (evt: WheelEvent) => {
  263. if (!flamegraphMiniMapCanvas || !flamegraphMiniMapView) {
  264. return;
  265. }
  266. const identity = mat3.identity(mat3.create());
  267. const scale = 1 - evt.deltaY * 0.001 * -1; // -1 to invert scale
  268. const mouseInConfigSpace = flamegraphMiniMapView.getConfigSpaceCursor(
  269. vec2.fromValues(evt.offsetX, evt.offsetY),
  270. flamegraphMiniMapCanvas
  271. );
  272. const configCenter = vec2.fromValues(
  273. mouseInConfigSpace[0],
  274. flamegraphMiniMapView.configView.y
  275. );
  276. const invertedConfigCenter = vec2.multiply(
  277. vec2.create(),
  278. vec2.fromValues(-1, -1),
  279. configCenter
  280. );
  281. const translated = mat3.translate(mat3.create(), identity, configCenter);
  282. const scaled = mat3.scale(mat3.create(), translated, vec2.fromValues(scale, 1));
  283. const translatedBack = mat3.translate(mat3.create(), scaled, invertedConfigCenter);
  284. canvasPoolManager.dispatch('transformConfigView', [translatedBack]);
  285. },
  286. [flamegraphMiniMapCanvas, flamegraphMiniMapView, canvasPoolManager]
  287. );
  288. const onMinimapCanvasMouseDown = useCallback(
  289. evt => {
  290. if (
  291. !configSpaceCursor ||
  292. !flamegraphMiniMapCanvas ||
  293. !flamegraphMiniMapView ||
  294. !canvasPoolManager
  295. ) {
  296. return;
  297. }
  298. const logicalMousePos = vec2.fromValues(
  299. evt.nativeEvent.offsetX,
  300. evt.nativeEvent.offsetY
  301. );
  302. const physicalMousePos = vec2.scale(
  303. vec2.create(),
  304. logicalMousePos,
  305. window.devicePixelRatio
  306. );
  307. if (flamegraphMiniMapView.configView.contains(configSpaceCursor)) {
  308. setLastDragVector(physicalMousePos);
  309. } else {
  310. const startConfigSpaceCursor = flamegraphMiniMapView.getConfigSpaceCursor(
  311. vec2.fromValues(evt.nativeEvent.offsetX, evt.nativeEvent.offsetY),
  312. flamegraphMiniMapCanvas
  313. );
  314. setStartDragConfigSpaceCursor(startConfigSpaceCursor);
  315. }
  316. setLastInteraction('select');
  317. },
  318. [configSpaceCursor, flamegraphMiniMapCanvas, flamegraphMiniMapView, canvasPoolManager]
  319. );
  320. const onMinimapCanvasMouseUp = useCallback(() => {
  321. setConfigSpaceCursor(null);
  322. setStartDragConfigSpaceCursor(null);
  323. setLastDragVector(null);
  324. setLastInteraction(null);
  325. }, []);
  326. useEffect(() => {
  327. if (!flamegraphMiniMapCanvasRef) {
  328. return undefined;
  329. }
  330. let wheelStopTimeoutId: number | undefined;
  331. function onCanvasWheel(evt: WheelEvent) {
  332. window.clearTimeout(wheelStopTimeoutId);
  333. wheelStopTimeoutId = window.setTimeout(() => {
  334. setLastInteraction(null);
  335. }, 300);
  336. evt.preventDefault();
  337. // When we zoom, we want to clear cursor so that any tooltips
  338. // rendered on the flamegraph are removed from the view
  339. setConfigSpaceCursor(null);
  340. if (evt.metaKey || evt.ctrlKey) {
  341. onMinimapZoom(evt);
  342. setLastInteraction('zoom');
  343. } else {
  344. onMinimapScroll(evt);
  345. setLastInteraction('scroll');
  346. }
  347. }
  348. flamegraphMiniMapCanvasRef.addEventListener('wheel', onCanvasWheel);
  349. return () => {
  350. window.clearTimeout(wheelStopTimeoutId);
  351. flamegraphMiniMapCanvasRef.removeEventListener('wheel', onCanvasWheel);
  352. };
  353. }, [flamegraphMiniMapCanvasRef, onMinimapZoom, onMinimapScroll]);
  354. useEffect(() => {
  355. window.addEventListener('mouseup', onMinimapCanvasMouseUp);
  356. return () => {
  357. window.removeEventListener('mouseup', onMinimapCanvasMouseUp);
  358. };
  359. }, [onMinimapCanvasMouseUp]);
  360. useEffect(() => {
  361. if (!flamegraphMiniMapCanvasRef) {
  362. return undefined;
  363. }
  364. const onCanvasWheel = (evt: WheelEvent) => {
  365. evt.preventDefault();
  366. const isZoom = evt.metaKey;
  367. // @TODO figure out what key to use for other platforms
  368. if (isZoom) {
  369. onMinimapZoom(evt);
  370. } else {
  371. onMinimapScroll(evt);
  372. }
  373. };
  374. flamegraphMiniMapCanvasRef.addEventListener('wheel', onCanvasWheel);
  375. return () => flamegraphMiniMapCanvasRef?.removeEventListener('wheel', onCanvasWheel);
  376. }, [flamegraphMiniMapCanvasRef, onMinimapScroll, onMinimapZoom]);
  377. return (
  378. <Fragment>
  379. <Canvas
  380. ref={c => setFlamegraphMiniMapCanvasRef(c)}
  381. onMouseDown={onMinimapCanvasMouseDown}
  382. onMouseMove={onMinimapCanvasMouseMove}
  383. onMouseLeave={onMinimapCanvasMouseUp}
  384. cursor={
  385. configSpaceCursor &&
  386. flamegraphMiniMapView?.configView.contains(configSpaceCursor)
  387. ? 'grab'
  388. : 'col-resize'
  389. }
  390. />
  391. <OverlayCanvas ref={c => setFlamegraphMiniMapOverlayCanvasRef(c)} />
  392. </Fragment>
  393. );
  394. }
  395. const Canvas = styled('canvas')<{cursor?: React.CSSProperties['cursor']}>`
  396. width: 100%;
  397. height: 100%;
  398. position: absolute;
  399. left: 0;
  400. top: 0;
  401. cursor: ${props => props.cursor ?? 'default'};
  402. user-select: none;
  403. `;
  404. const OverlayCanvas = styled(Canvas)`
  405. pointer-events: none;
  406. `;
  407. export {FlamegraphZoomViewMinimap};