performanceForSentry.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import {Fragment, Profiler, ReactNode, useEffect, useRef} from 'react';
  2. import {captureException} from '@sentry/react';
  3. import {browserPerformanceTimeOrigin, timestampWithMs} from '@sentry/utils';
  4. import getCurrentSentryReactTransaction from './getCurrentSentryReactTransaction';
  5. const MIN_UPDATE_SPAN_TIME = 5; // Frame boundary @ 60fps
  6. /**
  7. * Callback for React Profiler https://reactjs.org/docs/profiler.html
  8. */
  9. export function onRenderCallback(
  10. id: string,
  11. phase: 'mount' | 'update',
  12. actualDuration: number
  13. ) {
  14. try {
  15. const transaction = getCurrentSentryReactTransaction();
  16. if (transaction && actualDuration > MIN_UPDATE_SPAN_TIME) {
  17. const now = timestampWithMs();
  18. transaction.startChild({
  19. description: `<${id}>`,
  20. op: `ui.react.${phase}`,
  21. startTimestamp: now - actualDuration / 1000,
  22. endTimestamp: now,
  23. });
  24. }
  25. } catch (_) {
  26. // Add defensive catch since this wraps all of App
  27. }
  28. }
  29. class LongTaskObserver {
  30. private static observer: PerformanceObserver;
  31. private static longTaskCount = 0;
  32. private static currentId: string;
  33. static getPerformanceObserver(id: string): PerformanceObserver | null {
  34. try {
  35. LongTaskObserver.currentId = id;
  36. if (LongTaskObserver.observer) {
  37. LongTaskObserver.observer.disconnect();
  38. LongTaskObserver.observer.observe({entryTypes: ['longtask']});
  39. return LongTaskObserver.observer;
  40. }
  41. if (!window.PerformanceObserver || !browserPerformanceTimeOrigin) {
  42. return null;
  43. }
  44. const transaction: any = getCurrentSentryReactTransaction();
  45. const timeOrigin = browserPerformanceTimeOrigin / 1000;
  46. const observer = new PerformanceObserver(function (list) {
  47. const perfEntries = list.getEntries();
  48. if (!transaction) {
  49. return;
  50. }
  51. perfEntries.forEach(entry => {
  52. const startSeconds = timeOrigin + entry.startTime / 1000;
  53. LongTaskObserver.longTaskCount++;
  54. transaction.startChild({
  55. description: `Long Task - ${LongTaskObserver.currentId}`,
  56. op: `ui.sentry.long-task`,
  57. startTimestamp: startSeconds,
  58. endTimestamp: startSeconds + entry.duration / 1000,
  59. });
  60. });
  61. });
  62. if (!transaction) {
  63. return null;
  64. }
  65. transaction?.registerBeforeFinishCallback?.(t => {
  66. if (!browserPerformanceTimeOrigin) {
  67. return;
  68. }
  69. t.setTag('longTaskCount', LongTaskObserver.longTaskCount);
  70. });
  71. if (!observer || !observer.observe) {
  72. return null;
  73. }
  74. LongTaskObserver.observer = observer;
  75. LongTaskObserver.observer.observe({entryTypes: ['longtask']});
  76. return LongTaskObserver.observer;
  77. } catch (e) {
  78. captureException(e);
  79. // Defensive try catch.
  80. }
  81. return null;
  82. }
  83. }
  84. export const ProfilerWithTasks = ({id, children}: {children: ReactNode; id: string}) => {
  85. useEffect(() => {
  86. let observer;
  87. try {
  88. if (!window.PerformanceObserver || !browserPerformanceTimeOrigin) {
  89. return () => {};
  90. }
  91. observer = LongTaskObserver.getPerformanceObserver(id);
  92. } catch (e) {
  93. captureException(e);
  94. // Defensive since this is auxiliary code.
  95. }
  96. return () => {
  97. if (observer && observer.disconnect) {
  98. observer.disconnect();
  99. }
  100. };
  101. }, []);
  102. return (
  103. <Profiler id={id} onRender={onRenderCallback}>
  104. {children}
  105. </Profiler>
  106. );
  107. };
  108. export const VisuallyCompleteWithData = ({
  109. id,
  110. hasData,
  111. children,
  112. }: {
  113. children: ReactNode;
  114. hasData: boolean;
  115. id: string;
  116. }) => {
  117. const isVisuallyCompleteSet = useRef(false);
  118. const isDataCompleteSet = useRef(false);
  119. const longTaskCount = useRef(0);
  120. useEffect(() => {
  121. let observer;
  122. try {
  123. if (!window.PerformanceObserver || !browserPerformanceTimeOrigin) {
  124. return () => {};
  125. }
  126. observer = LongTaskObserver.getPerformanceObserver(id);
  127. } catch (_) {
  128. // Defensive since this is auxiliary code.
  129. }
  130. return () => {
  131. if (observer && observer.disconnect) {
  132. observer.disconnect();
  133. }
  134. };
  135. }, []);
  136. const num = useRef(1);
  137. const isVCDSet = useRef(false);
  138. if (isVCDSet && hasData && performance && performance.mark) {
  139. performance.mark(`${id}-vcsd-start`);
  140. isVCDSet.current = true;
  141. }
  142. useEffect(() => {
  143. try {
  144. const transaction: any = getCurrentSentryReactTransaction(); // Using any to override types for private api.
  145. if (!transaction) {
  146. return;
  147. }
  148. if (!isVisuallyCompleteSet.current) {
  149. const time = performance.now();
  150. transaction.registerBeforeFinishCallback((t, _) => {
  151. // Should be called after performance entries finish callback.
  152. t.setMeasurements({
  153. ...t._measurements,
  154. visuallyComplete: {value: time},
  155. });
  156. });
  157. isVisuallyCompleteSet.current = true;
  158. }
  159. if (!isDataCompleteSet.current && hasData) {
  160. isDataCompleteSet.current = true;
  161. performance.mark(`${id}-vcsd-end-pre-timeout`);
  162. window.setTimeout(() => {
  163. if (!browserPerformanceTimeOrigin) {
  164. return;
  165. }
  166. performance.mark(`${id}-vcsd-end`);
  167. const measureName = `VCD [${id}] #${num.current}`;
  168. performance.measure(
  169. `VCD [${id}] #${num.current}`,
  170. `${id}-vcsd-start`,
  171. `${id}-vcsd-end`
  172. );
  173. num.current = num.current++;
  174. const [measureEntry] = performance.getEntriesByName(measureName);
  175. if (!measureEntry) {
  176. return;
  177. }
  178. transaction.registerBeforeFinishCallback(t => {
  179. if (!browserPerformanceTimeOrigin) {
  180. return;
  181. }
  182. // Should be called after performance entries finish callback.
  183. const lcp = t._measurements.lcp?.value;
  184. // Adjust to be relative to transaction.startTimestamp
  185. const entryStartSeconds =
  186. browserPerformanceTimeOrigin / 1000 + measureEntry.startTime / 1000;
  187. const time = (entryStartSeconds - transaction.startTimestamp) * 1000;
  188. const newMeasurements = {
  189. ...t._measurements,
  190. visuallyCompleteData: {value: time},
  191. };
  192. if (lcp) {
  193. newMeasurements.lcpDiffVCD = {value: lcp - time};
  194. }
  195. t.setTag('longTaskCount', longTaskCount.current);
  196. t.setMeasurements(newMeasurements);
  197. });
  198. }, 0);
  199. }
  200. } catch (_) {
  201. // Defensive catch since this code is auxiliary.
  202. }
  203. }, [hasData]);
  204. return (
  205. <Profiler id={id} onRender={onRenderCallback}>
  206. <Fragment>{children}</Fragment>
  207. </Profiler>
  208. );
  209. };