utils.tsx 5.9 KB

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