Browse Source

feat(stats-detectors): Overlay throughput on chart for regression issues (#59208)

Instead of having 2 separate charts taking up 2x the vertical space,
overlay the throughput chart as with a lighter colour to make it less
prominent but still usable to identify if increase in throughput is the
cause.
Tony Xiao 1 year ago
parent
commit
b9a2b0a8fd

+ 48 - 7
static/app/components/events/eventStatisticalDetector/breakpointChart.tsx

@@ -1,4 +1,4 @@
-import {Fragment, useRef} from 'react';
+import {Fragment, useMemo, useRef} from 'react';
 import styled from '@emotion/styled';
 
 import {LinkButton} from 'sentry/components/button';
@@ -21,6 +21,7 @@ import {
   DisplayModes,
   transactionSummaryRouteWithQuery,
 } from 'sentry/views/performance/transactionSummary/utils';
+import {transformEventStats} from 'sentry/views/performance/trends/chart';
 import {
   NormalizedTrendsTransaction,
   TrendFunctionField,
@@ -83,7 +84,7 @@ function EventBreakpointChart({event}: EventBreakpointChartProps) {
       // Manually inject y-axis for events-stats because
       // getEventsAPIPayload doesn't pass it along
       ...eventView.getEventsAPIPayload(location),
-      yAxis: 'p95(transaction.duration)',
+      yAxis: ['p95(transaction.duration)', 'count()'],
     }),
   });
 
@@ -96,6 +97,49 @@ function EventBreakpointChart({event}: EventBreakpointChartProps) {
     display: DisplayModes.TREND,
   });
 
+  const p95Series = useMemo(
+    () =>
+      transformEventStats(
+        data?.['p95(transaction.duration)']?.data ?? [],
+        generateTrendFunctionAsString(TrendFunctionField.P95, 'transaction.duration')
+      ),
+    [data]
+  );
+
+  const throughputSeries = useMemo(() => {
+    const bucketSize = 12 * 60 * 60;
+
+    const bucketedData = (data?.['count()']?.data ?? []).reduce((acc, curr) => {
+      const timestamp = curr[0];
+      const bucket = Math.floor(timestamp / bucketSize) * bucketSize;
+      const prev = acc[acc.length - 1];
+      const value = curr[1][0].count;
+
+      if (prev?.bucket === bucket) {
+        prev.value += value;
+        prev.end = timestamp;
+        prev.count += 1;
+      } else {
+        acc.push({bucket, value, start: timestamp, end: timestamp, count: 1});
+      }
+
+      return acc;
+    }, []);
+
+    return transformEventStats(
+      bucketedData.map(item => [
+        item.bucket,
+        [
+          {
+            count:
+              item.value / (((item.end - item.start) / (item.count - 1)) * item.count),
+          },
+        ],
+      ]),
+      'throughput()'
+    )[0];
+  }, [data]);
+
   return (
     <DataSection>
       <TransitionChart loading={isLoading} reloading>
@@ -119,14 +163,11 @@ function EventBreakpointChart({event}: EventBreakpointChartProps) {
             </SummaryButtonWrapper>
           )}
           <Chart
-            statsData={data?.data ?? []}
+            percentileSeries={p95Series}
+            throughputSeries={throughputSeries}
             evidenceData={normalizedOccurrenceEvent}
             start={eventView.start}
             end={eventView.end}
-            chartLabel={generateTrendFunctionAsString(
-              TrendFunctionField.P95,
-              'transaction.duration'
-            )}
           />
         </Fragment>
       </TransitionChart>

+ 46 - 8
static/app/components/events/eventStatisticalDetector/functionBreakpointChart.tsx

@@ -7,6 +7,7 @@ import {Event} from 'sentry/types';
 import {defined} from 'sentry/utils';
 import {useProfileEventsStats} from 'sentry/utils/profiling/hooks/useProfileEventsStats';
 import {useRelativeDateTime} from 'sentry/utils/profiling/hooks/useRelativeDateTime';
