utils.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831
  1. import jaro from 'wink-jaro-distance';
  2. import {RawSpanType, SpanType} from 'app/components/events/interfaces/spans/types';
  3. import {
  4. generateRootSpan,
  5. isOrphanSpan,
  6. parseTrace,
  7. } from 'app/components/events/interfaces/spans/utils';
  8. import {toPercent} from 'app/components/performance/waterfall/utils';
  9. import {EventTransaction} from 'app/types/event';
  10. // Minimum threshold score for descriptions that are similar.
  11. const COMMON_SIMILARITY_DESCRIPTION_THRESHOLD = 0.8;
  12. export function isTransactionEvent(event: any): event is EventTransaction {
  13. if (!event) {
  14. return false;
  15. }
  16. return event?.type === 'transaction';
  17. }
  18. export type DiffSpanType =
  19. | {
  20. comparisonResult: 'matched';
  21. span_id: SpanId; // baselineSpan.span_id + regressionSpan.span_id
  22. op: string | undefined;
  23. description: string | undefined;
  24. baselineSpan: SpanType;
  25. regressionSpan: SpanType;
  26. }
  27. | {
  28. comparisonResult: 'baseline';
  29. baselineSpan: SpanType;
  30. }
  31. | {
  32. comparisonResult: 'regression';
  33. regressionSpan: SpanType;
  34. };
  35. type ComparableSpan = {
  36. type: 'descendent';
  37. parent_span_id: SpanId;
  38. baselineSpan: SpanType;
  39. regressionSpan: SpanType;
  40. };
  41. type SpanId = string;
  42. // map span_id to children whose parent_span_id is equal to span_id
  43. // invariant: spans that are matched will have children in this lookup map
  44. export type SpanChildrenLookupType = Record<SpanId, Array<DiffSpanType>>;
  45. export type ComparisonReport = {
  46. rootSpans: Array<DiffSpanType>;
  47. childSpans: SpanChildrenLookupType;
  48. };
  49. export function diffTransactions({
  50. baselineEvent,
  51. regressionEvent,
  52. }: {
  53. baselineEvent: EventTransaction;
  54. regressionEvent: EventTransaction;
  55. }): ComparisonReport {
  56. const baselineTrace = parseTrace(baselineEvent);
  57. const regressionTrace = parseTrace(regressionEvent);
  58. const rootSpans: Array<DiffSpanType> = [];
  59. const childSpans: SpanChildrenLookupType = {};
  60. // merge childSpans of baselineTrace and regressionTrace together
  61. for (const [parentSpanId, children] of Object.entries(baselineTrace.childSpans)) {
  62. childSpans[parentSpanId] = children.map(baselineSpan => {
  63. return {
  64. comparisonResult: 'baseline',
  65. baselineSpan,
  66. };
  67. });
  68. }
  69. for (const [parentSpanId, children] of Object.entries(regressionTrace.childSpans)) {
  70. childSpans[parentSpanId] = children.map(regressionSpan => {
  71. return {
  72. comparisonResult: 'regression',
  73. regressionSpan,
  74. };
  75. });
  76. }
  77. // merge the two transaction's span trees
  78. // we maintain a stack of spans to be compared
  79. const spansToBeCompared: Array<
  80. | {
  81. type: 'root';
  82. baselineSpan: RawSpanType;
  83. regressionSpan: RawSpanType;
  84. }
  85. | ComparableSpan
  86. > = [
  87. {
  88. type: 'root',
  89. baselineSpan: generateRootSpan(baselineTrace),
  90. regressionSpan: generateRootSpan(regressionTrace),
  91. },
  92. ];
  93. while (spansToBeCompared.length > 0) {
  94. const currentSpans = spansToBeCompared.pop();
  95. if (!currentSpans) {
  96. // typescript assumes currentSpans is undefined due to the nature of Array.prototype.pop()
  97. // returning undefined if spansToBeCompared is empty. the loop invariant guarantees that spansToBeCompared
  98. // is a non-empty array. we handle this case for sake of completeness
  99. break;
  100. }
  101. // invariant: the parents of currentSpans are matched spans; with the exception of the root spans of the baseline
  102. // transaction and the regression transaction.
  103. // invariant: any unvisited siblings of currentSpans are in spansToBeCompared.
  104. // invariant: currentSpans and their siblings are already in childSpans
  105. const {baselineSpan, regressionSpan} = currentSpans;
  106. // The span from the base transaction is considered 'identical' to the span from the regression transaction
  107. // only if they share the same op name, depth level, and description.
  108. //
  109. // baselineSpan and regressionSpan have equivalent depth levels due to the nature of the tree traversal algorithm.
  110. if (matchableSpans({baselineSpan, regressionSpan}) === 0) {
  111. if (currentSpans.type === 'root') {
  112. const spanComparisonResults: [DiffSpanType, DiffSpanType] = [
  113. {
  114. comparisonResult: 'baseline',
  115. baselineSpan,
  116. },
  117. {
  118. comparisonResult: 'regression',
  119. regressionSpan,
  120. },
  121. ];
  122. rootSpans.push(...spanComparisonResults);
  123. }
  124. // since baselineSpan and regressionSpan are considered not identical, we do not
  125. // need to compare their sub-trees
  126. continue;
  127. }
  128. const spanComparisonResult: DiffSpanType = {
  129. comparisonResult: 'matched',
  130. span_id: generateMergedSpanId({baselineSpan, regressionSpan}),
  131. op: baselineSpan.op,
  132. description: baselineSpan.description,
  133. baselineSpan,
  134. regressionSpan,
  135. };
  136. if (currentSpans.type === 'root') {
  137. rootSpans.push(spanComparisonResult);
  138. }
  139. const {comparablePairs, children} = createChildPairs({
  140. parent_span_id: spanComparisonResult.span_id,
  141. baseChildren: baselineTrace.childSpans[baselineSpan.span_id] ?? [],
  142. regressionChildren: regressionTrace.childSpans[regressionSpan.span_id] ?? [],
  143. });
  144. spansToBeCompared.push(...comparablePairs);
  145. if (children.length > 0) {
  146. childSpans[spanComparisonResult.span_id] = children;
  147. }
  148. }
  149. rootSpans.sort(sortByMostTimeAdded);
  150. const report = {
  151. rootSpans,
  152. childSpans,
  153. };
  154. return report;
  155. }
  156. function createChildPairs({
  157. parent_span_id,
  158. baseChildren,
  159. regressionChildren,
  160. }: {
  161. parent_span_id: SpanId;
  162. baseChildren: Array<SpanType>;
  163. regressionChildren: Array<SpanType>;
  164. }): {
  165. comparablePairs: Array<ComparableSpan>;
  166. children: Array<DiffSpanType>;
  167. } {
  168. // invariant: the parents of baseChildren and regressionChildren are matched spans
  169. // for each child in baseChildren, pair them with the closest matching child in regressionChildren
  170. const comparablePairs: Array<ComparableSpan> = [];
  171. const children: Array<DiffSpanType> = [];
  172. const remainingRegressionChildren = [...regressionChildren];
  173. for (const baselineSpan of baseChildren) {
  174. // reduce remainingRegressionChildren down to spans that are applicable candidate
  175. // of spans that can be paired with baselineSpan
  176. const candidates = remainingRegressionChildren.reduce(
  177. (
  178. acc: Array<{regressionSpan: SpanType; index: number; matchScore: number}>,
  179. regressionSpan: SpanType,
  180. index: number
  181. ) => {
  182. const matchScore = matchableSpans({baselineSpan, regressionSpan});
  183. if (matchScore !== 0) {
  184. acc.push({
  185. regressionSpan,
  186. index,
  187. matchScore,
  188. });
  189. }
  190. return acc;
  191. },
  192. []
  193. );
  194. if (candidates.length === 0) {
  195. children.push({
  196. comparisonResult: 'baseline',
  197. baselineSpan,
  198. });
  199. continue;
  200. }
  201. // the best candidate span is one that has the closest start timestamp to baselineSpan;
  202. // and one that has a duration that's close to baselineSpan
  203. const baselineSpanDuration = Math.abs(
  204. baselineSpan.timestamp - baselineSpan.start_timestamp
  205. );
  206. const {regressionSpan, index} = candidates.reduce((bestCandidate, nextCandidate) => {
  207. const {regressionSpan: thisSpan, matchScore: thisSpanMatchScore} = bestCandidate;
  208. const {regressionSpan: otherSpan, matchScore: otherSpanMatchScore} = nextCandidate;
  209. // calculate the deltas of the start timestamps relative to baselineSpan's
  210. // start timestamp
  211. const deltaStartTimestampThisSpan = Math.abs(
  212. thisSpan.start_timestamp - baselineSpan.start_timestamp
  213. );
  214. const deltaStartTimestampOtherSpan = Math.abs(
  215. otherSpan.start_timestamp - baselineSpan.start_timestamp
  216. );
  217. // calculate the deltas of the durations relative to the baselineSpan's
  218. // duration
  219. const thisSpanDuration = Math.abs(thisSpan.timestamp - thisSpan.start_timestamp);
  220. const otherSpanDuration = Math.abs(otherSpan.timestamp - otherSpan.start_timestamp);
  221. const deltaDurationThisSpan = Math.abs(thisSpanDuration - baselineSpanDuration);
  222. const deltaDurationOtherSpan = Math.abs(otherSpanDuration - baselineSpanDuration);
  223. const thisSpanScore =
  224. deltaDurationThisSpan + deltaStartTimestampThisSpan + (1 - thisSpanMatchScore);
  225. const otherSpanScore =
  226. deltaDurationOtherSpan + deltaStartTimestampOtherSpan + (1 - otherSpanMatchScore);
  227. if (thisSpanScore < otherSpanScore) {
  228. return bestCandidate;
  229. }
  230. if (thisSpanScore > otherSpanScore) {
  231. return nextCandidate;
  232. }
  233. return bestCandidate;
  234. });
  235. // remove regressionSpan from list of remainingRegressionChildren
  236. remainingRegressionChildren.splice(index, 1);
  237. comparablePairs.push({
  238. type: 'descendent',
  239. parent_span_id,
  240. baselineSpan,
  241. regressionSpan,
  242. });
  243. children.push({
  244. comparisonResult: 'matched',
  245. span_id: generateMergedSpanId({baselineSpan, regressionSpan}),
  246. op: baselineSpan.op,
  247. description: baselineSpan.description,
  248. baselineSpan,
  249. regressionSpan,
  250. });
  251. }
  252. // push any remaining un-matched regressionSpans
  253. for (const regressionSpan of remainingRegressionChildren) {
  254. children.push({
  255. comparisonResult: 'regression',
  256. regressionSpan,
  257. });
  258. }
  259. // sort children by most time added
  260. children.sort(sortByMostTimeAdded);
  261. return {
  262. comparablePairs,
  263. children,
  264. };
  265. }
  266. function jaroSimilarity(thisString: string, otherString: string): number {
  267. // based on https://winkjs.org/wink-distance/string-jaro-winkler.js.html
  268. // and https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance
  269. if (thisString === otherString) {
  270. return 1;
  271. }
  272. let jaroDistance: number = jaro(thisString, otherString).distance;
  273. // Constant scaling factor for how much the score is adjusted upwards for having common prefixes.
  274. // This is only used for the Jaro–Winkler Similarity procedure.
  275. const scalingFactor = 0.1;
  276. // boostThreshold is the upper bound threshold of which if the Jaro score was less-than or equal
  277. // to boostThreshold, then the Jaro–Winkler Similarity procedure is applied. Otherwise,
  278. // 1 - jaroDistance is returned.
  279. const boostThreshold = 0.3;
  280. if (jaroDistance > boostThreshold) {
  281. return 1 - jaroDistance;
  282. }
  283. const pLimit = Math.min(thisString.length, otherString.length, 4);
  284. let l = 0;
  285. for (let i = 0; i < pLimit; i += 1) {
  286. if (thisString[i] === otherString[i]) {
  287. l += 1;
  288. } else {
  289. break;
  290. }
  291. }
  292. jaroDistance -= l * scalingFactor * jaroDistance;
  293. return 1 - jaroDistance;
  294. }
  295. function matchableSpans({
  296. baselineSpan,
  297. regressionSpan,
  298. }: {
  299. baselineSpan: SpanType;
  300. regressionSpan: SpanType;
  301. }): number {
  302. const opNamesEqual = baselineSpan.op === regressionSpan.op;
  303. if (!opNamesEqual) {
  304. return 0;
  305. }
  306. // remove whitespace and convert string to lower case as the individual characters
  307. // adds noise to the edit distance function
  308. const baselineDescription = (baselineSpan.description || '')
  309. .replace(/\s+/g, '')
  310. .toLowerCase();
  311. const regressionDescription = (regressionSpan.description || '')
  312. .replace(/\s+/g, '')
  313. .toLowerCase();
  314. const score = jaroSimilarity(baselineDescription, regressionDescription);
  315. return score >= COMMON_SIMILARITY_DESCRIPTION_THRESHOLD ? score : 0;
  316. }
  317. function generateMergedSpanId({
  318. baselineSpan,
  319. regressionSpan,
  320. }: {
  321. baselineSpan: SpanType;
  322. regressionSpan: SpanType;
  323. }): string {
  324. return `${baselineSpan.span_id}${regressionSpan.span_id}`;
  325. }
  326. function getDiffSpanDuration(diffSpan: DiffSpanType): number {
  327. switch (diffSpan.comparisonResult) {
  328. case 'matched': {
  329. return Math.max(
  330. getSpanDuration(diffSpan.baselineSpan),
  331. getSpanDuration(diffSpan.regressionSpan)
  332. );
  333. }
  334. case 'baseline': {
  335. return getSpanDuration(diffSpan.baselineSpan);
  336. }
  337. case 'regression': {
  338. return getSpanDuration(diffSpan.regressionSpan);
  339. }
  340. default: {
  341. throw Error('Unknown comparisonResult');
  342. }
  343. }
  344. }
  345. export function getSpanDuration(span: RawSpanType): number {
  346. return Math.abs(span.timestamp - span.start_timestamp);
  347. }
  348. function getMatchedSpanDurationDeltas({
  349. baselineSpan,
  350. regressionSpan,
  351. }: {
  352. baselineSpan: RawSpanType;
  353. regressionSpan: RawSpanType;
  354. }): number {
  355. return getSpanDuration(regressionSpan) - getSpanDuration(baselineSpan);
  356. }
  357. function sortDiffSpansByDuration(
  358. firstSpan: DiffSpanType,
  359. secondSpan: DiffSpanType
  360. ): number {
  361. const firstSpanDuration = getDiffSpanDuration(firstSpan);
  362. const secondSpanDuration = getDiffSpanDuration(secondSpan);
  363. if (firstSpanDuration > secondSpanDuration) {
  364. // sort firstSpan before secondSpan
  365. return -1;
  366. }
  367. if (firstSpanDuration < secondSpanDuration) {
  368. // sort secondSpan before firstSpan
  369. return 1;
  370. }
  371. return 0;
  372. }
  373. function sortSpans(firstSpan: RawSpanType, secondSpan: RawSpanType): number {
  374. const firstSpanDuration = getSpanDuration(firstSpan);
  375. const secondSpanDuration = getSpanDuration(secondSpan);
  376. if (firstSpanDuration > secondSpanDuration) {
  377. // sort firstSpan before secondSpan
  378. return -1;
  379. }
  380. if (firstSpanDuration < secondSpanDuration) {
  381. // sort secondSpan before firstSpan
  382. return 1;
  383. }
  384. // try to break ties by sorting by start timestamp in ascending order
  385. if (firstSpan.start_timestamp < secondSpan.start_timestamp) {
  386. // sort firstSpan before secondSpan
  387. return -1;
  388. }
  389. if (firstSpan.start_timestamp > secondSpan.start_timestamp) {
  390. // sort secondSpan before firstSpan
  391. return 1;
  392. }
  393. return 0;
  394. }
  395. function sortByMostTimeAdded(firstSpan: DiffSpanType, secondSpan: DiffSpanType): number {
  396. // Sort the spans by most time added. This means that when comparing the spans of the regression transaction
  397. // against the spans of the baseline transaction, we sort the spans by those that have regressed the most
  398. // relative to their baseline counter parts first.
  399. //
  400. // In terms of sort, we display them in the following way:
  401. // - Regression only spans; sorted first by duration (descending), and then start timestamps (ascending)
  402. // - Matched spans:
  403. // - slower -- i.e. regression.duration - baseline.duration > 0 (sorted by duration deltas, and by duration)
  404. // - no change -- i.e. regression.duration - baseline.duration == 0 (sorted by duration)
  405. // - faster -- i.e. regression.duration - baseline.duration < 0 (sorted by duration deltas, and by duration)
  406. // - Baseline only spans; sorted by duration
  407. switch (firstSpan.comparisonResult) {
  408. case 'regression': {
  409. switch (secondSpan.comparisonResult) {
  410. case 'regression': {
  411. return sortSpans(firstSpan.regressionSpan, secondSpan.regressionSpan);
  412. }
  413. case 'baseline':
  414. case 'matched': {
  415. // sort firstSpan (regression) before secondSpan (baseline)
  416. return -1;
  417. }
  418. default: {
  419. throw Error('Unknown comparisonResult');
  420. }
  421. }
  422. }
  423. case 'baseline': {
  424. switch (secondSpan.comparisonResult) {
  425. case 'baseline': {
  426. return sortSpans(firstSpan.baselineSpan, secondSpan.baselineSpan);
  427. }
  428. case 'regression':
  429. case 'matched': {
  430. // sort secondSpan (regression or matched) before firstSpan (baseline)
  431. return 1;
  432. }
  433. default: {
  434. throw Error('Unknown comparisonResult');
  435. }
  436. }
  437. }
  438. case 'matched': {
  439. switch (secondSpan.comparisonResult) {
  440. case 'regression': {
  441. // sort secondSpan (regression) before firstSpan (matched)
  442. return 1;
  443. }
  444. case 'baseline': {
  445. // sort firstSpan (matched) before secondSpan (baseline)
  446. return -1;
  447. }
  448. case 'matched': {
  449. const firstSpanDurationDelta = getMatchedSpanDurationDeltas({
  450. regressionSpan: firstSpan.regressionSpan,
  451. baselineSpan: firstSpan.baselineSpan,
  452. });
  453. const secondSpanDurationDelta = getMatchedSpanDurationDeltas({
  454. regressionSpan: secondSpan.regressionSpan,
  455. baselineSpan: secondSpan.baselineSpan,
  456. });
  457. if (firstSpanDurationDelta > 0) {
  458. // firstSpan has slower regression span relative to the baseline span
  459. if (secondSpanDurationDelta > 0) {
  460. // secondSpan has slower regression span relative to the baseline span
  461. if (firstSpanDurationDelta > secondSpanDurationDelta) {
  462. // sort firstSpan before secondSpan
  463. return -1;
  464. }
  465. if (firstSpanDurationDelta < secondSpanDurationDelta) {
  466. // sort secondSpan before firstSpan
  467. return 1;
  468. }
  469. return sortDiffSpansByDuration(firstSpan, secondSpan);
  470. }
  471. // case: secondSpan is either "no change" or "faster"
  472. // sort firstSpan before secondSpan
  473. return -1;
  474. }
  475. if (firstSpanDurationDelta === 0) {
  476. // firstSpan has a regression span relative that didn't change relative to the baseline span
  477. if (secondSpanDurationDelta > 0) {
  478. // secondSpan has slower regression span relative to the baseline span
  479. // sort secondSpan before firstSpan
  480. return 1;
  481. }
  482. if (secondSpanDurationDelta < 0) {
  483. // faster
  484. // sort firstSpan before secondSpan
  485. return -1;
  486. }
  487. // secondSpan has a regression span relative that didn't change relative to the baseline span
  488. return sortDiffSpansByDuration(firstSpan, secondSpan);
  489. }
  490. // case: firstSpanDurationDelta < 0
  491. if (secondSpanDurationDelta >= 0) {
  492. // either secondSpan has slower regression span relative to the baseline span,
  493. // or the secondSpan has a regression span relative that didn't change relative to the baseline span
  494. // sort secondSpan before firstSpan
  495. return 1;
  496. }
  497. // case: secondSpanDurationDelta < 0
  498. if (firstSpanDurationDelta < secondSpanDurationDelta) {
  499. // sort firstSpan before secondSpan
  500. return -1;
  501. }
  502. if (firstSpanDurationDelta > secondSpanDurationDelta) {
  503. // sort secondSpan before firstSpan
  504. return 1;
  505. }
  506. return sortDiffSpansByDuration(firstSpan, secondSpan);
  507. }
  508. default: {
  509. throw Error('Unknown comparisonResult');
  510. }
  511. }
  512. }
  513. default: {
  514. throw Error('Unknown comparisonResult');
  515. }
  516. }
  517. }
  518. export function getSpanID(diffSpan: DiffSpanType): string {
  519. switch (diffSpan.comparisonResult) {
  520. case 'matched': {
  521. return diffSpan.span_id;
  522. }
  523. case 'baseline': {
  524. return diffSpan.baselineSpan.span_id;
  525. }
  526. case 'regression': {
  527. return diffSpan.regressionSpan.span_id;
  528. }
  529. default: {
  530. throw Error('Unknown comparisonResult');
  531. }
  532. }
  533. }
  534. export function getSpanOperation(diffSpan: DiffSpanType): string | undefined {
  535. switch (diffSpan.comparisonResult) {
  536. case 'matched': {
  537. return diffSpan.op;
  538. }
  539. case 'baseline': {
  540. return diffSpan.baselineSpan.op;
  541. }
  542. case 'regression': {
  543. return diffSpan.regressionSpan.op;
  544. }
  545. default: {
  546. throw Error('Unknown comparisonResult');
  547. }
  548. }
  549. }
  550. export function getSpanDescription(diffSpan: DiffSpanType): string | undefined {
  551. switch (diffSpan.comparisonResult) {
  552. case 'matched': {
  553. return diffSpan.description;
  554. }
  555. case 'baseline': {
  556. return diffSpan.baselineSpan.description;
  557. }
  558. case 'regression': {
  559. return diffSpan.regressionSpan.description;
  560. }
  561. default: {
  562. throw Error('Unknown comparisonResult');
  563. }
  564. }
  565. }
  566. export function isOrphanDiffSpan(diffSpan: DiffSpanType): boolean {
  567. switch (diffSpan.comparisonResult) {
  568. case 'matched': {
  569. return isOrphanSpan(diffSpan.baselineSpan) || isOrphanSpan(diffSpan.regressionSpan);
  570. }
  571. case 'baseline': {
  572. return isOrphanSpan(diffSpan.baselineSpan);
  573. }
  574. case 'regression': {
  575. return isOrphanSpan(diffSpan.regressionSpan);
  576. }
  577. default: {
  578. throw Error('Unknown comparisonResult');
  579. }
  580. }
  581. }
  582. export type SpanWidths =
  583. | {
  584. type: 'WIDTH_PIXEL';
  585. width: 1;
  586. }
  587. | {
  588. type: 'WIDTH_PERCENTAGE';
  589. width: number;
  590. };
  591. export type SpanGeneratedBoundsType = {
  592. background: SpanWidths;
  593. foreground: SpanWidths | undefined;
  594. baseline: SpanWidths | undefined;
  595. regression: SpanWidths | undefined;
  596. };
  597. function generateWidth({
  598. duration,
  599. largestDuration,
  600. }: {
  601. duration: number;
  602. largestDuration: number;
  603. }): SpanWidths {
  604. if (duration <= 0) {
  605. return {
  606. type: 'WIDTH_PIXEL',
  607. width: 1,
  608. };
  609. }
  610. return {
  611. type: 'WIDTH_PERCENTAGE',
  612. width: duration / largestDuration,
  613. };
  614. }
  615. export function boundsGenerator(rootSpans: Array<DiffSpanType>) {
  616. // get largest duration among the root spans.
  617. // invariant: this is the largest duration among all of the spans on the transaction
  618. // comparison page.
  619. const largestDuration = Math.max(
  620. ...rootSpans.map(rootSpan => {
  621. return getDiffSpanDuration(rootSpan);
  622. })
  623. );
  624. return (span: DiffSpanType): SpanGeneratedBoundsType => {
  625. switch (span.comparisonResult) {
  626. case 'matched': {
  627. const baselineDuration = getSpanDuration(span.baselineSpan);
  628. const regressionDuration = getSpanDuration(span.regressionSpan);
  629. const baselineWidth = generateWidth({
  630. duration: baselineDuration,
  631. largestDuration,
  632. });
  633. const regressionWidth = generateWidth({
  634. duration: regressionDuration,
  635. largestDuration,
  636. });
  637. if (baselineDuration >= regressionDuration) {
  638. return {
  639. background: baselineWidth,
  640. foreground: regressionWidth,
  641. baseline: baselineWidth,
  642. regression: regressionWidth,
  643. };
  644. }
  645. // case: baselineDuration < regressionDuration
  646. return {
  647. background: regressionWidth,
  648. foreground: baselineWidth,
  649. baseline: baselineWidth,
  650. regression: regressionWidth,
  651. };
  652. }
  653. case 'regression': {
  654. const regressionDuration = getSpanDuration(span.regressionSpan);
  655. const regressionWidth = generateWidth({
  656. duration: regressionDuration,
  657. largestDuration,
  658. });
  659. return {
  660. background: regressionWidth,
  661. foreground: undefined,
  662. baseline: undefined,
  663. regression: regressionWidth,
  664. };
  665. }
  666. case 'baseline': {
  667. const baselineDuration = getSpanDuration(span.baselineSpan);
  668. const baselineWidth = generateWidth({
  669. duration: baselineDuration,
  670. largestDuration,
  671. });
  672. return {
  673. background: baselineWidth,
  674. foreground: undefined,
  675. baseline: baselineWidth,
  676. regression: undefined,
  677. };
  678. }
  679. default: {
  680. const _exhaustiveCheck: never = span;
  681. return _exhaustiveCheck;
  682. }
  683. }
  684. };
  685. }
  686. export function generateCSSWidth(width: SpanWidths | undefined): string | undefined {
  687. if (!width) {
  688. return undefined;
  689. }
  690. switch (width.type) {
  691. case 'WIDTH_PIXEL': {
  692. return `${width.width}px`;
  693. }
  694. case 'WIDTH_PERCENTAGE': {
  695. return toPercent(width.width);
  696. }
  697. default: {
  698. const _exhaustiveCheck: never = width;
  699. return _exhaustiveCheck;
  700. }
  701. }
  702. }