utils.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import {formatSecondsToClock} from 'sentry/utils/formatters';
  2. import type {ReplayFrame, SpanFrame} from 'sentry/utils/replays/types';
  3. const SECOND = 1000;
  4. const MINUTE = 60 * SECOND;
  5. const HOUR = 60 * MINUTE;
  6. export function showPlayerTime(
  7. timestamp: ConstructorParameters<typeof Date>[0],
  8. relativeTimeMs: number,
  9. showMs: boolean = false
  10. ): string {
  11. return formatTime(Math.abs(new Date(timestamp).getTime() - relativeTimeMs), showMs);
  12. }
  13. export function formatTime(ms: number, showMs?: boolean): string {
  14. if (ms <= 0 || isNaN(ms)) {
  15. if (showMs) {
  16. return '00:00.000';
  17. }
  18. return '00:00';
  19. }
  20. const seconds = ms / 1000;
  21. return formatSecondsToClock(showMs ? seconds : Math.floor(seconds));
  22. }
  23. /**
  24. * Figure out how many ticks to show in an area.
  25. * If there is more space available, we can show more granular ticks, but if
  26. * less space is available, fewer ticks.
  27. * Similarly if the duration is short, the ticks will represent a short amount
  28. * of time (like every second) but if the duration is long one tick may
  29. * represent an hour.
  30. *
  31. * @param durationMs The amount of time that we need to chop up into even sections
  32. * @param width Total width available, pixels
  33. * @param minWidth Minimum space for each column, pixels. Ex: So we can show formatted time like `1:00:00` between major ticks
  34. * @returns
  35. */
  36. export function countColumns(durationMs: number, width: number, minWidth: number = 50) {
  37. let maxCols = Math.floor(width / minWidth);
  38. const remainder = durationMs - maxCols * width > 0 ? 1 : 0;
  39. maxCols -= remainder;
  40. // List of all the possible time granularities to display
  41. // We could generate the list, which is basically a version of fizzbuzz, hard-coding is quicker.
  42. const timeOptions = [
  43. 1 * HOUR,
  44. 30 * MINUTE,
  45. 20 * MINUTE,
  46. 15 * MINUTE,
  47. 10 * MINUTE,
  48. 5 * MINUTE,
  49. 2 * MINUTE,
  50. 1 * MINUTE,
  51. 30 * SECOND,
  52. 10 * SECOND,
  53. 5 * SECOND,
  54. 1 * SECOND,
  55. ];
  56. const timeBasedCols = timeOptions.reduce<Map<number, number>>((map, time) => {
  57. map.set(time, Math.floor(durationMs / time));
  58. return map;
  59. }, new Map());
  60. const [timespan, cols] = Array.from(timeBasedCols.entries())
  61. .filter(([_span, c]) => c <= maxCols) // Filter for any valid timespan option where all ticks would fit
  62. .reduce((best, next) => (next[1] > best[1] ? next : best), [0, 0]); // select the timespan option with the most ticks
  63. const remaining = (durationMs - timespan * cols) / timespan;
  64. return {timespan, cols, remaining};
  65. }
  66. /**
  67. * Group Crumbs for display along the timeline.
  68. *
  69. * The timeline is broken down into columns (aka buckets, or time-slices).
  70. * Columns translate to a fixed width on the screen, to prevent side-scrolling.
  71. *
  72. * This function groups crumbs into columns based on the number of columns available
  73. * and the timestamp of the crumb.
  74. */
  75. export function getFramesByColumn(
  76. durationMs: number,
  77. frames: ReplayFrame[],
  78. totalColumns: number
  79. ) {
  80. const safeDurationMs = isNaN(durationMs) ? 1 : durationMs;
  81. const columnFramePairs = frames.map(frame => {
  82. const columnPositionCalc =
  83. Math.floor((frame.offsetMs / safeDurationMs) * (totalColumns - 1)) + 1;
  84. // Should start at minimum in the first column
  85. const column = Math.max(1, columnPositionCalc);
  86. return [column, frame] as [number, ReplayFrame];
  87. });
  88. const framesByColumn = columnFramePairs.reduce<Map<number, ReplayFrame[]>>(
  89. (map, [column, frame]) => {
  90. if (map.has(column)) {
  91. map.get(column)?.push(frame);
  92. } else {
  93. map.set(column, [frame]);
  94. }
  95. return map;
  96. },
  97. new Map()
  98. );
  99. return framesByColumn;
  100. }
  101. type FlattenedSpanRange = {
  102. /**
  103. * Duration of this range
  104. */
  105. duration: number;
  106. /**
  107. * Absolute time in ms when the range ends
  108. */
  109. endTimestamp: number;
  110. /**
  111. * Number of spans that got flattened into this range
  112. */
  113. frameCount: number;
  114. /**
  115. * Absolute time in ms when the span starts
  116. */
  117. startTimestamp: number;
  118. };
  119. function doesOverlap(a: FlattenedSpanRange, b: FlattenedSpanRange) {
  120. const bStartsWithinA =
  121. a.startTimestamp <= b.startTimestamp && b.startTimestamp <= a.endTimestamp;
  122. const bEndsWithinA =
  123. a.startTimestamp <= b.endTimestamp && b.endTimestamp <= a.endTimestamp;
  124. return bStartsWithinA || bEndsWithinA;
  125. }
  126. export function flattenFrames(frames: SpanFrame[]): FlattenedSpanRange[] {
  127. if (!frames.length) {
  128. return [];
  129. }
  130. const [first, ...rest] = frames.map((span): FlattenedSpanRange => {
  131. return {
  132. frameCount: 1,
  133. startTimestamp: span.timestampMs,
  134. endTimestamp: span.endTimestampMs,
  135. duration: span.endTimestampMs - span.timestampMs,
  136. };
  137. });
  138. const flattened = [first];
  139. for (const span of rest) {
  140. let overlap = false;
  141. for (const range of flattened) {
  142. if (doesOverlap(range, span)) {
  143. overlap = true;
  144. range.frameCount += 1;
  145. range.startTimestamp = Math.min(range.startTimestamp, span.startTimestamp);
  146. range.endTimestamp = Math.max(range.endTimestamp, span.endTimestamp);
  147. range.duration = range.endTimestamp - range.startTimestamp;
  148. break;
  149. }
  150. }
  151. if (!overlap) {
  152. flattened.push(span);
  153. }
  154. }
  155. return flattened;
  156. }
  157. /**
  158. * Divide two numbers safely
  159. */
  160. export function divide(numerator: number, denominator: number | undefined) {
  161. if (denominator === undefined || isNaN(denominator) || denominator === 0) {
  162. return 0;
  163. }
  164. return numerator / denominator;
  165. }