+import {transformEventStats} from 'sentry/views/performance/trends/chart';
 import {NormalizedTrendsTransaction} from 'sentry/views/performance/trends/types';
 
 type EventFunctionBreakpointChartProps = {
@@ -52,7 +53,7 @@ type EventFunctionBreakpointChartInnerProps = {
   fingerprint: number;
 };
 
-const SERIES = 'p95()';
+const SERIES = ['p95()', 'count()'];
 
 function EventFunctionBreakpointChartInner({
   breakpoint,
@@ -69,17 +70,54 @@ function EventFunctionBreakpointChartInner({
     datetime,
     query: `fingerprint:${fingerprint}`,
     referrer: 'api.profiling.functions.regression.stats',
-    yAxes: [SERIES],
+    yAxes: SERIES,
   });
 
-  const series = useMemo(() => {
-    const rawData = functionStats?.data?.data?.find(({axis}) => axis === SERIES);
+  const p95Series = useMemo(() => {
+    const rawData = functionStats?.data?.data?.find(({axis}) => axis === 'p95()');
     const timestamps = functionStats?.data?.timestamps;
-    if (!rawData || !timestamps) {
+    if (!timestamps) {
       return [];
     }
+    return transformEventStats(
+      timestamps.map((timestamp, i) => [timestamp, [{count: rawData.values[i]}]]),
+      'p95()'
+    );
+  }, [functionStats]);
 
-    return timestamps.map((timestamp, i) => [timestamp, [{count: rawData.values[i]}]]);
+  const throughputSeries = useMemo(() => {
+    const rawData = functionStats?.data?.data?.find(({axis}) => axis === 'count()');
+    const timestamps = functionStats?.data?.timestamps ?? [];
+
+    const bucketSize = 12 * 60 * 60;
+
+    const bucketedData = timestamps.reduce((acc, timestamp, idx) => {
+      const bucket = Math.floor(timestamp / bucketSize) * bucketSize;
+      const prev = acc[acc.length - 1];
+      const value = rawData.values[idx];
+
+      if (prev?.bucket === bucket) {
+        prev.value += value;
+        prev.end = timestamp;
+        prev.count += 1;
+      } else {
+        acc.push({bucket, value, start: timestamp, end: timestamp, count: 1});
+      }
+      return acc;
+    }, []);
+
+    return transformEventStats(
+      bucketedData.map(data => [
+        data.bucket,
+        [
+          {
+            count:
+              data.value / (((data.end - data.start) / (data.count - 1)) * data.count),
+          },
+        ],
+      ]),
+      'throughput()'
+    )[0];
   }, [functionStats]);
 
   const normalizedOccurrenceEvent = {
@@ -91,11 +129,11 @@ function EventFunctionBreakpointChartInner({
   return (
     <DataSection>
       <Chart
-        statsData={series}
+        percentileSeries={p95Series}
+        throughputSeries={throughputSeries}
         evidenceData={normalizedOccurrenceEvent}
         start={(datetime.start as Date).toISOString()}
         end={(datetime.end as Date).toISOString()}
-        chartLabel={SERIES}
       />
     </DataSection>
   );

+ 115 - 79
static/app/components/events/eventStatisticalDetector/lineChart.tsx

@@ -1,113 +1,149 @@
+import {useMemo} from 'react';
 import {useTheme} from '@emotion/react';
 
+import BaseChart from 'sentry/components/charts/baseChart';
 import ChartZoom from 'sentry/components/charts/chartZoom';
-import VisualMap from 'sentry/components/charts/components/visualMap';
-import {
-  LineChart as EchartsLineChart,
-  LineChartProps,
-} from 'sentry/components/charts/lineChart';
-import {EventsStatsData} from 'sentry/types';
+import BarSeries from 'sentry/components/charts/series/barSeries';
+import LineSeries from 'sentry/components/charts/series/lineSeries';
+import {Series} from 'sentry/types/echarts';
 import {getUserTimezone} from 'sentry/utils/dates';
 import {
   axisLabelFormatter,
   getDurationUnit,
   tooltipFormatter,
 } from 'sentry/utils/discover/charts';
-import {aggregateOutputType} from 'sentry/utils/discover/fields';
+import {aggregateOutputType, RateUnits} from 'sentry/utils/discover/fields';
 import useRouter from 'sentry/utils/useRouter';
-import {transformEventStats} from 'sentry/views/performance/trends/chart';
 import {NormalizedTrendsTransaction} from 'sentry/views/performance/trends/types';
 import {getIntervalLine} from 'sentry/views/performance/utils';
 
 interface ChartProps {
-  chartLabel: string;
   end: string;
   evidenceData: NormalizedTrendsTransaction;
+  percentileSeries: Series[];
   start: string;
-  statsData: EventsStatsData;
+  throughputSeries: Series;
 }
 
-function LineChart({statsData, evidenceData, start, end, chartLabel}: ChartProps) {
+function LineChart({
+  percentileSeries,
+  throughputSeries,
+  evidenceData,
+  start,
+  end,
+}: ChartProps) {
   const theme = useTheme();
   const router = useRouter();
 
-  const resultSeries = transformEventStats(statsData, chartLabel);
+  const leftSeries = useMemo(() => {
+    const needsLabel = true;
+    const intervalSeries = getIntervalLine(
+      theme,
+      percentileSeries || [],
+      0.5,
+      needsLabel,
+      evidenceData,
+      true
+    );
+    return [
+      ...percentileSeries,
+      ...intervalSeries.filter(s => !s.markArea), // get rid of the shading
+    ];
+  }, [percentileSeries, evidenceData, theme]);
 
-  const needsLabel = true;
-  const intervalSeries = getIntervalLine(
-    theme,
-    resultSeries || [],
-    0.5,
-    needsLabel,
-    evidenceData,
-    true
-  );
+  const rightSeries = useMemo(() => [throughputSeries], [throughputSeries]);
+
+  const series = useMemo(() => {
+    return [
+      ...rightSeries.map(({seriesName, data, ...options}) =>
+        BarSeries({
+          ...options,
+          name: seriesName,
+          data: data?.map(({value, name, itemStyle}) => {
+            if (itemStyle === undefined) {
+              return [name, value];
+            }
+            return {value: [name, value], itemStyle};
+          }),
+          animation: false,
+          animationThreshold: 1,
+          animationDuration: 0,
+          yAxisIndex: 1,
+        })
+      ),
+      ...leftSeries.map(({seriesName, data, ...options}) =>
+        LineSeries({
+          ...options,
+          name: seriesName,
+          data: data?.map(({value, name}) => [name, value]),
+          animation: false,
+          animationThreshold: 1,
+          animationDuration: 0,
+          showSymbol: false,
+          yAxisIndex: 0,
+        })
+      ),
+    ];
+  }, [leftSeries, rightSeries]);
 
-  const series = [...resultSeries, ...intervalSeries];
+  const chartOptions: Omit<
+    React.ComponentProps<typeof BaseChart>,
+    'series'
+  > = useMemo(() => {
+    const legend = {
+      right: 16,
+      top: 12,
+      data: [...percentileSeries.map(s => s.seriesName), throughputSeries.seriesName],
+    };
+
+    const durationUnit = getDurationUnit(leftSeries);
+
+    const yAxes: React.ComponentProps<typeof BaseChart>['yAxes'] = [
+      {
+        minInterval: durationUnit,
+        axisLabel: {
+          color: theme.chartLabel,
+          formatter: (value: number) =>
+            axisLabelFormatter(value, 'duration', undefined, durationUnit),
+        },
+      },
+    ];
 
-  const durationUnit = getDurationUnit(series);
+    if (rightSeries.length) {
+      yAxes.push({
+        axisLabel: {
+          color: theme.chartLabel,
+          formatter: (value: number) =>
+            axisLabelFormatter(value, 'rate', true, undefined, RateUnits.PER_SECOND),
+        },
+      });
+    }
 
-  const chartOptions: Omit<LineChartProps, 'series'> = {
-    tooltip: {
-      valueFormatter: (value, seriesName) => {
-        return tooltipFormatter(value, aggregateOutputType(seriesName));
+    return {
+      colors: [theme.gray200, theme.gray500],
+      grid: {
+        left: '10px',
+        right: '10px',
+        top: '40px',
+        bottom: '0px',
       },
-    },
-    yAxis: {
-      minInterval: durationUnit,
-      axisLabel: {
-        color: theme.chartLabel,
-        formatter: (value: number) =>
-          axisLabelFormatter(value, 'duration', undefined, durationUnit),
+      legend,
+      toolBox: {show: false},
+      tooltip: {
+        valueFormatter: (value, seriesName) => {
+          return tooltipFormatter(value, aggregateOutputType(seriesName));
+        },
       },
-    },
-  };
+      xAxis: {type: 'time'},
+      yAxes,
+    };
+  }, [theme, leftSeries, rightSeries, percentileSeries, throughputSeries]);
 
   return (
     <ChartZoom router={router} start={start} end={end} utc={getUserTimezone() === 'UTC'}>
-      {zoomRenderProps => {
-        return (
-          <EchartsLineChart
-            {...zoomRenderProps}
-            {...chartOptions}
-            series={series}
-            seriesOptions={{
-              showSymbol: false,
-            }}
-            toolBox={{
-              show: false,
-            }}
-            grid={{
-              left: '10px',
-              right: '10px',
-              top: '20px',
-              bottom: '0px',
-            }}
-            options={{
-              visualMap: VisualMap({
-                show: false,
-                type: 'piecewise',
-                selectedMode: false,
-                dimension: 0,
-                pieces: [
-                  {
-                    gte: 0,
-                    lt: evidenceData?.breakpoint ? evidenceData.breakpoint * 1000 : 0,
-                    color: theme.gray500,
-                  },
-                  {
-                    gte: evidenceData?.breakpoint ? evidenceData.breakpoint * 1000 : 0,
-                    color: theme.red300,
-                  },
-                ],
-              }),
-            }}
-            xAxis={{
-              type: 'time',
-            }}
-          />
-        );
-      }}
+      {zoomRenderProps => (
+        <BaseChart {...zoomRenderProps} {...chartOptions} series={series} />
+      )}
     </ChartZoom>
   );
 }

+ 0 - 4
static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx

@@ -23,7 +23,6 @@ import {EventFunctionRegressionEvidence} from 'sentry/components/events/eventSta
 import {EventFunctionBreakpointChart} from 'sentry/components/events/eventStatisticalDetector/functionBreakpointChart';
 import RegressionMessage from 'sentry/components/events/eventStatisticalDetector/regressionMessage';
 import EventSpanOpBreakdown from 'sentry/components/events/eventStatisticalDetector/spanOpBreakdown';
-import TransactionFrequencyChart from 'sentry/components/events/eventStatisticalDetector/transactionFrequencyChart';
 import {EventTagsAndScreenshot} from 'sentry/components/events/eventTagsAndScreenshot';
 import {EventViewHierarchy} from 'sentry/components/events/eventViewHierarchy';
 import {EventGroupingInfo} from 'sentry/components/events/groupingInfo';
@@ -201,9 +200,6 @@ function PerformanceDurationRegressionIssueDetailsContent({
         <ErrorBoundary mini>
           <EventBreakpointChart event={event} />
         </ErrorBoundary>
-        <ErrorBoundary mini>
-          <TransactionFrequencyChart event={event} />
-        </ErrorBoundary>
         <ErrorBoundary mini>
           <EventSpanOpBreakdown event={event} />
         </ErrorBoundary>