uiFramesRendererWebGL.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. import * as Sentry from '@sentry/react';
  2. import {mat3} from 'gl-matrix';
  3. import type {FlamegraphTheme} from 'sentry/utils/profiling/flamegraph/flamegraphTheme';
  4. import {
  5. createAndBindBuffer,
  6. createProgram,
  7. createShader,
  8. getAttribute,
  9. getUniform,
  10. makeProjectionMatrix,
  11. pointToAndEnableVertexAttribute,
  12. resizeCanvasToDisplaySize,
  13. safeGetContext,
  14. } from 'sentry/utils/profiling/gl/utils';
  15. import {UIFramesRenderer} from 'sentry/utils/profiling/renderers/UIFramesRenderer';
  16. import {Rect} from 'sentry/utils/profiling/speedscope';
  17. import type {UIFrames} from 'sentry/utils/profiling/uiFrames';
  18. import {uiFramesFragment, uiFramesVertext} from './shaders';
  19. // These are both mutable and are used to avoid unnecessary allocations during rendering.
  20. const PHYSICAL_SPACE_PX = new Rect(0, 0, 1, 1);
  21. const CONFIG_TO_PHYSICAL_SPACE = mat3.create();
  22. const VERTICES_PER_FRAME = 6;
  23. class UIFramesRendererWebGL extends UIFramesRenderer {
  24. ctx: WebGLRenderingContext | null = null;
  25. program: WebGLProgram | null = null;
  26. // Vertex and color buffer
  27. positions: Float32Array = new Float32Array();
  28. frame_types: Float32Array = new Float32Array();
  29. bounds: Float32Array = new Float32Array();
  30. colorMap: Map<string | number, number[]> = new Map();
  31. attributes: {
  32. a_bounds: number | null;
  33. a_frame_type: number | null;
  34. a_position: number | null;
  35. } = {
  36. a_bounds: null,
  37. a_frame_type: null,
  38. a_position: null,
  39. };
  40. uniforms: {
  41. u_border_width: WebGLUniformLocation | null;
  42. u_model: WebGLUniformLocation | null;
  43. u_projection: WebGLUniformLocation | null;
  44. } = {
  45. u_border_width: null,
  46. u_model: null,
  47. u_projection: null,
  48. };
  49. constructor(
  50. canvas: HTMLCanvasElement,
  51. uiFrames: UIFrames,
  52. theme: FlamegraphTheme,
  53. options: {draw_border: boolean} = {draw_border: false}
  54. ) {
  55. super(canvas, uiFrames, theme, options);
  56. const initialized = this.initCanvasContext();
  57. if (!initialized) {
  58. Sentry.captureMessage('WebGL not supported');
  59. return;
  60. }
  61. this.initVertices();
  62. this.initShaders();
  63. }
  64. initVertices(): void {
  65. const POSITIONS = 2;
  66. const BOUNDS = 4;
  67. const FRAME_COUNT = this.uiFrames.frames.length;
  68. this.positions = new Float32Array(VERTICES_PER_FRAME * POSITIONS * FRAME_COUNT);
  69. this.frame_types = new Float32Array(FRAME_COUNT * VERTICES_PER_FRAME);
  70. this.bounds = new Float32Array(VERTICES_PER_FRAME * BOUNDS * FRAME_COUNT);
  71. for (let index = 0; index < FRAME_COUNT; index++) {
  72. const frame = this.uiFrames.frames[index];
  73. const x1 = frame.start;
  74. const x2 = frame.end;
  75. // UIFrames have no notion of depth
  76. const y1 = 0;
  77. const y2 = 1;
  78. // top left -> top right -> bottom left ->
  79. // bottom left -> top right -> bottom right
  80. const positionOffset = index * 12;
  81. this.positions[positionOffset] = x1;
  82. this.positions[positionOffset + 1] = y1;
  83. this.positions[positionOffset + 2] = x2;
  84. this.positions[positionOffset + 3] = y1;
  85. this.positions[positionOffset + 4] = x1;
  86. this.positions[positionOffset + 5] = y2;
  87. this.positions[positionOffset + 6] = x1;
  88. this.positions[positionOffset + 7] = y2;
  89. this.positions[positionOffset + 8] = x2;
  90. this.positions[positionOffset + 9] = y1;
  91. this.positions[positionOffset + 10] = x2;
  92. this.positions[positionOffset + 11] = y2;
  93. const type = frame.type === 'frozen' ? 1 : 0;
  94. const typeOffset = index * VERTICES_PER_FRAME;
  95. this.frame_types[typeOffset] = type;
  96. this.frame_types[typeOffset + 1] = type;
  97. this.frame_types[typeOffset + 2] = type;
  98. this.frame_types[typeOffset + 3] = type;
  99. this.frame_types[typeOffset + 4] = type;
  100. this.frame_types[typeOffset + 5] = type;
  101. // @TODO check if we can pack bounds across vertex calls,
  102. // we are allocating 6x the amount of memory here
  103. const boundsOffset = index * VERTICES_PER_FRAME * BOUNDS;
  104. for (let i = 0; i < VERTICES_PER_FRAME; i++) {
  105. const offset = boundsOffset + i * BOUNDS;
  106. this.bounds[offset] = x1;
  107. this.bounds[offset + 1] = y1;
  108. this.bounds[offset + 2] = x2;
  109. this.bounds[offset + 3] = y2;
  110. }
  111. }
  112. }
  113. initCanvasContext(): boolean {
  114. if (!this.canvas) {
  115. throw new Error('Cannot initialize context from null canvas');
  116. }
  117. this.ctx = safeGetContext(this.canvas, 'webgl');
  118. if (!this.ctx) {
  119. return false;
  120. }
  121. this.ctx.enable(this.ctx.BLEND);
  122. this.ctx.blendFuncSeparate(
  123. this.ctx.SRC_ALPHA,
  124. this.ctx.ONE_MINUS_SRC_ALPHA,
  125. this.ctx.ONE,
  126. this.ctx.ONE_MINUS_SRC_ALPHA
  127. );
  128. resizeCanvasToDisplaySize(this.canvas);
  129. return true;
  130. }
  131. initShaders(): void {
  132. if (!this.ctx) {
  133. throw new Error('Uninitialized WebGL context');
  134. }
  135. this.uniforms = {
  136. u_border_width: null,
  137. u_model: null,
  138. u_projection: null,
  139. };
  140. this.attributes = {
  141. a_position: null,
  142. a_bounds: null,
  143. a_frame_type: null,
  144. };
  145. const vertexShader = createShader(
  146. this.ctx,
  147. this.ctx.VERTEX_SHADER,
  148. uiFramesVertext()
  149. );
  150. const fragmentShader = createShader(
  151. this.ctx,
  152. this.ctx.FRAGMENT_SHADER,
  153. uiFramesFragment(this.theme)
  154. );
  155. // create program
  156. this.program = createProgram(this.ctx, vertexShader, fragmentShader);
  157. // initialize uniforms
  158. for (const uniform in this.uniforms) {
  159. this.uniforms[uniform] = getUniform(this.ctx, this.program, uniform);
  160. }
  161. // initialize and upload frame type information
  162. this.attributes.a_frame_type = getAttribute(this.ctx, this.program, 'a_frame_type');
  163. createAndBindBuffer(this.ctx, this.frame_types, this.ctx.STATIC_DRAW);
  164. pointToAndEnableVertexAttribute(this.ctx, this.attributes.a_frame_type, {
  165. size: 1,
  166. type: this.ctx.FLOAT,
  167. normalized: false,
  168. stride: 0,
  169. offset: 0,
  170. });
  171. // initialize and upload positions buffer data
  172. this.attributes.a_position = getAttribute(this.ctx, this.program, 'a_position');
  173. createAndBindBuffer(this.ctx, this.positions, this.ctx.STATIC_DRAW);
  174. pointToAndEnableVertexAttribute(this.ctx, this.attributes.a_position, {
  175. size: 2,
  176. type: this.ctx.FLOAT,
  177. normalized: false,
  178. stride: 0,
  179. offset: 0,
  180. });
  181. // initialize and upload bounds buffer data
  182. this.attributes.a_bounds = getAttribute(this.ctx, this.program, 'a_bounds');
  183. createAndBindBuffer(this.ctx, this.bounds, this.ctx.STATIC_DRAW);
  184. pointToAndEnableVertexAttribute(this.ctx, this.attributes.a_bounds, {
  185. size: 4,
  186. type: this.ctx.FLOAT,
  187. normalized: false,
  188. stride: 0,
  189. offset: 0,
  190. });
  191. // Use shader program
  192. // biome-ignore lint/correctness/useHookAtTopLevel: not a hook
  193. this.ctx.useProgram(this.program);
  194. }
  195. getColorForFrame(type: 'frozen' | 'slow'): [number, number, number, number] {
  196. if (type === 'frozen') {
  197. return this.theme.COLORS.UI_FRAME_COLOR_FROZEN;
  198. }
  199. if (type === 'slow') {
  200. return this.theme.COLORS.UI_FRAME_COLOR_SLOW;
  201. }
  202. throw new Error(`Invalid frame type - ${type}`);
  203. }
  204. draw(configViewToPhysicalSpace: mat3): void {
  205. if (!this.ctx) {
  206. throw new Error('Uninitialized WebGL context');
  207. }
  208. this.ctx.clearColor(0, 0, 0, 0);
  209. this.ctx.clear(this.ctx.COLOR_BUFFER_BIT);
  210. // We have no frames to draw
  211. if (!this.positions.length || !this.program) {
  212. return;
  213. }
  214. // biome-ignore lint/correctness/useHookAtTopLevel: not a hook
  215. this.ctx.useProgram(this.program);
  216. const projectionMatrix = makeProjectionMatrix(
  217. this.ctx.canvas.width,
  218. this.ctx.canvas.height
  219. );
  220. // Projection matrix
  221. this.ctx.uniformMatrix3fv(this.uniforms.u_projection, false, projectionMatrix);
  222. // Model to projection
  223. this.ctx.uniformMatrix3fv(this.uniforms.u_model, false, configViewToPhysicalSpace);
  224. // Tell webgl to convert clip space to px
  225. this.ctx.viewport(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
  226. const physicalToConfig = mat3.invert(
  227. CONFIG_TO_PHYSICAL_SPACE,
  228. configViewToPhysicalSpace
  229. );
  230. const configSpacePixel = PHYSICAL_SPACE_PX.transformRect(physicalToConfig);
  231. this.ctx.uniform2f(
  232. this.uniforms.u_border_width,
  233. configSpacePixel.width,
  234. configSpacePixel.height
  235. );
  236. this.ctx.drawArrays(
  237. this.ctx.TRIANGLES,
  238. 0,
  239. this.uiFrames.frames.length * VERTICES_PER_FRAME
  240. );
  241. }
  242. }
  243. export {UIFramesRendererWebGL};