Browse Source

feat(profiling): add battery usage chart (#55419)

Adds a new battery usage chart to profiling. 

It seems like cocoa might report byte units for cpu_usage which seems
odd? I'll verify and we can adjust it later

<img width="1074" alt="CleanShot 2023-08-29 at 19 30 06@2x"
src="https://github.com/getsentry/sentry/assets/9317857/d75ca085-14c0-48ea-b3e2-27cb16c3d1bc">
Jonas 1 year ago
parent
commit
cf2add48cc

+ 143 - 2
static/app/components/profiling/flamegraph/flamegraph.tsx

@@ -157,6 +157,7 @@ function findLongestMatchingFrame(
 const LOADING_OR_FALLBACK_FLAMEGRAPH = FlamegraphModel.Empty();
 const LOADING_OR_FALLBACK_SPAN_TREE = SpanTree.Empty;
 const LOADING_OR_FALLBACK_UIFRAMES = UIFrames.Empty;
+const LOADING_OR_FALLBACK_BATTERY_CHART = FlamegraphChartModel.Empty;
 const LOADING_OR_FALLBACK_CPU_CHART = FlamegraphChartModel.Empty;
 const LOADING_OR_FALLBACK_MEMORY_CHART = FlamegraphChartModel.Empty;
 
@@ -192,6 +193,9 @@ function Flamegraph(): ReactElement {
   const [uiFramesCanvasRef, setUIFramesCanvasRef] = useState<HTMLCanvasElement | null>(
     null
   );
+
+  const [batteryChartCanvasRef, setBatteryChartCanvasRef] =
+    useState<HTMLCanvasElement | null>(null);
   const [cpuChartCanvasRef, setCpuChartCanvasRef] = useState<HTMLCanvasElement | null>(
     null
   );
@@ -209,6 +213,14 @@ function Flamegraph(): ReactElement {
     );
   }, [organization.features, profileGroup.metadata.platform]);
 
+  const hasBatteryChart = useMemo(() => {
+    const platform = profileGroup.metadata.platform;
+    return (
+      platform === 'cocoa' &&
+      organization.features.includes('profiling-battery-usage-chart')
+    );
+  }, [profileGroup.metadata.platform, organization.features]);
+
   const hasCPUChart = useMemo(() => {
     const platform = profileGroup.metadata.platform;
     return (
@@ -309,6 +321,26 @@ function Flamegraph(): ReactElement {
     hasUIFrames,
   ]);
 
+  const batteryChart = useMemo(() => {
+    if (!hasCPUChart) {
+      return LOADING_OR_FALLBACK_BATTERY_CHART;
+    }
+
+    const measures: ProfileSeriesMeasurement[] = [];
+
+    for (const key in profileGroup.measurements) {
+      if (key === 'cpu_energy_usage') {
+        measures.push({...profileGroup.measurements[key]!, name: 'CPU energy usage'});
+      }
+    }
+
+    return new FlamegraphChartModel(
+      Rect.From(flamegraph.configSpace),
+      measures.length > 0 ? measures : [],
+      flamegraphTheme.COLORS.BATTERY_CHART_COLORS
+    );
+  }, [profileGroup.measurements, flamegraph.configSpace, flamegraphTheme, hasCPUChart]);
+
   const CPUChart = useMemo(() => {
     if (!hasCPUChart) {
       return LOADING_OR_FALLBACK_CPU_CHART;
@@ -400,6 +432,13 @@ function Flamegraph(): ReactElement {
     return new FlamegraphCanvas(uiFramesCanvasRef, vec2.fromValues(0, 0));
   }, [uiFramesCanvasRef]);
 
+  const batteryChartCanvas = useMemo(() => {
+    if (!batteryChartCanvasRef) {
+      return null;
+    }
+    return new FlamegraphCanvas(batteryChartCanvasRef, vec2.fromValues(0, 0));
+  }, [batteryChartCanvasRef]);
+
   const cpuChartCanvas = useMemo(() => {
     if (!cpuChartCanvasRef) {
       return null;
@@ -553,6 +592,48 @@ function Flamegraph(): ReactElement {
     [flamegraphView, flamegraphCanvas, flamegraph, uiFrames]
   );
 
+  const batteryChartView = useMemoWithPrevious<CanvasView<FlamegraphChartModel> | null>(
+    _previousView => {
+      if (!flamegraphView || !flamegraphCanvas || !batteryChart || !batteryChartCanvas) {
+        return null;
+      }
+
+      const newView = new CanvasView({
+        canvas: flamegraphCanvas,
+        model: batteryChart,
+        mode: 'anchorBottom',
+        options: {
+          // Invert chart so origin is at bottom left
+          // corner as opposed to top left
+          inverted: true,
+          minWidth: uiFrames.minFrameDuration,
+          barHeight: 0,
+          depthOffset: 0,
+          maxHeight: batteryChart.configSpace.height,
+          minHeight: batteryChart.configSpace.height,
+        },
+      });
+
+      // Compute the total size of the padding and stretch the view. This ensures that
+      // the total range is rendered and perfectly aligned from top to bottom.
+      newView.setConfigView(
+        flamegraphView.configView.withHeight(newView.configView.height),
+        {
+          width: {min: 0},
+        }
+      );
+
+      return newView;
+    },
+    [
+      flamegraphView,
+      flamegraphCanvas,
+      batteryChart,
+      uiFrames.minFrameDuration,
+      batteryChartCanvas,
+    ]
+  );
+
   const cpuChartView = useMemoWithPrevious<CanvasView<FlamegraphChartModel> | null>(
     _previousView => {
       if (!flamegraphView || !flamegraphCanvas || !CPUChart || !cpuChartCanvas) {
@@ -674,7 +755,8 @@ function Flamegraph(): ReactElement {
       spansView?.minWidth ?? Number.MAX_SAFE_INTEGER,
       uiFramesView?.minWidth ?? Number.MAX_SAFE_INTEGER,
       cpuChartView?.minWidth ?? Number.MAX_SAFE_INTEGER,
-      memoryChartView?.minWidth ?? Number.MAX_SAFE_INTEGER
+      memoryChartView?.minWidth ?? Number.MAX_SAFE_INTEGER,
+      batteryChartView?.minWidth ?? Number.MAX_SAFE_INTEGER
     );
 
     flamegraphView?.setMinWidth?.(minWidthBetweenViews);
@@ -682,7 +764,14 @@ function Flamegraph(): ReactElement {
     uiFramesView?.setMinWidth?.(minWidthBetweenViews);
     cpuChartView?.setMinWidth?.(minWidthBetweenViews);
     memoryChartView?.setMinWidth?.(minWidthBetweenViews);
-  }, [flamegraphView, spansView, uiFramesView, cpuChartView, memoryChartView]);
+  }, [
+    flamegraphView,
+    spansView,
+    uiFramesView,
+    cpuChartView,
+    memoryChartView,
+    batteryChartView,
+  ]);
 
   // 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
@@ -755,6 +844,9 @@ function Flamegraph(): ReactElement {
         if (uiFramesView) {
           uiFramesView.transformConfigView(mat);
         }
+        if (batteryChartView) {
+          batteryChartView.transformConfigView(mat);
+        }
         if (cpuChartView) {
           cpuChartView.transformConfigView(mat);
         }
@@ -771,6 +863,9 @@ function Flamegraph(): ReactElement {
         if (uiFramesView) {
           uiFramesView.transformConfigView(mat);
         }
+        if (batteryChartView) {
+          batteryChartView.transformConfigView(mat);
+        }
         if (cpuChartView) {
           cpuChartView.transformConfigView(mat);
         }
@@ -790,6 +885,9 @@ function Flamegraph(): ReactElement {
       if (uiFramesView && uiFramesCanvas) {
         uiFramesView.resetConfigView(uiFramesCanvas);
       }
+      if (batteryChartView && batteryChartCanvas) {
+        batteryChartView.resetConfigView(batteryChartCanvas);
+      }
       if (cpuChartView && cpuChartCanvas) {
         cpuChartView.resetConfigView(cpuChartCanvas);
       }
@@ -815,6 +913,11 @@ function Flamegraph(): ReactElement {
           newConfigView.withHeight(uiFramesView.configView.height)
         );
       }
+      if (batteryChartView) {
+        batteryChartView.setConfigView(
+          newConfigView.withHeight(batteryChartView.configView.height)
+        );
+      }
       if (cpuChartView) {
         cpuChartView.setConfigView(
           newConfigView.withHeight(cpuChartView.configView.height)
@@ -850,6 +953,11 @@ function Flamegraph(): ReactElement {
           newConfigView.withHeight(uiFramesView.configView.height)
         );
       }
+      if (batteryChartView) {
+        batteryChartView.setConfigView(
+          newConfigView.withHeight(batteryChartView.configView.height)
+        );
+      }
       if (cpuChartView) {
         cpuChartView.setConfigView(
           newConfigView.withHeight(cpuChartView.configView.height)
@@ -889,6 +997,8 @@ function Flamegraph(): ReactElement {
     cpuChartView,
     memoryChartCanvas,
     memoryChartView,
+    batteryChartView,
+    batteryChartCanvas,
   ]);
 
   const minimapCanvases = useMemo(() => {
@@ -924,6 +1034,17 @@ function Flamegraph(): ReactElement {
     uiFramesView
   );
 
+  const batteryChartCanvases = useMemo(() => {
+    return [batteryChartCanvasRef];
+  }, [batteryChartCanvasRef]);
+
+  const batteryChartCanvasBounds = useResizeCanvasObserver(
+    batteryChartCanvases,
+    canvasPoolManager,
+    batteryChartCanvas,
+    batteryChartView
+  );
+
   const cpuChartCanvases = useMemo(() => {
     return [cpuChartCanvasRef];
   }, [cpuChartCanvasRef]);
@@ -1149,6 +1270,26 @@ function Flamegraph(): ReactElement {
             />
           ) : null
         }
+        batteryChart={
+          hasBatteryChart ? (
+            <FlamegraphChart
+              chartCanvasRef={batteryChartCanvasRef}
+              chartCanvas={batteryChartCanvas}
+              setChartCanvasRef={setBatteryChartCanvasRef}
+              canvasBounds={batteryChartCanvasBounds}
+              chartView={batteryChartView}
+              canvasPoolManager={canvasPoolManager}
+              chart={batteryChart}
+              noMeasurementMessage={
+                profileGroup.metadata.platform === 'cocoa'
+                  ? t(
+                      'Upgrade to version 8.9.6 of sentry-cocoa SDK to enable battery usage collection'
+                    )
+                  : ''
+              }
+            />
+          ) : null
+        }
         memoryChart={
           hasMemoryChart ? (
             <FlamegraphChart

+ 55 - 11
static/app/components/profiling/flamegraph/flamegraphLayout.tsx

@@ -23,6 +23,7 @@ const TIMELINE_LABEL_HEIGHT = 20;
 const EMPTY_TIMELINE_HEIGHT = 80;
 
 interface FlamegraphLayoutProps {
+  batteryChart: React.ReactElement | null;
   cpuChart: React.ReactElement | null;
   flamegraph: React.ReactElement;
   flamegraphDrawer: React.ReactElement;
@@ -152,6 +153,20 @@ export function FlamegraphLayout(props: FlamegraphLayoutProps) {
     });
   }, [dispatch]);
 
+  const onOpenBatteryChart = useCallback(() => {
+    dispatch({
+      type: 'toggle timeline',
+      payload: {timeline: 'battery_chart', value: true},
+    });
+  }, [dispatch]);
+
+  const onCloseBatteryChart = useCallback(() => {
+    dispatch({
+      type: 'toggle timeline',
+      payload: {timeline: 'battery_chart', value: false},
+    });
+  }, [dispatch]);
+
   const spansTreeDepth = props.spansTreeDepth ?? 0;
   const spansTimelineHeight =
     Math.min(
@@ -197,6 +212,24 @@ export function FlamegraphLayout(props: FlamegraphLayoutProps) {
             </CollapsibleTimeline>
           </UIFramesContainer>
         ) : null}
+        {props.batteryChart ? (
+          <BatteryChartContainer
+            containerHeight={
+              timelines.battery_chart
+                ? flamegraphTheme.SIZES.BATTERY_CHART_HEIGHT
+                : TIMELINE_LABEL_HEIGHT
+            }
+          >
+            <CollapsibleTimeline
+              title={t('Battery')}
+              open={timelines.battery_chart}
+              onOpen={onOpenBatteryChart}
+              onClose={onCloseBatteryChart}
+            >
+              {props.batteryChart}
+            </CollapsibleTimeline>
+          </BatteryChartContainer>
+        ) : null}
         {props.memoryChart ? (
           <MemoryChartContainer
             containerHeight={
@@ -298,10 +331,10 @@ const FlamegraphGrid = styled('div')<{
   width: 100%;
   grid-template-rows: ${({layout}) =>
     layout === 'table bottom'
-      ? 'auto auto auto auto auto 1fr'
+      ? 'auto auto auto auto auto auto 1fr'
       : layout === 'table right'
-      ? 'min-content min-content min-content min-content min-content 1fr'
-      : 'min-content min-content min-content min-content min-content 1fr'};
+      ? 'min-content min-content min-content min-content min-content min-content 1fr'
+      : 'min-content min-content min-content min-content min-content min-content 1fr'};
   grid-template-columns: ${({layout}) =>
     layout === 'table bottom'
       ? '100%'
@@ -315,8 +348,9 @@ const FlamegraphGrid = styled('div')<{
     layout === 'table bottom'
       ? `
         'minimap'
-        'ui-frames'
         'spans'
+        'ui-frames'
+        'battery-chart'
         'memory-chart'
         'cpu-chart'
         'flamegraph'
@@ -324,18 +358,20 @@ const FlamegraphGrid = styled('div')<{
         `
       : layout === 'table right'
       ? `
-        'minimap      frame-stack'
-        'ui-frames    frame-stack'
-        'spans        frame-stack'
-        'memory-chart frame-stack'
-        'cpu-chart    frame-stack'
-        'flamegraph   frame-stack'
+        'minimap        frame-stack'
+        'spans          frame-stack'
+        'ui-frames      frame-stack'
+        'battery-chart  frame-stack'
+        'memory-chart   frame-stack'
+        'cpu-chart      frame-stack'
+        'flamegraph     frame-stack'
       `
       : layout === 'table left'
       ? `
         'frame-stack minimap'
-        'frame-stack ui-frames'
         'frame-stack spans'
+        'frame-stack ui-frames'
+        'frame-stack battery-chart'
         'frame-stack memory-chart'
         'frame-stack cpu-chart'
         'frame-stack flamegraph'
@@ -385,6 +421,14 @@ const MemoryChartContainer = styled('div')<{
   grid-area: memory-chart;
 `;
 
+const BatteryChartContainer = styled('div')<{
+  containerHeight: FlamegraphTheme['SIZES']['BATTERY_CHART_HEIGHT'];
+}>`
+  position: relative;
+  height: ${p => p.containerHeight}px;
+  grid-area: battery-chart;
+`;
+
 const UIFramesContainer = styled('div')<{
   containerHeight: FlamegraphTheme['SIZES']['UI_FRAMES_HEIGHT'];
 }>`

+ 1 - 0
static/app/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext.tsx

@@ -19,6 +19,7 @@ export const DEFAULT_FLAMEGRAPH_STATE: FlamegraphState = {
   },
   preferences: {
     timelines: {
+      battery_chart: true,
       ui_frames: true,
       minimap: true,
       transaction_spans: true,

+ 1 - 0
static/app/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphPreferences.tsx

@@ -18,6 +18,7 @@ export interface FlamegraphPreferences {
   layout: 'table right' | 'table bottom' | 'table left';
   sorting: FlamegraphSorting;
   timelines: {
+    battery_chart: boolean;
     cpu_chart: boolean;
     memory_chart: boolean;
     minimap: boolean;

+ 9 - 4
static/app/utils/profiling/flamegraph/flamegraphTheme.tsx

@@ -46,6 +46,7 @@ export interface FlamegraphTheme {
   // They should instead be defined as arrays of numbers so we can use them with glsl and avoid unnecessary parsing
   COLORS: {
     BAR_LABEL_FONT_COLOR: string;
+    BATTERY_CHART_COLORS: ColorChannels[];
     CHART_CURSOR_INDICATOR: string;
     CHART_LABEL_COLOR: string;
     COLOR_BUCKET: (t: number) => ColorChannels;
@@ -96,6 +97,7 @@ export interface FlamegraphTheme {
     BAR_FONT_SIZE: number;
     BAR_HEIGHT: number;
     BAR_PADDING: number;
+    BATTERY_CHART_HEIGHT: number;
     CHART_PX_PADDING: number;
     CPU_CHART_HEIGHT: number;
     FLAMEGRAPH_DEPTH_OFFSET: number;
@@ -154,6 +156,7 @@ const SIZES: FlamegraphTheme['SIZES'] = {
   BAR_FONT_SIZE: 11,
   BAR_HEIGHT: 20,
   BAR_PADDING: 4,
+  BATTERY_CHART_HEIGHT: 80,
   FLAMEGRAPH_DEPTH_OFFSET: 12,
   HOVERED_FRAME_BORDER_WIDTH: 2,
   HIGHLIGHTED_FRAME_BORDER_WIDTH: 3,
@@ -186,6 +189,7 @@ export const LightFlamegraphTheme: FlamegraphTheme = {
   SIZES,
   COLORS: {
     BAR_LABEL_FONT_COLOR: '#000',
+    BATTERY_CHART_COLORS: [[0.4, 0.56, 0.9, 0.65]],
     COLOR_BUCKET: makeColorBucketTheme(LCH_LIGHT),
     SPAN_COLOR_BUCKET: makeColorBucketTheme(SPAN_LCH_LIGHT, 140, 220),
     COLOR_MAPS: {
@@ -239,6 +243,7 @@ export const DarkFlamegraphTheme: FlamegraphTheme = {
   SIZES,
   COLORS: {
     BAR_LABEL_FONT_COLOR: 'rgb(255 255 255 / 80%)',
+    BATTERY_CHART_COLORS: [[0.4, 0.56, 0.9, 0.5]],
     COLOR_BUCKET: makeColorBucketTheme(LCH_DARK),
     SPAN_COLOR_BUCKET: makeColorBucketTheme(SPANS_LCH_DARK, 140, 220),
     COLOR_MAPS: {
@@ -252,8 +257,8 @@ export const DarkFlamegraphTheme: FlamegraphTheme = {
     },
     CPU_CHART_COLORS: CHART_PALETTE[12].map(c => hexToColorChannels(c, 0.8)),
     MEMORY_CHART_COLORS: [
-      hexToColorChannels(CHART_PALETTE[4][2], 0.8),
-      hexToColorChannels(CHART_PALETTE[4][3], 0.8),
+      hexToColorChannels(CHART_PALETTE[4][2], 0.5),
+      hexToColorChannels(CHART_PALETTE[4][3], 0.5),
     ],
     CHART_CURSOR_INDICATOR: 'rgba(255, 255, 255, 0.5)',
     CHART_LABEL_COLOR: 'rgba(255, 255, 255, 0.5)',
@@ -262,8 +267,8 @@ export const DarkFlamegraphTheme: FlamegraphTheme = {
     DIFFERENTIAL_INCREASE: [0.98, 0.2058, 0.4381],
     FOCUSED_FRAME_BORDER_COLOR: darkTheme.focus,
     FRAME_GRAYSCALE_COLOR: [0.5, 0.5, 0.5, 0.4],
-    FRAME_APPLICATION_COLOR: [0.1, 0.1, 0.8, 0.4],
-    FRAME_SYSTEM_COLOR: [0.7, 0.1, 0.1, 0.5],
+    FRAME_APPLICATION_COLOR: [0.1, 0.1, 0.5, 0.4],
+    FRAME_SYSTEM_COLOR: [0.6, 0.15, 0.25, 0.3],
     SPAN_FALLBACK_COLOR: [1, 1, 1, 0.3],
     GRID_FRAME_BACKGROUND_COLOR: 'rgb(26, 20, 31,1)',
     GRID_LINE_COLOR: '#222227',