flamegraph.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. import {
  2. Fragment,
  3. ReactElement,
  4. useCallback,
  5. useEffect,
  6. useLayoutEffect,
  7. useMemo,
  8. useState,
  9. } from 'react';
  10. import {mat3, vec2} from 'gl-matrix';
  11. import {FlamegraphOptionsMenu} from 'sentry/components/profiling/flamegraphOptionsMenu';
  12. import {FlamegraphSearch} from 'sentry/components/profiling/flamegraphSearch';
  13. import {FlamegraphToolbar} from 'sentry/components/profiling/flamegraphToolbar';
  14. import {FlamegraphViewSelectMenu} from 'sentry/components/profiling/flamegraphViewSelectMenu';
  15. import {FlamegraphZoomView} from 'sentry/components/profiling/flamegraphZoomView';
  16. import {FlamegraphZoomViewMinimap} from 'sentry/components/profiling/flamegraphZoomViewMinimap';
  17. import {FrameStack} from 'sentry/components/profiling/FrameStack/frameStack';
  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 {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/useFlamegraphPreferences';
  26. import {useFlamegraphProfiles} from 'sentry/utils/profiling/flamegraph/useFlamegraphProfiles';
  27. import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
  28. import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
  29. import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
  30. import {FlamegraphView} from 'sentry/utils/profiling/flamegraphView';
  31. import {
  32. computeConfigViewWithStategy,
  33. formatColorForFrame,
  34. Rect,
  35. watchForResize,
  36. } from 'sentry/utils/profiling/gl/utils';
  37. import {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile';
  38. import {Profile} from 'sentry/utils/profiling/profile/profile';
  39. import {FlamegraphRenderer} from 'sentry/utils/profiling/renderers/flamegraphRenderer';
  40. import {useDevicePixelRatio} from 'sentry/utils/useDevicePixelRatio';
  41. import {useMemoWithPrevious} from 'sentry/utils/useMemoWithPrevious';
  42. import {FlamegraphWarnings} from './FlamegraphWarnings';
  43. import {ProfilingFlamechartLayout} from './profilingFlamechartLayout';
  44. function getTransactionConfigSpace(profiles: Profile[]): Rect {
  45. const startedAt = Math.min(...profiles.map(p => p.startedAt));
  46. const endedAt = Math.max(...profiles.map(p => p.endedAt));
  47. return new Rect(startedAt, 0, endedAt - startedAt, 0);
  48. }
  49. const noopFormatDuration = () => '';
  50. interface FlamegraphProps {
  51. onImport: ProfileDragDropImportProps['onImport'];
  52. profiles: ProfileGroup;
  53. }
  54. function Flamegraph(props: FlamegraphProps): ReactElement {
  55. const [canvasBounds, setCanvasBounds] = useState<Rect>(Rect.Empty());
  56. const devicePixelRatio = useDevicePixelRatio();
  57. const flamegraphTheme = useFlamegraphTheme();
  58. const [{sorting, view, xAxis}, dispatch] = useFlamegraphPreferences();
  59. const [{threadId, selectedRoot}, dispatchThreadId] = useFlamegraphProfiles();
  60. const [flamegraphCanvasRef, setFlamegraphCanvasRef] =
  61. useState<HTMLCanvasElement | null>(null);
  62. const [flamegraphOverlayCanvasRef, setFlamegraphOverlayCanvasRef] =
  63. useState<HTMLCanvasElement | null>(null);
  64. const [flamegraphMiniMapCanvasRef, setFlamegraphMiniMapCanvasRef] =
  65. useState<HTMLCanvasElement | null>(null);
  66. const [flamegraphMiniMapOverlayCanvasRef, setFlamegraphMiniMapOverlayCanvasRef] =
  67. useState<HTMLCanvasElement | null>(null);
  68. const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
  69. const scheduler = useMemo(() => new CanvasScheduler(), []);
  70. const flamegraph = useMemo(() => {
  71. if (typeof threadId !== 'number') {
  72. return FlamegraphModel.Empty();
  73. }
  74. // This could happen if threadId was initialized from query string, but for some
  75. // reason the profile was removed from the list of profiles.
  76. const profile = props.profiles.profiles.find(p => p.threadId === threadId);
  77. if (!profile) {
  78. return FlamegraphModel.Empty();
  79. }
  80. return new FlamegraphModel(profile, threadId, {
  81. inverted: view === 'bottom up',
  82. leftHeavy: sorting === 'left heavy',
  83. configSpace:
  84. xAxis === 'transaction'
  85. ? getTransactionConfigSpace(props.profiles.profiles)
  86. : undefined,
  87. });
  88. }, [props.profiles, sorting, threadId, view, xAxis]);
  89. const flamegraphCanvas = useMemo(() => {
  90. if (!flamegraphCanvasRef) {
  91. return null;
  92. }
  93. return new FlamegraphCanvas(
  94. flamegraphCanvasRef,
  95. vec2.fromValues(0, flamegraphTheme.SIZES.TIMELINE_HEIGHT * devicePixelRatio)
  96. );
  97. }, [devicePixelRatio, flamegraphCanvasRef, flamegraphTheme]);
  98. const flamegraphMiniMapCanvas = useMemo(() => {
  99. if (!flamegraphMiniMapCanvasRef) {
  100. return null;
  101. }
  102. return new FlamegraphCanvas(flamegraphMiniMapCanvasRef, vec2.fromValues(0, 0));
  103. }, [flamegraphMiniMapCanvasRef]);
  104. const flamegraphView = useMemoWithPrevious<FlamegraphView | null>(
  105. previousView => {
  106. if (!flamegraphCanvas) {
  107. return null;
  108. }
  109. const newView = new FlamegraphView({
  110. canvas: flamegraphCanvas,
  111. flamegraph,
  112. theme: flamegraphTheme,
  113. });
  114. // if the profile or the config space of the flamegraph has changed, we do not
  115. // want to persist the config view. This is to avoid a case where the new config space
  116. // is larger than the previous one, meaning the new view could now be zoomed in even
  117. // though the user did not fire any zoom events.
  118. if (
  119. previousView?.flamegraph.profile === newView.flamegraph.profile &&
  120. previousView.configSpace.equals(newView.configSpace)
  121. ) {
  122. // if we're still looking at the same profile but only a preference other than
  123. // left heavy has changed, we do want to persist the config view
  124. if (previousView.flamegraph.leftHeavy === newView.flamegraph.leftHeavy) {
  125. newView.setConfigView(
  126. previousView.configView.withHeight(newView.configView.height)
  127. );
  128. }
  129. }
  130. return newView;
  131. },
  132. [flamegraph, flamegraphCanvas, flamegraphTheme]
  133. );
  134. useEffect(() => {
  135. if (!flamegraphCanvas || !flamegraphView) {
  136. return undefined;
  137. }
  138. const onConfigViewChange = (rect: Rect) => {
  139. flamegraphView.setConfigView(rect);
  140. canvasPoolManager.draw();
  141. };
  142. const onTransformConfigView = (mat: mat3) => {
  143. flamegraphView.transformConfigView(mat);
  144. canvasPoolManager.draw();
  145. };
  146. const onResetZoom = () => {
  147. flamegraphView.resetConfigView(flamegraphCanvas);
  148. canvasPoolManager.draw();
  149. };
  150. const onZoomIntoFrame = (frame: FlamegraphFrame, strategy: 'min' | 'exact') => {
  151. const newConfigView = computeConfigViewWithStategy(
  152. strategy,
  153. flamegraphView.configView,
  154. new Rect(frame.start, frame.depth, frame.end - frame.start, 1)
  155. );
  156. flamegraphView.setConfigView(newConfigView);
  157. canvasPoolManager.draw();
  158. };
  159. scheduler.on('set config view', onConfigViewChange);
  160. scheduler.on('transform config view', onTransformConfigView);
  161. scheduler.on('reset zoom', onResetZoom);
  162. scheduler.on('zoom at frame', onZoomIntoFrame);
  163. return () => {
  164. scheduler.off('set config view', onConfigViewChange);
  165. scheduler.off('transform config view', onTransformConfigView);
  166. scheduler.off('reset zoom', onResetZoom);
  167. scheduler.off('zoom at frame', onZoomIntoFrame);
  168. };
  169. }, [canvasPoolManager, flamegraphCanvas, flamegraphView, scheduler]);
  170. useEffect(() => {
  171. canvasPoolManager.registerScheduler(scheduler);
  172. return () => canvasPoolManager.unregisterScheduler(scheduler);
  173. }, [canvasPoolManager, scheduler]);
  174. useLayoutEffect(() => {
  175. if (
  176. !flamegraphView ||
  177. !flamegraphCanvas ||
  178. !flamegraphMiniMapCanvas ||
  179. !flamegraphCanvasRef ||
  180. !flamegraphOverlayCanvasRef ||
  181. !flamegraphMiniMapCanvasRef ||
  182. !flamegraphMiniMapOverlayCanvasRef
  183. ) {
  184. return undefined;
  185. }
  186. const flamegraphObserver = watchForResize(
  187. [flamegraphCanvasRef, flamegraphOverlayCanvasRef],
  188. () => {
  189. const bounds = flamegraphCanvasRef.getBoundingClientRect();
  190. setCanvasBounds(new Rect(bounds.x, bounds.y, bounds.width, bounds.height));
  191. flamegraphCanvas.initPhysicalSpace();
  192. flamegraphView.resizeConfigSpace(flamegraphCanvas);
  193. canvasPoolManager.drawSync();
  194. }
  195. );
  196. const flamegraphMiniMapObserver = watchForResize(
  197. [flamegraphMiniMapCanvasRef, flamegraphMiniMapOverlayCanvasRef],
  198. () => {
  199. flamegraphMiniMapCanvas.initPhysicalSpace();
  200. canvasPoolManager.drawSync();
  201. }
  202. );
  203. return () => {
  204. flamegraphObserver.disconnect();
  205. flamegraphMiniMapObserver.disconnect();
  206. };
  207. }, [
  208. canvasPoolManager,
  209. flamegraphCanvas,
  210. flamegraphCanvasRef,
  211. flamegraphMiniMapCanvas,
  212. flamegraphMiniMapCanvasRef,
  213. flamegraphMiniMapOverlayCanvasRef,
  214. flamegraphOverlayCanvasRef,
  215. flamegraphView,
  216. ]);
  217. const flamegraphRenderer = useMemo(() => {
  218. if (!flamegraphCanvasRef) {
  219. return null;
  220. }
  221. return new FlamegraphRenderer(flamegraphCanvasRef, flamegraph, flamegraphTheme, {
  222. draw_border: true,
  223. });
  224. }, [flamegraph, flamegraphCanvasRef, flamegraphTheme]);
  225. const getFrameColor = useCallback(
  226. (frame: FlamegraphFrame) => {
  227. if (!flamegraphRenderer) {
  228. return '';
  229. }
  230. return formatColorForFrame(frame, flamegraphRenderer);
  231. },
  232. [flamegraphRenderer]
  233. );
  234. // referenceNode is passed down to the frameStack and is used to determine
  235. // the weights of each frame. In other words, in case there is no user selected root, then all
  236. // of the frame weights and timing are relative to the entire profile. If there is a user selected
  237. // root however, all weights are relative to that sub tree.
  238. const referenceNode = useMemo(
  239. () => (selectedRoot ? selectedRoot : flamegraph.root),
  240. [selectedRoot, flamegraph.root]
  241. );
  242. // In case a user selected root is present, we will display that root + it's entire sub tree.
  243. // If no root is selected, we will display the entire sub tree down from the root. We start at
  244. // root.children because flamegraph.root is a virtual node that we do not want to show in the table.
  245. const rootNodes = useMemo(() => {
  246. return selectedRoot ? [selectedRoot] : flamegraph.root.children;
  247. }, [selectedRoot, flamegraph.root]);
  248. return (
  249. <Fragment>
  250. <FlamegraphToolbar>
  251. <ThreadMenuSelector
  252. profileGroup={props.profiles}
  253. threadId={threadId}
  254. onThreadIdChange={newThreadId =>
  255. dispatchThreadId({type: 'set thread id', payload: newThreadId})
  256. }
  257. />
  258. <FlamegraphViewSelectMenu
  259. view={view}
  260. sorting={sorting}
  261. onSortingChange={s => {
  262. dispatch({type: 'set sorting', payload: s});
  263. }}
  264. onViewChange={v => {
  265. dispatch({type: 'set view', payload: v});
  266. }}
  267. />
  268. <FlamegraphSearch
  269. flamegraphs={[flamegraph]}
  270. canvasPoolManager={canvasPoolManager}
  271. />
  272. <FlamegraphOptionsMenu canvasPoolManager={canvasPoolManager} />
  273. </FlamegraphToolbar>
  274. <ProfilingFlamechartLayout
  275. minimap={
  276. <FlamegraphZoomViewMinimap
  277. canvasPoolManager={canvasPoolManager}
  278. flamegraph={flamegraph}
  279. flamegraphMiniMapCanvas={flamegraphMiniMapCanvas}
  280. flamegraphMiniMapCanvasRef={flamegraphMiniMapCanvasRef}
  281. flamegraphMiniMapOverlayCanvasRef={flamegraphMiniMapOverlayCanvasRef}
  282. flamegraphMiniMapView={flamegraphView}
  283. setFlamegraphMiniMapCanvasRef={setFlamegraphMiniMapCanvasRef}
  284. setFlamegraphMiniMapOverlayCanvasRef={setFlamegraphMiniMapOverlayCanvasRef}
  285. />
  286. }
  287. flamechart={
  288. <ProfileDragDropImport onImport={props.onImport}>
  289. <FlamegraphWarnings flamegraph={flamegraph} />
  290. <FlamegraphZoomView
  291. flamegraphRenderer={flamegraphRenderer}
  292. canvasBounds={canvasBounds}
  293. canvasPoolManager={canvasPoolManager}
  294. flamegraph={flamegraph}
  295. flamegraphCanvas={flamegraphCanvas}
  296. flamegraphCanvasRef={flamegraphCanvasRef}
  297. flamegraphOverlayCanvasRef={flamegraphOverlayCanvasRef}
  298. flamegraphView={flamegraphView}
  299. setFlamegraphCanvasRef={setFlamegraphCanvasRef}
  300. setFlamegraphOverlayCanvasRef={setFlamegraphOverlayCanvasRef}
  301. />
  302. </ProfileDragDropImport>
  303. }
  304. frameStack={
  305. <FrameStack
  306. referenceNode={referenceNode}
  307. rootNodes={rootNodes}
  308. getFrameColor={getFrameColor}
  309. formatDuration={flamegraph ? flamegraph.formatter : noopFormatDuration}
  310. canvasPoolManager={canvasPoolManager}
  311. />
  312. }
  313. />
  314. </Fragment>
  315. );
  316. }
  317. export {Flamegraph};