Browse Source

feat(profiling): use color pallete on chart (#54101)

Collect all cpu measures and display them as a line chart. In case of
android where avg is reported render the area chart instead of line
chart. I reused the chart color palette for colors
Jonas 1 year ago
parent
commit
e868b7bc7e

+ 10 - 10
static/app/components/profiling/flamegraph/flamegraph.tsx

@@ -299,20 +299,20 @@ function Flamegraph(): ReactElement {
       return LOADING_OR_FALLBACK_CPU_CHART;
     }
 
+    const measures: Profiling.Measurement[] = [];
+
+    for (const key in profileGroup.measurements) {
+      if (key.startsWith('cpu_usage')) {
+        measures.push(profileGroup.measurements[key]!);
+      }
+    }
+
     return new FlamegraphChart(
       Rect.From(flamegraph.configSpace),
-      profileGroup.measurements?.cpu_usage_0 ?? {
-        unit: 'percentage',
-        values: [],
-      },
+      measures.length > 0 ? measures : [],
       flamegraphTheme.COLORS.CPU_CHART_COLORS
     );
-  }, [
-    profileGroup.measurements?.cpu_usage_0,
-    flamegraph.configSpace,
-    flamegraphTheme,
-    hasCPUChart,
-  ]);
+  }, [profileGroup.measurements, flamegraph.configSpace, flamegraphTheme, hasCPUChart]);
 
   const flamegraphCanvas = useMemo(() => {
     if (!flamegraphCanvasRef) {

+ 3 - 1
static/app/utils/profiling/flamegraph/flamegraphTheme.tsx

@@ -1,3 +1,4 @@
+import {CHART_PALETTE} from 'sentry/constants/chartPalette';
 import {
   makeColorMapByApplicationFrame,
   makeColorMapByFrequency,
@@ -11,6 +12,7 @@ import {
 import {FlamegraphColorCodings} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphPreferences';
 import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
 import {Frame} from 'sentry/utils/profiling/frame';
+import {hexToColorChannels} from 'sentry/utils/profiling/gl/utils';
 import {darkTheme, lightTheme} from 'sentry/utils/theme';
 
 import {makeColorBucketTheme} from '../speedscope';
@@ -189,7 +191,7 @@ export const LightFlamegraphTheme: FlamegraphTheme = {
       'by frequency': makeColorMapByFrequency,
       'by system vs application frame': makeColorMapBySystemVsApplicationFrame,
     },
-    CPU_CHART_COLORS: [[0.96, 0.69, 0.0, 0.5]],
+    CPU_CHART_COLORS: CHART_PALETTE[12].map(c => hexToColorChannels(c, 0.8)),
     CPU_CHART_LABEL_COLOR: 'rgba(31,35,58,.75)',
     CURSOR_CROSSHAIR: '#bbbbbb',
     DIFFERENTIAL_DECREASE: [0.309, 0.2058, 0.98],

+ 38 - 30
static/app/utils/profiling/flamegraphChart.tsx

@@ -23,55 +23,63 @@ export class FlamegraphChart {
     y: [0, 0],
   };
 
-  static Empty = new FlamegraphChart(Rect.Empty(), {unit: 'percent', values: []}, [
-    [0, 0, 0, 0],
-  ]);
+  static Empty = new FlamegraphChart(Rect.Empty(), [], [[0, 0, 0, 0]]);
 
   constructor(
     configSpace: Rect,
-    measurement: Profiling.Measurement,
+    measurements: Profiling.Measurement[],
     colors: ColorChannels[]
   ) {
     this.series = new Array<Series>();
 
-    if (!measurement || !measurement.values.length) {
+    if (!measurements || !measurements.length) {
       this.formatter = makeFormatter('percent');
       this.configSpace = configSpace.clone();
       return;
     }
 
-    this.series[0] = {
-      type: 'area',
-      lineColor: colorComponentsToRGBA(colors[0]),
-      fillColor: colorComponentsToRGBA(colors[0]),
-      points: new Array(measurement.values.length),
-    };
+    const type = measurements.length > 0 ? 'line' : 'area';
 
-    for (let i = 0; i < measurement.values.length; i++) {
-      const m = measurement.values[i];
+    for (let j = 0; j < measurements.length; j++) {
+      const measurement = measurements[j];
+      this.series[j] = {
+        type,
+        lineColor: colorComponentsToRGBA(colors[j]),
+        fillColor: colorComponentsToRGBA(colors[j]),
+        points: new Array(measurement.values.length).fill(0),
+      };
 
-      // Track and update Y max and min
-      if (m.value > this.domains.y[1]) {
-        this.domains.y[1] = m.value;
-      }
-      if (m.value < this.domains.y[0]) {
-        this.domains.y[0] = m.value;
-      }
+      for (let i = 0; i < measurement.values.length; i++) {
+        const m = measurement.values[i];
 
-      // Track and update X domain max and min
-      if (m.elapsed_since_start_ns > this.domains.x[1]) {
-        this.domains.x[1] = m.elapsed_since_start_ns;
-      }
-      if (m.elapsed_since_start_ns < this.domains.x[0]) {
-        this.domains.x[1] = m.elapsed_since_start_ns;
-      }
+        // Track and update Y max and min
+        if (m.value > this.domains.y[1]) {
+          this.domains.y[1] = m.value;
+        }
+        if (m.value < this.domains.y[0]) {
+          this.domains.y[0] = m.value;
+        }
 
-      this.series[0].points[i] = {x: m.elapsed_since_start_ns, y: m.value};
+        // Track and update X domain max and min
+        if (m.elapsed_since_start_ns > this.domains.x[1]) {
+          this.domains.x[1] = m.elapsed_since_start_ns;
+        }
+        if (m.elapsed_since_start_ns < this.domains.x[0]) {
+          this.domains.x[1] = m.elapsed_since_start_ns;
+        }
+
+        this.series[j].points[i] = {x: m.elapsed_since_start_ns, y: m.value};
+      }
     }
 
+    this.series.sort((a, b) => {
+      const aAvg = a.points.reduce((acc, point) => acc + point.y, 0) / a.points.length;
+      const bAvg = b.points.reduce((acc, point) => acc + point.y, 0) / b.points.length;
+      return bAvg - aAvg;
+    });
+
     this.domains.y[1] = 100;
     this.configSpace = configSpace.withHeight(this.domains.y[1] - this.domains.y[0]);
-
-    this.formatter = makeFormatter(measurement.unit, 0);
+    this.formatter = makeFormatter(measurements[0].unit, 0);
   }
 }

+ 9 - 0
static/app/utils/profiling/gl/utils.ts

@@ -3,6 +3,7 @@ import Fuse from 'fuse.js';
 import {mat3, vec2} from 'gl-matrix';
 
 import {CanvasView} from 'sentry/utils/profiling/canvasView';
+import {ColorChannels} from 'sentry/utils/profiling/flamegraph/flamegraphTheme';
 import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
 import {FlamegraphRenderer} from 'sentry/utils/profiling/renderers/flamegraphRenderer';
 
@@ -382,6 +383,14 @@ export interface TrimTextCenter {
   text: string;
 }
 
+export function hexToColorChannels(color: string, alpha: number): ColorChannels {
+  return [
+    parseInt(color.slice(1, 3), 16) / 255,
+    parseInt(color.slice(3, 5), 16) / 255,
+    parseInt(color.slice(5, 7), 16) / 255,
+    alpha,
+  ];
+}
 // Utility function to compute a clamped view. This is essentially a bounds check
 // to ensure that zoomed viewports stays in the bounds and does not escape the view.
 export function computeClampedConfigView(

+ 4 - 4
static/app/utils/profiling/renderers/chartRenderer.tsx

@@ -100,11 +100,11 @@ export class FlamegraphChartRenderer {
 
     // Draw series
     for (let i = 0; i < this.chart.series.length; i++) {
-      this.context.lineWidth = 1;
+      this.context.lineWidth = 1 * window.devicePixelRatio;
       this.context.fillStyle = this.chart.series[i].fillColor;
       this.context.strokeStyle = this.chart.series[i].lineColor;
-      this.context.beginPath();
       this.context.lineCap = 'round';
+      this.context.beginPath();
       const serie = this.chart.series[i];
 
       const origin = vec3.fromValues(0, 0, 1);
@@ -116,11 +116,11 @@ export class FlamegraphChartRenderer {
         const r = vec3.fromValues(point.x, point.y, 1);
         vec3.transformMat3(r, r, configViewToPhysicalSpace);
 
-        if (j === 0) {
+        if (serie.type === 'area' && j === 0) {
           this.context.lineTo(r[0], origin[1]);
         }
         this.context.lineTo(r[0], r[1]);
-        if (j === serie.points.length - 1) {
+        if (serie.type === 'area' && j === serie.points.length - 1) {
           this.context.lineTo(r[0], origin[1]);
         }
       }