opsBreakdown.tsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. import React from 'react';
  2. import styled from '@emotion/styled';
  3. import isFinite from 'lodash/isFinite';
  4. import {SectionHeading} from 'app/components/charts/styles';
  5. import {
  6. RawSpanType,
  7. SpanEntry,
  8. TraceContextType,
  9. } from 'app/components/events/interfaces/spans/types';
  10. import QuestionTooltip from 'app/components/questionTooltip';
  11. import {pickBarColour} from 'app/components/waterfallTree/utils';
  12. import {t} from 'app/locale';
  13. import space from 'app/styles/space';
  14. import {EntryType, Event, EventTransaction} from 'app/types/event';
  15. type StartTimestamp = number;
  16. type EndTimestamp = number;
  17. type Duration = number;
  18. type TimeWindowSpan = [StartTimestamp, EndTimestamp];
  19. const OtherOperation = Symbol('Other');
  20. type OperationName = string | typeof OtherOperation;
  21. // mapping an operation name to a disjoint set of time intervals (start/end timestamp).
  22. // this is an intermediary data structure to help calculate the coverage of an operation name
  23. // with respect to the root transaction span's operation lifetime
  24. type OperationNameIntervals = Record<OperationName, Array<TimeWindowSpan>>;
  25. type OperationNameCoverage = Record<OperationName, Duration>;
  26. type OpStats = {
  27. name: OperationName;
  28. percentage: number;
  29. totalInterval: number;
  30. };
  31. const TOP_N_SPANS = 4;
  32. type OpBreakdownType = OpStats[];
  33. type DefaultProps = {
  34. topN: number;
  35. hideHeader: boolean;
  36. };
  37. type Props = DefaultProps & {
  38. event: Event;
  39. };
  40. class OpsBreakdown extends React.Component<Props> {
  41. static defaultProps: DefaultProps = {
  42. topN: TOP_N_SPANS,
  43. hideHeader: false,
  44. };
  45. getTransactionEvent(): EventTransaction | undefined {
  46. const {event} = this.props;
  47. if (event.type === 'transaction') {
  48. return event as EventTransaction;
  49. }
  50. return undefined;
  51. }
  52. generateStats(): OpBreakdownType {
  53. const {topN} = this.props;
  54. const event = this.getTransactionEvent();
  55. if (!event) {
  56. return [];
  57. }
  58. const traceContext: TraceContextType | undefined = event?.contexts?.trace;
  59. if (!traceContext) {
  60. return [];
  61. }
  62. const spanEntry = event.entries.find((entry: SpanEntry | any): entry is SpanEntry => {
  63. return entry.type === EntryType.SPANS;
  64. });
  65. let spans: RawSpanType[] = spanEntry?.data ?? [];
  66. spans =
  67. spans.length > 0
  68. ? spans
  69. : // if there are no descendent spans, then use the transaction root span
  70. [
  71. {
  72. op: traceContext.op,
  73. timestamp: event.endTimestamp,
  74. start_timestamp: event.startTimestamp,
  75. trace_id: traceContext.trace_id || '',
  76. span_id: traceContext.span_id || '',
  77. data: {},
  78. },
  79. ];
  80. const operationNameIntervals = spans.reduce(
  81. (intervals: Partial<OperationNameIntervals>, span: RawSpanType) => {
  82. let startTimestamp = span.start_timestamp;
  83. let endTimestamp = span.timestamp;
  84. if (endTimestamp < startTimestamp) {
  85. // reverse timestamps
  86. startTimestamp = span.timestamp;
  87. endTimestamp = span.start_timestamp;
  88. }
  89. // invariant: startTimestamp <= endTimestamp
  90. let operationName = span.op;
  91. if (typeof operationName !== 'string') {
  92. // a span with no operation name is considered an 'unknown' op
  93. operationName = 'unknown';
  94. }
  95. const cover: TimeWindowSpan = [startTimestamp, endTimestamp];
  96. const operationNameInterval = intervals[operationName];
  97. if (!Array.isArray(operationNameInterval)) {
  98. intervals[operationName] = [cover];
  99. return intervals;
  100. }
  101. operationNameInterval.push(cover);
  102. intervals[operationName] = mergeInterval(operationNameInterval);
  103. return intervals;
  104. },
  105. {}
  106. ) as OperationNameIntervals;
  107. const operationNameCoverage = Object.entries(operationNameIntervals).reduce(
  108. (
  109. acc: Partial<OperationNameCoverage>,
  110. [operationName, intervals]: [OperationName, TimeWindowSpan[]]
  111. ) => {
  112. const duration = intervals.reduce((sum: number, [start, end]) => {
  113. return sum + Math.abs(end - start);
  114. }, 0);
  115. acc[operationName] = duration;
  116. return acc;
  117. },
  118. {}
  119. ) as OperationNameCoverage;
  120. const sortedOpsBreakdown = Object.entries(operationNameCoverage).sort(
  121. (first: [OperationName, Duration], second: [OperationName, Duration]) => {
  122. const firstDuration = first[1];
  123. const secondDuration = second[1];
  124. if (firstDuration === secondDuration) {
  125. return 0;
  126. }
  127. if (firstDuration < secondDuration) {
  128. // sort second before first
  129. return 1;
  130. }
  131. // otherwise, sort first before second
  132. return -1;
  133. }
  134. );
  135. const breakdown = sortedOpsBreakdown.slice(0, topN).map(
  136. ([operationName, duration]: [OperationName, Duration]): OpStats => {
  137. return {
  138. name: operationName,
  139. // percentage to be recalculated after the ops breakdown group is decided
  140. percentage: 0,
  141. totalInterval: duration,
  142. };
  143. }
  144. );
  145. const other = sortedOpsBreakdown.slice(topN).reduce(
  146. (accOther: OpStats, [_operationName, duration]: [OperationName, Duration]) => {
  147. accOther.totalInterval += duration;
  148. return accOther;
  149. },
  150. {
  151. name: OtherOperation,
  152. // percentage to be recalculated after the ops breakdown group is decided
  153. percentage: 0,
  154. totalInterval: 0,
  155. }
  156. );
  157. if (other.totalInterval > 0) {
  158. breakdown.push(other);
  159. }
  160. // calculate breakdown total duration
  161. const total = breakdown.reduce((sum: number, operationNameGroup) => {
  162. return sum + operationNameGroup.totalInterval;
  163. }, 0);
  164. // recalculate percentage values
  165. breakdown.forEach(operationNameGroup => {
  166. operationNameGroup.percentage = operationNameGroup.totalInterval / total;
  167. });
  168. return breakdown;
  169. }
  170. render() {
  171. const {hideHeader} = this.props;
  172. const event = this.getTransactionEvent();
  173. if (!event) {
  174. return null;
  175. }
  176. const breakdown = this.generateStats();
  177. const contents = breakdown.map(currOp => {
  178. const {name, percentage, totalInterval} = currOp;
  179. const isOther = name === OtherOperation;
  180. const operationName = typeof name === 'string' ? name : t('Other');
  181. const durLabel = Math.round(totalInterval * 1000 * 100) / 100;
  182. const pctLabel = isFinite(percentage) ? Math.round(percentage * 100) : '∞';
  183. const opsColor: string = pickBarColour(operationName);
  184. return (
  185. <OpsLine key={operationName}>
  186. <OpsNameContainer>
  187. <OpsDot style={{backgroundColor: isOther ? 'transparent' : opsColor}} />
  188. <OpsName>{operationName}</OpsName>
  189. </OpsNameContainer>
  190. <OpsContent>
  191. <Dur>{durLabel}ms</Dur>
  192. <Pct>{pctLabel}%</Pct>
  193. </OpsContent>
  194. </OpsLine>
  195. );
  196. });
  197. if (!hideHeader) {
  198. return (
  199. <StyledBreakdown>
  200. <SectionHeading>
  201. {t('Operation Breakdown')}
  202. <QuestionTooltip
  203. position="top"
  204. size="sm"
  205. containerDisplayMode="block"
  206. title={t(
  207. 'Durations are calculated by summing span durations over the course of the transaction. Percentages are then calculated by dividing the individual op duration by the sum of total op durations. Overlapping/parallel spans are only counted once.'
  208. )}
  209. />
  210. </SectionHeading>
  211. {contents}
  212. </StyledBreakdown>
  213. );
  214. }
  215. return <StyledBreakdownNoHeader>{contents}</StyledBreakdownNoHeader>;
  216. }
  217. }
  218. const StyledBreakdown = styled('div')`
  219. font-size: ${p => p.theme.fontSizeMedium};
  220. margin-bottom: ${space(4)};
  221. `;
  222. const StyledBreakdownNoHeader = styled('div')`
  223. font-size: ${p => p.theme.fontSizeMedium};
  224. margin: ${space(2)} ${space(3)};
  225. `;
  226. const OpsLine = styled('div')`
  227. display: flex;
  228. justify-content: space-between;
  229. margin-bottom: ${space(0.5)};
  230. * + * {
  231. margin-left: ${space(0.5)};
  232. }
  233. `;
  234. const OpsDot = styled('div')`
  235. content: '';
  236. display: block;
  237. width: 8px;
  238. min-width: 8px;
  239. height: 8px;
  240. margin-right: ${space(1)};
  241. border-radius: 100%;
  242. `;
  243. const OpsContent = styled('div')`
  244. display: flex;
  245. align-items: center;
  246. `;
  247. const OpsNameContainer = styled(OpsContent)`
  248. overflow: hidden;
  249. `;
  250. const OpsName = styled('div')`
  251. white-space: nowrap;
  252. overflow: hidden;
  253. text-overflow: ellipsis;
  254. `;
  255. const Dur = styled('div')`
  256. color: ${p => p.theme.gray300};
  257. `;
  258. const Pct = styled('div')`
  259. min-width: 40px;
  260. text-align: right;
  261. `;
  262. function mergeInterval(intervals: TimeWindowSpan[]): TimeWindowSpan[] {
  263. // sort intervals by start timestamps
  264. intervals.sort((first: TimeWindowSpan, second: TimeWindowSpan) => {
  265. if (first[0] < second[0]) {
  266. // sort first before second
  267. return -1;
  268. }
  269. if (second[0] < first[0]) {
  270. // sort second before first
  271. return 1;
  272. }
  273. return 0;
  274. });
  275. // array of disjoint intervals
  276. const merged: TimeWindowSpan[] = [];
  277. for (const currentInterval of intervals) {
  278. if (merged.length === 0) {
  279. merged.push(currentInterval);
  280. continue;
  281. }
  282. const lastInterval = merged[merged.length - 1];
  283. const lastIntervalEnd = lastInterval[1];
  284. const [currentIntervalStart, currentIntervalEnd] = currentInterval;
  285. if (lastIntervalEnd < currentIntervalStart) {
  286. // if currentInterval does not overlap with lastInterval,
  287. // then add currentInterval
  288. merged.push(currentInterval);
  289. continue;
  290. }
  291. // currentInterval and lastInterval overlaps; so we merge these intervals
  292. // invariant: lastIntervalStart <= currentIntervalStart
  293. lastInterval[1] = Math.max(lastIntervalEnd, currentIntervalEnd);
  294. }
  295. return merged;
  296. }
  297. export default OpsBreakdown;