flamegraph.tsx 13 KB

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