Browse Source

feat(profiling): safely initialize ui frames renderer (#60628)

If gl context is not available, fallback to the 2d renderer. This is the
last renderer we had to safely initialize in order for the page to no
longer throw an error.
Jonas 1 year ago
parent
commit
b6677bf9d0

+ 23 - 1
static/app/components/profiling/flamegraph/flamegraphUIFrames.tsx

@@ -1,7 +1,9 @@
 import {CSSProperties, Fragment, useCallback, useEffect, useMemo, useState} from 'react';
 import styled from '@emotion/styled';
+import * as Sentry from '@sentry/react';
 import {vec2} from 'gl-matrix';
 
+import {addErrorMessage} from 'sentry/actionCreators/indicator';
 import {t} from 'sentry/locale';
 import {
   CanvasPoolManager,
@@ -13,7 +15,9 @@ import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
 import {
   getConfigViewTranslationBetweenVectors,
   getPhysicalSpacePositionFromOffset,
+  initializeFlamegraphRenderer,
 } from 'sentry/utils/profiling/gl/utils';
+import {UIFramesRenderer2D} from 'sentry/utils/profiling/renderers/UIFramesRenderer2D';
 import {UIFramesRendererWebGL} from 'sentry/utils/profiling/renderers/uiFramesRendererWebGL';
 import {Rect} from 'sentry/utils/profiling/speedscope';
 import {UIFrameNode, UIFrames} from 'sentry/utils/profiling/uiFrames';
@@ -63,7 +67,25 @@ export function FlamegraphUIFrames({
       return null;
     }
 
-    return new UIFramesRendererWebGL(uiFramesCanvasRef, uiFrames, flamegraphTheme);
+    const renderer = initializeFlamegraphRenderer(
+      [UIFramesRendererWebGL, UIFramesRenderer2D],
+      [
+        uiFramesCanvasRef,
+        uiFrames,
+        flamegraphTheme,
+        {
+          draw_border: true,
+        },
+      ]
+    );
+
+    if (renderer === null) {
+      Sentry.captureException('Failed to initialize a flamegraph renderer');
+      addErrorMessage('Failed to initialize renderer');
+      return null;
+    }
+
+    return renderer;
   }, [uiFramesCanvasRef, uiFrames, flamegraphTheme]);
 
   const hoveredNode: UIFrameNode[] | null = useMemo(() => {

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

@@ -2,28 +2,43 @@ import {useLayoutEffect, useState} from 'react';
 import Fuse from 'fuse.js';
 import {mat3, vec2} from 'gl-matrix';
 
+import {CanvasPoolManager} from 'sentry/utils/profiling/canvasScheduler';
 import {CanvasView} from 'sentry/utils/profiling/canvasView';
+import {clamp, colorComponentsToRGBA} from 'sentry/utils/profiling/colors/utils';
 import {ColorChannels} from 'sentry/utils/profiling/flamegraph/flamegraphTheme';
+import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
 import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
 import {
   FlamegraphRenderer,
   FlamegraphRendererConstructor,
 } from 'sentry/utils/profiling/renderers/flamegraphRenderer';
-
-import {CanvasPoolManager} from '../canvasScheduler';
-import {clamp, colorComponentsToRGBA} from '../colors/utils';
-import {FlamegraphCanvas} from '../flamegraphCanvas';
-import {SpanChartRenderer2D} from '../renderers/spansRenderer';
-import {SpanChartNode} from '../spanChart';
-import {Rect} from '../speedscope';
+import {SpanChartRenderer2D} from 'sentry/utils/profiling/renderers/spansRenderer';
+import {
+  UIFramesRenderer,
+  UIFramesRendererConstructor,
+} from 'sentry/utils/profiling/renderers/UIFramesRenderer';
+import {SpanChartNode} from 'sentry/utils/profiling/spanChart';
+import {Rect} from 'sentry/utils/profiling/speedscope';
 
 export function initializeFlamegraphRenderer(
   renderers: FlamegraphRendererConstructor[],
   constructorArgs: ConstructorParameters<FlamegraphRendererConstructor>
-): FlamegraphRenderer | null {
+): FlamegraphRenderer | null;
+export function initializeFlamegraphRenderer(
+  renderers: UIFramesRendererConstructor[],
+  constructorArgs: ConstructorParameters<UIFramesRendererConstructor>
+): UIFramesRenderer | null;
+export function initializeFlamegraphRenderer(
+  renderers: FlamegraphRendererConstructor[] | UIFramesRendererConstructor[],
+  constructorArgs:
+    | ConstructorParameters<FlamegraphRendererConstructor>
+    | ConstructorParameters<UIFramesRendererConstructor>
+): FlamegraphRenderer | UIFramesRenderer | null {
   for (const renderer of renderers) {
-    let r: FlamegraphRenderer | null = null;
+    let r: FlamegraphRenderer | UIFramesRenderer | null = null;
     try {
+      // @ts-expect-error ts complains that constructor args are not of tuple
+      // type, even though they are.
       r = new renderer(...constructorArgs);
       // eslint-disable-next-line no-empty
     } catch (e) {}

+ 48 - 2
static/app/utils/profiling/renderers/UIFramesRenderer.tsx

@@ -1,7 +1,19 @@
-import {mat3} from 'gl-matrix';
+import {mat3, vec2} from 'gl-matrix';
 
 import {FlamegraphTheme} from 'sentry/utils/profiling/flamegraph/flamegraphTheme';
-import {UIFrames} from 'sentry/utils/profiling/uiFrames';
+import {Rect} from 'sentry/utils/profiling/speedscope';
+import {UIFrameNode, UIFrames} from 'sentry/utils/profiling/uiFrames';
+
+import {upperBound} from '../gl/utils';
+
+export interface UIFramesRendererConstructor {
+  new (
+    canvas: HTMLCanvasElement,
+    uiFrames: UIFrames,
+    theme: FlamegraphTheme,
+    options?: {draw_border: boolean}
+  ): UIFramesRenderer;
+}
 
 export abstract class UIFramesRenderer {
   ctx: CanvasRenderingContext2D | WebGLRenderingContext | null = null;
@@ -24,6 +36,40 @@ export abstract class UIFramesRenderer {
     this.options = options;
   }
 
+  findHoveredNode(configSpaceCursor: vec2, configSpace: Rect): UIFrameNode[] | null {
+    // ConfigSpace origin is at top of rectangle, so we need to offset bottom by 1
+    // to account for size of renderered rectangle.
+    if (configSpaceCursor[1] > configSpace.bottom + 1) {
+      return null;
+    }
+
+    if (configSpaceCursor[0] < configSpace.left) {
+      return null;
+    }
+
+    if (configSpaceCursor[0] > configSpace.right) {
+      return null;
+    }
+
+    const overlaps: UIFrameNode[] = [];
+    // We can find the upper boundary, but because frames might overlap, we need to also check anything
+    // before the upper boundary to see if it overlaps... Performance does not seem to be a big concern
+    // here as the max number of slow frames we can have is max profile duration / slow frame = 30000/
+    const end = upperBound(configSpaceCursor[0], this.uiFrames.frames);
+
+    for (let i = 0; i < end; i++) {
+      const frame = this.uiFrames.frames[i];
+      if (configSpaceCursor[0] <= frame.end && configSpaceCursor[0] >= frame.start) {
+        overlaps.push(frame);
+      }
+    }
+
+    if (overlaps.length > 0) {
+      return overlaps;
+    }
+    return null;
+  }
+
   getColorForFrame(
     type: UIFrames['frames'][0]['type']
   ): [number, number, number, number] {

+ 1 - 5
static/app/utils/profiling/renderers/UIFramesRenderer2D.tsx

@@ -3,10 +3,6 @@ import {mat3} from 'gl-matrix';
 import {colorComponentsToRGBA} from 'sentry/utils/profiling/colors/utils';
 import {FlamegraphTheme} from 'sentry/utils/profiling/flamegraph/flamegraphTheme';
 import {getContext, resizeCanvasToDisplaySize} from 'sentry/utils/profiling/gl/utils';
-import {
-  DEFAULT_FLAMEGRAPH_RENDERER_OPTIONS,
-  FlamegraphRendererOptions,
-} from 'sentry/utils/profiling/renderers/flamegraphRenderer';
 import {UIFramesRenderer} from 'sentry/utils/profiling/renderers/UIFramesRenderer';
 import {Rect} from 'sentry/utils/profiling/speedscope';
 import {UIFrames} from 'sentry/utils/profiling/uiFrames';
@@ -18,7 +14,7 @@ export class UIFramesRenderer2D extends UIFramesRenderer {
     canvas: HTMLCanvasElement,
     uiFrames: UIFrames,
     theme: FlamegraphTheme,
-    options: FlamegraphRendererOptions = DEFAULT_FLAMEGRAPH_RENDERER_OPTIONS
+    options: {draw_border: boolean} = {draw_border: false}
   ) {
     super(canvas, uiFrames, theme, options);
     this.initCanvasContext();

+ 2 - 37
static/app/utils/profiling/renderers/uiFramesRendererWebGL.tsx

@@ -1,5 +1,5 @@
 import * as Sentry from '@sentry/react';
-import {mat3, vec2} from 'gl-matrix';
+import {mat3} from 'gl-matrix';
 
 import {FlamegraphTheme} from 'sentry/utils/profiling/flamegraph/flamegraphTheme';
 import {
@@ -12,11 +12,10 @@ import {
   pointToAndEnableVertexAttribute,
   resizeCanvasToDisplaySize,
   safeGetContext,
-  upperBound,
 } from 'sentry/utils/profiling/gl/utils';
 import {UIFramesRenderer} from 'sentry/utils/profiling/renderers/UIFramesRenderer';
 import {Rect} from 'sentry/utils/profiling/speedscope';
-import {UIFrameNode, UIFrames} from 'sentry/utils/profiling/uiFrames';
+import {UIFrames} from 'sentry/utils/profiling/uiFrames';
 
 import {uiFramesFragment, uiFramesVertext} from './shaders';
 
@@ -239,40 +238,6 @@ class UIFramesRendererWebGL extends UIFramesRenderer {
     throw new Error(`Invalid frame type - ${type}`);
   }
 
-  findHoveredNode(configSpaceCursor: vec2, configSpace: Rect): UIFrameNode[] | null {
-    // ConfigSpace origin is at top of rectangle, so we need to offset bottom by 1
-    // to account for size of renderered rectangle.
-    if (configSpaceCursor[1] > configSpace.bottom + 1) {
-      return null;
-    }
-
-    if (configSpaceCursor[0] < configSpace.left) {
-      return null;
-    }
-
-    if (configSpaceCursor[0] > configSpace.right) {
-      return null;
-    }
-
-    const overlaps: UIFrameNode[] = [];
-    // We can find the upper boundary, but because frames might overlap, we need to also check anything
-    // before the upper boundary to see if it overlaps... Performance does not seem to be a big concern
-    // here as the max number of slow frames we can have is max profile duration / slow frame = 30000/
-    const end = upperBound(configSpaceCursor[0], this.uiFrames.frames);
-
-    for (let i = 0; i < end; i++) {
-      const frame = this.uiFrames.frames[i];
-      if (configSpaceCursor[0] <= frame.end && configSpaceCursor[0] >= frame.start) {
-        overlaps.push(frame);
-      }
-    }
-
-    if (overlaps.length > 0) {
-      return overlaps;
-    }
-    return null;
-  }
-
   draw(configViewToPhysicalSpace: mat3): void {
     if (!this.ctx) {
       throw new Error('Uninitialized WebGL context');