flamegraphRenderer.spec.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. import {vec2} from 'gl-matrix';
  2. import {
  3. makeCanvasMock,
  4. makeContextMock,
  5. makeFlamegraph,
  6. } from 'sentry-test/profiling/utils';
  7. import {CanvasView} from 'sentry/utils/profiling/canvasView';
  8. import {FlamegraphSearch} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphSearch';
  9. import {
  10. LightFlamegraphTheme,
  11. LightFlamegraphTheme as theme,
  12. } from 'sentry/utils/profiling/flamegraph/flamegraphTheme';
  13. import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
  14. import {Rect} from 'sentry/utils/profiling/gl/utils';
  15. import {FlamegraphRenderer} from 'sentry/utils/profiling/renderers/flamegraphRenderer';
  16. import {Flamegraph} from '../flamegraph';
  17. import {getFlamegraphFrameSearchId} from '../flamegraphFrame';
  18. const originalDpr = window.devicePixelRatio;
  19. describe('flamegraphRenderer', () => {
  20. beforeEach(() => {
  21. // We simulate regular screens unless differently specified
  22. window.devicePixelRatio = 1;
  23. });
  24. afterEach(() => {
  25. window.devicePixelRatio = originalDpr;
  26. });
  27. describe('colors', () => {
  28. it('generates new colors if none are set on the flamegraph', () => {
  29. const canvas = makeCanvasMock({
  30. getContext: jest.fn().mockReturnValue(makeContextMock()),
  31. });
  32. const flamegraph = makeFlamegraph();
  33. const renderer = new FlamegraphRenderer(
  34. canvas as HTMLCanvasElement,
  35. flamegraph,
  36. {
  37. ...theme,
  38. COLORS: {
  39. ...theme.COLORS,
  40. // @ts-ignore overridee the colors implementation
  41. STACK_TO_COLOR: () => {
  42. const colorMap = new Map<string, number[]>([['f0', [1, 0, 0, 1]]]);
  43. return {colorBuffer: [1, 0, 0, 1], colorMap};
  44. },
  45. },
  46. },
  47. vec2.fromValues(0, 0)
  48. );
  49. expect(JSON.stringify(renderer.colors)).toEqual(
  50. JSON.stringify(new Float32Array([1, 0, 0, 1]))
  51. );
  52. });
  53. });
  54. it('inits vertices', () => {
  55. const canvas = makeCanvasMock({
  56. getContext: jest.fn().mockReturnValue(makeContextMock()),
  57. });
  58. const flamegraph = makeFlamegraph();
  59. const renderer = new FlamegraphRenderer(
  60. canvas as HTMLCanvasElement,
  61. flamegraph,
  62. theme
  63. );
  64. // Helper rect for the only frame in our flamegraph
  65. const rect = new Rect(0, 0, 10, 1);
  66. // To draw a rect, we need to draw 2 triangles, each with 3 vertices
  67. // First triangle: top left -> top right -> bottom left
  68. // Second triangle: bottom left -> top right -> bottom right
  69. expect(JSON.stringify(renderer.positions.slice(0, 2))).toEqual(
  70. JSON.stringify(new Float32Array([rect.left, rect.top]))
  71. );
  72. expect(JSON.stringify(renderer.positions.slice(2, 4))).toEqual(
  73. JSON.stringify(new Float32Array([rect.right, rect.top]))
  74. );
  75. expect(JSON.stringify(renderer.positions.slice(4, 6))).toEqual(
  76. JSON.stringify(new Float32Array([rect.left, rect.bottom]))
  77. );
  78. expect(JSON.stringify(renderer.positions.slice(6, 8))).toEqual(
  79. JSON.stringify(new Float32Array([rect.left, rect.bottom]))
  80. );
  81. expect(JSON.stringify(renderer.positions.slice(8, 10))).toEqual(
  82. JSON.stringify(new Float32Array([rect.right, rect.top]))
  83. );
  84. expect(JSON.stringify(renderer.positions.slice(10, 12))).toEqual(
  85. JSON.stringify(new Float32Array([rect.right, rect.bottom]))
  86. );
  87. });
  88. it('inits shaders', () => {
  89. const VERTEX = `void main() { gl_Position = vec4(pos, 0.0, 1.0); }`;
  90. const FRAGMENT = `void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }`;
  91. const context = makeContextMock({
  92. createShader: jest.fn().mockReturnValueOnce(VERTEX).mockReturnValueOnce(FRAGMENT),
  93. });
  94. const canvas = makeCanvasMock({
  95. getContext: jest.fn().mockReturnValue(context),
  96. });
  97. const flamegraph = makeFlamegraph();
  98. // @ts-ignore shaders are init from the constructor
  99. const _renderer = new FlamegraphRenderer(
  100. canvas as HTMLCanvasElement,
  101. flamegraph,
  102. theme
  103. );
  104. expect(context.createShader).toHaveBeenCalledTimes(2);
  105. expect(context.getShaderParameter).toHaveBeenCalledTimes(2);
  106. // @ts-ignore this is a mock
  107. expect(context.getShaderParameter.mock.calls[0][0]).toEqual(VERTEX);
  108. // @ts-ignore this is a mock
  109. expect(context.getShaderParameter.mock.calls[1][0]).toEqual(FRAGMENT);
  110. });
  111. it('getColorForFrame', () => {
  112. const canvas = makeCanvasMock({
  113. getContext: jest.fn().mockReturnValue(makeContextMock()),
  114. });
  115. const flamegraph = makeFlamegraph();
  116. const renderer = new FlamegraphRenderer(
  117. canvas as HTMLCanvasElement,
  118. flamegraph,
  119. theme
  120. );
  121. expect(renderer.getColorForFrame(flamegraph.frames[0])).toEqual([
  122. 0.9625, 0.7125, 0.7125,
  123. ]);
  124. expect(
  125. renderer.getColorForFrame({
  126. key: 20,
  127. frame: flamegraph.frames[0].frame,
  128. node: flamegraph.frames[0].node,
  129. parent: null,
  130. children: [],
  131. depth: 0,
  132. start: 0,
  133. end: 0,
  134. })
  135. ).toEqual(LightFlamegraphTheme.COLORS.FRAME_FALLBACK_COLOR);
  136. });
  137. it('getHoveredNode', () => {
  138. const flamegraph = makeFlamegraph(
  139. {
  140. events: [
  141. {type: 'O', at: 0, frame: 0},
  142. {type: 'O', at: 1, frame: 1},
  143. {type: 'C', at: 2, frame: 1},
  144. {type: 'C', at: 3, frame: 0},
  145. {type: 'O', at: 4, frame: 2},
  146. {type: 'O', at: 5, frame: 3},
  147. {type: 'C', at: 7, frame: 3},
  148. {type: 'C', at: 8, frame: 2},
  149. {type: 'O', at: 9, frame: 4},
  150. {type: 'O', at: 10, frame: 5},
  151. {type: 'C', at: 11, frame: 5},
  152. {type: 'C', at: 12, frame: 4},
  153. ],
  154. },
  155. [{name: 'f0'}, {name: 'f1'}, {name: 'f2'}, {name: 'f3'}, {name: 'f4'}, {name: 'f5'}]
  156. );
  157. const renderer = new FlamegraphRenderer(
  158. makeCanvasMock() as HTMLCanvasElement,
  159. flamegraph,
  160. theme
  161. );
  162. expect(renderer.findHoveredNode(vec2.fromValues(-1, 0))).toBeNull();
  163. expect(renderer.findHoveredNode(vec2.fromValues(-1, 0))).toBeNull();
  164. expect(renderer.findHoveredNode(vec2.fromValues(0, 0))?.frame?.name).toBe('f0');
  165. expect(renderer.findHoveredNode(vec2.fromValues(5, 2))?.frame?.name).toBe('f3');
  166. });
  167. describe('draw', () => {
  168. it('sets search results buffer', () => {
  169. const context = makeContextMock();
  170. const canvas = makeCanvasMock({
  171. getContext: jest.fn().mockReturnValue(context),
  172. }) as HTMLCanvasElement;
  173. const flamegraph = makeFlamegraph(
  174. {
  175. startValue: 0,
  176. endValue: 100,
  177. events: [
  178. {
  179. type: 'O',
  180. frame: 0,
  181. at: 0,
  182. },
  183. {
  184. type: 'C',
  185. frame: 0,
  186. at: 1,
  187. },
  188. {
  189. type: 'O',
  190. frame: 1,
  191. at: 1,
  192. },
  193. {
  194. type: 'C',
  195. frame: 1,
  196. at: 2,
  197. },
  198. ],
  199. },
  200. [{name: 'f0'}, {name: 'f1'}]
  201. );
  202. const results: FlamegraphSearch['results'] = new Map();
  203. const flamegraphCanvas = new FlamegraphCanvas(canvas, vec2.fromValues(0, 0));
  204. const flamegraphView = new CanvasView<Flamegraph>({
  205. canvas: flamegraphCanvas,
  206. model: flamegraph,
  207. options: {
  208. inverted: flamegraph.inverted,
  209. minWidth: flamegraph.profile.minFrameDuration,
  210. barHeight: theme.SIZES.BAR_HEIGHT,
  211. depthOffset: theme.SIZES.FLAMEGRAPH_DEPTH_OFFSET,
  212. },
  213. });
  214. const renderer = new FlamegraphRenderer(canvas, flamegraph, theme);
  215. // @ts-ignore we only need a partial frame mock
  216. results.set(getFlamegraphFrameSearchId(flamegraph.frames[0]), {});
  217. renderer.setSearchResults(results);
  218. expect(JSON.stringify(renderer.searchResults.slice(0, 6))).toEqual(
  219. JSON.stringify(new Float32Array([1, 1, 1, 1, 1, 1]))
  220. );
  221. renderer.draw(flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace));
  222. expect(context.drawArrays).toHaveBeenCalledTimes(1);
  223. });
  224. it('draws all frames', () => {
  225. const context = makeContextMock();
  226. const canvas = makeCanvasMock({
  227. getContext: jest.fn().mockReturnValue(context),
  228. }) as HTMLCanvasElement;
  229. const flamegraph = makeFlamegraph(
  230. {
  231. startValue: 0,
  232. endValue: 100,
  233. events: [
  234. {
  235. type: 'O',
  236. frame: 0,
  237. at: 0,
  238. },
  239. {
  240. type: 'C',
  241. frame: 0,
  242. at: 1,
  243. },
  244. {
  245. type: 'O',
  246. frame: 1,
  247. at: 1,
  248. },
  249. {
  250. type: 'C',
  251. frame: 1,
  252. at: 2,
  253. },
  254. ],
  255. },
  256. [{name: 'f0'}, {name: 'f1'}]
  257. );
  258. const flamegraphCanvas = new FlamegraphCanvas(canvas, vec2.fromValues(0, 0));
  259. const flamegraphView = new CanvasView<Flamegraph>({
  260. canvas: flamegraphCanvas,
  261. model: flamegraph,
  262. options: {
  263. inverted: flamegraph.inverted,
  264. minWidth: flamegraph.profile.minFrameDuration,
  265. barHeight: theme.SIZES.BAR_HEIGHT,
  266. depthOffset: theme.SIZES.FLAMEGRAPH_DEPTH_OFFSET,
  267. },
  268. });
  269. const renderer = new FlamegraphRenderer(canvas, flamegraph, theme);
  270. renderer.draw(flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace));
  271. expect(context.drawArrays).toHaveBeenCalledTimes(1);
  272. });
  273. });
  274. });