flamegraphTextRenderer.spec.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import {mat3} from 'gl-matrix';
  2. import {Flamegraph} from 'sentry/utils/profiling/flamegraph';
  3. import {LightFlamegraphTheme as Theme} from 'sentry/utils/profiling/flamegraph/flamegraphTheme';
  4. import {EventedProfile} from 'sentry/utils/profiling/profile/eventedProfile';
  5. import {createFrameIndex} from 'sentry/utils/profiling/profile/utils';
  6. import {FlamegraphTextRenderer} from 'sentry/utils/profiling/renderers/flamegraphTextRenderer';
  7. import {Rect, trimTextCenter} from 'sentry/utils/profiling/speedscope';
  8. const makeBaseFlamegraph = (): Flamegraph => {
  9. const profile = EventedProfile.FromProfile(
  10. {
  11. name: 'profile',
  12. startValue: 0,
  13. threadID: 0,
  14. endValue: 1000,
  15. unit: 'milliseconds',
  16. type: 'evented',
  17. events: [
  18. {type: 'O', at: 0, frame: 0},
  19. {type: 'O', at: 1, frame: 1},
  20. {type: 'C', at: 2, frame: 1},
  21. {type: 'C', at: 3, frame: 0},
  22. ],
  23. },
  24. createFrameIndex('mobile', [{name: 'f0'}, {name: 'f1'}]),
  25. {type: 'flamechart'}
  26. );
  27. return new Flamegraph(profile, 0, {inverted: false, sort: 'call order'});
  28. };
  29. describe('TextRenderer', () => {
  30. it('invalidates cache if cached measurements do not match new measurements', () => {
  31. const context: Partial<CanvasRenderingContext2D> = {
  32. measureText: jest
  33. .fn()
  34. .mockReturnValueOnce({width: 1}) // first call for test
  35. .mockReturnValueOnce({width: 10})
  36. .mockReturnValueOnce({width: 20}),
  37. };
  38. const canvas: Partial<HTMLCanvasElement> = {
  39. getContext: jest.fn().mockReturnValue(context),
  40. };
  41. const textRenderer = new FlamegraphTextRenderer(
  42. canvas as HTMLCanvasElement,
  43. Theme,
  44. makeBaseFlamegraph()
  45. );
  46. textRenderer.measureAndCacheText('test');
  47. textRenderer.maybeInvalidateCache();
  48. textRenderer.maybeInvalidateCache();
  49. expect(textRenderer.textCache.test).toBe(undefined);
  50. expect(textRenderer.textCache).toEqual({
  51. 'Who knows if this changed, font-display: swap wont tell me': {
  52. width: 20,
  53. },
  54. });
  55. });
  56. it('caches measure text', () => {
  57. const context: Partial<CanvasRenderingContext2D> = {
  58. measureText: jest.fn().mockReturnValue({width: 10}),
  59. };
  60. const canvas: Partial<HTMLCanvasElement> = {
  61. getContext: jest.fn().mockReturnValue(context),
  62. };
  63. const textRenderer = new FlamegraphTextRenderer(
  64. canvas as HTMLCanvasElement,
  65. Theme,
  66. makeBaseFlamegraph()
  67. );
  68. textRenderer.measureAndCacheText('text');
  69. textRenderer.measureAndCacheText('text');
  70. expect(context.measureText).toHaveBeenCalledTimes(1);
  71. });
  72. it('skips rendering node if it is not visible', () => {
  73. // Flamegraph looks like this
  74. // f0----f0 f2
  75. // f1
  76. const profile = EventedProfile.FromProfile(
  77. {
  78. name: 'profile',
  79. startValue: 0,
  80. endValue: 1000,
  81. unit: 'milliseconds',
  82. type: 'evented',
  83. threadID: 0,
  84. events: [
  85. {type: 'O', at: 0, frame: 0},
  86. {type: 'O', at: 100, frame: 1},
  87. {type: 'C', at: 200, frame: 1},
  88. {type: 'C', at: 300, frame: 0},
  89. {type: 'O', at: 300, frame: 2},
  90. {type: 'C', at: 400, frame: 2},
  91. ],
  92. },
  93. createFrameIndex('mobile', [{name: 'f0'}, {name: 'f1'}, {name: 'f2'}]),
  94. {type: 'flamechart'}
  95. );
  96. const flamegraph = new Flamegraph(profile, 0, {inverted: false, sort: 'call order'});
  97. const context: Partial<CanvasRenderingContext2D> = {
  98. measureText: jest.fn().mockReturnValue({width: 10}),
  99. fillText: jest.fn(),
  100. };
  101. const canvas: Partial<HTMLCanvasElement> = {
  102. getContext: jest.fn().mockReturnValue(context),
  103. };
  104. const textRenderer = new FlamegraphTextRenderer(
  105. canvas as HTMLCanvasElement,
  106. Theme,
  107. flamegraph
  108. );
  109. textRenderer.draw(new Rect(0, 0, 200, 2), mat3.create(), new Map());
  110. expect(context.fillText).toHaveBeenCalledTimes(2);
  111. });
  112. it("trims output text if it doesn't fit", () => {
  113. const longFrameName =
  114. 'very long frame name that needs to be truncated to fit the rect';
  115. const profile = EventedProfile.FromProfile(
  116. {
  117. name: 'profile',
  118. startValue: 0,
  119. endValue: 1000,
  120. unit: 'milliseconds',
  121. type: 'evented',
  122. threadID: 0,
  123. events: [
  124. {type: 'O', at: 0, frame: 0},
  125. {type: 'C', at: longFrameName.length, frame: 0},
  126. ],
  127. },
  128. createFrameIndex('mobile', [{name: longFrameName}]),
  129. {type: 'flamechart'}
  130. );
  131. const flamegraph = new Flamegraph(profile, 0, {inverted: false, sort: 'call order'});
  132. const context: Partial<CanvasRenderingContext2D> = {
  133. measureText: jest.fn().mockImplementation(n => {
  134. return {width: n.length - 1};
  135. }),
  136. fillText: jest.fn(),
  137. };
  138. const canvas: Partial<HTMLCanvasElement> = {
  139. getContext: jest.fn().mockReturnValue(context),
  140. };
  141. const textRenderer = new FlamegraphTextRenderer(
  142. canvas as HTMLCanvasElement,
  143. Theme,
  144. flamegraph
  145. );
  146. textRenderer.draw(
  147. new Rect(0, 0, Math.floor(longFrameName.length / 2), 10),
  148. mat3.create(),
  149. new Map()
  150. );
  151. expect(context.fillText).toHaveBeenCalledTimes(1);
  152. expect(context.fillText).toHaveBeenCalledWith(
  153. trimTextCenter(
  154. longFrameName,
  155. Math.floor(longFrameName.length / 2) - Theme.SIZES.BAR_PADDING * 2
  156. ).text,
  157. Theme.SIZES.BAR_PADDING,
  158. Theme.SIZES.BAR_HEIGHT - Theme.SIZES.BAR_FONT_SIZE / 2 // center text vertically inside the rect
  159. );
  160. });
  161. it('pins text to left and respects right boundary', () => {
  162. const longFrameName =
  163. 'very long frame name that needs to be truncated to fit the rect';
  164. const profile = EventedProfile.FromProfile(
  165. {
  166. name: 'profile',
  167. startValue: 0,
  168. endValue: 1000,
  169. unit: 'milliseconds',
  170. type: 'evented',
  171. threadID: 0,
  172. events: [
  173. {type: 'O', at: 0, frame: 0},
  174. {type: 'C', at: longFrameName.length, frame: 0},
  175. ],
  176. },
  177. createFrameIndex('mobile', [{name: longFrameName}]),
  178. {type: 'flamechart'}
  179. );
  180. const flamegraph = new Flamegraph(profile, 0, {inverted: false, sort: 'call order'});
  181. const context: Partial<CanvasRenderingContext2D> = {
  182. measureText: jest.fn().mockImplementation(n => {
  183. return {width: n.length - 1};
  184. }),
  185. fillText: jest.fn(),
  186. };
  187. const canvas: Partial<HTMLCanvasElement> = {
  188. getContext: jest.fn().mockReturnValue(context),
  189. };
  190. const textRenderer = new FlamegraphTextRenderer(
  191. canvas as HTMLCanvasElement,
  192. Theme,
  193. flamegraph
  194. );
  195. textRenderer.draw(
  196. new Rect(
  197. Math.floor(longFrameName.length / 2),
  198. 0,
  199. Math.floor(longFrameName.length / 2 / 2),
  200. 10
  201. ),
  202. mat3.create(),
  203. new Map()
  204. );
  205. expect(context.fillText).toHaveBeenCalledTimes(1);
  206. expect(context.fillText).toHaveBeenCalledWith(
  207. trimTextCenter(
  208. longFrameName,
  209. Math.floor(longFrameName.length / 2 / 2) - Theme.SIZES.BAR_PADDING * 2
  210. ).text,
  211. Math.floor(longFrameName.length / 2) + Theme.SIZES.BAR_PADDING,
  212. Theme.SIZES.BAR_HEIGHT - Theme.SIZES.BAR_FONT_SIZE / 2 // center text vertically inside the rect
  213. );
  214. });
  215. });