flamegraphRendererWebGL.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. import {mat3, vec2} from 'gl-matrix';
  2. import {Flamegraph} from 'sentry/utils/profiling/flamegraph';
  3. import {FlamegraphSearch} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphSearch';
  4. import {FlamegraphTheme} from 'sentry/utils/profiling/flamegraph/flamegraphTheme';
  5. import {getFlamegraphFrameSearchId} from 'sentry/utils/profiling/flamegraphFrame';
  6. import {
  7. createAndBindBuffer,
  8. createProgram,
  9. createShader,
  10. getAttribute,
  11. getContext,
  12. getUniform,
  13. makeProjectionMatrix,
  14. pointToAndEnableVertexAttribute,
  15. resizeCanvasToDisplaySize,
  16. } from 'sentry/utils/profiling/gl/utils';
  17. import {
  18. DEFAULT_FLAMEGRAPH_RENDERER_OPTIONS,
  19. FlamegraphRenderer,
  20. FlamegraphRendererOptions,
  21. } from 'sentry/utils/profiling/renderers/flamegraphRenderer';
  22. import {Rect} from 'sentry/utils/profiling/speedscope';
  23. import {fragment, vertex} from './shaders';
  24. // These are both mutable and are used to avoid unnecessary allocations during rendering.
  25. const PHYSICAL_SPACE_PX = new Rect(0, 0, 1, 1);
  26. const CONFIG_TO_PHYSICAL_SPACE = mat3.create();
  27. const VERTICES_PER_FRAME = 6;
  28. const COLOR_COMPONENTS = 4;
  29. const MATCHED_SEARCH_FRAME_ATTRIBUTES: Readonly<Float32Array> = new Float32Array(
  30. VERTICES_PER_FRAME
  31. ).fill(1);
  32. const UNMATCHED_SEARCH_FRAME_ATTRIBUTES: Readonly<Float32Array> = new Float32Array(
  33. VERTICES_PER_FRAME
  34. ).fill(0);
  35. export class FlamegraphRendererWebGL extends FlamegraphRenderer {
  36. gl: WebGLRenderingContext | null = null;
  37. program: WebGLProgram | null = null;
  38. // Vertex and color buffer
  39. positions: Float32Array = new Float32Array();
  40. bounds: Float32Array = new Float32Array();
  41. colors: Float32Array = new Float32Array();
  42. searchResults: Float32Array = new Float32Array();
  43. lastDragPosition: vec2 | null = null;
  44. attributes: {
  45. a_bounds: number | null;
  46. a_color: number | null;
  47. a_is_search_result: number | null;
  48. a_position: number | null;
  49. } = {
  50. a_position: null,
  51. a_color: null,
  52. a_bounds: null,
  53. a_is_search_result: null,
  54. };
  55. uniforms: {
  56. u_border_width: WebGLUniformLocation | null;
  57. u_draw_border: WebGLUniformLocation | null;
  58. u_grayscale: WebGLUniformLocation | null;
  59. u_model: WebGLUniformLocation | null;
  60. u_projection: WebGLUniformLocation | null;
  61. } = {
  62. u_border_width: null,
  63. u_draw_border: null,
  64. u_model: null,
  65. u_grayscale: null,
  66. u_projection: null,
  67. };
  68. constructor(
  69. canvas: HTMLCanvasElement,
  70. flamegraph: Flamegraph,
  71. theme: FlamegraphTheme,
  72. options: FlamegraphRendererOptions = DEFAULT_FLAMEGRAPH_RENDERER_OPTIONS
  73. ) {
  74. super(canvas, flamegraph, theme, options);
  75. if (
  76. VERTICES_PER_FRAME * COLOR_COMPONENTS * this.frames.length !==
  77. this.colorBuffer.length
  78. ) {
  79. throw new Error('Color buffer length does not match the number of vertices');
  80. }
  81. this.colors = new Float32Array(this.colorBuffer);
  82. this.initCanvasContext();
  83. this.initVertices();
  84. this.initShaders();
  85. }
  86. initVertices(): void {
  87. const POSITIONS = 2;
  88. const BOUNDS = 4;
  89. const FRAME_COUNT = this.frames.length;
  90. this.bounds = new Float32Array(VERTICES_PER_FRAME * BOUNDS * FRAME_COUNT);
  91. this.positions = new Float32Array(VERTICES_PER_FRAME * POSITIONS * FRAME_COUNT);
  92. this.searchResults = new Float32Array(FRAME_COUNT * VERTICES_PER_FRAME);
  93. for (let index = 0; index < FRAME_COUNT; index++) {
  94. const frame = this.frames[index];
  95. const x1 = frame.start;
  96. const x2 = frame.end;
  97. const y1 = frame.depth;
  98. const y2 = frame.depth + 1;
  99. // top left -> top right -> bottom left ->
  100. // bottom left -> top right -> bottom right
  101. const positionOffset = index * 12;
  102. this.positions[positionOffset] = x1;
  103. this.positions[positionOffset + 1] = y1;
  104. this.positions[positionOffset + 2] = x2;
  105. this.positions[positionOffset + 3] = y1;
  106. this.positions[positionOffset + 4] = x1;
  107. this.positions[positionOffset + 5] = y2;
  108. this.positions[positionOffset + 6] = x1;
  109. this.positions[positionOffset + 7] = y2;
  110. this.positions[positionOffset + 8] = x2;
  111. this.positions[positionOffset + 9] = y1;
  112. this.positions[positionOffset + 10] = x2;
  113. this.positions[positionOffset + 11] = y2;
  114. // @TODO check if we can pack bounds across vertex calls,
  115. // we are allocating 6x the amount of memory here
  116. const boundsOffset = index * VERTICES_PER_FRAME * BOUNDS;
  117. for (let i = 0; i < VERTICES_PER_FRAME; i++) {
  118. const offset = boundsOffset + i * BOUNDS;
  119. this.bounds[offset] = x1;
  120. this.bounds[offset + 1] = y1;
  121. this.bounds[offset + 2] = x2;
  122. this.bounds[offset + 3] = y2;
  123. }
  124. }
  125. }
  126. initCanvasContext(): void {
  127. if (!this.canvas) {
  128. throw new Error('Cannot initialize context from null canvas');
  129. }
  130. // Setup webgl canvas context
  131. this.gl = getContext(this.canvas, 'webgl');
  132. if (!this.gl) {
  133. throw new Error('Uninitialized WebGL context');
  134. }
  135. this.gl.enable(this.gl.BLEND);
  136. this.gl.blendFuncSeparate(
  137. this.gl.SRC_ALPHA,
  138. this.gl.ONE_MINUS_SRC_ALPHA,
  139. this.gl.ONE,
  140. this.gl.ONE_MINUS_SRC_ALPHA
  141. );
  142. resizeCanvasToDisplaySize(this.canvas);
  143. }
  144. initShaders(): void {
  145. if (!this.gl) {
  146. throw new Error('Uninitialized WebGL context');
  147. }
  148. this.uniforms = {
  149. u_border_width: null,
  150. u_draw_border: null,
  151. u_grayscale: null,
  152. u_model: null,
  153. u_projection: null,
  154. };
  155. this.attributes = {
  156. a_bounds: null,
  157. a_color: null,
  158. a_is_search_result: null,
  159. a_position: null,
  160. };
  161. const vertexShader = createShader(this.gl, this.gl.VERTEX_SHADER, vertex());
  162. const fragmentShader = createShader(
  163. this.gl,
  164. this.gl.FRAGMENT_SHADER,
  165. fragment(this.theme)
  166. );
  167. // create program
  168. this.program = createProgram(this.gl, vertexShader, fragmentShader);
  169. // initialize uniforms
  170. for (const uniform in this.uniforms) {
  171. this.uniforms[uniform] = getUniform(this.gl, this.program, uniform);
  172. }
  173. // initialize and upload search results buffer data
  174. this.attributes.a_is_search_result = getAttribute(
  175. this.gl,
  176. this.program,
  177. 'a_is_search_result'
  178. );
  179. createAndBindBuffer(this.gl, this.searchResults, this.gl.STATIC_DRAW);
  180. pointToAndEnableVertexAttribute(this.gl, this.attributes.a_is_search_result, {
  181. size: 1,
  182. type: this.gl.FLOAT,
  183. normalized: false,
  184. stride: 0,
  185. offset: 0,
  186. });
  187. // initialize and upload color buffer data
  188. this.attributes.a_color = getAttribute(this.gl, this.program, 'a_color');
  189. createAndBindBuffer(this.gl, this.colors, this.gl.STATIC_DRAW);
  190. pointToAndEnableVertexAttribute(this.gl, this.attributes.a_color, {
  191. size: 4,
  192. type: this.gl.FLOAT,
  193. normalized: false,
  194. stride: 0,
  195. offset: 0,
  196. });
  197. // initialize and upload positions buffer data
  198. this.attributes.a_position = getAttribute(this.gl, this.program, 'a_position');
  199. createAndBindBuffer(this.gl, this.positions, this.gl.STATIC_DRAW);
  200. pointToAndEnableVertexAttribute(this.gl, this.attributes.a_position, {
  201. size: 2,
  202. type: this.gl.FLOAT,
  203. normalized: false,
  204. stride: 0,
  205. offset: 0,
  206. });
  207. // initialize and upload bounds buffer data
  208. this.attributes.a_bounds = getAttribute(this.gl, this.program, 'a_bounds');
  209. createAndBindBuffer(this.gl, this.bounds, this.gl.STATIC_DRAW);
  210. pointToAndEnableVertexAttribute(this.gl, this.attributes.a_bounds, {
  211. size: 4,
  212. type: this.gl.FLOAT,
  213. normalized: false,
  214. stride: 0,
  215. offset: 0,
  216. });
  217. // Use shader program
  218. this.gl.useProgram(this.program);
  219. // Check if we should draw border - order matters here
  220. // https://stackoverflow.com/questions/60673970/uniform-value-not-stored-if-i-put-the-gluniform1f-call-before-the-render-loop
  221. this.gl.uniform1i(this.uniforms.u_draw_border, this.options.draw_border ? 1 : 0);
  222. this.gl.uniform1i(this.uniforms.u_grayscale, 0);
  223. }
  224. setSearchResults(query: string, searchResults: FlamegraphSearch['results']['frames']) {
  225. if (!this.program || !this.gl) {
  226. return;
  227. }
  228. this.gl.uniform1i(
  229. this.uniforms.u_grayscale,
  230. query.length > 0 || searchResults.size > 0 ? 1 : 0
  231. );
  232. this.updateSearchResultsBuffer(searchResults);
  233. }
  234. private updateSearchResultsBuffer(
  235. searchResults: FlamegraphSearch['results']['frames']
  236. ) {
  237. if (!this.program || !this.gl) {
  238. return;
  239. }
  240. for (let i = 0; i < this.frames.length; i++) {
  241. this.searchResults.set(
  242. searchResults.has(getFlamegraphFrameSearchId(this.frames[i]))
  243. ? MATCHED_SEARCH_FRAME_ATTRIBUTES
  244. : UNMATCHED_SEARCH_FRAME_ATTRIBUTES,
  245. i * 6
  246. );
  247. }
  248. this.attributes.a_is_search_result = getAttribute(
  249. this.gl,
  250. this.program,
  251. 'a_is_search_result'
  252. );
  253. createAndBindBuffer(this.gl, this.searchResults, this.gl.STATIC_DRAW);
  254. pointToAndEnableVertexAttribute(this.gl, this.attributes.a_is_search_result, {
  255. size: 1,
  256. type: this.gl.FLOAT,
  257. normalized: false,
  258. stride: 0,
  259. offset: 0,
  260. });
  261. }
  262. draw(configViewToPhysicalSpace: mat3): void {
  263. if (!this.gl) {
  264. throw new Error('Uninitialized WebGL context');
  265. }
  266. this.gl.clearColor(0, 0, 0, 0);
  267. this.gl.clear(this.gl.COLOR_BUFFER_BIT);
  268. // We have no frames to draw
  269. if (!this.positions.length || !this.program) {
  270. return;
  271. }
  272. const projectionMatrix = makeProjectionMatrix(
  273. this.gl.canvas.width,
  274. this.gl.canvas.height
  275. );
  276. // Projection matrix
  277. this.gl.uniformMatrix3fv(this.uniforms.u_projection, false, projectionMatrix);
  278. // Model to projection
  279. this.gl.uniformMatrix3fv(this.uniforms.u_model, false, configViewToPhysicalSpace);
  280. // Tell webgl to convert clip space to px
  281. this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
  282. const physicalToConfig = mat3.invert(
  283. CONFIG_TO_PHYSICAL_SPACE,
  284. configViewToPhysicalSpace
  285. );
  286. const configSpacePixel = PHYSICAL_SPACE_PX.transformRect(physicalToConfig);
  287. this.gl.uniform2f(
  288. this.uniforms.u_border_width,
  289. configSpacePixel.width,
  290. configSpacePixel.height
  291. );
  292. this.gl.drawArrays(this.gl.TRIANGLES, 0, this.frames.length * VERTICES_PER_FRAME);
  293. }
  294. }