differentialFlamegraph.tsx 14 KB

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