spansTextRenderer.tsx 7.0 KB

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