From d0c0cf7f7ea01942c4feae06053d9f688a669a64 Mon Sep 17 00:00:00 2001 From: lucas Date: Thu, 22 Aug 2024 17:49:52 -0300 Subject: [PATCH 01/47] implement fallback system --- src/js/tracing/reactnavigation.ts | 10 ++- src/js/utils/sentryeventemitterfallback.ts | 82 ++++++++++++++++++++++ 2 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 src/js/utils/sentryeventemitterfallback.ts diff --git a/src/js/tracing/reactnavigation.ts b/src/js/tracing/reactnavigation.ts index f4225d902..afa5158be 100644 --- a/src/js/tracing/reactnavigation.ts +++ b/src/js/tracing/reactnavigation.ts @@ -5,6 +5,7 @@ import { logger, timestampInSeconds } from '@sentry/utils'; import type { NewFrameEvent } from '../utils/sentryeventemitter'; import { type SentryEventEmitter, createSentryEventEmitter, NewFrameEventName } from '../utils/sentryeventemitter'; +import { type SentryEventEmitterFallback, createSentryFallbackEventEmitter } from '../utils/sentryeventemitterfallback'; import { RN_GLOBAL_OBJ } from '../utils/worldwide'; import { NATIVE } from '../wrapper'; import type { OnConfirmRoute, TransactionCreator } from './routingInstrumentation'; @@ -69,7 +70,7 @@ export class ReactNavigationInstrumentation extends InternalRoutingInstrumentati private _navigationContainer: NavigationContainer | null = null; private _newScreenFrameEventEmitter: SentryEventEmitter | null = null; - + private _newFallbackEventEmitter: SentryEventEmitterFallback | null = null; private readonly _maxRecentRouteLen: number = 200; private _latestRoute?: NavigationRoute; @@ -92,7 +93,9 @@ export class ReactNavigationInstrumentation extends InternalRoutingInstrumentati if (this._options.enableTimeToInitialDisplay) { this._newScreenFrameEventEmitter = createSentryEventEmitter(); + this._newFallbackEventEmitter = createSentryFallbackEventEmitter(); this._newScreenFrameEventEmitter.initAsync(NewFrameEventName); + this._newFallbackEventEmitter.initAsync(); NATIVE.initNativeReactNavigationNewFrameTracking().catch((reason: unknown) => { logger.error(`[ReactNavigationInstrumentation] Failed to initialize native new frame tracking: ${reason}`); }); @@ -238,8 +241,8 @@ export class ReactNavigationInstrumentation extends InternalRoutingInstrumentati isAutoInstrumented: true, }); - !routeHasBeenSeen && - latestTtidSpan && + if (!routeHasBeenSeen && latestTtidSpan) { + this._newFallbackEventEmitter?.startListenerAsync(); this._newScreenFrameEventEmitter?.once( NewFrameEventName, ({ newFrameTimestampInSeconds }: NewFrameEvent) => { @@ -256,6 +259,7 @@ export class ReactNavigationInstrumentation extends InternalRoutingInstrumentati setSpanDurationAsMeasurementOnTransaction(latestTransaction, 'time_to_initial_display', latestTtidSpan); }, ); + } this._navigationProcessingSpan?.updateName(`Processing navigation to ${route.name}`); this._navigationProcessingSpan?.setStatus('ok'); diff --git a/src/js/utils/sentryeventemitterfallback.ts b/src/js/utils/sentryeventemitterfallback.ts new file mode 100644 index 000000000..ea68bad51 --- /dev/null +++ b/src/js/utils/sentryeventemitterfallback.ts @@ -0,0 +1,82 @@ +import { logger } from '@sentry/utils'; +import type { EmitterSubscription } from 'react-native'; +import { DeviceEventEmitter } from 'react-native'; + +import { NewFrameEventName } from './sentryeventemitter'; + +export type FallBackNewFrameEvent = { newFrameTimestampInSeconds: number, isFallback?: boolean }; +export interface SentryEventEmitterFallback { + /** + * Initializes the fallback event emitter + * This method is synchronous in JS but the event emitter starts asynchronously + * https://github.com/facebook/react-native/blob/d09c02f9e2d468e4d0bde51890e312ae7003a3e6/packages/react-native/React/Modules/RCTEventEmitter.m#L95 + */ + initAsync: () => void; + closeAllAsync: () => void; + startListenerAsync: () => void; +} + +function timeNowNanosecond(): number { + return Date.now() / 1000; // Convert to nanoseconds +} + +/** + * Creates emitter that allows to listen to UI Frame events when ready. + */ +export function createSentryFallbackEventEmitter(): SentryEventEmitterFallback { + let NativeEmitterCalled: boolean = false; + let subscription: EmitterSubscription | undefined = undefined; + let isListening = false; + return { + initAsync() { + + subscription = DeviceEventEmitter.addListener(NewFrameEventName, () => { + // Avoid noise from pages that we do not want to track. + if (isListening) { + NativeEmitterCalled = true; + } + }); + }, + + startListenerAsync() { + isListening = true; + + // Schedule the callback to be executed when all UI Frames have flushed. + requestAnimationFrame(() => { + if (NativeEmitterCalled) { + NativeEmitterCalled = false; + isListening = false; + return; + } + const timestampInSeconds = timeNowNanosecond(); + const maxRetries = 3; + let retries = 0; + + const retryCheck = (): void => { + if (NativeEmitterCalled) { + NativeEmitterCalled = false; + isListening = false; + return; // Native Repplied the bridge with a given timestamp. + } + + retries++; + if (retries < maxRetries) { + setTimeout(retryCheck, 1_000); + } else { + logger.log('Native timestamp did not reply in time, using fallback.'); + isListening = false; + DeviceEventEmitter.emit(NewFrameEventName, { newFrameTimestampInSeconds: timestampInSeconds, isFallback: true }); + } + }; + + // Start the retry process + retryCheck(); + + }); + }, + + closeAllAsync() { + subscription?.remove(); + } + } +} From 1a526203d1c9300a925cd70d955cc4362e68712b Mon Sep 17 00:00:00 2001 From: lucas Date: Thu, 22 Aug 2024 23:05:56 -0300 Subject: [PATCH 02/47] clear format --- src/js/utils/sentryeventemitterfallback.ts | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/js/utils/sentryeventemitterfallback.ts b/src/js/utils/sentryeventemitterfallback.ts index ea68bad51..5fb007872 100644 --- a/src/js/utils/sentryeventemitterfallback.ts +++ b/src/js/utils/sentryeventemitterfallback.ts @@ -2,17 +2,16 @@ import { logger } from '@sentry/utils'; import type { EmitterSubscription } from 'react-native'; import { DeviceEventEmitter } from 'react-native'; -import { NewFrameEventName } from './sentryeventemitter'; +import { NewFrameEventName } from './sentryeventemitter'; -export type FallBackNewFrameEvent = { newFrameTimestampInSeconds: number, isFallback?: boolean }; +export type FallBackNewFrameEvent = { newFrameTimestampInSeconds: number; isFallback?: boolean }; export interface SentryEventEmitterFallback { /** * Initializes the fallback event emitter - * This method is synchronous in JS but the event emitter starts asynchronously - * https://github.com/facebook/react-native/blob/d09c02f9e2d468e4d0bde51890e312ae7003a3e6/packages/react-native/React/Modules/RCTEventEmitter.m#L95 + * This method is synchronous in JS but the event emitter starts asynchronously. */ initAsync: () => void; - closeAllAsync: () => void; + closeAll: () => void; startListenerAsync: () => void; } @@ -24,12 +23,11 @@ function timeNowNanosecond(): number { * Creates emitter that allows to listen to UI Frame events when ready. */ export function createSentryFallbackEventEmitter(): SentryEventEmitterFallback { - let NativeEmitterCalled: boolean = false; + let NativeEmitterCalled: boolean = false; let subscription: EmitterSubscription | undefined = undefined; let isListening = false; return { initAsync() { - subscription = DeviceEventEmitter.addListener(NewFrameEventName, () => { // Avoid noise from pages that we do not want to track. if (isListening) { @@ -56,7 +54,7 @@ export function createSentryFallbackEventEmitter(): SentryEventEmitterFallback { if (NativeEmitterCalled) { NativeEmitterCalled = false; isListening = false; - return; // Native Repplied the bridge with a given timestamp. + return; // Native Replied the bridge with a given timestamp. } retries++; @@ -65,18 +63,20 @@ export function createSentryFallbackEventEmitter(): SentryEventEmitterFallback { } else { logger.log('Native timestamp did not reply in time, using fallback.'); isListening = false; - DeviceEventEmitter.emit(NewFrameEventName, { newFrameTimestampInSeconds: timestampInSeconds, isFallback: true }); + DeviceEventEmitter.emit(NewFrameEventName, { + newFrameTimestampInSeconds: timestampInSeconds, + isFallback: true, + }); } }; // Start the retry process retryCheck(); - }); }, - closeAllAsync() { + closeAll() { subscription?.remove(); - } - } + }, + }; } From 84301f791d811769bbb2ce2afdf3b034f3ce02c7 Mon Sep 17 00:00:00 2001 From: lucas Date: Fri, 30 Aug 2024 11:54:39 -0300 Subject: [PATCH 03/47] backup --- .../io/sentry/react/RNSentryModuleImpl.java | 18 +- .../java/io/sentry/react/RNSentryModule.java | 5 + .../java/io/sentry/react/RNSentryModule.java | 5 + .../src/Screens/PerformanceScreen.tsx | 793 +++++++++++++++++- src/js/NativeRNSentry.ts | 2 + src/js/utils/sentryeventemitter.ts | 2 + src/js/utils/sentryeventemitterfallback.ts | 5 +- src/js/wrapper.ts | 13 +- 8 files changed, 839 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 7f13e5e21..4c759ef63 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -1,5 +1,5 @@ package io.sentry.react; - +import android.view.Choreographer; import static java.util.concurrent.TimeUnit.SECONDS; import static io.sentry.android.core.internal.util.ScreenshotUtils.takeScreenshot; import static io.sentry.vendor.Base64.NO_PADDING; @@ -470,6 +470,22 @@ public void captureEnvelope(String rawBytes, ReadableMap options, Promise promis promise.resolve(true); } + public void requestAnimationFrame(Promise promise) { + Choreographer choreographer = Choreographer.getInstance(); + + choreographer.postFrameCallback(new Choreographer.FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + final @NotNull SentryDateProvider dateProvider = new SentryAndroidDateProvider(); + + // Invoke the callback after the frame is rendered + final SentryDate endDate = dateProvider.now(); + + promise.resolve(endDate.nanoTimestamp() / 1e9); + } + }); + } + public void captureScreenshot(Promise promise) { final Activity activity = getCurrentActivity(); diff --git a/android/src/newarch/java/io/sentry/react/RNSentryModule.java b/android/src/newarch/java/io/sentry/react/RNSentryModule.java index 3d585b6b1..d148a60af 100644 --- a/android/src/newarch/java/io/sentry/react/RNSentryModule.java +++ b/android/src/newarch/java/io/sentry/react/RNSentryModule.java @@ -43,6 +43,11 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { this.impl.initNativeSdk(rnOptions, promise); } + @Override + public void requestAnimationFrame(Promise promise) { + this.impl.requestAnimationFrame(promise); + } + @Override public void crash() { this.impl.crash(); diff --git a/android/src/oldarch/java/io/sentry/react/RNSentryModule.java b/android/src/oldarch/java/io/sentry/react/RNSentryModule.java index 33fa7283b..fc92c6111 100644 --- a/android/src/oldarch/java/io/sentry/react/RNSentryModule.java +++ b/android/src/oldarch/java/io/sentry/react/RNSentryModule.java @@ -38,6 +38,11 @@ public void initNativeReactNavigationNewFrameTracking(Promise promise) { this.impl.initNativeReactNavigationNewFrameTracking(promise); } + @ReactMethod + public void requestAnimationFrame(Promise promise) { + this.impl.requestAnimationFrame(promise); + } + @ReactMethod public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { this.impl.initNativeSdk(rnOptions, promise); diff --git a/samples/react-native/src/Screens/PerformanceScreen.tsx b/samples/react-native/src/Screens/PerformanceScreen.tsx index c768d4b54..b6bb8d93d 100644 --- a/samples/react-native/src/Screens/PerformanceScreen.tsx +++ b/samples/react-native/src/Screens/PerformanceScreen.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { StatusBar, ScrollView, @@ -32,6 +32,16 @@ const PerformanceScreen = (props: Props) => { }), ); }; + const [isLoading, setIsLoading] = useState(true); + useEffect(() => { + // Simulate heavy computation for 5 seconds + const start = Date.now(); + while (Date.now() - start < 5000) { + // Perform some meaningless computation to occupy the CPU + Math.sqrt(Math.random() * Math.random()); + } + setIsLoading(false); + }, []); return ( <> @@ -62,6 +72,787 @@ const PerformanceScreen = (props: Props) => { props.navigation.navigate('Redux'); }} /> +