Browse Source

fix(profiling): style summary page header (#57142)

Jonas 1 year ago
parent
commit
446c84b75a

+ 2 - 2
static/app/components/profiling/aggregateFlamegraphPanel.tsx

@@ -3,7 +3,7 @@ import styled from '@emotion/styled';
 import EmptyStateWarning from 'sentry/components/emptyStateWarning';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import Panel from 'sentry/components/panels/panel';
-import {AggregateFlamegraph} from 'sentry/components/profiling/flamegraph/aggregateFlamegraph';
+import {DeprecatedAggregateFlamegraph} from 'sentry/components/profiling/flamegraph/deprecatedAggregateFlamegraph';
 import {Flex} from 'sentry/components/profiling/flex';
 import QuestionTooltip from 'sentry/components/questionTooltip';
 import {t} from 'sentry/locale';
@@ -68,7 +68,7 @@ export function AggregateFlamegraphPanel({transaction}: {transaction: string}) {
                     <p>{t(`Aggregate flamegraph isn't available for your query`)}</p>
                   </EmptyStateWarning>
                 ) : (
-                  <AggregateFlamegraph
+                  <DeprecatedAggregateFlamegraph
                     hideSystemFrames={hideSystemFrames}
                     setHideSystemFrames={setHideSystemFrames}
                   />

+ 35 - 149
static/app/components/profiling/flamegraph/aggregateFlamegraph.tsx

@@ -1,35 +1,15 @@
-import {
-  Fragment,
-  ReactElement,
-  useEffect,
-  useLayoutEffect,
-  useMemo,
-  useState,
-} from 'react';
-import styled from '@emotion/styled';
-import * as Sentry from '@sentry/react';
+import {ReactElement, useEffect, useLayoutEffect, useMemo, useState} from 'react';
 import {mat3, vec2} from 'gl-matrix';
 
-import {Button} from 'sentry/components/button';
 import {FlamegraphZoomView} from 'sentry/components/profiling/flamegraph/flamegraphZoomView';
-import {Flex} from 'sentry/components/profiling/flex';
-import SwitchButton from 'sentry/components/switchButton';
-import {t} from 'sentry/locale';
-import {space} from 'sentry/styles/space';
 import {defined} from 'sentry/utils';
-import {
-  CanvasPoolManager,
-  useCanvasScheduler,
-} from 'sentry/utils/profiling/canvasScheduler';
+import {CanvasPoolManager, CanvasScheduler} from 'sentry/utils/profiling/canvasScheduler';
 import {CanvasView} from 'sentry/utils/profiling/canvasView';
 import {Flamegraph as FlamegraphModel} from 'sentry/utils/profiling/flamegraph';
 import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphPreferences';
 import {useFlamegraphProfiles} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphProfiles';
 import {useDispatchFlamegraphState} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphState';
-import {
-  useFlamegraphTheme,
-  useMutateFlamegraphTheme,
-} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
+import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
 import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
 import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
 import {
@@ -39,70 +19,31 @@ import {
 import {FlamegraphRendererWebGL} from 'sentry/utils/profiling/renderers/flamegraphRendererWebGL';
 import {Rect} from 'sentry/utils/profiling/speedscope';
 import {useDevicePixelRatio} from 'sentry/utils/useDevicePixelRatio';
+import {useFlamegraph} from 'sentry/views/profiling/flamegraphProvider';
 import {useProfileGroup} from 'sentry/views/profiling/profileGroupProvider';
 
-const LOADING_OR_FALLBACK_FLAMEGRAPH = FlamegraphModel.Empty();
-
 interface AggregateFlamegraphProps {
-  hideSystemFrames: boolean;
-  setHideSystemFrames: (hideSystemFrames: boolean) => void;
-  hideToolbar?: boolean;
+  canvasPoolManager: CanvasPoolManager;
+  scheduler: CanvasScheduler;
 }
 
 export function AggregateFlamegraph(props: AggregateFlamegraphProps): ReactElement {
   const devicePixelRatio = useDevicePixelRatio();
   const dispatch = useDispatchFlamegraphState();
 
+  const flamegraph = useFlamegraph();
+
   const profileGroup = useProfileGroup();
 
   const flamegraphTheme = useFlamegraphTheme();
-  const setFlamegraphThemeMutation = useMutateFlamegraphTheme();
   const profiles = useFlamegraphProfiles();
-  const {colorCoding, sorting, view} = useFlamegraphPreferences();
-  const {threadId} = profiles;
+  const {colorCoding} = useFlamegraphPreferences();
 
   const [flamegraphCanvasRef, setFlamegraphCanvasRef] =
     useState<HTMLCanvasElement | null>(null);
   const [flamegraphOverlayCanvasRef, setFlamegraphOverlayCanvasRef] =
     useState<HTMLCanvasElement | null>(null);
 
-  const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
-  const scheduler = useCanvasScheduler(canvasPoolManager);
-
-  const profile = useMemo(() => {
-    return profileGroup.profiles.find(p => p.threadId === threadId);
-  }, [profileGroup, threadId]);
-
-  const flamegraph = useMemo(() => {
-    if (typeof threadId !== 'number') {
-      return LOADING_OR_FALLBACK_FLAMEGRAPH;
-    }
-
-    // This could happen if threadId was initialized from query string, but for some
-    // reason the profile was removed from the list of profiles.
-    if (!profile) {
-      return LOADING_OR_FALLBACK_FLAMEGRAPH;
-    }
-
-    const transaction = Sentry.startTransaction({
-      op: 'import',
-      name: 'flamegraph.constructor',
-    });
-
-    transaction.setTag('sorting', sorting.split(' ').join('_'));
-    transaction.setTag('view', view.split(' ').join('_'));
-
-    const newFlamegraph = new FlamegraphModel(profile, threadId, {
-      inverted: view === 'bottom up',
-      sort: sorting,
-      configSpace: undefined,
-    });
-
-    transaction.finish();
-
-    return newFlamegraph;
-  }, [profile, sorting, threadId, view]);
-
   const flamegraphCanvas = useMemo(() => {
     if (!flamegraphCanvasRef) {
       return null;
@@ -141,29 +82,6 @@ export function AggregateFlamegraph(props: AggregateFlamegraphProps): ReactEleme
     [flamegraph, flamegraphCanvas, flamegraphTheme]
   );
 
-  useEffect(() => {
-    const canvasHeight = flamegraphCanvas?.logicalSpace.height;
-    if (!canvasHeight) {
-      return;
-    }
-
-    setFlamegraphThemeMutation(theme => {
-      const flamegraphFitTo = canvasHeight / flamegraph.depth;
-      const minReadableRatio = 0.8; // this is quite small
-      const fitToRatio = flamegraphFitTo / theme.SIZES.BAR_HEIGHT;
-      const barHeightRatio = Math.min(Math.max(minReadableRatio, fitToRatio), 1.2);
-
-      // reduce the offset to leave just enough space for the toolbar
-      theme.SIZES.FLAMEGRAPH_DEPTH_OFFSET = 2.5;
-      theme.SIZES.BAR_HEIGHT = theme.SIZES.BAR_HEIGHT * barHeightRatio;
-      theme.SIZES.BAR_FONT_SIZE = theme.SIZES.BAR_FONT_SIZE * barHeightRatio;
-      return theme;
-    });
-
-    // We skip `flamegraphCanvas` as it causes an infinite loop
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [flamegraph, setFlamegraphThemeMutation, flamegraphCanvas?.logicalSpace.height]);
-
   // Uses a useLayoutEffect to ensure that these top level/global listeners are added before
   // any of the children components effects actually run. This way we do not lose events
   // when we register/unregister these top level listeners.
@@ -180,7 +98,7 @@ export function AggregateFlamegraph(props: AggregateFlamegraphProps): ReactEleme
         flamegraphView.setConfigView(rect.withHeight(flamegraphView.configView.height));
       }
 
-      canvasPoolManager.draw();
+      props.canvasPoolManager.draw();
     };
 
     const onTransformConfigView = (
@@ -191,13 +109,13 @@ export function AggregateFlamegraph(props: AggregateFlamegraphProps): ReactEleme
         flamegraphView.transformConfigView(mat);
       }
 
-      canvasPoolManager.draw();
+      props.canvasPoolManager.draw();
     };
 
     const onResetZoom = () => {
       flamegraphView.resetConfigView(flamegraphCanvas);
 
-      canvasPoolManager.draw();
+      props.canvasPoolManager.draw();
     };
 
     const onZoomIntoFrame = (frame: FlamegraphFrame, strategy: 'min' | 'exact') => {
@@ -209,21 +127,21 @@ export function AggregateFlamegraph(props: AggregateFlamegraphProps): ReactEleme
 
       flamegraphView.setConfigView(newConfigView);
 
-      canvasPoolManager.draw();
+      props.canvasPoolManager.draw();
     };
 
-    scheduler.on('set config view', onConfigViewChange);
-    scheduler.on('transform config view', onTransformConfigView);
-    scheduler.on('reset zoom', onResetZoom);
-    scheduler.on('zoom at frame', onZoomIntoFrame);
+    props.scheduler.on('set config view', onConfigViewChange);
+    props.scheduler.on('transform config view', onTransformConfigView);
+    props.scheduler.on('reset zoom', onResetZoom);
+    props.scheduler.on('zoom at frame', onZoomIntoFrame);
 
     return () => {
-      scheduler.off('set config view', onConfigViewChange);
-      scheduler.off('transform config view', onTransformConfigView);
-      scheduler.off('reset zoom', onResetZoom);
-      scheduler.off('zoom at frame', onZoomIntoFrame);
+      props.scheduler.off('set config view', onConfigViewChange);
+      props.scheduler.off('transform config view', onTransformConfigView);
+      props.scheduler.off('reset zoom', onResetZoom);
+      props.scheduler.off('zoom at frame', onZoomIntoFrame);
     };
-  }, [canvasPoolManager, flamegraphCanvas, flamegraphView, scheduler]);
+  }, [props.canvasPoolManager, flamegraphCanvas, flamegraphView, props.scheduler]);
 
   const flamegraphCanvases = useMemo(() => {
     return [flamegraphCanvasRef, flamegraphOverlayCanvasRef];
@@ -231,7 +149,7 @@ export function AggregateFlamegraph(props: AggregateFlamegraphProps): ReactEleme
 
   const flamegraphCanvasBounds = useResizeCanvasObserver(
     flamegraphCanvases,
-    canvasPoolManager,
+    props.canvasPoolManager,
     flamegraphCanvas,
     flamegraphView
   );
@@ -266,49 +184,17 @@ export function AggregateFlamegraph(props: AggregateFlamegraphProps): ReactEleme
   }, [profileGroup, profiles.threadId, dispatch]);
 
   return (
-    <Fragment>
-      <FlamegraphZoomView
-        canvasBounds={flamegraphCanvasBounds}
-        canvasPoolManager={canvasPoolManager}
-        flamegraph={flamegraph}
-        flamegraphRenderer={flamegraphRenderer}
-        flamegraphCanvas={flamegraphCanvas}
-        flamegraphCanvasRef={flamegraphCanvasRef}
-        flamegraphOverlayCanvasRef={flamegraphOverlayCanvasRef}
-        flamegraphView={flamegraphView}
-        setFlamegraphCanvasRef={setFlamegraphCanvasRef}
-        setFlamegraphOverlayCanvasRef={setFlamegraphOverlayCanvasRef}
-        disablePanX
-        disableZoom
-        disableGrid
-        disableCallOrderSort
-      />
-      {props.hideToolbar ? null : (
-        <AggregateFlamegraphToolbar>
-          <Flex justify="space-between" align="center">
-            <Button size="xs" onClick={() => scheduler.dispatch('reset zoom')}>
-              {t('Reset Zoom')}
-            </Button>
-            <Flex align="center" gap={space(1)}>
-              <span>{t('Hide System Frames')}</span>
-              <SwitchButton
-                toggle={() => props.setHideSystemFrames(!props.hideSystemFrames)}
-                isActive={props.hideSystemFrames}
-              />
-            </Flex>
-          </Flex>
-        </AggregateFlamegraphToolbar>
-      )}
-    </Fragment>
+    <FlamegraphZoomView
+      canvasBounds={flamegraphCanvasBounds}
+      canvasPoolManager={props.canvasPoolManager}
+      flamegraph={flamegraph}
+      flamegraphRenderer={flamegraphRenderer}
+      flamegraphCanvas={flamegraphCanvas}
+      flamegraphCanvasRef={flamegraphCanvasRef}
+      flamegraphOverlayCanvasRef={flamegraphOverlayCanvasRef}
+      flamegraphView={flamegraphView}
+      setFlamegraphCanvasRef={setFlamegraphCanvasRef}
+      setFlamegraphOverlayCanvasRef={setFlamegraphOverlayCanvasRef}
+    />
   );
 }
