flamegraph.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import {
  2. Fragment,
  3. ReactElement,
  4. useEffect,
  5. useLayoutEffect,
  6. useMemo,
  7. useRef,
  8. useState,
  9. } from 'react';
  10. import styled from '@emotion/styled';
  11. import {mat3, vec2} from 'gl-matrix';
  12. import {FlamegraphOptionsMenu} from 'sentry/components/profiling/flamegraphOptionsMenu';
  13. import {FlamegraphSearch} from 'sentry/components/profiling/flamegraphSearch';
  14. import {FlamegraphToolbar} from 'sentry/components/profiling/flamegraphToolbar';
  15. import {FlamegraphViewSelectMenu} from 'sentry/components/profiling/flamegraphViewSelectMenu';
  16. import {FlamegraphZoomView} from 'sentry/components/profiling/flamegraphZoomView';
  17. import {FlamegraphZoomViewMinimap} from 'sentry/components/profiling/flamegraphZoomViewMinimap';
  18. import {
  19. ProfileDragDropImport,
  20. ProfileDragDropImportProps,
  21. } from 'sentry/components/profiling/profileDragDropImport';
  22. import {ThreadMenuSelector} from 'sentry/components/profiling/threadSelector';
  23. import {CanvasPoolManager, CanvasScheduler} from 'sentry/utils/profiling/canvasScheduler';
  24. import {Flamegraph as FlamegraphModel} from 'sentry/utils/profiling/flamegraph';
  25. import {FlamegraphTheme} from 'sentry/utils/profiling/flamegraph/flamegraphTheme';
  26. import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/useFlamegraphPreferences';
  27. import {useFlamegraphProfiles} from 'sentry/utils/profiling/flamegraph/useFlamegraphProfiles';
  28. import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
  29. import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
  30. import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
  31. import {FlamegraphView} from 'sentry/utils/profiling/flamegraphView';
  32. import {Rect, watchForResize} from 'sentry/utils/profiling/gl/utils';
  33. import {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile';
  34. import {Profile} from 'sentry/utils/profiling/profile/profile';
  35. import {useDevicePixelRatio} from 'sentry/utils/useDevicePixelRatio';
  36. import {useMemoWithPrevious} from 'sentry/utils/useMemoWithPrevious';
  37. function getTransactionConfigSpace(profiles: Profile[]): Rect {
  38. const startedAt = Math.min(...profiles.map(p => p.startedAt));
  39. const endedAt = Math.max(...profiles.map(p => p.endedAt));
  40. return new Rect(startedAt, 0, endedAt - startedAt, 0);
  41. }
  42. interface FlamegraphProps {
  43. onImport: ProfileDragDropImportProps['onImport'];
  44. profiles: ProfileGroup;
  45. }
  46. function Flamegraph(props: FlamegraphProps): ReactElement {
  47. const devicePixelRatio = useDevicePixelRatio();
  48. const flamegraphTheme = useFlamegraphTheme();
  49. const [{sorting, view, xAxis}, dispatch] = useFlamegraphPreferences();
  50. const [{threadId}, dispatchThreadId] = useFlamegraphProfiles();
  51. const canvasBounds = useRef<Rect>(Rect.Empty());
  52. const [flamegraphCanvasRef, setFlamegraphCanvasRef] =
  53. useState<HTMLCanvasElement | null>(null);
  54. const [flamegraphOverlayCanvasRef, setFlamegraphOverlayCanvasRef] =
  55. useState<HTMLCanvasElement | null>(null);
  56. const [flamegraphMiniMapCanvasRef, setFlamegraphMiniMapCanvasRef] =
  57. useState<HTMLCanvasElement | null>(null);
  58. const [flamegraphMiniMapOverlayCanvasRef, setFlamegraphMiniMapOverlayCanvasRef] =
  59. useState<HTMLCanvasElement | null>(null);
  60. const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
  61. const scheduler = useMemo(() => new CanvasScheduler(), []);
  62. const flamegraph = useMemo(() => {
  63. if (typeof threadId !== 'number') {
  64. return FlamegraphModel.Empty();
  65. }
  66. // This could happen if threadId was initialized from query string, but for some
  67. // reason the profile was removed from the list of profiles.
  68. const profile = props.profiles.profiles.find(p => p.threadId === threadId);
  69. if (!profile) {
  70. return FlamegraphModel.Empty();
  71. }
  72. return new FlamegraphModel(profile, threadId, {
  73. inverted: view === 'bottom up',
  74. leftHeavy: sorting === 'left heavy',
  75. configSpace:
  76. xAxis === 'transaction'
  77. ? getTransactionConfigSpace(props.profiles.profiles)
  78. : undefined,
  79. });
  80. }, [props.profiles, sorting, threadId, view, xAxis]);
  81. const flamegraphCanvas = useMemo(() => {
  82. if (!flamegraphCanvasRef) {
  83. return null;
  84. }
  85. return new FlamegraphCanvas(
  86. flamegraphCanvasRef,
  87. vec2.fromValues(0, flamegraphTheme.SIZES.TIMELINE_HEIGHT * devicePixelRatio)
  88. );
  89. }, [devicePixelRatio, flamegraphCanvasRef, flamegraphTheme]);
  90. const flamegraphMiniMapCanvas = useMemo(() => {
  91. if (!flamegraphMiniMapCanvasRef) {
  92. return null;
  93. }
  94. return new FlamegraphCanvas(flamegraphMiniMapCanvasRef, vec2.fromValues(0, 0));
  95. }, [flamegraphMiniMapCanvasRef]);
  96. const flamegraphView = useMemoWithPrevious<FlamegraphView | null>(
  97. previousView => {
  98. if (!flamegraphCanvas) {
  99. return null;
  100. }
  101. const newView = new FlamegraphView({
  102. canvas: flamegraphCanvas,
  103. flamegraph,
  104. theme: flamegraphTheme,
  105. });
  106. // if the profile or the config space of the flamegraph has changed, we do not
  107. // want to persist the config view. This is to avoid a case where the new config space
  108. // is larger than the previous one, meaning the new view could now be zoomed in even
  109. // though the user did not fire any zoom events.
  110. if (
  111. previousView?.flamegraph.profile === newView.flamegraph.profile &&
  112. previousView.configSpace.equals(newView.configSpace)
  113. ) {
  114. // if we're still looking at the same profile but only a preference other than
  115. // left heavy has changed, we do want to persist the config view
  116. if (previousView.flamegraph.leftHeavy === newView.flamegraph.leftHeavy) {
  117. newView.setConfigView(
  118. previousView.configView.withHeight(newView.configView.height)
  119. );
  120. }
  121. }
  122. return newView;
  123. },
  124. [flamegraph, flamegraphCanvas, flamegraphTheme]
  125. );
  126. useEffect(() => {
  127. if (!flamegraphCanvas || !flamegraphView) {
  128. return undefined;
  129. }
  130. const onConfigViewChange = (rect: Rect) => {
  131. flamegraphView.setConfigView(rect);
  132. canvasPoolManager.draw();
  133. };
  134. const onTransformConfigView = (mat: mat3) => {
  135. flamegraphView.transformConfigView(mat);
  136. canvasPoolManager.draw();
  137. };
  138. const onResetZoom = () => {
  139. flamegraphView.resetConfigView(flamegraphCanvas);
  140. canvasPoolManager.draw();
  141. };
  142. const onZoomIntoFrame = (frame: FlamegraphFrame) => {
  143. flamegraphView.setConfigView(
  144. new Rect(
  145. frame.start,
  146. frame.depth,
  147. frame.end - frame.start,
  148. flamegraphView.configView.height
  149. )
  150. );
  151. canvasPoolManager.draw();
  152. };
  153. scheduler.on('setConfigView', onConfigViewChange);
  154. scheduler.on('transformConfigView', onTransformConfigView);
  155. scheduler.on('resetZoom', onResetZoom);
  156. scheduler.on('zoomIntoFrame', onZoomIntoFrame);
  157. return () => {
  158. scheduler.off('setConfigView', onConfigViewChange);
  159. scheduler.off('transformConfigView', onTransformConfigView);
  160. scheduler.off('resetZoom', onResetZoom);
  161. scheduler.off('zoomIntoFrame', onZoomIntoFrame);
  162. };
  163. }, [canvasPoolManager, flamegraphCanvas, flamegraphView, scheduler]);
  164. useEffect(() => {
  165. canvasPoolManager.registerScheduler(scheduler);
  166. return () => canvasPoolManager.unregisterScheduler(scheduler);
  167. }, [canvasPoolManager, scheduler]);
  168. useLayoutEffect(() => {
  169. if (
  170. !flamegraphView ||
  171. !flamegraphCanvas ||
  172. !flamegraphMiniMapCanvas ||
  173. !flamegraphCanvasRef ||
  174. !flamegraphOverlayCanvasRef ||
  175. !flamegraphMiniMapCanvasRef ||
  176. !flamegraphMiniMapOverlayCanvasRef
  177. ) {
  178. return undefined;
  179. }
  180. const flamegraphObserver = watchForResize(
  181. [flamegraphCanvasRef, flamegraphOverlayCanvasRef],
  182. () => {
  183. const bounds = flamegraphCanvasRef.getBoundingClientRect();
  184. canvasBounds.current = new Rect(bounds.x, bounds.y, bounds.width, bounds.height);
  185. flamegraphCanvas.initPhysicalSpace();
  186. flamegraphView.resizeConfigSpace(flamegraphCanvas);
  187. canvasPoolManager.drawSync();
  188. }
  189. );
  190. const flamegraphMiniMapObserver = watchForResize(
  191. [flamegraphMiniMapCanvasRef, flamegraphMiniMapOverlayCanvasRef],
  192. () => {
  193. flamegraphMiniMapCanvas.initPhysicalSpace();
  194. canvasPoolManager.drawSync();
  195. }
  196. );
  197. return () => {
  198. flamegraphObserver.disconnect();
  199. flamegraphMiniMapObserver.disconnect();
  200. };
  201. }, [
  202. canvasPoolManager,
  203. flamegraphCanvas,
  204. flamegraphCanvasRef,
  205. flamegraphMiniMapCanvas,
  206. flamegraphMiniMapCanvasRef,
  207. flamegraphMiniMapOverlayCanvasRef,
  208. flamegraphOverlayCanvasRef,
  209. flamegraphView,
  210. ]);
  211. return (
  212. <Fragment>
  213. <FlamegraphToolbar>
  214. <ThreadMenuSelector
  215. profileGroup={props.profiles}
  216. threadId={threadId}
  217. onThreadIdChange={newThreadId =>
  218. dispatchThreadId({type: 'set thread id', payload: newThreadId})
  219. }
  220. />
  221. <FlamegraphViewSelectMenu
  222. view={view}
  223. sorting={sorting}
  224. onSortingChange={s => {
  225. dispatch({type: 'set sorting', payload: s});
  226. }}
  227. onViewChange={v => {
  228. dispatch({type: 'set view', payload: v});
  229. }}
  230. />
  231. <FlamegraphSearch
  232. flamegraphs={[flamegraph]}
  233. canvasPoolManager={canvasPoolManager}
  234. />
  235. <FlamegraphOptionsMenu canvasPoolManager={canvasPoolManager} />
  236. </FlamegraphToolbar>
  237. <FlamegraphZoomViewMinimapContainer height={flamegraphTheme.SIZES.MINIMAP_HEIGHT}>
  238. <FlamegraphZoomViewMinimap
  239. canvasPoolManager={canvasPoolManager}
  240. flamegraph={flamegraph}
  241. flamegraphMiniMapCanvas={flamegraphMiniMapCanvas}
  242. flamegraphMiniMapCanvasRef={flamegraphMiniMapCanvasRef}
  243. flamegraphMiniMapOverlayCanvasRef={flamegraphMiniMapOverlayCanvasRef}
  244. flamegraphMiniMapView={flamegraphView}
  245. setFlamegraphMiniMapCanvasRef={setFlamegraphMiniMapCanvasRef}
  246. setFlamegraphMiniMapOverlayCanvasRef={setFlamegraphMiniMapOverlayCanvasRef}
  247. />
  248. </FlamegraphZoomViewMinimapContainer>
  249. <FlamegraphZoomViewContainer>
  250. <ProfileDragDropImport onImport={props.onImport}>
  251. <FlamegraphZoomView
  252. canvasBounds={canvasBounds.current}
  253. canvasPoolManager={canvasPoolManager}
  254. flamegraph={flamegraph}
  255. flamegraphCanvas={flamegraphCanvas}
  256. flamegraphCanvasRef={flamegraphCanvasRef}
  257. flamegraphOverlayCanvasRef={flamegraphOverlayCanvasRef}
  258. flamegraphView={flamegraphView}
  259. setFlamegraphCanvasRef={setFlamegraphCanvasRef}
  260. setFlamegraphOverlayCanvasRef={setFlamegraphOverlayCanvasRef}
  261. />
  262. </ProfileDragDropImport>
  263. </FlamegraphZoomViewContainer>
  264. </Fragment>
  265. );
  266. }
  267. const FlamegraphZoomViewMinimapContainer = styled('div')<{
  268. height: FlamegraphTheme['SIZES']['MINIMAP_HEIGHT'];
  269. }>`
  270. position: relative;
  271. height: ${p => p.height}px;
  272. flex-shrink: 0;
  273. `;
  274. const FlamegraphZoomViewContainer = styled('div')`
  275. position: relative;
  276. display: flex;
  277. flex: 1 1 100%;
  278. `;
  279. export {Flamegraph};