differentialFlamegraph.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import {useCallback, useLayoutEffect, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import {mat3, vec2} from 'gl-matrix';
  5. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  6. import Feature from 'sentry/components/acl/feature';
  7. import {DifferentialFlamegraphLayout} from 'sentry/components/profiling/flamegraph/differentialFlamegraphLayout';
  8. import {DifferentialFlamegraphDrawer} from 'sentry/components/profiling/flamegraph/flamegraphDrawer/differentialFlamegraphDrawer';
  9. import {DifferentialFlamegraphToolbar} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/differentialFlamegraphToolbar';
  10. import {FlamegraphZoomView} from 'sentry/components/profiling/flamegraph/flamegraphZoomView';
  11. import {FlamegraphZoomViewMinimap} from 'sentry/components/profiling/flamegraph/flamegraphZoomViewMinimap';
  12. import {
  13. CanvasPoolManager,
  14. useCanvasScheduler,
  15. } from 'sentry/utils/profiling/canvasScheduler';
  16. import {CanvasView} from 'sentry/utils/profiling/canvasView';
  17. import {DifferentialFlamegraph as DifferentialFlamegraphModel} from 'sentry/utils/profiling/differentialFlamegraph';
  18. import {FlamegraphStateProvider} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContextProvider';
  19. import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegraphThemeProvider';
  20. import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphPreferences';
  21. import {useFlamegraphProfiles} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphProfiles';
  22. import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
  23. import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
  24. import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
  25. import {Frame} from 'sentry/utils/profiling/frame';
  26. import {
  27. computeConfigViewWithStrategy,
  28. formatColorForFrame,
  29. initializeFlamegraphRenderer,
  30. useResizeCanvasObserver,
  31. } from 'sentry/utils/profiling/gl/utils';
  32. import {useCurrentProjectFromRouteParam} from 'sentry/utils/profiling/hooks/useCurrentProjectFromRouteParam';
  33. import {useDifferentialFlamegraphModel} from 'sentry/utils/profiling/hooks/useDifferentialFlamegraphModel';
  34. import {useDifferentialFlamegraphQuery} from 'sentry/utils/profiling/hooks/useDifferentialFlamegraphQuery';
  35. import {FlamegraphRenderer2D} from 'sentry/utils/profiling/renderers/flamegraphRenderer2D';
  36. import {FlamegraphRendererWebGL} from 'sentry/utils/profiling/renderers/flamegraphRendererWebGL';
  37. import {Rect} from 'sentry/utils/profiling/speedscope';
  38. import {useLocation} from 'sentry/utils/useLocation';
  39. import usePageFilters from 'sentry/utils/usePageFilters';
  40. import {LOADING_PROFILE_GROUP} from 'sentry/views/profiling/profileGroupProvider';
  41. const noopFormatDuration = () => '';
  42. function applicationFrameOnly(frame: Frame): boolean {
  43. return frame.is_application;
  44. }
  45. function systemFrameOnly(frame: Frame): boolean {
  46. return !frame.is_application;
  47. }
  48. function DifferentialFlamegraphView() {
  49. const location = useLocation();
  50. const selection = usePageFilters();
  51. const flamegraphTheme = useFlamegraphTheme();
  52. const {colorCoding} = useFlamegraphPreferences();
  53. const {selectedRoot} = useFlamegraphProfiles();
  54. const [frameFilterSetting, setFrameFilterSetting] = useState<
  55. 'application' | 'system' | 'all'
  56. >('all');
  57. const frameFilter =
  58. frameFilterSetting === 'application'
  59. ? applicationFrameOnly
  60. : frameFilterSetting === 'system'
  61. ? systemFrameOnly
  62. : undefined;
  63. const project = useCurrentProjectFromRouteParam();
  64. const [negated, setNegated] = useState<boolean>(false);
  65. const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
  66. const scheduler = useCanvasScheduler(canvasPoolManager);
  67. const {before, after} = useDifferentialFlamegraphQuery({
  68. projectID: parseInt((project?.id as string) ?? 0, 10),
  69. breakpoint: location.query.breakpoint as unknown as number,
  70. environments: selection.selection.environments,
  71. fingerprint: location.query.fingerprint as unknown as string,
  72. transaction: location.query.transaction as unknown as string,
  73. });
  74. const differentialFlamegraph = useDifferentialFlamegraphModel({
  75. before,
  76. after,
  77. frameFilter,
  78. negated,
  79. });
  80. const [flamegraphCanvasRef, setFlamegraphCanvasRef] =
  81. useState<HTMLCanvasElement | null>(null);
  82. const [flamegraphOverlayCanvasRef, setFlamegraphOverlayCanvasRef] =
  83. useState<HTMLCanvasElement | null>(null);
  84. const [flamegraphMiniMapCanvasRef, setFlamegraphMiniMapCanvasRef] =
  85. useState<HTMLCanvasElement | null>(null);
  86. const [flamegraphMiniMapOverlayCanvasRef, setFlamegraphMiniMapOverlayCanvasRef] =
  87. useState<HTMLCanvasElement | null>(null);
  88. const flamegraphCanvas = useMemo(() => {
  89. if (!flamegraphCanvasRef) {
  90. return null;
  91. }
  92. return new FlamegraphCanvas(flamegraphCanvasRef, vec2.fromValues(0, 0));
  93. }, [flamegraphCanvasRef]);
  94. const flamegraphView = useMemo<CanvasView<DifferentialFlamegraphModel> | null>(
  95. () => {
  96. if (!flamegraphCanvas || !differentialFlamegraph.differentialFlamegraph) {
  97. return null;
  98. }
  99. const newView = new CanvasView({
  100. canvas: flamegraphCanvas,
  101. model: differentialFlamegraph.differentialFlamegraph,
  102. options: {
  103. inverted: differentialFlamegraph.differentialFlamegraph.inverted,
  104. minWidth:
  105. differentialFlamegraph.differentialFlamegraph.profile.minFrameDuration,
  106. barHeight: flamegraphTheme.SIZES.BAR_HEIGHT,
  107. depthOffset: flamegraphTheme.SIZES.AGGREGATE_FLAMEGRAPH_DEPTH_OFFSET,
  108. configSpaceTransform: undefined,
  109. },
  110. });
  111. // Find p75 of the graphtree depth and set the view to 3/4 of that
  112. const depths: number[] = [];
  113. for (const frame of differentialFlamegraph.differentialFlamegraph.frames) {
  114. if (frame.children.length > 0) {
  115. continue;
  116. }
  117. depths.push(frame.depth);
  118. }
  119. if (depths.length > 0) {
  120. depths.sort();
  121. const d = depths[Math.floor(depths.length - 1 * 0.75)];
  122. const depth = Math.max(d, 0);
  123. newView.setConfigView(
  124. newView.configView.withY(depth - (newView.configView.height * 3) / 4)
  125. );
  126. }
  127. return newView;
  128. },
  129. // We skip position.view dependency because it will go into an infinite loop
  130. // eslint-disable-next-line react-hooks/exhaustive-deps
  131. [differentialFlamegraph.differentialFlamegraph, flamegraphCanvas, flamegraphTheme]
  132. );
  133. // Uses a useLayoutEffect to ensure that these top level/global listeners are added before
  134. // any of the children components effects actually run. This way we do not lose events
  135. // when we register/unregister these top level listeners.
  136. useLayoutEffect(() => {
  137. if (!flamegraphCanvas || !flamegraphView) {
  138. return undefined;
  139. }
  140. // This code below manages the synchronization of the config views between spans and flamegraph
  141. // We do so by listening to the config view change event and then updating the other views accordingly which
  142. // allows us to keep the X axis in sync between the two views but keep the Y axis independent
  143. const onConfigViewChange = (rect: Rect, sourceConfigViewChange: CanvasView<any>) => {
  144. if (sourceConfigViewChange === flamegraphView) {
  145. flamegraphView.setConfigView(rect.withHeight(flamegraphView.configView.height));
  146. }
  147. canvasPoolManager.draw();
  148. };
  149. const onTransformConfigView = (
  150. mat: mat3,
  151. sourceTransformConfigView: CanvasView<any>
  152. ) => {
  153. if (sourceTransformConfigView === flamegraphView) {
  154. flamegraphView.transformConfigView(mat);
  155. }
  156. canvasPoolManager.draw();
  157. };
  158. const onResetZoom = () => {
  159. flamegraphView.resetConfigView(flamegraphCanvas);
  160. canvasPoolManager.draw();
  161. };
  162. const onZoomIntoFrame = (frame: FlamegraphFrame, strategy: 'min' | 'exact') => {
  163. const newConfigView = computeConfigViewWithStrategy(
  164. strategy,
  165. flamegraphView.configView,
  166. new Rect(frame.start, frame.depth, frame.end - frame.start, 1)
  167. ).transformRect(flamegraphView.configSpaceTransform);
  168. flamegraphView.setConfigView(newConfigView);
  169. canvasPoolManager.draw();
  170. };
  171. scheduler.on('set config view', onConfigViewChange);
  172. scheduler.on('transform config view', onTransformConfigView);
  173. scheduler.on('reset zoom', onResetZoom);
  174. scheduler.on('zoom at frame', onZoomIntoFrame);
  175. return () => {
  176. scheduler.off('set config view', onConfigViewChange);
  177. scheduler.off('transform config view', onTransformConfigView);
  178. scheduler.off('reset zoom', onResetZoom);
  179. scheduler.off('zoom at frame', onZoomIntoFrame);
  180. };
  181. }, [canvasPoolManager, flamegraphCanvas, flamegraphView, scheduler]);
  182. const flamegraphCanvases = useMemo(() => {
  183. return [flamegraphCanvasRef, flamegraphOverlayCanvasRef];
  184. }, [flamegraphCanvasRef, flamegraphOverlayCanvasRef]);
  185. const flamegraphCanvasBounds = useResizeCanvasObserver(
  186. flamegraphCanvases,
  187. canvasPoolManager,
  188. flamegraphCanvas,
  189. flamegraphView
  190. );
  191. const minimapCanvases = useMemo(() => {
  192. return [flamegraphMiniMapCanvasRef, flamegraphMiniMapOverlayCanvasRef];
  193. }, [flamegraphMiniMapCanvasRef, flamegraphMiniMapOverlayCanvasRef]);
  194. const flamegraphMiniMapCanvas = useMemo(() => {
  195. if (!flamegraphMiniMapCanvasRef) {
  196. return null;
  197. }
  198. return new FlamegraphCanvas(flamegraphMiniMapCanvasRef, vec2.fromValues(0, 0));
  199. }, [flamegraphMiniMapCanvasRef]);
  200. useResizeCanvasObserver(
  201. minimapCanvases,
  202. canvasPoolManager,
  203. flamegraphMiniMapCanvas,
  204. null
  205. );
  206. const flamegraphRenderer = useMemo(() => {
  207. if (!flamegraphCanvasRef || !differentialFlamegraph) {
  208. return null;
  209. }
  210. const renderer = initializeFlamegraphRenderer(
  211. [FlamegraphRendererWebGL, FlamegraphRenderer2D],
  212. [
  213. flamegraphCanvasRef,
  214. differentialFlamegraph.differentialFlamegraph,
  215. flamegraphTheme,
  216. {
  217. colorCoding,
  218. draw_border: true,
  219. },
  220. ]
  221. );
  222. if (renderer === null) {
  223. Sentry.captureException('Failed to initialize a flamegraph renderer');
  224. addErrorMessage('Failed to initialize renderer');
  225. return null;
  226. }
  227. return renderer;
  228. }, [colorCoding, differentialFlamegraph, flamegraphCanvasRef, flamegraphTheme]);
  229. const getFrameColor = useCallback(
  230. (frame: FlamegraphFrame) => {
  231. if (!flamegraphRenderer) {
  232. return '';
  233. }
  234. return formatColorForFrame(frame, flamegraphRenderer);
  235. },
  236. [flamegraphRenderer]
  237. );
  238. const rootNodes = useMemo(() => {
  239. return selectedRoot
  240. ? [selectedRoot]
  241. : differentialFlamegraph.differentialFlamegraph.root.children;
  242. }, [selectedRoot, differentialFlamegraph.differentialFlamegraph.root]);
  243. const referenceNode = useMemo(
  244. () =>
  245. selectedRoot ? selectedRoot : differentialFlamegraph.differentialFlamegraph.root,
  246. [selectedRoot, differentialFlamegraph.differentialFlamegraph.root]
  247. );
  248. return (
  249. <Feature features={['profiling-differential-flamegraph-page']}>
  250. <DifferentialFlamegraphContainer>
  251. <DifferentialFlamegraphToolbar
  252. frameFilter={frameFilterSetting}
  253. onFrameFilterChange={setFrameFilterSetting}
  254. negated={negated}
  255. onNegatedChange={setNegated}
  256. flamegraph={differentialFlamegraph.differentialFlamegraph}
  257. canvasPoolManager={canvasPoolManager}
  258. />
  259. <DifferentialFlamegraphLayout
  260. minimap={
  261. <FlamegraphZoomViewMinimap
  262. canvasPoolManager={canvasPoolManager}
  263. flamegraph={differentialFlamegraph.differentialFlamegraph}
  264. flamegraphMiniMapCanvas={flamegraphMiniMapCanvas}
  265. flamegraphMiniMapCanvasRef={flamegraphMiniMapCanvasRef}
  266. flamegraphMiniMapOverlayCanvasRef={flamegraphMiniMapOverlayCanvasRef}
  267. flamegraphMiniMapView={flamegraphView}
  268. setFlamegraphMiniMapCanvasRef={setFlamegraphMiniMapCanvasRef}
  269. setFlamegraphMiniMapOverlayCanvasRef={setFlamegraphMiniMapOverlayCanvasRef}
  270. />
  271. }
  272. flamegraph={
  273. <FlamegraphZoomView
  274. profileGroup={
  275. differentialFlamegraph.afterProfileGroup ?? LOADING_PROFILE_GROUP
  276. }
  277. disableGrid
  278. disableCallOrderSort
  279. disableColorCoding
  280. canvasBounds={flamegraphCanvasBounds}
  281. canvasPoolManager={canvasPoolManager}
  282. flamegraph={differentialFlamegraph.differentialFlamegraph}
  283. flamegraphRenderer={flamegraphRenderer}
  284. flamegraphCanvas={flamegraphCanvas}
  285. flamegraphCanvasRef={flamegraphCanvasRef}
  286. flamegraphOverlayCanvasRef={flamegraphOverlayCanvasRef}
  287. flamegraphView={flamegraphView}
  288. setFlamegraphCanvasRef={setFlamegraphCanvasRef}
  289. setFlamegraphOverlayCanvasRef={setFlamegraphOverlayCanvasRef}
  290. />
  291. }
  292. flamegraphDrawer={
  293. <DifferentialFlamegraphDrawer
  294. profileGroup={
  295. differentialFlamegraph.afterProfileGroup ?? LOADING_PROFILE_GROUP
  296. }
  297. getFrameColor={getFrameColor}
  298. referenceNode={referenceNode}
  299. rootNodes={rootNodes}
  300. flamegraph={differentialFlamegraph.differentialFlamegraph}
  301. formatDuration={
  302. differentialFlamegraph.differentialFlamegraph
  303. ? differentialFlamegraph.differentialFlamegraph.formatter
  304. : noopFormatDuration
  305. }
  306. canvasPoolManager={canvasPoolManager}
  307. canvasScheduler={scheduler}
  308. />
  309. }
  310. />
  311. </DifferentialFlamegraphContainer>
  312. </Feature>
  313. );
  314. }
  315. const DifferentialFlamegraphContainer = styled('div')`
  316. display: flex;
  317. flex-direction: column;
  318. flex: 1;
  319. ~ footer {
  320. display: none;
  321. }
  322. `;
  323. function DifferentialFlamegraphWithProviders() {
  324. return (
  325. <FlamegraphThemeProvider>
  326. <FlamegraphStateProvider
  327. initialState={{
  328. preferences: {
  329. sorting: 'alphabetical',
  330. view: 'top down',
  331. },
  332. }}
  333. >
  334. <DifferentialFlamegraphView />
  335. </FlamegraphStateProvider>
  336. </FlamegraphThemeProvider>
  337. );
  338. }
  339. export default DifferentialFlamegraphWithProviders;