canvasView.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. import {mat3, 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 {Flamegraph} from 'sentry/utils/profiling/flamegraph';
  9. import {LightFlamegraphTheme as theme} from 'sentry/utils/profiling/flamegraph/flamegraphTheme';
  10. import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
  11. import {Rect} from 'sentry/utils/profiling/speedscope';
  12. const makeCanvasAndView = (
  13. canvas: HTMLCanvasElement,
  14. flamegraph: Flamegraph,
  15. origin: vec2 = vec2.fromValues(0, 0),
  16. configSpaceTransform: Rect = Rect.Empty()
  17. ) => {
  18. const flamegraphCanvas = new FlamegraphCanvas(canvas, origin);
  19. const canvasView = new CanvasView<Flamegraph>({
  20. canvas: flamegraphCanvas,
  21. model: flamegraph,
  22. options: {
  23. inverted: flamegraph.inverted,
  24. minWidth: flamegraph.profile.minFrameDuration,
  25. barHeight: theme.SIZES.BAR_HEIGHT,
  26. depthOffset: theme.SIZES.FLAMEGRAPH_DEPTH_OFFSET,
  27. configSpaceTransform,
  28. },
  29. });
  30. return {flamegraphCanvas, view: canvasView};
  31. };
  32. describe('CanvasView', () => {
  33. beforeEach(() => {
  34. // We simulate regular screens unless differently specified
  35. window.devicePixelRatio = 1;
  36. });
  37. describe('initializes', () => {
  38. it('initializes config space', () => {
  39. const canvas = makeCanvasMock();
  40. const flamegraph = makeFlamegraph();
  41. const {view} = makeCanvasAndView(canvas, flamegraph);
  42. expect(view.configSpace).toEqual(new Rect(0, 0, 10, 50));
  43. });
  44. it('initializes config view', () => {
  45. const canvas = makeCanvasMock();
  46. const flamegraph = makeFlamegraph();
  47. const {view} = makeCanvasAndView(canvas, flamegraph);
  48. expect(view.configView).toEqual(new Rect(0, 0, 10, 50));
  49. });
  50. it('initializes config space transform', () => {
  51. const canvas = makeCanvasMock();
  52. const flamegraph = makeFlamegraph();
  53. const {view} = makeCanvasAndView(canvas, flamegraph);
  54. expect(mat3.exactEquals(view.configSpaceTransform, mat3.create())).toBe(true);
  55. });
  56. it('prevents invalid values on transform config space transform', () => {
  57. const canvas = makeCanvasMock();
  58. const flamegraph = makeFlamegraph();
  59. const {view} = makeCanvasAndView(
  60. canvas,
  61. flamegraph,
  62. undefined,
  63. new Rect(0, 0, 0, 0)
  64. );
  65. expect(mat3.exactEquals(view.configSpaceTransform, mat3.create())).toBe(true);
  66. });
  67. it('initializes config view with insufficient height', () => {
  68. const canvas = makeCanvasMock({height: 100});
  69. const flamegraph = makeFlamegraph();
  70. const {view} = makeCanvasAndView(canvas, flamegraph);
  71. // 20 pixels tall each, and canvas is 100 pixels tall
  72. expect(view.configView).toEqual(new Rect(0, 0, 10, 5));
  73. });
  74. it('resizes config space and config view', () => {
  75. const canvas = makeCanvasMock({width: 200, height: 200});
  76. const flamegraph = makeFlamegraph();
  77. const {flamegraphCanvas, view} = makeCanvasAndView(canvas, flamegraph);
  78. expect(view.configSpace).toEqual(new Rect(0, 0, 10, 12));
  79. expect(view.configView).toEqual(new Rect(0, 0, 10, 10));
  80. // make it smaller
  81. canvas.width = 100;
  82. canvas.height = 100;
  83. flamegraphCanvas.initPhysicalSpace();
  84. view.resizeConfigSpace(flamegraphCanvas);
  85. expect(view.configSpace).toEqual(new Rect(0, 0, 10, 12));
  86. expect(view.configView).toEqual(new Rect(0, 0, 10, 5));
  87. // make it bigger
  88. canvas.width = 1000;
  89. canvas.height = 1000;
  90. flamegraphCanvas.initPhysicalSpace();
  91. view.resizeConfigSpace(flamegraphCanvas);
  92. expect(view.configSpace).toEqual(new Rect(0, 0, 10, 50));
  93. expect(view.configView).toEqual(new Rect(0, 0, 10, 50));
  94. });
  95. });
  96. describe('getConfigSpaceCursor', () => {
  97. it('when view is not zoomed', () => {
  98. const canvas = makeCanvasMock({
  99. getContext: jest
  100. .fn()
  101. // @ts-expect-error
  102. .mockReturnValue(makeContextMock({canvas: {width: 1000, height: 2000}})),
  103. });
  104. const flamegraph = makeFlamegraph({startValue: 0, endValue: 100});
  105. const {flamegraphCanvas, view} = makeCanvasAndView(canvas, flamegraph);
  106. // x=250 is 1/4 of the width of the viewport, so it should map to flamegraph duration / 4
  107. // y=250 is at 1/8th the height of the viewport, so it should map to view height / 8
  108. const cursor = view.getConfigSpaceCursor(
  109. vec2.fromValues(250, 250),
  110. flamegraphCanvas
  111. );
  112. expect(cursor).toEqual(vec2.fromValues(25, 2000 / theme.SIZES.BAR_HEIGHT / 8));
  113. });
  114. });
  115. describe('setConfigView', () => {
  116. const canvas = makeCanvasMock();
  117. const flamegraph = makeFlamegraph(
  118. {
  119. startValue: 0,
  120. endValue: 1000,
  121. events: [
  122. {type: 'O', frame: 0, at: 0},
  123. {type: 'C', frame: 0, at: 500},
  124. ],
  125. },
  126. [{name: 'f0'}]
  127. );
  128. it('does not allow zooming in more than the min width of a frame', () => {
  129. const {view} = makeCanvasAndView(canvas, flamegraph);
  130. view.setConfigView(new Rect(0, 0, 10, 50));
  131. expect(view.configView).toEqual(new Rect(0, 0, 500, 50));
  132. });
  133. it('does not allow zooming out more than the duration of a profile', () => {
  134. const {view} = makeCanvasAndView(canvas, flamegraph);
  135. view.setConfigView(new Rect(0, 0, 2000, 50));
  136. expect(view.configView).toEqual(new Rect(0, 0, 1000, 50));
  137. });
  138. describe('edge detection on X axis', () => {
  139. it('is not zoomed in', () => {
  140. const {view} = makeCanvasAndView(canvas, flamegraph);
  141. // Check that we cant go negative X from start of profile
  142. view.setConfigView(new Rect(-100, 0, 1000, 50));
  143. expect(view.configView).toEqual(new Rect(0, 0, 1000, 50));
  144. // Check that we cant go over X from end of profile
  145. view.setConfigView(new Rect(2000, 0, 1000, 50));
  146. expect(view.configView).toEqual(new Rect(0, 0, 1000, 50));
  147. });
  148. it('is zoomed in', () => {
  149. const {view} = makeCanvasAndView(canvas, flamegraph);
  150. // Duration is is 1000, so we can't go over the end of the profile
  151. view.setConfigView(new Rect(600, 0, 500, 50));
  152. expect(view.configView).toEqual(new Rect(500, 0, 500, 50));
  153. });
  154. });
  155. describe('edge detection on Y axis', () => {
  156. it('is not zoomed in', () => {
  157. const {view} = makeCanvasAndView(canvas, flamegraph);
  158. // Check that we cant go under stack height
  159. view.setConfigView(new Rect(0, -50, 1000, 50));
  160. expect(view.configView).toEqual(new Rect(0, 0, 1000, 50));
  161. // Check that we cant go over stack height
  162. view.setConfigView(new Rect(0, 50, 1000, 50));
  163. expect(view.configView).toEqual(new Rect(0, 0, 1000, 50));
  164. });
  165. it('is zoomed in', () => {
  166. const {view} = makeCanvasAndView(canvas, flamegraph);
  167. // Check that we cant go over stack height
  168. view.setConfigView(new Rect(0, 50, 1000, 25));
  169. expect(view.configView).toEqual(new Rect(0, 25, 1000, 25));
  170. });
  171. });
  172. });
  173. describe('configSpaceTransform', () => {
  174. it('initializes transform matrix', () => {
  175. const canvas = makeCanvasMock({width: 1000, height: 1000});
  176. const flamegraph = makeFlamegraph(
  177. {
  178. startValue: 0,
  179. endValue: 1000,
  180. events: [
  181. {type: 'O', frame: 0, at: 0},
  182. {type: 'C', frame: 0, at: 1000},
  183. ],
  184. },
  185. [{name: 'f0'}]
  186. );
  187. const {view} = makeCanvasAndView(
  188. canvas,
  189. flamegraph,
  190. vec2.fromValues(0, 0),
  191. new Rect(500, 0, 0, 0)
  192. );
  193. expect(view.configSpace).toEqual(new Rect(0, 0, 1000, 50));
  194. expect(
  195. mat3.exactEquals(
  196. view.configSpaceTransform,
  197. mat3.fromValues(1, 0, 0, 0, 1, 0, 500, 0, 1)
  198. )
  199. ).toBe(true);
  200. });
  201. it('fromTransformedConfigView', () => {
  202. const canvas = makeCanvasMock({width: 1000, height: 1000});
  203. const flamegraph = makeFlamegraph({
  204. startValue: 0,
  205. endValue: 1000,
  206. events: [],
  207. });
  208. const {view} = makeCanvasAndView(
  209. canvas,
  210. flamegraph,
  211. vec2.fromValues(0, 0),
  212. new Rect(500, 0, 0, 0)
  213. );
  214. // Our frame origin is at 0, but we expect it to be at
  215. // 500 because of the configSpaceTransform
  216. const frame = new Rect(0, 0, 1000, 1).transformRect(
  217. view.fromTransformedConfigView(new Rect(0, 0, 1000, 1000))
  218. );
  219. expect(frame.width).toBe(1000);
  220. expect(frame.x).toBe(500);
  221. expect(
  222. // x is 2x scale,
  223. mat3.exactEquals(
  224. view.fromTransformedConfigView(new Rect(0, 0, 1000, 1000)),
  225. mat3.fromValues(1, 0, 0, 0, 20, 0, 500, 0, 1)
  226. )
  227. ).toBe(true);
  228. });
  229. it('fromTransformedConfigSpace', () => {
  230. const canvas = makeCanvasMock({width: 1000, height: 1000});
  231. const flamegraph = makeFlamegraph(
  232. {
  233. startValue: 0,
  234. endValue: 1000,
  235. events: [
  236. {type: 'O', frame: 0, at: 0},
  237. {type: 'C', frame: 0, at: 1000},
  238. ],
  239. },
  240. [{name: 'f0'}]
  241. );
  242. const {view} = makeCanvasAndView(
  243. canvas,
  244. flamegraph,
  245. vec2.fromValues(0, 0),
  246. new Rect(500, 0, 0, 0)
  247. );
  248. // we simulate config view change and expect the same result
  249. view.setConfigView(new Rect(0, 0, 10, 1000));
  250. const frame = new Rect(0, 0, 1000, 1).transformRect(
  251. view.fromTransformedConfigSpace(new Rect(0, 0, 1000, 1000))
  252. );
  253. expect(frame.width).toBe(1000);
  254. expect(frame.x).toBe(500);
  255. expect(
  256. // x is 2x scale,
  257. mat3.exactEquals(
  258. view.fromTransformedConfigSpace(new Rect(0, 0, 1000, 1000)),
  259. mat3.fromValues(1, 0, 0, 0, 20, 0, 500, 0, 1)
  260. )
  261. ).toBe(true);
  262. });
  263. it('getTransformedConfigSpaceCursor', () => {
  264. const canvas = makeCanvasMock({width: 1000, height: 1000});
  265. const flamegraph = makeFlamegraph(
  266. {
  267. startValue: 0,
  268. endValue: 1000,
  269. events: [
  270. {type: 'O', frame: 0, at: 0},
  271. {type: 'C', frame: 0, at: 1000},
  272. ],
  273. },
  274. [{name: 'f0'}]
  275. );
  276. const {flamegraphCanvas, view} = makeCanvasAndView(
  277. canvas,
  278. flamegraph,
  279. vec2.fromValues(0, 0),
  280. new Rect(500, 0, 0, 0)
  281. );
  282. // we simulate config view change and expect the same result
  283. view.setConfigView(new Rect(0, 0, 10, 1000));
  284. const cursor = view.getTransformedConfigSpaceCursor(
  285. vec2.fromValues(500, 500),
  286. flamegraphCanvas
  287. );
  288. // 500 - 500 offset = 0
  289. expect(cursor[0]).toEqual(0);
  290. expect(cursor[1]).toEqual(25);
  291. });
  292. it('getTransformedConfigViewCursor', () => {
  293. const canvas = makeCanvasMock({width: 1000, height: 1000});
  294. const flamegraph = makeFlamegraph({
  295. startValue: 0,
  296. endValue: 2000,
  297. events: [
  298. {type: 'O', frame: 0, at: 0},
  299. {type: 'C', frame: 0, at: 10},
  300. ],
  301. });
  302. const {flamegraphCanvas, view} = makeCanvasAndView(
  303. canvas,
  304. flamegraph,
  305. vec2.fromValues(0, 0),
  306. new Rect(100, 0, 0, 0)
  307. );
  308. view.setConfigView(new Rect(200, 0, 1000, 50));
  309. // middle of screen at
  310. // 200-100 at half screen of 1000 = 600
  311. const cursor = view.getTransformedConfigViewCursor(
  312. vec2.fromValues(500, 500),
  313. flamegraphCanvas
  314. );
  315. expect(cursor[0]).toEqual(600);
  316. expect(cursor[1]).toEqual(25);
  317. });
  318. });
  319. });