Browse Source

feat(profiling): calltree links and context menu (#58720)

Adds links to recent profiles and all profiles, adjusts the chart colors
and adds the before/after baseline indicators and enables context menu
toggle so users can visualize the call tree in top-down and bottom-up
view
Jonas 1 year ago
parent
commit
0c24434cd4

+ 1 - 0
static/app/components/profiling/flamegraph/aggregateFlamegraph.tsx

@@ -197,6 +197,7 @@ export function AggregateFlamegraph(props: AggregateFlamegraphProps): ReactEleme
     <FlamegraphZoomView
       disableGrid
       disableCallOrderSort
+      disableColorCoding
       canvasBounds={flamegraphCanvasBounds}
       canvasPoolManager={props.canvasPoolManager}
       flamegraph={flamegraph}

+ 56 - 0
static/app/components/profiling/flamegraph/aggregateFlamegraphTreeContextMenu.tsx

@@ -0,0 +1,56 @@
+import {Fragment, useCallback} from 'react';
+
+import {
+  ProfilingContextMenu,
+  ProfilingContextMenuGroup,
+  ProfilingContextMenuHeading,
+  ProfilingContextMenuItem,
+  ProfilingContextMenuLayer,
+} from 'sentry/components/profiling/profilingContextMenu';
+import {t} from 'sentry/locale';
+import {useContextMenu} from 'sentry/utils/profiling/hooks/useContextMenu';
+
+interface AggregateFlamegraphTreeContextMenuProps {
+  contextMenu: ReturnType<typeof useContextMenu>;
+  onBottomUpClick: (evt: React.MouseEvent<HTMLDivElement>) => void;
+  onTopDownClick: (evt: React.MouseEvent<HTMLDivElement>) => void;
+}
+
+export function AggregateFlamegraphTreeContextMenu(
+  props: AggregateFlamegraphTreeContextMenuProps
+) {
+  const closeContextMenu = useCallback(
+    () => props.contextMenu.setOpen(false),
+    [props.contextMenu]
+  );
+  return props.contextMenu.open ? (
+    <Fragment>
+      <ProfilingContextMenuLayer onClick={closeContextMenu} />
+      <ProfilingContextMenu
+        {...props.contextMenu.getMenuProps()}
+        style={{
+          position: 'absolute',
+          left: props.contextMenu.position?.left ?? -9999,
+          top: props.contextMenu.position?.top ?? -9999,
+          maxHeight: props.contextMenu.containerCoordinates?.height ?? 'auto',
+        }}
+      >
+        <ProfilingContextMenuGroup>
+          <ProfilingContextMenuHeading>{t('View')}</ProfilingContextMenuHeading>
+          <ProfilingContextMenuItem
+            {...props.contextMenu.getMenuItemProps()}
+            onClick={props.onBottomUpClick}
+          >
+            {t('Bottom Up')}
+          </ProfilingContextMenuItem>
+          <ProfilingContextMenuItem
+            {...props.contextMenu.getMenuItemProps()}
+            onClick={props.onTopDownClick}
+          >
+            {t('Top Down')}
+          </ProfilingContextMenuItem>
+        </ProfilingContextMenuGroup>
+      </ProfilingContextMenu>
+    </Fragment>
+  ) : null;
+}

+ 75 - 39
static/app/components/profiling/flamegraph/aggregateFlamegraphTreeTable.tsx

@@ -15,6 +15,7 @@ import {useDispatchFlamegraphState} from 'sentry/utils/profiling/flamegraph/hook
 import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
 import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
 import {formatColorForFrame} from 'sentry/utils/profiling/gl/utils';
+import {useContextMenu} from 'sentry/utils/profiling/hooks/useContextMenu';
 import {
   useVirtualizedTree,
   UseVirtualizedTreeProps,
@@ -23,9 +24,12 @@ import {VirtualizedTree} from 'sentry/utils/profiling/hooks/useVirtualizedTree/V
 import {VirtualizedTreeNode} from 'sentry/utils/profiling/hooks/useVirtualizedTree/VirtualizedTreeNode';
 import {VirtualizedTreeRenderedRow} from 'sentry/utils/profiling/hooks/useVirtualizedTree/virtualizedTreeUtils';
 import {invertCallTree} from 'sentry/utils/profiling/profile/utils';
+import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
 import {useFlamegraph} from 'sentry/views/profiling/flamegraphProvider';
 import {useProfileGroup} from 'sentry/views/profiling/profileGroupProvider';
 
+import {AggregateFlamegraphTreeContextMenu} from './aggregateFlamegraphTreeContextMenu';
+
 function computeRelativeWeight(base: number, value: number) {
   // Make sure we dont divide by zero
   if (!base || !value) {
@@ -63,6 +67,7 @@ interface FastFrameCallersRowsProps {
 interface FastFrameCallerRowProps {
   children: React.ReactNode;
   onClick: () => void;
+  onContextMenu: (e: React.MouseEvent) => void;
   onKeyDown: (event: React.KeyboardEvent) => void;
   onMouseEnter: () => void;
   tabIndex: number;
@@ -79,6 +84,7 @@ const FastFrameCallersRow = forwardRef<HTMLDivElement, FastFrameCallerRowProps>(
         onClick={props.onClick}
         onKeyDown={props.onKeyDown}
         onMouseEnter={props.onMouseEnter}
+        onContextMenu={props.onContextMenu}
       >
         {props.children}
       </div>
@@ -395,6 +401,11 @@ export function AggregateFlamegraphTreeTable({
   const theme = useFlamegraphTheme();
   const referenceNode = flamegraph.root;
 
+  const [treeView, setTreeView] = useLocalStorageState<'bottom up' | 'top down'>(
+    'profiling-aggregate-call-tree-view',
+    'bottom up'
+  );
+
   const rootNodes = useMemo(() => {
     return flamegraph.root.children;
   }, [flamegraph.root.children]);
@@ -409,10 +420,13 @@ export function AggregateFlamegraphTreeTable({
     }
 
     const maybeFilteredRoots =
-      frameFilter !== 'all' ? filterFlamegraphTree(rootNodes, skipFunction) : rootNodes;
+      frameFilter === 'all' ? rootNodes : filterFlamegraphTree(rootNodes, skipFunction);
 
+    if (treeView === 'top down') {
+      return maybeFilteredRoots;
+    }
     return invertCallTree(maybeFilteredRoots);
-  }, [frameFilter, rootNodes]);
+  }, [frameFilter, rootNodes, treeView]);
 
   const {colorMap} = useMemo(() => {
     return theme.COLORS.STACK_TO_COLOR(
@@ -461,6 +475,11 @@ export function AggregateFlamegraphTreeTable({
     return makeSortFunction(sort, direction);
   }, [sort, direction]);
 
+  const [tableParentContainer, setTableParentContainer] = useState<HTMLDivElement | null>(
+    null
+  );
+  const contextMenu = useContextMenu({container: tableParentContainer});
+
   const fixedRenderRow: UseVirtualizedTreeProps<FlamegraphFrame>['renderRow'] =
     useCallback(
       (
@@ -484,6 +503,7 @@ export function AggregateFlamegraphTreeTable({
             onKeyDown={handleRowKeyDown}
             onClick={handleRowClick}
             onMouseEnter={handleRowMouseEnter}
+            onContextMenu={contextMenu.handleContextMenu}
           >
             <FastFrameCallersFixedRows
               node={r.item}
@@ -496,7 +516,7 @@ export function AggregateFlamegraphTreeTable({
           </FastFrameCallersRow>
         );
       },
-      [referenceNode, flamegraph.formatter, getFrameColor]
+      [referenceNode, flamegraph.formatter, getFrameColor, contextMenu]
     );
 
   const dynamicRenderRow: UseVirtualizedTreeProps<FlamegraphFrame>['renderRow'] =
@@ -522,6 +542,7 @@ export function AggregateFlamegraphTreeTable({
             onKeyDown={handleRowKeyDown}
             onClick={handleRowClick}
             onMouseEnter={handleRowMouseEnter}
+            onContextMenu={contextMenu.handleContextMenu}
           >
             <FastFrameCallersDynamicRows
               node={r.item}
@@ -534,7 +555,7 @@ export function AggregateFlamegraphTreeTable({
           </FastFrameCallersRow>
         );
       },
-      [referenceNode, flamegraph.formatter, getFrameColor]
+      [referenceNode, flamegraph.formatter, getFrameColor, contextMenu]
     );
 
   // This is slighlty unfortunate and ugly, but because our two columns are sticky
@@ -712,55 +733,70 @@ export function AggregateFlamegraphTreeTable({
             </TableHeaderButton>
           </FrameNameCell>
         </FrameCallersTableHeader>
-        <FixedTableItemsContainer>
-          {/*
+        <AggregateFlamegraphTableContainer ref={setTableParentContainer}>
+          <AggregateFlamegraphTreeContextMenu
+            onBottomUpClick={() => setTreeView('bottom up')}
+            onTopDownClick={() => setTreeView('top down')}
+            contextMenu={contextMenu}
+          />
+          <FixedTableItemsContainer>
+            {/*
           The order of these two matters because we want clicked state to
           be on top of hover in cases where user is hovering a clicked row.
            */}
-          <div ref={setFixedScrollContainerRef} style={scrollContainerStyles}>
-            <div style={fixedContainerStyles}>
-              {renderItems.map(r => {
-                return fixedRenderRow(r, {
-                  handleRowClick: handleRowClick(r.key),
-                  handleRowMouseEnter: handleRowMouseEnter(r.key),
-                  handleExpandTreeNode,
-                  handleRowKeyDown,
-                  selectedNodeIndex,
-                });
-              })}
-              <div className={FastFrameCallersTableClassNames.GHOST_ROW_CONTAINER}>
-                <div className={FastFrameCallersTableClassNames.GHOST_ROW_CELL} />
-                <div className={FastFrameCallersTableClassNames.GHOST_ROW_CELL} />
+            <div ref={setFixedScrollContainerRef} style={scrollContainerStyles}>
+              <div style={fixedContainerStyles}>
+                {renderItems.map(r => {
+                  return fixedRenderRow(r, {
+                    handleRowClick: handleRowClick(r.key),
+                    handleRowMouseEnter: handleRowMouseEnter(r.key),
+                    handleExpandTreeNode,
+                    handleRowKeyDown,
+                    selectedNodeIndex,
+                  });
+                })}
+                <div className={FastFrameCallersTableClassNames.GHOST_ROW_CONTAINER}>
+                  <div className={FastFrameCallersTableClassNames.GHOST_ROW_CELL} />
+                  <div className={FastFrameCallersTableClassNames.GHOST_ROW_CELL} />
+                </div>
               </div>
             </div>
-          </div>
-        </FixedTableItemsContainer>
-        <DynamicTableItemsContainer>
-          {/*
+          </FixedTableItemsContainer>
+          <DynamicTableItemsContainer>
+            {/*
           The order of these two matters because we want clicked state to
           be on top of hover in cases where user is hovering a clicked row.
            */}
-          <div ref={setDynamicScrollContainerRef} style={scrollContainerStyles}>
-            <div style={fixedContainerStyles}>
-              {renderItems.map(r => {
-                return dynamicRenderRow(r, {
-                  handleRowClick: handleRowClick(r.key),
-                  handleRowMouseEnter: handleRowMouseEnter(r.key),
-                  handleExpandTreeNode,
-                  handleRowKeyDown,
-                  selectedNodeIndex,
-                });
-              })}
+            <div ref={setDynamicScrollContainerRef} style={scrollContainerStyles}>
+              <div style={fixedContainerStyles}>
+                {renderItems.map(r => {
+                  return dynamicRenderRow(r, {
+                    handleRowClick: handleRowClick(r.key),
+                    handleRowMouseEnter: handleRowMouseEnter(r.key),
+                    handleExpandTreeNode,
+                    handleRowKeyDown,
+                    selectedNodeIndex,
+                  });
+                })}
+              </div>
             </div>
-          </div>
-        </DynamicTableItemsContainer>
-        <div ref={hoveredGhostRowRef} style={{zIndex: 0}} />
-        <div ref={clickedGhostRowRef} style={{zIndex: 0}} />
+          </DynamicTableItemsContainer>
+          <div ref={hoveredGhostRowRef} style={{zIndex: 0}} />
+          <div ref={clickedGhostRowRef} style={{zIndex: 0}} />
+        </AggregateFlamegraphTableContainer>
       </FrameCallersTable>
     </FrameBar>
   );
 }
 
+const AggregateFlamegraphTableContainer = styled('div')`
+  position: absolute;
+  left: 0;
+  top: 0;
+  bottom: 0;
+  right: 0;
+`;
+
 const FRAME_WEIGHT_CELL_WIDTH_PX = 164;
 const FixedTableItemsContainer = styled('div')`
   position: absolute;

+ 18 - 15
static/app/components/profiling/flamegraph/flamegraphContextMenu.tsx

@@ -57,6 +57,7 @@ interface FlamegraphContextMenuProps {
   onHighlightAllOccurrencesClick: () => void;
   profileGroup: ProfileGroup | null;
   disableCallOrderSort?: boolean;
+  disableColorCoding?: boolean;
 }
 
 function isSupportedPlatformForGitHubLink(platform: string | undefined): boolean {
@@ -223,21 +224,23 @@ export function FlamegraphContextMenu(props: FlamegraphContextMenuProps) {
             )}
           </ProfilingContextMenuGroup>
         ) : null}
-        <ProfilingContextMenuGroup>
-          <ProfilingContextMenuHeading>{t('Color Coding')}</ProfilingContextMenuHeading>
-          {FLAMEGRAPH_COLOR_CODINGS.map((coding, idx) => (
-            <ProfilingContextMenuItemCheckbox
-              key={idx}
-              {...props.contextMenu.getMenuItemProps({
-                onClick: () => dispatch({type: 'set color coding', payload: coding}),
-              })}
-              onClick={() => dispatch({type: 'set color coding', payload: coding})}
-              checked={preferences.colorCoding === coding}
-            >
-              {coding}
-            </ProfilingContextMenuItemCheckbox>
-          ))}
-        </ProfilingContextMenuGroup>
+        {props.disableColorCoding ? null : (
+          <ProfilingContextMenuGroup>
+            <ProfilingContextMenuHeading>{t('Color Coding')}</ProfilingContextMenuHeading>
+            {FLAMEGRAPH_COLOR_CODINGS.map((coding, idx) => (
+              <ProfilingContextMenuItemCheckbox
+                key={idx}
+                {...props.contextMenu.getMenuItemProps({
+                  onClick: () => dispatch({type: 'set color coding', payload: coding}),
+                })}
+                onClick={() => dispatch({type: 'set color coding', payload: coding})}
+                checked={preferences.colorCoding === coding}
+              >
+                {coding}
+              </ProfilingContextMenuItemCheckbox>
+            ))}
+          </ProfilingContextMenuGroup>
+        )}
         <ProfilingContextMenuGroup>
           <ProfilingContextMenuHeading>{t('View')}</ProfilingContextMenuHeading>
           {FLAMEGRAPH_VIEW_OPTIONS.map((view, idx) => (

+ 1 - 0
static/app/components/profiling/flamegraph/flamegraphTooltip.tsx

@@ -44,6 +44,7 @@ export interface FlamegraphTooltipProps {
   flamegraphView: CanvasView<Flamegraph>;
   frame: FlamegraphFrame;
   platform: 'javascript' | 'python' | 'ios' | 'android' | string | undefined;
+  disableColorCoding?: boolean;
 }
 
 export function FlamegraphTooltip(props: FlamegraphTooltipProps) {

+ 3 - 0
static/app/components/profiling/flamegraph/flamegraphZoomView.tsx

@@ -74,6 +74,7 @@ interface FlamegraphZoomViewProps {
     React.SetStateAction<HTMLCanvasElement | null>
   >;
   disableCallOrderSort?: boolean;
+  disableColorCoding?: boolean;
   disableGrid?: boolean;
   disablePanX?: boolean;
   disableZoom?: boolean;
@@ -94,6 +95,7 @@ function FlamegraphZoomView({
   disableZoom = false,
   disableGrid = false,
   disableCallOrderSort = false,
+  disableColorCoding = false,
 }: FlamegraphZoomViewProps): React.ReactElement {
   const flamegraphTheme = useFlamegraphTheme();
   const profileGroup = useProfileGroup();
@@ -749,6 +751,7 @@ function FlamegraphZoomView({
         onCopyFunctionNameClick={handleCopyFunctionName}
         onHighlightAllOccurrencesClick={handleHighlightAllFramesClick}
         disableCallOrderSort={disableCallOrderSort}
+        disableColorCoding={disableColorCoding}
       />
       {flamegraphCanvas &&
       flamegraphRenderer &&

+ 38 - 5
static/app/views/profiling/profileSummary/index.tsx

@@ -12,6 +12,7 @@ import ErrorBoundary from 'sentry/components/errorBoundary';
 import SearchBar from 'sentry/components/events/searchBar';
 import IdBadge from 'sentry/components/idBadge';
 import * as Layout from 'sentry/components/layouts/thirds';
+import Link from 'sentry/components/links/link';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
 import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
@@ -51,6 +52,7 @@ import {useAggregateFlamegraphQuery} from 'sentry/utils/profiling/hooks/useAggre
 import {useCurrentProjectFromRouteParam} from 'sentry/utils/profiling/hooks/useCurrentProjectFromRouteParam';
 import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents';
 import {useProfileFilters} from 'sentry/utils/profiling/hooks/useProfileFilters';
+import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes';
 import {decodeScalar} from 'sentry/utils/queryString';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
@@ -91,7 +93,7 @@ const DEFAULT_FLAMEGRAPH_PREFERENCES: DeepPartial<FlamegraphState> = {
 };
 interface ProfileSummaryHeaderProps {
   location: Location;
-  onViewChange: (newVie: 'flamegraph' | 'profiles') => void;
+  onViewChange: (newView: 'flamegraph' | 'profiles') => void;
   organization: Organization;
   project: Project | null;
   query: string;
@@ -487,7 +489,7 @@ function ProfileSummaryPage(props: ProfileSummaryPageProps) {
               </ProfileVisualization>
               <ProfileDigestContainer>
                 <ProfileDigestScrollContainer>
-                  <ProfileDigest />
+                  <ProfileDigest onViewChange={onSetView} />
                   <MostRegressedProfileFunctions transaction={transaction} />
                   <SlowestProfileFunctions transaction={transaction} />
                 </ProfileDigestScrollContainer>
@@ -672,8 +674,15 @@ const PROFILE_DIGEST_FIELDS = [
 
 const percentiles = ['p75()', 'p95()', 'p99()'] as const;
 
-function ProfileDigest() {
+interface ProfileDigestProps {
+  onViewChange: (newView: 'flamegraph' | 'profiles') => void;
+}
+
+function ProfileDigest(props: ProfileDigestProps) {
   const location = useLocation();
+  const organization = useOrganization();
+  const project = useCurrentProjectFromRouteParam();
+
   const profilesCursor = useMemo(
     () => decodeScalar(location.query.cursor),
     [location.query.cursor]
@@ -686,9 +695,27 @@ function ProfileDigest() {
     sort: {key: 'last_seen()', order: 'desc'},
     referrer: 'api.profiling.profile-summary-table',
   });
-
   const data = profiles.data?.data?.[0];
 
+  const latestProfile = useProfileEvents<ProfilingFieldType>({
+    cursor: profilesCursor,
+    fields: ['profile.id', 'timestamp'],
+    query: '',
+    sort: {key: 'timestamp', order: 'desc'},
+    limit: 1,
+    referrer: 'api.profiling.profile-summary-table',
+  });
+  const profile = latestProfile.data?.data?.[0];
+
+  const flamegraphTarget =
+    project && profile
+      ? generateProfileFlamechartRoute({
+          orgSlug: organization.slug,
+          projectSlug: project.slug,
+          profileId: profile?.['profile.id'] as string,
+        })
+      : undefined;
+
   return (
     <ProfileDigestHeader>
       <div>
@@ -698,6 +725,10 @@ function ProfileDigest() {
             ''
           ) : profiles.isError ? (
             ''
+          ) : flamegraphTarget ? (
+            <Link to={flamegraphTarget}>
+              <DateTime date={new Date(data?.['last_seen()'] as string)} />
+            </Link>
           ) : (
             <DateTime date={new Date(data?.['last_seen()'] as string)} />
           )}
@@ -728,7 +759,9 @@ function ProfileDigest() {
           ) : profiles.isError ? (
             ''
           ) : (
-            <Count value={data?.['count()'] as number} />
+            <Link onClick={() => props.onViewChange('profiles')} to="">
+              <Count value={data?.['count()'] as number} />
+            </Link>
           )}
         </div>
       </ProfileDigestColumn>

+ 153 - 6
static/app/views/profiling/profileSummary/profilingSparklineChart.tsx

@@ -1,32 +1,179 @@
 import {useMemo} from 'react';
+import {type Theme, useTheme} from '@emotion/react';
 
-import {LineChart, LineChartProps} from 'sentry/components/charts/lineChart';
+import {
+  LineChart,
+  LineChartProps,
+  LineChartSeries,
+} from 'sentry/components/charts/lineChart';
+import {t} from 'sentry/locale';
+import {tooltipFormatter} from 'sentry/utils/discover/charts';
 import {makeFormatter} from 'sentry/utils/profiling/units/units';
 
 const durationFormatter = makeFormatter('nanoseconds', 0);
 
-function asSeries(seriesName: string, data: {timestamp: number; value: number}[]) {
+function asSeries(
+  seriesName: string,
+  color: string | undefined,
+  data: {timestamp: number; value: number}[]
+) {
   return {
     data: data.map(p => ({
       name: p.timestamp * 1e3,
-      value: p.value ?? 0,
+      value: p.value / 1e6 ?? 0,
     })),
+    color,
     seriesName,
   };
 }
 
-interface ProfilingSparklineChartProps {
+function getTooltipFormatter(label: string, baseline: number) {
+  return [
+    '<div class="tooltip-series tooltip-series-solo">',
+    '<div>',
+    `<span class="tooltip-label"><strong>${label}</strong></span>`,
+    tooltipFormatter(baseline / 1e6, 'duration'),
+    '</div>',
+    '</div>',
+    '<div class="tooltip-arrow"></div>',
+  ].join('');
+}
+
+interface BaseSparklineChartProps {
   name: string;
   points: {timestamp: number; value: number}[];
   chartProps?: Partial<LineChartProps>;
+  color?: string;
+}
+interface ProfilingSparklineChartPropsWithBreakpoint extends BaseSparklineChartProps {
+  aggregate_range_1: number;
+  aggregate_range_2: number;
+  breakpoint: number;
+  end: number;
+  start: number;
+}
+
+interface ProfilingSparklineChartPropsWithoutBreakpoint extends BaseSparklineChartProps {}
+
+function isBreakPointProps(
+  props: ProfilingSparklineChartProps
+): props is ProfilingSparklineChartPropsWithBreakpoint {
+  return typeof (props as any).breakpoint === 'number';
+}
+
+type ProfilingSparklineChartProps =
+  | ProfilingSparklineChartPropsWithBreakpoint
+  | ProfilingSparklineChartPropsWithoutBreakpoint;
+
+function makeSeriesBeforeAfterLines(
+  start: number,
+  breakpoint: number,
+  end: number,
+  aggregate_range_1: number,
+  aggregate_range_2: number,
+  theme: Theme
+): LineChartSeries[] {
+  const dividingLine = {
+    data: [],
+    color: theme.textColor,
+    seriesName: 'dividing line',
+    markLine: {},
+  };
+  dividingLine.markLine = {
+    data: [{xAxis: breakpoint * 1e3}],
+    label: {show: false},
+    lineStyle: {
+      color: theme.textColor,
+      type: 'solid',
+      width: 2,
+    },
+    symbol: ['none', 'none'],
+    tooltip: {
+      show: false,
+    },
+    silent: true,
+  };
+
+  const beforeLine = {
+    data: [],
+    color: theme.textColor,
+    seriesName: 'before line',
+    markLine: {},
+  };
+  beforeLine.markLine = {
+    data: [
+      [
+        {value: 'Past', coord: [start * 1e3, aggregate_range_1 / 1e6]},
+        {coord: [breakpoint * 1e3, aggregate_range_1 / 1e6]},
+      ],
+    ],
+    label: {
+      show: false,
+    },
+    lineStyle: {
+      color: theme.textColor,
+      type: 'dashed',
+      width: 1,
+    },
+    symbol: ['none', 'none'],
+    tooltip: {
+      formatter: getTooltipFormatter(t('Past Baseline'), aggregate_range_1),
+    },
+  };
+
+  const afterLine = {
+    data: [],
+    color: theme.textColor,
+    seriesName: 'after line',
+    markLine: {},
+  };
+  afterLine.markLine = {
+    data: [
+      [
+        {
+          value: 'Present',
+          coord: [breakpoint * 1e3, aggregate_range_2 / 1e6],
+        },
+        {coord: [end * 1e3, aggregate_range_2 / 1e6]},
+      ],
+    ],
+    label: {
+      show: false,
+    },
+    lineStyle: {
+      color: theme.textColor,
+      type: 'dashed',
+      width: 1,
+    },
+    symbol: ['none', 'none'],
+    tooltip: {
+      formatter: getTooltipFormatter(t('Present Baseline'), aggregate_range_2),
+    },
+  };
+  return [dividingLine, beforeLine, afterLine];
 }
 
 export function ProfilingSparklineChart(props: ProfilingSparklineChartProps) {
+  const theme = useTheme();
+
   const chartProps: LineChartProps = useMemo(() => {
+    const additionalSeries: LineChartSeries[] = [];
+    if (isBreakPointProps(props)) {
+      additionalSeries.push(
+        ...makeSeriesBeforeAfterLines(
+          props.start,
+          props.breakpoint,
+          props.end,
+          props.aggregate_range_1,
+          props.aggregate_range_2,
+          theme
+        )
+      );
+    }
     const baseProps: LineChartProps = {
       height: 26,
       width: 'auto',
-      series: [asSeries(props.name, props.points)],
+      series: [asSeries(props.name, props.color, props.points), ...additionalSeries],
       grid: [
         {
           containLabel: false,
@@ -64,7 +211,7 @@ export function ProfilingSparklineChart(props: ProfilingSparklineChartProps) {
     };
 
     return baseProps;
-  }, [props.points, props.name, props.chartProps]);
+  }, [props, theme]);
 
   return <LineChart {...chartProps} isGroupedByDate showTimeInTooltip />;
 }

+ 12 - 1
static/app/views/profiling/profileSummary/regressedProfileFunctions.tsx

@@ -1,5 +1,6 @@
 import {useCallback, useMemo, useState} from 'react';
 import {browserHistory} from 'react-router';
+import {useTheme} from '@emotion/react';
 import styled from '@emotion/styled';
 
 import type {SelectOption} from 'sentry/components/compactSelect';
@@ -115,6 +116,7 @@ export function MostRegressedProfileFunctions(props: MostRegressedProfileFunctio
   const organization = useOrganization();
   const project = useCurrentProjectFromRouteParam();
   const location = useLocation();
+  const theme = useTheme();
 
   const fnTrendCursor = useMemo(
     () => decodeScalar(location.query[REGRESSED_FUNCTIONS_CURSOR]),
@@ -271,7 +273,16 @@ export function MostRegressedProfileFunctions(props: MostRegressedProfileFunctio
                 </div>
               </RegressedFunctionMetricsRow>
               <RegressedFunctionSparklineContainer>
-                <ProfilingSparklineChart points={trendToPoints(fn)} name="" />
+                <ProfilingSparklineChart
+                  name=""
+                  points={trendToPoints(fn)}
+                  color={trendType === 'improvement' ? theme.green300 : theme.red300}
+                  aggregate_range_1={fn.aggregate_range_1}
+                  aggregate_range_2={fn.aggregate_range_2}
+                  breakpoint={fn.breakpoint}
+                  start={fn.stats.data[0][0]}
+                  end={fn.stats.data[fn.stats.data.length - 1][0]}
+                />
               </RegressedFunctionSparklineContainer>
             </RegressedFunctionRow>
           );