uiFramesRenderer.tsx 9.3 KB

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