traceGrid.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import {Fragment, useRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  4. import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
  5. import {space} from 'sentry/styles/space';
  6. import {Project} from 'sentry/types';
  7. import toPercent from 'sentry/utils/number/toPercent';
  8. import toPixels from 'sentry/utils/number/toPixels';
  9. import type {TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types';
  10. import {useDimensions} from 'sentry/utils/useDimensions';
  11. import useProjects from 'sentry/utils/useProjects';
  12. import ResizeableContainer from 'sentry/views/replays/detail/perfTable/resizeableContainer';
  13. import type {FlattenedTrace} from 'sentry/views/replays/detail/perfTable/useReplayPerfData';
  14. import useVirtualScrolling from 'sentry/views/replays/detail/perfTable/useVirtualScrolling';
  15. const EMDASH = '\u2013';
  16. interface Props {
  17. flattenedTrace: FlattenedTrace;
  18. onDimensionChange: () => void;
  19. }
  20. export default function TraceGrid({flattenedTrace, onDimensionChange}: Props) {
  21. const measureRef = useRef<HTMLDivElement>(null);
  22. const {width} = useDimensions<HTMLDivElement>({elementRef: measureRef});
  23. const scrollableWindowRef = useRef<HTMLDivElement>(null);
  24. const scrollableContentRef = useRef<HTMLDivElement>(null);
  25. const {offsetX, reclamp: adjustScrollPosition} = useVirtualScrolling({
  26. windowRef: scrollableWindowRef,
  27. contentRef: scrollableContentRef,
  28. });
  29. const hasSize = width > 0;
  30. return (
  31. <Relative ref={measureRef}>
  32. {hasSize ? (
  33. <ResizeableContainer
  34. containerWidth={width}
  35. min={100}
  36. max={width - 100}
  37. onResize={() => {
  38. adjustScrollPosition();
  39. onDimensionChange();
  40. }}
  41. >
  42. <OverflowHidden ref={scrollableWindowRef}>
  43. <TxnList
  44. ref={scrollableContentRef}
  45. style={{
  46. transform: `translate(${toPixels(offsetX)}, 0)`,
  47. minWidth: 'max-content',
  48. }}
  49. >
  50. <SpanNameList flattenedTrace={flattenedTrace} />
  51. </TxnList>
  52. </OverflowHidden>
  53. <OverflowHidden>
  54. <TxnList>
  55. <SpanDurations flattenedTrace={flattenedTrace} />
  56. </TxnList>
  57. </OverflowHidden>
  58. </ResizeableContainer>
  59. ) : null}
  60. </Relative>
  61. );
  62. }
  63. function SpanNameList({flattenedTrace}: {flattenedTrace: FlattenedTrace}) {
  64. const {projects} = useProjects();
  65. return (
  66. <Fragment>
  67. {flattenedTrace.map(flattened => {
  68. const project = projects.find(p => p.id === String(flattened.trace.project_id));
  69. const labelStyle = {
  70. paddingLeft: `calc(${space(2)} * ${flattened.indent})`,
  71. };
  72. return (
  73. <TxnCell key={flattened.trace.event_id + '_name'}>
  74. <TxnLabel style={labelStyle}>
  75. <ProjectAvatar size={12} project={project as Project} />
  76. <strong>{flattened.trace['transaction.op']}</strong>
  77. <span>{EMDASH}</span>
  78. <span>{flattened.trace.transaction}</span>
  79. </TxnLabel>
  80. </TxnCell>
  81. );
  82. })}
  83. </Fragment>
  84. );
  85. }
  86. function SpanDurations({flattenedTrace}: {flattenedTrace: FlattenedTrace}) {
  87. const traces = flattenedTrace.map(flattened => flattened.trace);
  88. const startTimestampMs = Math.min(...traces.map(trace => trace.start_timestamp)) * 1000;
  89. const endTimestampMs = Math.max(
  90. ...traces.map(trace => trace.start_timestamp * 1000 + trace['transaction.duration'])
  91. );
  92. return (
  93. <Fragment>
  94. {flattenedTrace.map(flattened => (
  95. <TwoColumns key={flattened.trace.event_id + '_duration'}>
  96. <TxnCell>
  97. <TxnDurationBar
  98. style={{
  99. ...barCSSPosition(startTimestampMs, endTimestampMs, flattened.trace),
  100. background: pickBarColor(flattened.trace.transaction['transaction.op']),
  101. }}
  102. />
  103. </TxnCell>
  104. <TxnCell>
  105. <TxnDuration>{flattened.trace['transaction.duration']}ms</TxnDuration>
  106. </TxnCell>
  107. </TwoColumns>
  108. ))}
  109. </Fragment>
  110. );
  111. }
  112. function barCSSPosition(
  113. startTimestampMs: number,
  114. endTimestampMs: number,
  115. trace: TraceFullDetailed
  116. ) {
  117. const fullDuration = Math.abs(endTimestampMs - startTimestampMs) || 1;
  118. const sinceStart = trace.start_timestamp * 1000 - startTimestampMs;
  119. const duration = trace['transaction.duration'];
  120. return {
  121. left: toPercent(sinceStart / fullDuration),
  122. width: toPercent(duration / fullDuration),
  123. };
  124. }
  125. const TwoColumns = styled('div')`
  126. display: grid;
  127. grid-template-columns: 1fr max-content;
  128. `;
  129. const Relative = styled('div')`
  130. position: relative;
  131. `;
  132. const OverflowHidden = styled('div')`
  133. overflow: hidden;
  134. `;
  135. const TxnList = styled('div')`
  136. font-size: ${p => p.theme.fontSizeRelativeSmall};
  137. & > :nth-child(2n + 1) {
  138. background: ${p => p.theme.backgroundTertiary};
  139. }
  140. `;
  141. const TxnCell = styled('div')`
  142. position: relative;
  143. display: flex;
  144. align-items: center;
  145. justify-self: auto;
  146. padding: ${space(0.25)} ${space(0.5)};
  147. overflow: hidden;
  148. `;
  149. const TxnLabel = styled('div')`
  150. display: flex;
  151. gap: ${space(0.5)};
  152. align-items: center;
  153. white-space: nowrap;
  154. `;
  155. const TxnDuration = styled('div')`
  156. display: flex;
  157. flex: 1 1 auto;
  158. align-items: center;
  159. justify-content: flex-end;
  160. `;
  161. const TxnDurationBar = styled('div')`
  162. position: absolute;
  163. content: '';
  164. top: 50%;
  165. transform: translate(0, -50%);
  166. height: ${space(1.5)};
  167. margin-block: ${space(0.25)};
  168. user-select: none;
  169. min-width: 1px;
  170. `;