Browse Source

feat(flamechart): allow rendering the chart on arbitrary x axis (#33577)

* ref(profiles): use weight of profiles as duration

* ref(import): check if transaction is present and add sampled profile

* ref(flamegraphRenderer): rename configToPhysicalSpace to configViewToPhysicalSpace

* ref(flamegraph): remove storing some unnecessary properties

* feat(flamegraph): allow passing transaction

* feat(flamegraph): allow using custom configSpace and translate chart

* feat(flamegraph): avoid reiterating frames and move configSpace to contructor

* feat(flamegraph): use transaction axis

* feat(flamegraph): add menu

* ref(flamegraph): pass configSpace as param and sync minimap effect

* fix(flamegraph): remove inverted check

* fix(flamegraph): remove inverted check
Jonas 2 years ago
parent
commit
cf63cf1b99

File diff suppressed because it is too large
+ 0 - 0
docs-ui/stories/components/profiling/EventedTrace.json


File diff suppressed because it is too large
+ 4405 - 0
docs-ui/stories/components/profiling/SampledTrace.json


+ 14 - 0
docs-ui/stories/components/profiling/flamegraphZoomView.stories.js

@@ -22,6 +22,20 @@ export const EventedTrace = () => {
   );
 };
 
+const sampledTrace = importProfile(require('./SampledTrace.json'));
+
+export const SampledTrace = () => {
+  return (
+    <FlamegraphStateProvider>
+      <FlamegraphThemeProvider>
+        <FullScreenFlamegraphContainer>
+          <Flamegraph profiles={sampledTrace} />
+        </FullScreenFlamegraphContainer>
+      </FlamegraphThemeProvider>
+    </FlamegraphStateProvider>
+  );
+};
+
 const jsSelfProfile = importProfile(require('./JSSelfProfilingTrace.json'));
 
 export const JSSelfProfiling = () => {

+ 3 - 3
static/app/components/profiling/boundTooltip.tsx

@@ -32,14 +32,14 @@ const useCachedMeasure = (string: string, font: string): Rect => {
 
 interface BoundTooltipProps {
   bounds: Rect;
-  configToPhysicalSpace: mat3;
+  configViewToPhysicalSpace: mat3;
   cursor: vec2 | null;
   children?: React.ReactNode;
 }
 
 function BoundTooltip({
   bounds,
-  configToPhysicalSpace,
+  configViewToPhysicalSpace,
   cursor,
   children,
 }: BoundTooltipProps): React.ReactElement | null {
@@ -88,7 +88,7 @@ function BoundTooltip({
     vec2.create(),
     vec2.fromValues(cursor[0], cursor[1]),
 
-    configToPhysicalSpace
+    configViewToPhysicalSpace
   );
 
   const logicalSpaceCursor = vec2.transformMat3(

+ 21 - 6
static/app/components/profiling/flamegraph.tsx

@@ -14,15 +14,21 @@ import {Flamegraph as FlamegraphModel} from 'sentry/utils/profiling/flamegraph';
 import {FlamegraphTheme} from 'sentry/utils/profiling/flamegraph/flamegraphTheme';
 import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/useFlamegraphPreferences';
 import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
+import {Rect} from 'sentry/utils/profiling/gl/utils';
 import {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile';
+import {Profile} from 'sentry/utils/profiling/profile/profile';
 
+function getTransactionConfigSpace(profiles: Profile[]): Rect {
+  return new Rect(0, 0, Math.max(...profiles.map(p => p.endedAt)), 0);
+}
 interface FlamegraphProps {
   profiles: ProfileGroup;
 }
 
 function Flamegraph(props: FlamegraphProps): ReactElement {
   const flamegraphTheme = useFlamegraphTheme();
-  const [{sorting, view}, dispatch] = useFlamegraphPreferences();
+  const [{sorting, view, synchronizeXAxisWithTransaction}, dispatch] =
+    useFlamegraphPreferences();
   const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
 
   const [activeProfileIndex, setActiveProfileIndex] = useState<number | null>(null);
@@ -35,11 +41,20 @@ function Flamegraph(props: FlamegraphProps): ReactElement {
     // if the activeProfileIndex is null, use the activeProfileIndex from the profile group
     const profileIndex = activeProfileIndex ?? profiles.activeProfileIndex;
 
-    return new FlamegraphModel(profiles.profiles[profileIndex], profileIndex, {
-      inverted: view === 'bottom up',
-      leftHeavy: sorting === 'left heavy',
-    });
-  }, [profiles, activeProfileIndex, sorting, view]);
+    const flamegraphModel = new FlamegraphModel(
+      profiles.profiles[profileIndex],
+      profileIndex,
+      {
+        inverted: view === 'bottom up',
+        leftHeavy: sorting === 'left heavy',
+        configSpace: synchronizeXAxisWithTransaction
+          ? getTransactionConfigSpace(profiles.profiles)
+          : undefined,
+      }
+    );
+
+    return flamegraphModel;
+  }, [profiles, activeProfileIndex, sorting, synchronizeXAxisWithTransaction, view]);
 
   const onImport = useCallback((profile: ProfileGroup) => {
     setActiveProfileIndex(null);

+ 60 - 0
static/app/components/profiling/flamegraphAxisOptionsMenu.tsx

@@ -0,0 +1,60 @@
+import styled from '@emotion/styled';
+
+import DropdownButton from 'sentry/components/dropdownButton';
+import DropdownControl, {DropdownItem} from 'sentry/components/dropdownControl';
+import {t} from 'sentry/locale';
+import space from 'sentry/styles/space';
+import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/useFlamegraphPreferences';
+
+function FlamegraphXAxisOptionsMenu(): React.ReactElement {
+  const [{synchronizeXAxisWithTransaction}, dispatch] = useFlamegraphPreferences();
+
+  return (
+    <OptionsMenuContainer>
+      <DropdownControl
+        button={({isOpen, getActorProps}) => (
+          <DropdownButton
+            {...getActorProps()}
+            isOpen={isOpen}
+            prefix={t('X Axis')}
+            size="xsmall"
+          >
+            {synchronizeXAxisWithTransaction ? 'Transaction' : 'Standalone'}
+          </DropdownButton>
+        )}
+      >
+        <DropdownItem
+          onSelect={() =>
+            dispatch({
+              type: 'set synchronizeXAxisWithTransaction',
+              payload: false,
+            })
+          }
+          isActive={!synchronizeXAxisWithTransaction}
+        >
+          Standalone
+        </DropdownItem>
+        <DropdownItem
+          onSelect={() =>
+            dispatch({
+              type: 'set synchronizeXAxisWithTransaction',
+              payload: true,
+            })
+          }
+          isActive={synchronizeXAxisWithTransaction}
+        >
+          Transaction
+        </DropdownItem>
+      </DropdownControl>
+    </OptionsMenuContainer>
+  );
+}
+
+const OptionsMenuContainer = styled('div')`
+  display: flex;
+  flex-direction: row;
+  gap: ${space(0.5)};
+  justify-content: flex-end;
+`;
+
+export {FlamegraphXAxisOptionsMenu};

+ 3 - 0
static/app/components/profiling/flamegraphOptionsMenu.tsx

@@ -9,6 +9,8 @@ import {CanvasPoolManager} from 'sentry/utils/profiling/canvasScheduler';
 import {FlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider';
 import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/useFlamegraphPreferences';
 
+import {FlamegraphXAxisOptionsMenu} from './flamegraphAxisOptionsMenu';
+
 interface FlamegraphOptionsMenuProps {
   canvasPoolManager: CanvasPoolManager;
 }
@@ -50,6 +52,7 @@ function FlamegraphOptionsMenu({
           )
         )}
       </DropdownControl>
+      <FlamegraphXAxisOptionsMenu />
       <Button size="xsmall" onClick={() => canvasPoolManager.dispatch('resetZoom', [])}>
         {t('Reset Zoom')}
       </Button>

+ 11 - 7
static/app/components/profiling/flamegraphZoomView.tsx

@@ -61,6 +61,10 @@ function FlamegraphZoomView({
           {draw_border: true}
         );
 
+        if (!previousRenderer?.configSpace.equals(renderer.configSpace)) {
+          return renderer;
+        }
+
         if (previousRenderer?.flamegraph.profile === renderer.flamegraph.profile) {
           if (previousRenderer.flamegraph.inverted !== renderer.flamegraph.inverted) {
             // Preserve the position where the user just was before they toggled
@@ -195,7 +199,7 @@ function FlamegraphZoomView({
             BORDER_WIDTH: flamegraphRenderer.theme.SIZES.FRAME_BORDER_WIDTH,
           },
           selectedFrameRenderer.context,
-          flamegraphRenderer.configToPhysicalSpace
+          flamegraphRenderer.configViewToPhysicalSpace
         );
       }
 
@@ -214,7 +218,7 @@ function FlamegraphZoomView({
             BORDER_WIDTH: flamegraphRenderer.theme.SIZES.HOVERED_FRAME_BORDER_WIDTH,
           },
           selectedFrameRenderer.context,
-          flamegraphRenderer.configToPhysicalSpace
+          flamegraphRenderer.configViewToPhysicalSpace
         );
       }
     };
@@ -223,7 +227,7 @@ function FlamegraphZoomView({
       textRenderer.draw(
         flamegraphRenderer.configView,
         flamegraphRenderer.configSpace,
-        flamegraphRenderer.configToPhysicalSpace
+        flamegraphRenderer.configViewToPhysicalSpace
       );
     };
 
@@ -231,7 +235,7 @@ function FlamegraphZoomView({
       gridRenderer.draw(
         flamegraphRenderer.configView,
         flamegraphRenderer.physicalSpace,
-        flamegraphRenderer.configToPhysicalSpace
+        flamegraphRenderer.configViewToPhysicalSpace
       );
     };
 
@@ -417,7 +421,7 @@ function FlamegraphZoomView({
 
       const physicalToConfig = mat3.invert(
         mat3.create(),
-        flamegraphRenderer.configToPhysicalSpace
+        flamegraphRenderer.configViewToPhysicalSpace
       );
       const [m00, m01, m02, m10, m11, m12] = physicalToConfig;
 
@@ -511,7 +515,7 @@ function FlamegraphZoomView({
       const physicalDelta = vec2.fromValues(evt.deltaX, evt.deltaY);
       const physicalToConfig = mat3.invert(
         mat3.create(),
-        flamegraphRenderer.configToPhysicalSpace
+        flamegraphRenderer.configViewToPhysicalSpace
       );
       const [m00, m01, m02, m10, m11, m12] = physicalToConfig;
 
@@ -580,7 +584,7 @@ function FlamegraphZoomView({
         <BoundTooltip
           bounds={canvasBounds}
           cursor={configSpaceCursor}
-          configToPhysicalSpace={flamegraphRenderer?.configToPhysicalSpace}
+          configViewToPhysicalSpace={flamegraphRenderer?.configViewToPhysicalSpace}
         >
           {hoveredNode?.frame?.name}
         </BoundTooltip>

+ 28 - 2
static/app/components/profiling/flamegraphZoomViewMinimap.tsx

@@ -50,8 +50,34 @@ function FlamegraphZoomViewMinimap({
         },
       });
 
-      if (previousRenderer?.flamegraph.name === renderer.flamegraph.name) {
-        renderer.setConfigView(previousRenderer.configView);
+      if (!previousRenderer?.configSpace.equals(renderer.configSpace)) {
+        return renderer;
+      }
+
+      if (previousRenderer?.flamegraph.profile === renderer.flamegraph.profile) {
+        if (previousRenderer.flamegraph.inverted !== renderer.flamegraph.inverted) {
+          // Preserve the position where the user just was before they toggled
+          // inverted. This means that the horizontal position is unchanged
+          // while the vertical position needs to determined based on the
+          // current position.
+          renderer.setConfigView(
+            previousRenderer.configView.translateY(
+              previousRenderer.configSpace.height -
+                previousRenderer.configView.height -
+                previousRenderer.configView.y
+            )
+          );
+        } else if (
+          previousRenderer.flamegraph.leftHeavy !== renderer.flamegraph.leftHeavy
+        ) {
+          /*
+           * When the user toggles left heavy, the entire flamegraph will take
+           * on a different shape. In this case, there's no obvious position
+           * that can be carried over.
+           */
+        } else {
+          renderer.setConfigView(previousRenderer.configView);
+        }
       }
 
       return renderer;

+ 1 - 0
static/app/types/profiling.d.ts

@@ -56,6 +56,7 @@ namespace Profiling {
   type Schema = {
     name: string;
     activeProfileIndex?: number;
+    duration_ns: number;
     profiles: ReadonlyArray<ProfileTypes>;
     shared: {
       frames: ReadonlyArray<Omit<FrameInfo, 'key'>>;

Some files were not shown because too many files changed in this diff