-
-const AggregateFlamegraphToolbar = styled('div')`
-  position: absolute;
-  left: 0;
-  top: 0;
-  padding: ${space(1)};
-  padding-left: ${space(1)};
-  background-color: rgba(255, 255, 255, 0.6);
-  width: 100%;
-`;

+ 326 - 0
static/app/components/profiling/flamegraph/deprecatedAggregateFlamegraph.tsx

@@ -0,0 +1,326 @@
+import React, {
+  Fragment,
+  ReactElement,
+  useEffect,
+  useLayoutEffect,
+  useMemo,
+  useState,
+} from 'react';
+import styled from '@emotion/styled';
+import * as Sentry from '@sentry/react';
+import {mat3, vec2} from 'gl-matrix';
+
+import {Button} from 'sentry/components/button';
+import {FlamegraphZoomView} from 'sentry/components/profiling/flamegraph/flamegraphZoomView';
+import {Flex} from 'sentry/components/profiling/flex';
+import SwitchButton from 'sentry/components/switchButton';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {defined} from 'sentry/utils';
+import {
+  CanvasPoolManager,
+  CanvasScheduler,
+  useCanvasScheduler,
+} from 'sentry/utils/profiling/canvasScheduler';
+import {CanvasView} from 'sentry/utils/profiling/canvasView';
+import {
+  Flamegraph,
+  Flamegraph as FlamegraphModel,
+} from 'sentry/utils/profiling/flamegraph';
+import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphPreferences';
+import {useFlamegraphProfiles} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphProfiles';
+import {useDispatchFlamegraphState} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphState';
+import {
+  useFlamegraphTheme,
+  useMutateFlamegraphTheme,
+} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
+import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
+import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
+import {
+  computeConfigViewWithStrategy,
+  useResizeCanvasObserver,
+} from 'sentry/utils/profiling/gl/utils';
+import {FlamegraphRendererWebGL} from 'sentry/utils/profiling/renderers/flamegraphRendererWebGL';
+import {Rect} from 'sentry/utils/profiling/speedscope';
+import {useDevicePixelRatio} from 'sentry/utils/useDevicePixelRatio';
+import {useProfileGroup} from 'sentry/views/profiling/profileGroupProvider';
+
+const LOADING_OR_FALLBACK_FLAMEGRAPH = FlamegraphModel.Empty();
+
+interface DeprecatedAggregateFlamegraphProps {
+  hideSystemFrames: boolean;
+  setHideSystemFrames: (hideSystemFrames: boolean) => void;
+  children?: (props: {
+    canvasPoolManager: CanvasPoolManager;
+    flamegraph: Flamegraph;
+    scheduler: CanvasScheduler;
+  }) => React.ReactNode;
+  hideToolbar?: boolean;
+}
+
+export function DeprecatedAggregateFlamegraph(
+  props: DeprecatedAggregateFlamegraphProps
+): ReactElement {
+  const devicePixelRatio = useDevicePixelRatio();
+  const dispatch = useDispatchFlamegraphState();
+
+  const profileGroup = useProfileGroup();
+
+  const flamegraphTheme = useFlamegraphTheme();
+  const setFlamegraphThemeMutation = useMutateFlamegraphTheme();
+  const profiles = useFlamegraphProfiles();
+  const {colorCoding, sorting, view} = useFlamegraphPreferences();
+  const {threadId} = profiles;
+
+  const [flamegraphCanvasRef, setFlamegraphCanvasRef] =
+    useState<HTMLCanvasElement | null>(null);
+  const [flamegraphOverlayCanvasRef, setFlamegraphOverlayCanvasRef] =
+    useState<HTMLCanvasElement | null>(null);
+
+  const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
+  const scheduler = useCanvasScheduler(canvasPoolManager);
+
+  const profile = useMemo(() => {
+    return profileGroup.profiles.find(p => p.threadId === threadId);
+  }, [profileGroup, threadId]);
+
+  const flamegraph = useMemo(() => {
+    if (typeof threadId !== 'number') {
+      return LOADING_OR_FALLBACK_FLAMEGRAPH;
+    }
+
+    // This could happen if threadId was initialized from query string, but for some
+    // reason the profile was removed from the list of profiles.
+    if (!profile) {
+      return LOADING_OR_FALLBACK_FLAMEGRAPH;
+    }
+
+    const transaction = Sentry.startTransaction({
+      op: 'import',
+      name: 'flamegraph.constructor',
+    });
+
+    transaction.setTag('sorting', sorting.split(' ').join('_'));
+    transaction.setTag('view', view.split(' ').join('_'));
+
+    const newFlamegraph = new FlamegraphModel(profile, threadId, {
+      inverted: view === 'bottom up',
+      sort: sorting,
+      configSpace: undefined,
+    });
+
+    transaction.finish();
+
+    return newFlamegraph;
+  }, [profile, sorting, threadId, view]);
+
+  const flamegraphCanvas = useMemo(() => {
+    if (!flamegraphCanvasRef) {
+      return null;
+    }
+    const yOrigin = flamegraphTheme.SIZES.TIMELINE_HEIGHT * devicePixelRatio;
+    return new FlamegraphCanvas(flamegraphCanvasRef, vec2.fromValues(0, yOrigin));
+  }, [devicePixelRatio, flamegraphCanvasRef, flamegraphTheme]);
+
+  const flamegraphView = useMemo<CanvasView<FlamegraphModel> | null>(
+    () => {
+      if (!flamegraphCanvas) {
+        return null;
+      }
+
+      const newView = new CanvasView({
+        canvas: flamegraphCanvas,
+        model: flamegraph,
+        options: {
+          inverted: flamegraph.inverted,
+          minWidth: flamegraph.profile.minFrameDuration,
+          barHeight: flamegraphTheme.SIZES.BAR_HEIGHT,
+          depthOffset: flamegraphTheme.SIZES.FLAMEGRAPH_DEPTH_OFFSET,
+          configSpaceTransform: undefined,
+        },
+      });
+
+      // Set to 3/4 of the view up, magic number... Would be best to comput some weighted visual score
+      // based on the number of frames and the depth of the frames, but lets see if we can make it work
+      // with this for now
+      newView.setConfigView(newView.configView.withY(newView.configView.height * 0.75));
+      return newView;
+    },
+
+    // We skip position.view dependency because it will go into an infinite loop
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    [flamegraph, flamegraphCanvas, flamegraphTheme]
+  );
+
+  useEffect(() => {
+    const canvasHeight = flamegraphCanvas?.logicalSpace.height;
+    if (!canvasHeight) {
+      return;
+    }
+
+    setFlamegraphThemeMutation(theme => {
+      const flamegraphFitTo = canvasHeight / flamegraph.depth;
+      const minReadableRatio = 0.8; // this is quite small
+      const fitToRatio = flamegraphFitTo / theme.SIZES.BAR_HEIGHT;
+      const barHeightRatio = Math.min(Math.max(minReadableRatio, fitToRatio), 1.2);
+
+      // reduce the offset to leave just enough space for the toolbar
+      theme.SIZES.FLAMEGRAPH_DEPTH_OFFSET = 2.5;
+      theme.SIZES.BAR_HEIGHT = theme.SIZES.BAR_HEIGHT * barHeightRatio;
+      theme.SIZES.BAR_FONT_SIZE = theme.SIZES.BAR_FONT_SIZE * barHeightRatio;
+      return theme;
+    });
+
+    // We skip `flamegraphCanvas` as it causes an infinite loop
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [flamegraph, setFlamegraphThemeMutation, flamegraphCanvas?.logicalSpace.height]);
+
+  // Uses a useLayoutEffect to ensure that these top level/global listeners are added before
+  // any of the children components effects actually run. This way we do not lose events
+  // when we register/unregister these top level listeners.
+  useLayoutEffect(() => {
+    if (!flamegraphCanvas || !flamegraphView) {
+      return undefined;
+    }
+
+    // This code below manages the synchronization of the config views between spans and flamegraph
+    // We do so by listening to the config view change event and then updating the other views accordingly which
+    // allows us to keep the X axis in sync between the two views but keep the Y axis independent
+    const onConfigViewChange = (rect: Rect, sourceConfigViewChange: CanvasView<any>) => {
+      if (sourceConfigViewChange === flamegraphView) {
+        flamegraphView.setConfigView(rect.withHeight(flamegraphView.configView.height));
+      }
+
+      canvasPoolManager.draw();
+    };
+
+    const onTransformConfigView = (
+      mat: mat3,
+      sourceTransformConfigView: CanvasView<any>
+    ) => {
+      if (sourceTransformConfigView === flamegraphView) {
+        flamegraphView.transformConfigView(mat);
+      }
+
+      canvasPoolManager.draw();
+    };
+
+    const onResetZoom = () => {
+      flamegraphView.resetConfigView(flamegraphCanvas);
+
+      canvasPoolManager.draw();
+    };
+
+    const onZoomIntoFrame = (frame: FlamegraphFrame, strategy: 'min' | 'exact') => {
+      const newConfigView = computeConfigViewWithStrategy(
+        strategy,
+        flamegraphView.configView,
+        new Rect(frame.start, frame.depth, frame.end - frame.start, 1)
+      ).transformRect(flamegraphView.configSpaceTransform);
+
+      flamegraphView.setConfigView(newConfigView);
+
+      canvasPoolManager.draw();
+    };
+
+    scheduler.on('set config view', onConfigViewChange);
+    scheduler.on('transform config view', onTransformConfigView);
+    scheduler.on('reset zoom', onResetZoom);
+    scheduler.on('zoom at frame', onZoomIntoFrame);
+
+    return () => {
+      scheduler.off('set config view', onConfigViewChange);
+      scheduler.off('transform config view', onTransformConfigView);
+      scheduler.off('reset zoom', onResetZoom);
+      scheduler.off('zoom at frame', onZoomIntoFrame);
+    };
+  }, [canvasPoolManager, flamegraphCanvas, flamegraphView, scheduler]);
+
+  const flamegraphCanvases = useMemo(() => {
+    return [flamegraphCanvasRef, flamegraphOverlayCanvasRef];
+  }, [flamegraphCanvasRef, flamegraphOverlayCanvasRef]);
+
+  const flamegraphCanvasBounds = useResizeCanvasObserver(
+    flamegraphCanvases,
+    canvasPoolManager,
+    flamegraphCanvas,
+    flamegraphView
+  );
+
+  const flamegraphRenderer = useMemo(() => {
+    if (!flamegraphCanvasRef) {
+      return null;
+    }
+
+    return new FlamegraphRendererWebGL(flamegraphCanvasRef, flamegraph, flamegraphTheme, {
+      colorCoding,
+      draw_border: true,
+    });
+  }, [colorCoding, flamegraph, flamegraphCanvasRef, flamegraphTheme]);
+
+  useEffect(() => {
+    if (defined(profiles.threadId)) {
+      return;
+    }
+    const threadID =
+      typeof profileGroup.activeProfileIndex === 'number'
+        ? profileGroup.profiles[profileGroup.activeProfileIndex]?.threadId
+        : null;
+    // fall back case, when we finally load the active profile index from the profile,
+    // make sure we update the thread id so that it is show first
+    if (defined(threadID)) {
+      dispatch({
+        type: 'set thread id',
+        payload: threadID,
+      });
+    }
+  }, [profileGroup, profiles.threadId, dispatch]);
+
+  return (
+    <Fragment>
+      {props.children ? props.children({canvasPoolManager, scheduler, flamegraph}) : null}
+      <FlamegraphZoomView
+        canvasBounds={flamegraphCanvasBounds}
+        canvasPoolManager={canvasPoolManager}
+        flamegraph={flamegraph}
+        flamegraphRenderer={flamegraphRenderer}
+        flamegraphCanvas={flamegraphCanvas}
+        flamegraphCanvasRef={flamegraphCanvasRef}
+        flamegraphOverlayCanvasRef={flamegraphOverlayCanvasRef}
+        flamegraphView={flamegraphView}
+        setFlamegraphCanvasRef={setFlamegraphCanvasRef}
+        setFlamegraphOverlayCanvasRef={setFlamegraphOverlayCanvasRef}
+        disablePanX
+        disableZoom
+        disableGrid
+        disableCallOrderSort
+      />
+      {props.hideToolbar ? null : (
+        <AggregateFlamegraphToolbar>
+          <Flex justify="space-between" align="center">
+            <Button size="xs" onClick={() => scheduler.dispatch('reset zoom')}>
+              {t('Reset Zoom')}
+            </Button>
+            <Flex align="center" gap={space(1)}>
+              <span>{t('Hide System Frames')}</span>
+              <SwitchButton
+                toggle={() => props.setHideSystemFrames(!props.hideSystemFrames)}
+                isActive={props.hideSystemFrames}
+              />
+            </Flex>
+          </Flex>
+        </AggregateFlamegraphToolbar>
+      )}
+    </Fragment>
+  );
+}
+
+const AggregateFlamegraphToolbar = styled('div')`
+  position: absolute;
+  left: 0;
+  top: 0;
+  padding: ${space(1)};
+  padding-left: ${space(1)};
+  background-color: rgba(255, 255, 255, 0.6);
+  width: 100%;
+`;

+ 69 - 0
static/app/views/profiling/flamegraphProvider.tsx

@@ -0,0 +1,69 @@
+import {createContext, useContext, useMemo} from 'react';
+import * as Sentry from '@sentry/react';
+
+import {Flamegraph} from 'sentry/utils/profiling/flamegraph';
+import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphPreferences';
+import {useFlamegraphProfiles} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphProfiles';
+import {useProfileGroup} from 'sentry/views/profiling/profileGroupProvider';
+
+const LOADING_OR_FALLBACK_FLAMEGRAPH = Flamegraph.Empty();
+
+const FlamegraphContext = createContext<Flamegraph | null>(null);
+
+export const useFlamegraph = () => {
+  const context = useContext(FlamegraphContext);
+  if (!context) {
+    throw new Error('useFlamegraph was called outside of FlamegraphProvider');
+  }
+  return context;
+};
+
+interface FlamegraphProviderProps {
+  children: React.ReactNode;
+}
+
+export function FlamegraphProvider(props: FlamegraphProviderProps) {
+  const profileGroup = useProfileGroup();
+  const {threadId} = useFlamegraphProfiles();
+  const {sorting, view} = useFlamegraphPreferences();
+
+  const activeProfile = useMemo(() => {
+    return profileGroup.profiles.find(p => p.threadId === threadId);
+  }, [profileGroup, threadId]);
+
+  const flamegraph = useMemo(() => {
+    if (typeof threadId !== 'number') {
+      return LOADING_OR_FALLBACK_FLAMEGRAPH;
+    }
+
+    // This could happen if threadId was initialized from query string, but for some
+    // reason the profile was removed from the list of profiles.
+    if (!activeProfile) {
+      return LOADING_OR_FALLBACK_FLAMEGRAPH;
+    }
+
+    const transaction = Sentry.startTransaction({
+      op: 'import',
+      name: 'flamegraph.constructor',
+    });
+
+    transaction.setTag('sorting', sorting.split(' ').join('_'));
+    transaction.setTag('view', view.split(' ').join('_'));
+
+    const newFlamegraph = new Flamegraph(activeProfile, threadId, {
+      inverted: view === 'bottom up',
+      sort: sorting,
+      configSpace: undefined,
+    });
+
+    transaction.finish();
+
+    return newFlamegraph;
+  }, [activeProfile, sorting, threadId, view]);
+
+  return (
+    <FlamegraphContext.Provider value={flamegraph}>
+      {props.children}
+    </FlamegraphContext.Provider>
+  );
+}

+ 35 - 17
static/app/views/profiling/landing/profilesSummaryChart.tsx

@@ -1,8 +1,11 @@
 import {useMemo} from 'react';
 import {useTheme} from '@emotion/react';
+import styled from '@emotion/styled';
 
 import ChartZoom from 'sentry/components/charts/chartZoom';
 import {LineChart, LineChartProps} from 'sentry/components/charts/lineChart';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
 import {PageFilters} from 'sentry/types';
 import {Series} from 'sentry/types/echarts';
 import {axisLabelFormatter, tooltipFormatter} from 'sentry/utils/discover/charts';
@@ -103,16 +106,16 @@ export function ProfilesSummaryChart({
       series,
       grid: [
         {
-          top: '32px',
-          left: '24px',
-          right: '52%',
+          top: '8px',
+          left: '16px',
+          right: '8px',
           bottom: '16px',
         },
         {
-          top: '32px',
-          left: hideCount ? '24px' : '52%',
-          right: '24px',
-          bottom: '16px',
+          top: '8px',
+          left: '8px',
+          right: '16px',
+          bottom: '8px',
         },
       ],
       legend: {
@@ -170,15 +173,30 @@ export function ProfilesSummaryChart({
   }, [hideCount, series, seriesOrder, theme.chartLabel]);
 
   return (
-    <ChartZoom router={router} {...selection?.datetime}>
-      {zoomRenderProps => (
-        <LineChart
-          {...chartProps}
-          isGroupedByDate
-          showTimeInTooltip
-          {...zoomRenderProps}
-        />
-      )}
-    </ChartZoom>
+    <ProfilesChartContainer>
+      <ProfilesChartTitle>{t('Profile durations')}</ProfilesChartTitle>
+      <ChartZoom router={router} {...selection?.datetime}>
+        {zoomRenderProps => (
+          <LineChart
+            {...chartProps}
+            isGroupedByDate
+            showTimeInTooltip
+            {...zoomRenderProps}
+          />
+        )}
+      </ChartZoom>
+    </ProfilesChartContainer>
   );
 }
+
+const ProfilesChartTitle = styled('div')`
+  font-size: ${p => p.theme.fontSizeSmall};
+  color: ${p => p.theme.textColor};
+  font-weight: 600;
+  padding: ${space(1)};
+`;
+
+const ProfilesChartContainer = styled('div')`
+  background-color: ${p => p.theme.background};
+  border-bottom: 1px solid ${p => p.theme.border};
+`;

+ 149 - 9
static/app/views/profiling/profileSummary/index.tsx

@@ -3,7 +3,9 @@ import {browserHistory} from 'react-router';
 import styled from '@emotion/styled';
 import type {Location} from 'history';
 
-import {LinkButton} from 'sentry/components/button';
+import {Button, LinkButton} from 'sentry/components/button';
+import {CompactSelect} from 'sentry/components/compactSelect';
+import type {SelectOption} from 'sentry/components/compactSelect/types';
 import DatePageFilter from 'sentry/components/datePageFilter';
 import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
 import ErrorBoundary from 'sentry/components/errorBoundary';
@@ -13,10 +15,12 @@ import * as Layout from 'sentry/components/layouts/thirds';
 import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
 import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
 import {AggregateFlamegraph} from 'sentry/components/profiling/flamegraph/aggregateFlamegraph';
+import {FlamegraphSearch} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphSearch';
 import {
   ProfilingBreadcrumbs,
   ProfilingBreadcrumbsProps,
 } from 'sentry/components/profiling/profilingBreadcrumbs';
+import {SegmentedControl} from 'sentry/components/segmentedControl';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
 import SmartSearchBar from 'sentry/components/smartSearchBar';
@@ -27,15 +31,26 @@ import type {Organization, PageFilters, Project} from 'sentry/types';
 import {defined} from 'sentry/utils';
 import EventView from 'sentry/utils/discover/eventView';
 import {isAggregateField} from 'sentry/utils/discover/fields';
+import {
+  CanvasPoolManager,
+  CanvasScheduler,
+  useCanvasScheduler,
+} from 'sentry/utils/profiling/canvasScheduler';
 import {FlamegraphStateProvider} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContextProvider';
 import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegraphThemeProvider';
+import {Frame} from 'sentry/utils/profiling/frame';
 import {useAggregateFlamegraphQuery} from 'sentry/utils/profiling/hooks/useAggregateFlamegraphQuery';
 import {useCurrentProjectFromRouteParam} from 'sentry/utils/profiling/hooks/useCurrentProjectFromRouteParam';
 import {useProfileFilters} from 'sentry/utils/profiling/hooks/useProfileFilters';
 import {decodeScalar} from 'sentry/utils/queryString';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
+import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
 import useOrganization from 'sentry/utils/useOrganization';
 import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
+import {
+  FlamegraphProvider,
+  useFlamegraph,
+} from 'sentry/views/profiling/flamegraphProvider';
 import {ProfilesSummaryChart} from 'sentry/views/profiling/landing/profilesSummaryChart';
 import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider';
 import {LegacySummaryPage} from 'sentry/views/profiling/profileSummary/legacySummaryPage';
@@ -186,9 +201,10 @@ function ProfileFilters(props: ProfileFiltersProps) {
 
 const ActionBar = styled('div')`
   display: grid;
-  gap: ${space(2)};
+  gap: ${space(1)};
   grid-template-columns: min-content auto;
-  margin-bottom: ${space(2)};
+  padding: ${space(1)} ${space(1)};
+  background-color: ${p => p.theme.background};
 `;
 
 interface ProfileSummaryPageProps {
@@ -256,6 +272,41 @@ function ProfileSummaryPage(props: ProfileSummaryPageProps) {
 
   const {data} = useAggregateFlamegraphQuery({transaction});
 
+  const [visualization, setVisualization] = useLocalStorageState<
+    'flamegraph' | 'call tree'
+  >('flamegraph-visualization', 'flamegraph');
+
+  const onVisualizationChange = useCallback(
+    (value: 'flamegraph' | 'call tree') => {
+      setVisualization(value);
+    },
+    [setVisualization]
+  );
+
+  const [frameFilter, setFrameFilter] = useLocalStorageState<
+    'system' | 'application' | 'all'
+  >('flamegraph-frame-filter', 'application');
+
+  const onFrameFilterChange = useCallback(
+    (value: 'system' | 'application' | 'all') => {
+      setFrameFilter(value);
+    },
+    [setFrameFilter]
+  );
+
+  const flamegraphFrameFilter: ((frame: Frame) => boolean) | undefined = useMemo(() => {
+    if (frameFilter === 'all') {
+      return () => true;
+    }
+    if (frameFilter === 'application') {
+      return frame => frame.is_application;
+    }
+    return frame => !frame.is_application;
+  }, [frameFilter]);
+
+  const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
+  const scheduler = useCanvasScheduler(canvasPoolManager);
+
   return (
     <SentryDocumentTitle
       title={t('Profiling \u2014 Profile Summary')}
@@ -299,7 +350,7 @@ function ProfileSummaryPage(props: ProfileSummaryPageProps) {
                 type="flamegraph"
                 input={data ?? null}
                 traceID=""
-                frameFilter={undefined}
+                frameFilter={flamegraphFrameFilter}
               >
                 <FlamegraphStateProvider
                   initialState={{
@@ -309,11 +360,24 @@ function ProfileSummaryPage(props: ProfileSummaryPageProps) {
                   }}
                 >
                   <FlamegraphThemeProvider>
-                    <AggregateFlamegraph
-                      hideToolbar
-                      hideSystemFrames={false}
-                      setHideSystemFrames={() => void 0}
-                    />
+                    <FlamegraphProvider>
+                      <AggregateFlamegraphToolbar
+                        scheduler={scheduler}
+                        canvasPoolManager={canvasPoolManager}
+                        visualization={visualization}
+                        onVisualizationChange={onVisualizationChange}
+                        frameFilter={frameFilter}
+                        onFrameFilterChange={onFrameFilterChange}
+                        hideSystemFrames={false}
+                        setHideSystemFrames={() => void 0}
+                      />
+                      {visualization === 'flamegraph' ? (
+                        <AggregateFlamegraph
+                          canvasPoolManager={canvasPoolManager}
+                          scheduler={scheduler}
+                        />
+                      ) : null}
+                    </FlamegraphProvider>
                   </FlamegraphThemeProvider>
                 </FlamegraphStateProvider>
               </ProfileGroupProvider>
@@ -329,6 +393,82 @@ function ProfileSummaryPage(props: ProfileSummaryPageProps) {
   );
 }
 
+interface AggregateFlamegraphToolbarProps {
+  canvasPoolManager: CanvasPoolManager;
+  frameFilter: 'system' | 'application' | 'all';
+  hideSystemFrames: boolean;
+  onFrameFilterChange: (value: 'system' | 'application' | 'all') => void;
+  onVisualizationChange: (value: 'flamegraph' | 'call tree') => void;
+  scheduler: CanvasScheduler;
+  setHideSystemFrames: (value: boolean) => void;
+  visualization: 'flamegraph' | 'call tree';
+}
+function AggregateFlamegraphToolbar(props: AggregateFlamegraphToolbarProps) {
+  const flamegraph = useFlamegraph();
+  const flamegraphs = useMemo(() => [flamegraph], [flamegraph]);
+  const spans = useMemo(() => [], []);
+
+  const frameSelectOptions: SelectOption<'system' | 'application' | 'all'>[] =
+    useMemo(() => {
+      return [
+        {value: 'system', label: t('System Frames')},
+        {value: 'application', label: t('Application Frames')},
+        {value: 'all', label: t('All Frames')},
+      ];
+    }, []);
+
+  const onResetZoom = useCallback(() => {
+    props.scheduler.dispatch('reset zoom');
+  }, [props.scheduler]);
+
+  const onFrameFilterChange = useCallback(
+    (value: {value: 'application' | 'system' | 'all'}) => {
+      props.onFrameFilterChange(value.value);
+    },
+    [props]
+  );
+
+  return (
+    <AggregateFlamegraphToolbarContainer>
+      <SegmentedControl
+        aria-label={t('View')}
+        size="xs"
+        value={props.visualization}
+        onChange={props.onVisualizationChange}
+      >
+        <SegmentedControl.Item key="flamegraph">{t('Flamegraph')}</SegmentedControl.Item>
+        <SegmentedControl.Item key="call tree">{t('Call Tree')}</SegmentedControl.Item>
+      </SegmentedControl>
+      <AggregateFlamegraphSearch
+        spans={spans}
+        canvasPoolManager={props.canvasPoolManager}
+        flamegraphs={flamegraphs}
+      />
+      <Button size="xs" onClick={onResetZoom}>
+        {t('Reset Zoom')}
+      </Button>
+      <CompactSelect
+        onChange={onFrameFilterChange}
+        value={props.frameFilter}
+        size="xs"
+        options={frameSelectOptions}
+      />
+    </AggregateFlamegraphToolbarContainer>
+  );
+}
+
+const AggregateFlamegraphToolbarContainer = styled('div')`
+  display: flex;
+  justify-content: space-between;
+  gap: ${space(1)};
+  padding: ${space(0.5)};
+  background: ${p => p.theme.background};
+`;
+
+const AggregateFlamegraphSearch = styled(FlamegraphSearch)`
+  max-width: 300px;
+`;
+
 const ProfileVisualization = styled('div')`
   grid-area: visualization;
 `;