flamegraphTextRenderer.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import {mat3} from 'gl-matrix';
  2. import {FlamegraphSearch} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphSearch';
  3. import {
  4. computeHighlightedBounds,
  5. ELLIPSIS,
  6. getContext,
  7. lowerBound,
  8. resizeCanvasToDisplaySize,
  9. upperBound,
  10. } from 'sentry/utils/profiling/gl/utils';
  11. import {TextRenderer} from 'sentry/utils/profiling/renderers/textRenderer';
  12. import {Flamegraph} from '../flamegraph';
  13. import {FlamegraphTheme} from '../flamegraph/flamegraphTheme';
  14. import {FlamegraphFrame, getFlamegraphFrameSearchId} from '../flamegraphFrame';
  15. import {findRangeBinarySearch, Rect, trimTextCenter} from '../speedscope';
  16. class FlamegraphTextRenderer extends TextRenderer {
  17. flamegraph: Flamegraph;
  18. constructor(canvas: HTMLCanvasElement, theme: FlamegraphTheme, flamegraph: Flamegraph) {
  19. super(canvas, theme);
  20. this.canvas = canvas;
  21. this.theme = theme;
  22. this.flamegraph = flamegraph;
  23. this.textCache = {};
  24. this.context = getContext(canvas, '2d');
  25. resizeCanvasToDisplaySize(canvas);
  26. }
  27. draw(
  28. configView: Rect,
  29. configViewToPhysicalSpace: mat3,
  30. flamegraphSearchResults?: FlamegraphSearch['results']['frames']
  31. ): void {
  32. // Make sure we set font size before we measure text for the first draw
  33. const FONT_SIZE = this.theme.SIZES.BAR_FONT_SIZE * window.devicePixelRatio;
  34. this.context.font = `${FONT_SIZE}px ${this.theme.FONTS.FRAME_FONT}`;
  35. this.context.textBaseline = 'alphabetic';
  36. this.maybeInvalidateCache();
  37. const MIN_WIDTH = this.measureAndCacheText(ELLIPSIS).width;
  38. const SIDE_PADDING = 2 * this.theme.SIZES.BAR_PADDING * window.devicePixelRatio;
  39. const HALF_SIDE_PADDING = SIDE_PADDING / 2;
  40. const BASELINE_OFFSET =
  41. (this.theme.SIZES.BAR_HEIGHT - this.theme.SIZES.BAR_FONT_SIZE / 2) *
  42. window.devicePixelRatio;
  43. const HIGHLIGHT_BACKGROUND_COLOR = `rgb(${this.theme.COLORS.HIGHLIGHTED_LABEL_COLOR.join(
  44. ', '
  45. )})`;
  46. const TOP_BOUNDARY = configView.top - 1;
  47. const BOTTOM_BOUNDARY = configView.bottom + 1;
  48. const HAS_SEARCH_RESULTS =
  49. flamegraphSearchResults && flamegraphSearchResults.size > 0;
  50. const TEXT_Y_POSITION = FONT_SIZE / 2 - BASELINE_OFFSET;
  51. // We start by iterating over root frames, so we draw the call stacks top-down.
  52. // This allows us to do a couple optimizations that improve our best case performance.
  53. // 1. We can skip drawing the entire tree if the root frame is not visible
  54. // 2. We can skip drawing and
  55. // Find the upper and lower bounds of the frames we need to draw so we dont end up
  56. // iterating over all of the root frames and avoid creating shallow copies if we dont need to.
  57. const start = lowerBound(configView.left, this.flamegraph.root.children);
  58. const end = upperBound(configView.right, this.flamegraph.root.children);
  59. // Populate the initial set of frames to draw
  60. const frames: FlamegraphFrame[] = this.flamegraph.root.children.slice(start, end);
  61. while (frames.length > 0) {
  62. const frame = frames.pop()!;
  63. if (frame.depth > BOTTOM_BOUNDARY) {
  64. continue;
  65. }
  66. // We pin the start and end of the frame, so scrolling around keeps text pinned to the left or right side of the viewport
  67. const pinnedStart = Math.max(frame.start, configView.left);
  68. const pinnedEnd = Math.min(frame.end, configView.right);
  69. // Transform frame to physical space coordinates. This does the same operation as
  70. // Rect.transformRect, but without allocating a new Rect object.
  71. const frameWidth =
  72. (pinnedEnd - pinnedStart) * configViewToPhysicalSpace[0] +
  73. configViewToPhysicalSpace[3];
  74. // Since the text is not exactly aligned to the left/right bounds of the frame, we need to subtract the padding
  75. // from the total width, so that we can truncate the center of the text accurately.
  76. const paddedRectangleWidth = frameWidth - SIDE_PADDING;
  77. // Since children of a frame cannot be wider than the frame itself, we can exit early and discard the entire subtree
  78. if (paddedRectangleWidth <= MIN_WIDTH) {
  79. continue;
  80. }
  81. const endChild = upperBound(configView.right, frame.children);
  82. for (let i = lowerBound(configView.left, frame.children); i < endChild; i++) {
  83. frames.push(frame.children[i]);
  84. }
  85. // If a frame is lower than the top, we can skip drawing its text, however
  86. // we can only do so after we have pushed it's children into the queue or else
  87. // those children will never be drawn and the entire sub-tree will be skipped.
  88. if (frame.depth < TOP_BOUNDARY) {
  89. continue;
  90. }
  91. // Transform frame to physical space coordinates. This does the same operation as
  92. // Rect.transformRect, but without allocating a new Rect object.
  93. const frameHeight =
  94. (pinnedEnd - pinnedStart) * configViewToPhysicalSpace[1] +
  95. configViewToPhysicalSpace[4];
  96. const frameX =
  97. pinnedStart * configViewToPhysicalSpace[0] +
  98. frame.depth * configViewToPhysicalSpace[3] +
  99. configViewToPhysicalSpace[6];
  100. const frameY =
  101. pinnedStart * configViewToPhysicalSpace[1] +
  102. frame.depth * configViewToPhysicalSpace[4] +
  103. configViewToPhysicalSpace[7];
  104. // We want to draw the text in the vertical center of the frame, so we substract half the height of the text.
  105. // Since the origin of the rect in the inverted view is also inverted, we need to add the height.
  106. const y = frameY + (frameHeight < 0 ? frameHeight : 0) + BASELINE_OFFSET;
  107. const x = frameX + (frameWidth < 0 ? frameWidth : 0) + HALF_SIDE_PADDING;
  108. const trim = trimTextCenter(
  109. frame.frame.name,
  110. findRangeBinarySearch(
  111. {low: 0, high: paddedRectangleWidth},
  112. n => this.measureAndCacheText(frame.frame.name.substring(0, n)).width,
  113. paddedRectangleWidth
  114. )[0]
  115. );
  116. if (HAS_SEARCH_RESULTS) {
  117. const frameId = getFlamegraphFrameSearchId(frame);
  118. const frameResults = flamegraphSearchResults.get(frameId);
  119. if (frameResults) {
  120. this.context.fillStyle = HIGHLIGHT_BACKGROUND_COLOR;
  121. for (let i = 0; i < frameResults.match.length; i++) {
  122. const highlightedBounds = computeHighlightedBounds(
  123. frameResults.match[i],
  124. trim
  125. );
  126. const frontMatter = trim.text.slice(0, highlightedBounds[0]);
  127. const highlightWidth = this.measureAndCacheText(
  128. trim.text.substring(highlightedBounds[0], highlightedBounds[1])
  129. ).width;
  130. this.context.fillRect(
  131. x + this.measureAndCacheText(frontMatter).width,
  132. y + TEXT_Y_POSITION,
  133. highlightWidth,
  134. FONT_SIZE
  135. );
  136. }
  137. }
  138. }
  139. this.context.fillStyle = this.theme.COLORS.LABEL_FONT_COLOR;
  140. this.context.fillText(trim.text, x, y);
  141. }
  142. }
  143. }
  144. export {FlamegraphTextRenderer};